post_policy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ pkg/*
@@ -0,0 +1,7 @@
1
+ = Change Log
2
+
3
+ Below is a complete listing of changes for each revision of PostPolicy.
4
+
5
+ === 0.0.1
6
+
7
+ * Initial release
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Michał Łomnicki
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,43 @@
1
+ = PostPolicy: Postfix Policy Server in Ruby
2
+
3
+ PostPolicy uses ACL system, which allow administrators to create rules based on mail source.
4
+ Unlike simple Postfix policy restrictions in PostPolicy one can create very complex rules against incoming mail.
5
+ PostPolicy is built on top of eventmachine, event-driven network library used for critical networked applications.
6
+
7
+ <b>PostPolicy is under heavy development so don't expect too much at the moment ;)</b>
8
+
9
+ == DEPENDENCIES
10
+
11
+ * eventmachine
12
+ * rpsec (only for tests)
13
+
14
+ == USAGE
15
+
16
+ Configure postpolicy in /etc/postpolicy.yml
17
+
18
+ Read http://www.postfix.org/SMTPD_POLICY_README.html
19
+
20
+ append to your master.cf
21
+
22
+ policy unix - n n - 0 spawn user=nobody argv=/path/to/postpolicy
23
+
24
+ in your main.cf
25
+
26
+ smtpd_recipient_restrictions =
27
+ ...
28
+ reject_unauth_destination
29
+ check_policy_service unix:private/policy
30
+
31
+ == ABOUT
32
+
33
+ Author:: Michał Łomnicki <michal@lomnicki.com.pl>
34
+ License:: Copyright 2009 by Michał Łomnicki
35
+ Released under a MIT license.
36
+
37
+ == Warranty
38
+
39
+ This software is provided "as is" and without any express or
40
+ implied warranties, including, without limitation, the implied
41
+ warranties of merchantibility and fitness for a particular
42
+ purpose.
43
+
@@ -0,0 +1,15 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "post_policy"
5
+ gemspec.summary = "Postfix Policy Server in Ruby"
6
+ gemspec.description = 'PostPolicy uses ACL system, which allow administrators to create rules based on mail source.'
7
+ gemspec.email = "michal@lomnicki.com.pl"
8
+ gemspec.homepage = "http://github.com/mlomnicki/post_policy"
9
+ gemspec.authors = ["Michał Łomnicki"]
10
+ end
11
+ Jeweler::GemcutterTasks.new
12
+ rescue LoadError
13
+ puts "Jeweler not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
14
+ end
15
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require File.join( File.dirname( __FILE__ ), '..', 'lib', 'postpolicy' )
5
+
6
+ DEFAULT_CONFIG = File.exists?( '/etc/postpolicy.yml' ) ? '/etc/postpolicy.yml' :
7
+ File.join( File.dirname( __FILE__ ), '../postpolicy.yml')
8
+
9
+ DEFAULT_OPTIONS = {
10
+ :verbose => false,
11
+ :config => DEFAULT_CONFIG
12
+ }
13
+
14
+ begin
15
+ options = DEFAULT_OPTIONS
16
+ OptionParser.new do |opts|
17
+ opts.banner = "Usage: #{$0} [options]"
18
+
19
+ opts.on("-v", "--verbose", "Verbose logging") do |v|
20
+ options[:verbose] = v
21
+ end
22
+
23
+ opts.on("-c", "--config", "Path to configuration file") do |c|
24
+ options[:config] = c
25
+ end
26
+ end.parse!
27
+
28
+ Logger.info( "Starting PostPolicy #{PostPolicy::VERSION::STRING}"
29
+ PostPolicy::Config.load_from_file( options[:config] )
30
+ PostPolicy::Protocol.new.start!
31
+ rescue
32
+ Logger.error( $!.message )
33
+ raise $!
34
+ end
@@ -0,0 +1,6 @@
1
+ host: localhost
2
+ user: username
3
+ password: secret
4
+ driver: postgres
5
+ database: test
6
+
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+
4
+ $:.unshift File.dirname( __FILE__ )
5
+
6
+ require 'postpolicy/config'
7
+ require 'postpolicy/protocol'
8
+ require 'postpolicy/access_manager'
9
+ require 'postpolicy/logger'
10
+ require 'postpolicy/extensions'
11
+ require 'postpolicy/server'
12
+ require 'postpolicy/rule'
13
+ require 'postpolicy/version'
14
+
15
+ VERBOSE = false unless defined?( VERBOSE )
@@ -0,0 +1,36 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+
4
+ module PostPolicy
5
+
6
+ class AccessManager
7
+
8
+ include EventMachine::Deferrable
9
+
10
+ DEFAULT_ACTION = "DUNNO"
11
+
12
+ @@access_list = []
13
+
14
+ def self.access_list
15
+ @@access_list
16
+ end
17
+
18
+ def self.<<( action )
19
+ @@access_list << action
20
+ end
21
+
22
+ def check( args )
23
+ action = DEFAULT_ACTION
24
+ @@access_list.each do |access|
25
+ if access.match?( args )
26
+ action = access.action
27
+ break
28
+ end
29
+ end
30
+ yield action if block_given?
31
+ return action
32
+ end
33
+
34
+ end
35
+ end
36
+
@@ -0,0 +1,64 @@
1
+ require 'yaml'
2
+ require 'singleton'
3
+
4
+ Dir.glob( File.join( File.dirname( __FILE__ ), "plugins/base/*" ) ).each { |base| require base }
5
+ Dir.glob( File.join( File.dirname( __FILE__ ), "plugins/acl/*" ) ).each { |acl| require acl }
6
+ Dir.glob( File.join( File.dirname( __FILE__ ), "plugins/datasource/*" ) ).each { |datasource| require datasource }
7
+
8
+ module PostPolicy
9
+
10
+ class Config
11
+
12
+ def self.load_from_file( filename )
13
+ load( YAML.load_file( filename ) )
14
+ end
15
+
16
+ def self.load( config_hash )
17
+ acls = Hash.new { |hsh, key| hsh[key] = Array.new }
18
+ config_acls = config_hash.delete( "acl" )
19
+ config_acls.each_pair do |human_name, klass_value_maps|
20
+ klass_value_maps.each_pair do |klass, value|
21
+ acls[human_name] << ACL.const_get(klass.classify).new( resolve_datasource( value ) )
22
+ end
23
+ end
24
+ actions = config_hash.delete( "action" )
25
+ accesses = config_hash.delete( "access" )
26
+ accesses.each_pair do |action, acl|
27
+ AccessManager << Access.new( acls[acl], actions[action] )
28
+ end
29
+ Db.load( config_hash["database"] )
30
+ end
31
+
32
+ class Db
33
+ class << self
34
+ attr_reader :dbconfig, :dbi_params
35
+
36
+ def load( filename )
37
+ if filename
38
+ @dbconfig = YAML.load_file( filename ).stringify_keys!.freeze
39
+ require "do_#{@dbconfig[:driver]}"
40
+ @dbi_params = "#{@dbconfig[:driver]}://#{@dbconfig[:user]}:#{@dbconfig[:password]}@#{@dbconfig[:host]}:#{@dbconfig[:port]}/#{@dbconfig[:database]}"
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+
47
+ protected
48
+
49
+ def self.resolve_datasource( klass_and_value )
50
+ @@datasource_cache ||= {}
51
+ klass, value = klass_and_value.split( "://" )
52
+ if value == nil # consider as constant value
53
+ value = klass
54
+ klass = "value"
55
+ end
56
+ @@datasource_cache[klass] ||= DataSource.const_get(klass.classify)
57
+
58
+ return @@datasource_cache[klass].new( value )
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+
@@ -0,0 +1,2 @@
1
+ require File.join( File.dirname( __FILE__ ), 'extensions/string' )
2
+ require File.join( File.dirname( __FILE__ ), 'extensions/hash' )
@@ -0,0 +1,7 @@
1
+ class Hash
2
+
3
+ def stringify_keys!
4
+ self.replace( inject({}) { |hsh, (k, v)| hsh[k.to_sym]=v; hsh } )
5
+ end
6
+
7
+ end
@@ -0,0 +1,11 @@
1
+ class String
2
+
3
+ def camelize
4
+ self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
5
+ end
6
+
7
+ def classify
8
+ self.sub(/.*\./, '').camelize
9
+ end
10
+
11
+ end
@@ -0,0 +1,28 @@
1
+ require 'syslog'
2
+
3
+ module Logger
4
+
5
+ @@app_name = "rpolicy"
6
+ @@log_opts = (Syslog::LOG_PID | Syslog::LOG_CONS)
7
+ @@facility = Syslog::LOG_MAIL
8
+ Syslog.open( @@app_name, @@log_opts, @@facility )
9
+
10
+ def self.error( msg )
11
+ Syslog.err( msg )
12
+ end
13
+
14
+ def self.info( msg )
15
+ Syslog.info( msg )
16
+ end
17
+
18
+ def self.debug( msg )
19
+ Syslog.debug( msg )
20
+ end
21
+
22
+ def self.warn( msg )
23
+ Syslog.warning( msg )
24
+ end
25
+
26
+
27
+ end
28
+
@@ -0,0 +1,13 @@
1
+ module PostPolicy
2
+ module ACL
3
+
4
+ class Recipient < Base
5
+
6
+ def match?( args )
7
+ datasource.exists? args[:recipient]
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module PostPolicy
2
+ module ACL
3
+
4
+ class Sender < Base
5
+
6
+ def match?( args )
7
+ datasource.exists? args[:sender]
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module PostPolicy
2
+
3
+ class Access
4
+
5
+ def initialize( acls, action )
6
+ @acls = acls
7
+ @action = action
8
+ end
9
+
10
+ def match?( args )
11
+ @acls.all? { |acl| acl.match?( args ) }
12
+ end
13
+
14
+ def action
15
+ @action
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,19 @@
1
+ module PostPolicy
2
+ module ACL
3
+
4
+ class Base
5
+
6
+ attr_reader :datasource
7
+
8
+ def initialize( datasource )
9
+ @datasource = datasource
10
+ end
11
+
12
+ def match?( args )
13
+ :dunno
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module PostPolicy
2
+ module DataSource
3
+
4
+ class Base
5
+
6
+ def initialize( values = [] )
7
+ @values = values
8
+ end
9
+
10
+ def exists?( object )
11
+ @values.include?( object )
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module PostPolicy
2
+ module DataSource
3
+
4
+ class File < Base
5
+
6
+ def initialize( filename )
7
+ @values = []
8
+ ::File.open(filename).each_line { |line| @values << line.chomp }
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module PostPolicy
2
+ module DataSource
3
+
4
+ class Regex < Base
5
+
6
+ def initialize( regex )
7
+ @regex = regex
8
+ end
9
+
10
+ def exists?( value )
11
+ !!(value =~ @regex )
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ module PostPolicy
2
+ module DataSource
3
+
4
+ class Sql < Base
5
+
6
+ def initialize( query )
7
+ @query = query
8
+ end
9
+
10
+ def exists?( value )
11
+ result = false
12
+ connection do |conn|
13
+ command = conn.create_command( @query )
14
+ reader = command.execute_reader
15
+ while reader.next!
16
+ if reader.values[0] && reader.values[0] == value
17
+ result = true
18
+ break
19
+ end
20
+ end
21
+ end
22
+ result
23
+ end
24
+
25
+ protected
26
+ def connection( &block )
27
+ conn = DataObjects::Connection.new( Config::Db.dbi_params )
28
+ block.call( conn )
29
+ conn.close
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ module PostPolicy
2
+ module DataSource
3
+
4
+ class Value < Base
5
+
6
+ def initialize( values )
7
+ @values = [values].flatten #make it always an array
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,100 @@
1
+ require 'set'
2
+
3
+ module PostPolicy
4
+ class Protocol
5
+
6
+ PROTOCOLS = %w(ESMTP SMTP)
7
+ STATES = %w(CONNECT EHLO HELO MAIL RCPT DATA END-OF-MESSAGE VRFY ETRN)
8
+
9
+ TERMINATOR = "\n\n"
10
+
11
+ attr_reader :attributes
12
+
13
+ @@required_request_attributes = [:request, :protocol_state, :protocol_name, :helo_name, :queue_id, :sender, :recipient, :recipient_count, :client_address, :client_name, :reverse_client_name, :instance].to_set # Postfix 2.1 and later
14
+
15
+ @@request_attributes = @@required_request_attributes +
16
+ [:sasl_method, :sasl_username, :sasl_sender, :size, :ccert_subject, :ccert_issuer, :ccert_fingerprint, # Postifx 2.2 and later
17
+ :encryption_protocol, :encryption_cipher, :encryption_keysize, :etrn_domain, # Postifx 2.3 and later
18
+ :stress].to_set # Postifx 2.5 and later
19
+
20
+ def initialize
21
+ @attributes = {}
22
+ $stdout.sync = true
23
+ @buffer = []
24
+ end
25
+
26
+ def start!
27
+ while line = gets do
28
+ receive_line( line.chomp )
29
+ end
30
+ end
31
+
32
+ def receive_line( line )
33
+ unless line.empty?
34
+ @buffer << line
35
+ else
36
+ Logger.info @buffer.inspect if VERBOSE
37
+ parse_request
38
+ @buffer.clear
39
+ end
40
+ end
41
+
42
+ def response( action )
43
+ puts "action=#{action}#{TERMINATOR}" unless POST_POLICY_ENV == 'test'
44
+ end
45
+
46
+ protected
47
+ def parse_request
48
+ @buffer.each do |line|
49
+ key, value = line.split( '=' )
50
+ @attributes[key.to_sym] = value
51
+ end
52
+ return false if( sanitize_arguments == false || validate_arguments == false )
53
+ run_actions
54
+ end
55
+
56
+ def run_actions
57
+ am = PostPolicy::AccessManager.new
58
+ am.callback do |args|
59
+ am.check( args ) do |action|
60
+ Logger.debug "Returning response #{action}" if VERBOSE
61
+ response( action )
62
+ end
63
+ end
64
+ am.set_deferred_status :succeeded, @attributes
65
+ end
66
+
67
+ def sanitize_arguments
68
+ missing_attributes = @@required_request_attributes - @attributes.keys.to_set
69
+ unless missing_attributes.empty?
70
+ Logger.err "missing #{missing_attributes.to_a.join( ',' )}"
71
+ return false
72
+ end
73
+ unknown_attributes = @attributes.keys.to_set - @@request_attributes
74
+ unless unknown_attributes.empty?
75
+ Logger.warn( 'Unknown attributes in policy request %s' % unknown_attributes.to_a.join( ',' ) ) if VERBOSE
76
+ end
77
+ unknown_attributes.each { |attr| @attributes.delete( attr ) }
78
+ true
79
+ end
80
+
81
+
82
+ def validate_arguments
83
+ unless @attributes[:request] == "smtpd_access_policy"
84
+ Logger.err( 'only smtpd_access_policy request allowed' )
85
+ return false
86
+ end
87
+ unless PROTOCOLS.include?( @attributes[:protocol_name] )
88
+ Logger.err( "only #{PROTOCOLS.join(',')} protocols allowed" )
89
+ return false
90
+ end
91
+ unless STATES.include?( @attributes[:protocol_state] )
92
+ Logger.err( "only #{STATES.join(',')} states allowed" )
93
+ return false
94
+ end
95
+
96
+ true
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,67 @@
1
+ module PostPolicy
2
+
3
+ # rule do
4
+ # sender { format.value "michal.lomnicki@gmail.com" }
5
+ # recipient { format.regex /lomnicki.com.pl$/ }
6
+ # action "REJECT"
7
+ # end
8
+ #
9
+ class Rule
10
+ class Format
11
+
12
+ class << self
13
+
14
+ def format
15
+ self
16
+ end
17
+
18
+ def value( val )
19
+ DataSource::Value.new( val )
20
+ end
21
+
22
+ def regex( val )
23
+ DataSource::Regex.new( val )
24
+ end
25
+
26
+ def file( val )
27
+ DataSource::File.new( val )
28
+ end
29
+
30
+ def sql( val )
31
+ DataSource::Sql.new( val )
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ def initialize( &block )
39
+ instance_eval &block
40
+ end
41
+
42
+ def self.rule( &block )
43
+ AccessManager << new( &block ).to_access
44
+ end
45
+
46
+ def sender( &block )
47
+ @rule_sender = ACL::Sender.new( Format.class_eval( &block ) )
48
+ end
49
+
50
+ def recipient( &block )
51
+ @rule_recipient = ACL::Recipient.new( Format.class_eval( &block ) )
52
+ end
53
+
54
+ def action( value )
55
+ @rule_action = value
56
+ end
57
+
58
+ def to_access
59
+ Access.new( [@rule_sender, @rule_recipient].compact, @rule_action )
60
+ end
61
+
62
+ end
63
+
64
+
65
+ end
66
+
67
+
@@ -0,0 +1,24 @@
1
+ # the simplest PostPolicy server using DSL
2
+ #
3
+ # require 'post_policy'
4
+ #
5
+ # PostPolicy::Server.run do
6
+ #
7
+ # rule do
8
+ # sender { format.value "michal.lomnicki@gmail.com" }
9
+ # action "REJECT"
10
+ # end
11
+ #
12
+ # end
13
+ module PostPolicy
14
+
15
+ class Server
16
+
17
+ def self.run( &block )
18
+ Rule.class_eval( &block )
19
+ PostPolicy::Protocol.new.start!
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,9 @@
1
+ module PostPolicy
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MAJOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,85 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{post_policy}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Micha\305\202 \305\201omnicki"]
12
+ s.date = %q{2009-10-12}
13
+ s.default_executable = %q{postpolicy.rb}
14
+ s.description = %q{PostPolicy uses ACL system, which allow administrators to create rules based on mail source.}
15
+ s.email = %q{michal@lomnicki.com.pl}
16
+ s.executables = ["postpolicy.rb"]
17
+ s.extra_rdoc_files = [
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "CHANGELOG",
23
+ "MIT-LICENSE",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "bin/postpolicy.rb",
28
+ "dbconfig.yml",
29
+ "lib/postpolicy.rb",
30
+ "lib/postpolicy/access_manager.rb",
31
+ "lib/postpolicy/config.rb",
32
+ "lib/postpolicy/extensions.rb",
33
+ "lib/postpolicy/extensions/hash.rb",
34
+ "lib/postpolicy/extensions/string.rb",
35
+ "lib/postpolicy/logger.rb",
36
+ "lib/postpolicy/plugins/acl/recipient.rb",
37
+ "lib/postpolicy/plugins/acl/sender.rb",
38
+ "lib/postpolicy/plugins/base/access.rb",
39
+ "lib/postpolicy/plugins/base/acl.rb",
40
+ "lib/postpolicy/plugins/base/datasource.rb",
41
+ "lib/postpolicy/plugins/datasource/file.rb",
42
+ "lib/postpolicy/plugins/datasource/regex.rb",
43
+ "lib/postpolicy/plugins/datasource/sql.rb",
44
+ "lib/postpolicy/plugins/datasource/value.rb",
45
+ "lib/postpolicy/protocol.rb",
46
+ "lib/postpolicy/rule.rb",
47
+ "lib/postpolicy/server.rb",
48
+ "lib/postpolicy/version.rb",
49
+ "post_policy.gemspec",
50
+ "postpolicy.yml",
51
+ "spec/access_manager_spec.rb",
52
+ "spec/acl_spec.rb",
53
+ "spec/config_spec.rb",
54
+ "spec/datasource_spec.rb",
55
+ "spec/format_spec.rb",
56
+ "spec/protocol_spec.rb",
57
+ "spec/rule_spec.rb",
58
+ "spec/test_helper.rb"
59
+ ]
60
+ s.homepage = %q{http://github.com/mlomnicki/post_policy}
61
+ s.rdoc_options = ["--charset=UTF-8"]
62
+ s.require_paths = ["lib"]
63
+ s.rubygems_version = %q{1.3.5}
64
+ s.summary = %q{Postfix Policy Server in Ruby}
65
+ s.test_files = [
66
+ "spec/acl_spec.rb",
67
+ "spec/access_manager_spec.rb",
68
+ "spec/format_spec.rb",
69
+ "spec/datasource_spec.rb",
70
+ "spec/protocol_spec.rb",
71
+ "spec/test_helper.rb",
72
+ "spec/rule_spec.rb",
73
+ "spec/config_spec.rb"
74
+ ]
75
+
76
+ if s.respond_to? :specification_version then
77
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
78
+ s.specification_version = 3
79
+
80
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
81
+ else
82
+ end
83
+ else
84
+ end
85
+ end
@@ -0,0 +1,14 @@
1
+ acl:
2
+ foo:
3
+ sender: michal@lomnicki.com.pl
4
+ bar:
5
+ #sender: file:///home/michal/test/filetest
6
+ recipient: sql://"SELECT * FROM users WHERE login = '%l'"
7
+
8
+ action:
9
+ pass: REJECT
10
+
11
+ access:
12
+ pass: foo
13
+
14
+ database: "dbconfig.yml"
@@ -0,0 +1,47 @@
1
+ require 'test_helper'
2
+
3
+ CONFIG = {
4
+ "acl" => {
5
+ "michal-lomnicki" => {
6
+ "sender" => "michal@lomnicki.com.pl"
7
+ },
8
+ "foo-to-bar" => {
9
+ "sender" => "foo",
10
+ "recipient" => "bar"
11
+ }
12
+ },
13
+
14
+ "action" => { "reject" => "REJECT", "defer" => "DEFER_IF_PERMIT" },
15
+ "access" => {
16
+ "reject" => "michal-lomnicki",
17
+ "defer" => "foo-to-bar"
18
+ }
19
+ }
20
+
21
+ describe PostPolicy::AccessManager do
22
+
23
+ before(:all) do
24
+ PostPolicy::Config.load( CONFIG )
25
+ @am = PostPolicy::AccessManager.new
26
+ end
27
+
28
+ it "should reject michal@lomnicki.com.pl" do
29
+ @am.check( { :sender => "michal@lomnicki.com.pl" } ) do |action|
30
+ action.should == "REJECT"
31
+ end
32
+ end
33
+
34
+ it "should defer foo to bar" do
35
+ @am.check( { :sender => "foo", :recipient => "bar" } ) do |action|
36
+ action.should == "DEFER_IF_PERMIT"
37
+ end
38
+ end
39
+
40
+ # on any other acl default action should be given
41
+ it "should return default action on bar to foo" do
42
+ @am.check( { :sender => "bar", :recipient => "foo" } ) do |action|
43
+ action.should == PostPolicy::AccessManager::DEFAULT_ACTION
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,21 @@
1
+ require 'test_helper'
2
+
3
+ describe PostPolicy::ACL::Sender do
4
+
5
+ it "should match given sender" do
6
+ ds = PostPolicy::DataSource::Value.new( ATTRS[:sender] )
7
+ sender_acl = PostPolicy::ACL::Sender.new( ds )
8
+ sender_acl.match?( ATTRS ).should == true
9
+ end
10
+
11
+ end
12
+
13
+ describe PostPolicy::ACL::Recipient do
14
+
15
+ it "should match given recipient" do
16
+ ds = PostPolicy::DataSource::Value.new( ATTRS[:recipient] )
17
+ recipient_acl = PostPolicy::ACL::Recipient.new( ds )
18
+ recipient_acl.match?( ATTRS ).should == true
19
+ end
20
+
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ describe PostPolicy::Config::Db do
4
+
5
+ it "should load config from file and build dbi_params" do
6
+ filename = '/tmp/dbconfig'
7
+ config = { :host => 'localhost',
8
+ :database => 'postpolicy',
9
+ :user => 'foo',
10
+ :password => 'secret',
11
+ :port => 5432,
12
+ :driver => 'postgres' }
13
+
14
+ File.open( filename, 'w' ) do |f|
15
+ f.puts config.to_yaml
16
+ end
17
+
18
+ PostPolicy::Config::Db.load( filename )
19
+ PostPolicy::Config::Db.dbconfig.should == config
20
+ dbi_params = "#{config[:driver]}://#{config[:user]}:#{config[:password]}@#{config[:host]}:#{config[:port]}/#{config[:database]}"
21
+ PostPolicy::Config::Db.dbi_params.should == dbi_params
22
+
23
+ FileUtils.rm( filename )
24
+ end
25
+
26
+ end
@@ -0,0 +1,61 @@
1
+ require 'test_helper'
2
+
3
+ GOOD_VALUES = ["michal@lomnicki.com.pl", "foo@example.com", "bar@baz.com"]
4
+ BAD_VALUES = ["michal@bar.com", "bar@baz.net", "foo@bar.baz"]
5
+
6
+ describe PostPolicy::DataSource::Value do
7
+
8
+ it "should allow passing one or multiple values" do
9
+ ds = PostPolicy::DataSource::Value.new( 1 )
10
+ ds.exists?( 1 ).should == true
11
+ ds = PostPolicy::DataSource::Value.new( [1,2] )
12
+ ds.exists?( 1 ).should == true
13
+ ds.exists?( 2 ).should == true
14
+ end
15
+
16
+ it "should match initialized dss" do
17
+ ds = PostPolicy::DataSource::Value.new( GOOD_VALUES )
18
+ ds.exists?( GOOD_VALUES.first ).should == true
19
+ ds.exists?( GOOD_VALUES.last ).should == true
20
+ ds.exists?( BAD_VALUES.first ).should == false
21
+ ds.exists?( BAD_VALUES.last ).should == false
22
+ end
23
+
24
+ end
25
+
26
+ describe PostPolicy::DataSource::File do
27
+
28
+ it "should read from file" do
29
+ filename = '/tmp/file_datasource'
30
+ File.open( filename, 'w' ) do |f|
31
+ GOOD_VALUES.each { |value| f.puts value }
32
+ end
33
+ ds = PostPolicy::DataSource::File.new( filename )
34
+ GOOD_VALUES.each do |value|
35
+ ds.exists?( value ).should == true
36
+ end
37
+ BAD_VALUES.each do |value|
38
+ ds.exists?( value ).should == false
39
+ end
40
+ FileUtils.rm( filename )
41
+ end
42
+
43
+ end
44
+
45
+ describe PostPolicy::DataSource::Regex do
46
+
47
+ it "should match regex" do
48
+ ds = PostPolicy::DataSource::Regex.new( /^michal@.*\.pl$/ )
49
+ ds.exists?( "michal@lomnicki.com.pl" ).should == true
50
+ ds.exists?( "foo@lomnicki.com.pl" ).should == false
51
+ end
52
+
53
+ end
54
+
55
+ describe PostPolicy::DataSource::Sql do
56
+
57
+ it "should check in db" do
58
+ ds = PostPolicy::DataSource::Sql.new( "SELECT id from test" )
59
+ end
60
+
61
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ describe PostPolicy::Rule::Format do
4
+
5
+ before(:each) do
6
+ @klass = PostPolicy::Rule::Format
7
+ end
8
+
9
+ it "should return DataSource::Value for format.value" do
10
+ value = "michal.lomnicki@gmail.com"
11
+ ds = @klass.format.value( value )
12
+ ds.class.should == PostPolicy::DataSource::Value
13
+ ds.exists?( value ).should == true
14
+ end
15
+
16
+ it "should return DataSource::Regex for format.regex" do
17
+ value = /gmail.com$/
18
+ ds = @klass.format.regex( value )
19
+ ds.class.should == PostPolicy::DataSource::Regex
20
+ ds.exists?( "michal.lomnicki@gmail.com" ).should == true
21
+ end
22
+
23
+ it "should return DataSource::File for format.file" do
24
+ filename = '/tmp/file_datasource'
25
+ value = "michal.lomnicki@gmail.com"
26
+ File.open( filename, 'w' ) do |f|
27
+ f.puts value
28
+ end
29
+ ds = @klass.format.file( filename )
30
+ ds.class.should == PostPolicy::DataSource::File
31
+ ds.exists?( value ).should == true
32
+ FileUtils.rm( filename )
33
+ end
34
+
35
+ end
@@ -0,0 +1,42 @@
1
+ require 'stringio'
2
+ require 'test_helper'
3
+
4
+ describe PostPolicy::Protocol do
5
+
6
+ before(:each) do
7
+ @protocol = PostPolicy::Protocol.new
8
+ end
9
+
10
+ it "should parse attributes" do
11
+ form_string( ATTRS ).each_line { |l| @protocol.receive_line( l.strip ) }
12
+ @protocol.attributes.should == ATTRS
13
+ end
14
+
15
+ it "should discard unknown attributes" do
16
+ ATTRS.merge( :foo => 'bar', :bar => 'baz' ).each_pair { |k,v| @protocol.receive_line( [k,v].join( '=' ) ) }
17
+ form_string( ATTRS ).each_line { |l| @protocol.receive_line( l.strip ) }
18
+ @protocol.attributes.should == ATTRS
19
+ end
20
+
21
+ #it "should return false on missing attributes" do
22
+ # ATTRS.reject { |k,v| k == :protocol_name }.each_pair { |k,v| @protocol.receive_line( [k,v].join( '=' ) ) }
23
+ # form_string( ATTRS ).each_line { |l| @protocol.receive_line( l.strip ) }
24
+ # @protocol.receive.should == false
25
+ #end
26
+
27
+ #it "should validate arguments" do
28
+ # ATTRS.merge( :request => "bar" ).each_pair { |k,v| @protocol.receive_line( [k,v].join( '=' ) ) }
29
+ # form_string( ATTRS ).each_line { |l| @protocol.receive_line( l.strip ) }
30
+ # @protocol.receive.should == false
31
+ #end
32
+
33
+ #it "should terminate response" do
34
+ # @protocol.response("dunno").should == "action=dunno\n\n"
35
+ #end
36
+
37
+ protected
38
+ def form_string( args )
39
+ StringIO.new( args.each.inject("") { |str, kv| str << (kv.join('=') + "\n") } << "\n" )
40
+ end
41
+
42
+ end
@@ -0,0 +1,32 @@
1
+ require 'test_helper'
2
+
3
+ describe PostPolicy::Rule do
4
+
5
+ before(:all) do
6
+ SENDER = "foo@example.com"
7
+ RECIPIENT = "bar@example.com"
8
+ ACTION = "REJECT"
9
+ end
10
+
11
+ before(:each) do
12
+ @rule = PostPolicy::Rule.new do
13
+ sender { format.value SENDER }
14
+ recipient { format.value RECIPIENT }
15
+ action ACTION
16
+ end
17
+
18
+ end
19
+
20
+ it "should respond_to to_access" do
21
+ @rule.should respond_to( :to_access )
22
+ end
23
+
24
+ it "should match given arguments after converting to access" do
25
+ @rule.to_access.match?( :sender => SENDER, :recipient => RECIPIENT ).should == true
26
+ end
27
+
28
+ it "should return given action after converting to access" do
29
+ @rule.to_access.action.should == ACTION
30
+ end
31
+
32
+ end
@@ -0,0 +1,27 @@
1
+ $LOAD_PATH.unshift File.join( File.dirname( __FILE__ ), '../lib' )
2
+ require 'postpolicy'
3
+
4
+ POST_POLICY_ENV = ENV['POST_POLICY_ENV'] || "test"
5
+
6
+ ATTRS = {
7
+ :request => "smtpd_access_policy",
8
+ :protocol_state => "RCPT",
9
+ :protocol_name => "SMTP",
10
+ :helo_name => "some.domain.tld",
11
+ :queue_id => "8045F2AB23",
12
+ :sender => "foo@bar.tld",
13
+ :recipient => "bar@foo.tld",
14
+ :recipient_count => "0",
15
+ :client_address => "1.2.3.4",
16
+ :client_name => "another.domain.tld",
17
+ :reverse_client_name => "another.domain.tld",
18
+ :instance => "123.456.7"
19
+ }
20
+
21
+ Object.send(:remove_const, :Logger)
22
+
23
+ module Logger
24
+ def self.method_missing(symbol, *args)
25
+ #simpy do nothing
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: post_policy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - "Micha\xC5\x82 \xC5\x81omnicki"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-12 00:00:00 +02:00
13
+ default_executable: postpolicy.rb
14
+ dependencies: []
15
+
16
+ description: PostPolicy uses ACL system, which allow administrators to create rules based on mail source.
17
+ email: michal@lomnicki.com.pl
18
+ executables:
19
+ - postpolicy.rb
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - .gitignore
26
+ - CHANGELOG
27
+ - MIT-LICENSE
28
+ - README.rdoc
29
+ - Rakefile
30
+ - VERSION
31
+ - bin/postpolicy.rb
32
+ - dbconfig.yml
33
+ - lib/postpolicy.rb
34
+ - lib/postpolicy/access_manager.rb
35
+ - lib/postpolicy/config.rb
36
+ - lib/postpolicy/extensions.rb
37
+ - lib/postpolicy/extensions/hash.rb
38
+ - lib/postpolicy/extensions/string.rb
39
+ - lib/postpolicy/logger.rb
40
+ - lib/postpolicy/plugins/acl/recipient.rb
41
+ - lib/postpolicy/plugins/acl/sender.rb
42
+ - lib/postpolicy/plugins/base/access.rb
43
+ - lib/postpolicy/plugins/base/acl.rb
44
+ - lib/postpolicy/plugins/base/datasource.rb
45
+ - lib/postpolicy/plugins/datasource/file.rb
46
+ - lib/postpolicy/plugins/datasource/regex.rb
47
+ - lib/postpolicy/plugins/datasource/sql.rb
48
+ - lib/postpolicy/plugins/datasource/value.rb
49
+ - lib/postpolicy/protocol.rb
50
+ - lib/postpolicy/rule.rb
51
+ - lib/postpolicy/server.rb
52
+ - lib/postpolicy/version.rb
53
+ - post_policy.gemspec
54
+ - postpolicy.yml
55
+ - spec/access_manager_spec.rb
56
+ - spec/acl_spec.rb
57
+ - spec/config_spec.rb
58
+ - spec/datasource_spec.rb
59
+ - spec/format_spec.rb
60
+ - spec/protocol_spec.rb
61
+ - spec/rule_spec.rb
62
+ - spec/test_helper.rb
63
+ has_rdoc: true
64
+ homepage: http://github.com/mlomnicki/post_policy
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options:
69
+ - --charset=UTF-8
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
84
+ requirements: []
85
+
86
+ rubyforge_project:
87
+ rubygems_version: 1.3.5
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Postfix Policy Server in Ruby
91
+ test_files:
92
+ - spec/acl_spec.rb
93
+ - spec/access_manager_spec.rb
94
+ - spec/format_spec.rb
95
+ - spec/datasource_spec.rb
96
+ - spec/protocol_spec.rb
97
+ - spec/test_helper.rb
98
+ - spec/rule_spec.rb
99
+ - spec/config_spec.rb