From 84ff5d959d1b20c983751477399f2a7941395e50 Mon Sep 17 00:00:00 2001 From: "R.I.Pienaar" Date: Tue, 19 Mar 2013 16:39:33 +0000 Subject: [PATCH] 19210 - Publish the actionpolicy simple rpc authorization plugin Refactored, added tests and updated docs --- README.md | 91 +++- Rakefile | 20 + puppet.policy | 2 + spec/actionpolicy/actionpolicy_spec.rb | 510 +++++++++++++++++++++++ spec/actionpolicy/fixtures/default_allow | 1 + spec/actionpolicy/fixtures/default_deny | 1 + spec/actionpolicy/fixtures/example1 | 2 + spec/actionpolicy/fixtures/example10 | 2 + spec/actionpolicy/fixtures/example11 | 2 + spec/actionpolicy/fixtures/example12 | 2 + spec/actionpolicy/fixtures/example13 | 2 + spec/actionpolicy/fixtures/example14 | 2 + spec/actionpolicy/fixtures/example15 | 5 + spec/actionpolicy/fixtures/example2 | 2 + spec/actionpolicy/fixtures/example3 | 2 + spec/actionpolicy/fixtures/example4 | 2 + spec/actionpolicy/fixtures/example5 | 2 + spec/actionpolicy/fixtures/example6 | 2 + spec/actionpolicy/fixtures/example7 | 2 + spec/actionpolicy/fixtures/example8 | 2 + spec/actionpolicy/fixtures/example9 | 2 + spec/spec.opts | 1 + spec/spec_helper.rb | 18 + util/actionpolicy.ddl | 9 + util/actionpolicy.rb | 219 ++++++++++ 25 files changed, 903 insertions(+), 2 deletions(-) create mode 100644 Rakefile create mode 100644 puppet.policy create mode 100644 spec/actionpolicy/actionpolicy_spec.rb create mode 100644 spec/actionpolicy/fixtures/default_allow create mode 100644 spec/actionpolicy/fixtures/default_deny create mode 100644 spec/actionpolicy/fixtures/example1 create mode 100644 spec/actionpolicy/fixtures/example10 create mode 100644 spec/actionpolicy/fixtures/example11 create mode 100644 spec/actionpolicy/fixtures/example12 create mode 100644 spec/actionpolicy/fixtures/example13 create mode 100644 spec/actionpolicy/fixtures/example14 create mode 100644 spec/actionpolicy/fixtures/example15 create mode 100644 spec/actionpolicy/fixtures/example2 create mode 100644 spec/actionpolicy/fixtures/example3 create mode 100644 spec/actionpolicy/fixtures/example4 create mode 100644 spec/actionpolicy/fixtures/example5 create mode 100644 spec/actionpolicy/fixtures/example6 create mode 100644 spec/actionpolicy/fixtures/example7 create mode 100644 spec/actionpolicy/fixtures/example8 create mode 100644 spec/actionpolicy/fixtures/example9 create mode 100644 spec/spec.opts create mode 100644 spec/spec_helper.rb create mode 100644 util/actionpolicy.ddl create mode 100644 util/actionpolicy.rb diff --git a/README.md b/README.md index fc0a90e..edf8b89 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,91 @@ -mcollective-actionpolicy-auth +Action Policy Authorization Plugin ============================= -MCollective Authorization plugin allowing fine grained ACLs \ No newline at end of file +This is a plugin that provides fine grained action level authorization for agents. + +Installation +============================= + + * Follow the [basic plugin install guide](http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/InstalingPlugins) by placing actionpolicy.rb + and actionpolicy.ddl in the util directory. + +Note that it is not currently possible to use the 'mco plugin package' command to package this plugin. + +Configuration +============================= + +There are three configuration options for the actionpolicy plugin + + * allow_unconfigured - allow requests to agents that do not have policy files configured + * enable_default - enables a default policy file + * default_name - the name of the default policy file + +General authentication configuration options can also be set in the config file. + + # Enables system wide rpc authorization + rpcauthorization = 1 + # Sets the authorization provider to use the actionpolicy plugin + rpcauthprovider = action_policy + +Enabling a default policy + + plugin.actionpolicy.enable_default = 1 + plugin.actionpolicy.default_name = default + +This allows you to create a policy file called default.policy which will be used unless a specific policy file exists. + +Usage +============================= + +Policies are defined in files like /policies/.policy + +Example: Puppet agent policy file + + policy default deny + allow uid=500 * * * + allow uid=600 * customer=acme acme::devserver + allow uid=600 enable disable status customer=acme * + allow uid=700 restart (puppet().enabled=false and environment=production) or environment=development + +The above policy can be described as: + + * allow unix user id 500 to do all actions on all servers. + * allow unix user id 600 to do all actions on machines with the fact customer=acme and the config class acme::devserver + * allow unix user id 600 to do enable, disable and status on all other machines with fact customer=acme + * allow unix user id 700 to restart services at any time in development but in production only when Puppet has been disabled + * Everything else gets denied + +The format of the userid will depend on your security plugin, other plugins might have a certificate name as caller it. + +Like with actions you can space separate facts and config classes too which means all facts of classes listed has to be present on the system. + +The last line in the example uses the compound statement language to do matching on facts and classes and allows any data plugin to be used. +This requires at least MCollective 2.2.x. When using data plugins in action policies you should avoid using slow ones as this will impact +the response times of agents and impact the client waiting time etc. + +Using it in a specific Agent +============================= + +You can now activate it in your agents: + + module MCollective::Agent + class Service :test diff --git a/puppet.policy b/puppet.policy new file mode 100644 index 0000000..e1e66db --- /dev/null +++ b/puppet.policy @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 status enable disable country=uk apache diff --git a/spec/actionpolicy/actionpolicy_spec.rb b/spec/actionpolicy/actionpolicy_spec.rb new file mode 100644 index 0000000..16ee7f4 --- /dev/null +++ b/spec/actionpolicy/actionpolicy_spec.rb @@ -0,0 +1,510 @@ +#!/bin/env rspec + +require 'spec_helper' +require File.join(File.dirname(__FILE__), '../../', 'util', 'actionpolicy.rb') + +module MCollective + module Util + describe ActionPolicy do + let(:request) do + request = mock + request.stubs(:agent).returns('rspec_agent') + request.stubs(:caller).returns('rspec_caller') + request.stubs(:action).returns('rspec_action') + request + end + + let(:config) do + config = mock + config.stubs(:configdir).returns('/rspecdir') + config.stubs(:pluginconf).returns({}) + config + end + + let(:actionpolicy) { ActionPolicy.new(request) } + + before do + Config.stubs(:instance).returns(config) + @fixtures_dir = File.join(File.dirname(__FILE__), 'fixtures') + end + + describe '#authorize' do + it 'should create a new ActionPolicy object and call #authorize_request' do + actionpolicy.expects(:authorize_request) + ActionPolicy.expects(:new).returns(actionpolicy) + ActionPolicy.authorize(request) + end + end + + describe '#initialize' do + it 'should set the default values' do + actionpolicy.config.should == config + actionpolicy.agent.should == 'rspec_agent' + actionpolicy.caller.should == 'rspec_caller' + actionpolicy.action.should == 'rspec_action' + actionpolicy.allow_unconfigured.should == false + actionpolicy.configdir.should == '/rspecdir' + end + + it 'should set allow_unconfigured if set in config file' do + config.stubs(:pluginconf).returns({'actionpolicy.allow_unconfigured' => '1'}) + result = ActionPolicy.new(request) + result.allow_unconfigured.should == true + end + end + + describe '#authorize_request' do + before do + Log.stubs(:debug) + end + + it 'should deny the request if policy file does not exist and allow_unconfigured is false' do + ActionPolicy.any_instance.expects(:lookup_policy_file).returns(nil) + + expect{ + actionpolicy.authorize_request + }.to raise_error RPCAborted + end + + it 'should return true if policy file does not exist but allow_unconfigured is true' do + ActionPolicy.any_instance.expects(:lookup_policy_file).returns(nil) + config.stubs(:pluginconf).returns({'actionpolicy.allow_unconfigured' => 'y'}) + + actionpolicy.authorize_request.should be_true + end + + it 'should parse the policy file if it exists' do + ActionPolicy.any_instance.expects(:lookup_policy_file).returns('/rspecdir/policyfile') + ActionPolicy.any_instance.expects(:parse_policy_file).with('/rspecdir/policyfile') + actionpolicy.authorize_request + end + end + + describe '#parse_policy_file' do + + before do + Log.stubs(:debug) + end + + it 'should deny the request if allow_unconfigured is false and no lines match' do + File.expects(:read).with('policyfile').returns('') + + expect{ + actionpolicy.parse_policy_file('policyfile') + }.to raise_error RPCAborted + end + + it 'should skip comment lines' do + File.expects(:read).with('policyfile').returns('#') + + expect{ + actionpolicy.parse_policy_file('policyfile') + }.to raise_error RPCAborted + end + + # Fixtures + + it 'should parse the default alllow policy' do + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'default_allow')).should be_true + end + + it 'should parse the default deny policy' do + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'default_deny')) + }.to raise_error RPCAborted + end + + # Example fixtures + + it 'should parse example1 correctly' do + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example1')).should be_true + end + + it 'should parse example2 correctly' do + request.stubs(:caller).returns('uid=500') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example2')).should be_true + + request.stubs(:caller).returns('uid=501') + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example2')) + }.to raise_error RPCAborted + + end + + it 'should parse example3 correctly' do + request.stubs(:action).returns('rspec') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example3')).should be_true + + request.stubs(:action).returns('notrspec') + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example3')) + }.to raise_error RPCAborted + + end + + it 'should parse example4 correctly' do + Util.stubs(:get_fact).with('foo').returns('bar') + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example4')).should be_true + + Util.stubs(:get_fact).with('foo').returns('notbar') + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example4')) + }.to raise_error RPCAborted + + end + + it 'should parse example5 correctly' do + Util.stubs(:has_cf_class?).with('rspec').returns(true) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example5')).should be_true + + Util.stubs(:has_cf_class?).with('rspec').returns(false) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example5')) + }.to raise_error RPCAborted + + end + + it 'should parse example6 correctly' do + request.stubs(:caller).returns('uid=500') + request.stubs(:action).returns('rspec') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example6')).should be_true + + request.stubs(:caller).returns('uid=501') + request.stubs(:action).returns('notrspec') + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example6')) + }.to raise_error RPCAborted + + end + + it 'should parse example7 correctly' do + request.stubs(:caller).returns('uid=500') + Util.stubs(:get_fact).with('foo').returns('bar') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example7')).should be_true + + request.stubs(:caller).returns('uid=501') + Util.stubs(:get_fact).with('foo').returns('notbar') + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example7')) + }.to raise_error RPCAborted + + end + + it 'should parse example8 correctly' do + request.stubs(:caller).returns('uid=500') + Util.stubs(:has_cf_class?).with('rspec').returns(true) + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example8')).should be_true + + Util.stubs(:has_cf_class?).with('rspec').returns(false) + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example8')) + }.to raise_error RPCAborted + + end + + it 'should parse example9 correctly' do + request.stubs(:caller).returns('uid=500') + request.stubs(:action).returns('rspec') + Util.stubs(:get_fact).with('foo').returns('bar') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example9')).should be_true + + request.stubs(:caller).returns('uid=501') + request.stubs(:action).returns('notrspec') + Util.stubs(:get_fact).with('foo').returns('notbar') + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example9')) + }.to raise_error RPCAborted + + end + + it 'should parse example10 correctly' do + request.stubs(:caller).returns('uid=500') + request.stubs(:action).returns('rspec') + Util.stubs(:has_cf_class?).with('rspec').returns(true) + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10')).should be_true + + request.stubs(:caller).returns('uid=501') + request.stubs(:action).returns('notrspec') + Util.stubs(:has_cf_class?).with('rspec').returns(false) + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10')) + }.to raise_error RPCAborted + + + + end + + it 'should parse example11 correctly' do + request.stubs(:caller).returns('uid=500') + request.stubs(:action).returns('rspec') + Util.stubs(:has_cf_class?).with('rspec').returns(true) + Util.stubs(:get_fact).with('foo').returns('bar') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10')).should be_true + + request.stubs(:caller).returns('uid=501') + request.stubs(:action).returns('notrspec') + Util.stubs(:has_cf_class?).with('rspec').returns(false) + Util.stubs(:get_fact).with('foo').returns('notbar') + actionpolicy = ActionPolicy.new(request) + expect{ + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example10')) + }.to raise_error RPCAborted + end + + it 'should parse example12 correctly' do + request.stubs(:caller).returns('uid=500') + request.stubs(:action).returns('rspec') + Util.stubs(:has_cf_class?).with('rspec').returns(true) + Util.stubs(:get_fact).with('foo').returns('bar') + Util.stubs(:get_fact).with('bar').returns('foo') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example12')).should be_true + end + + it 'should parse example13 correctly' do + request.stubs(:caller).returns('uid=500') + request.stubs(:action).returns('rspec') + Util.stubs(:has_cf_class?).with('one').returns(true) + Util.stubs(:has_cf_class?).with('two').returns(true) + Util.stubs(:has_cf_class?).with('three').returns(false) + Util.stubs(:get_fact).with('foo').returns('bar') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example13')).should be_true + end + + it 'should parse example14 correctly' do + request.stubs(:caller).returns('uid=500') + request.stubs(:action).returns('rspec') + Util.stubs(:has_cf_class?).with('one').returns(true) + Util.stubs(:has_cf_class?).with('two').returns(false) + Util.stubs(:get_fact).with('foo').returns('bar') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example14')).should be_true + end + + it 'should parse example15 correctly' do + # first field + request.stubs(:caller).returns('uid=500') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true + + # second field + request.stubs(:caller).returns('uid=600') + Util.stubs(:get_fact).with('customer').returns('acme') + Util.stubs(:has_cf_class?).with('acme::devserver').returns(true) + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true + + # third field + request.stubs(:caller).returns('uid=600') + request.stubs(:action).returns('status') + Util.stubs(:get_fact).with('customer').returns('acme') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true + + # forth field + request.stubs(:caller).returns('uid=600') + request.stubs(:action).returns('status') + Util.stubs(:get_fact).with('customer').returns('acme') + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true + + # fith field + request.stubs(:caller).returns('uid=700') + request.stubs(:action).returns('restart') + Util.stubs(:get_fact).with('environment').returns('development') + Matcher.stubs(:eval_compound_fstatement).with('value' => 'enabled', 'name' => 'puppet', 'operator' => '==', 'params' => nil, 'r_compare' => 'false').returns(true) + actionpolicy = ActionPolicy.new(request) + actionpolicy.parse_policy_file(File.join(@fixtures_dir, 'example15')).should be_true + + + end + end + + describe '#check_policy' do + it 'should return false if the policy line does not include the caller' do + actionpolicy.check_policy('caller', nil, nil, nil).should be_false + end + + it 'should return false if the policy line does not include the action' do + actionpolicy.check_policy(nil, 'action', nil, nil).should be_false + end + + it 'should parse both facts and classes if callers and actions match' do + actionpolicy.expects(:parse_facts).with('*').returns(true) + actionpolicy.expects(:parse_classes).with('*').returns(true) + actionpolicy.check_policy('rspec_caller', 'rspec_action', '*', '*').should be_true + end + + it 'should parse a compound statement if callers and actions match but classes are excluded' do + actionpolicy.expects(:parse_compound).with('*').returns(true) + actionpolicy.check_policy('rspec_caller', 'rspec_action', '*', nil).should be_true + end + end + + describe '#parse_facts' do + it 'should return true if facts is a wildcard' do + actionpolicy.parse_facts('*').should be_true + end + + it 'should parse compound fact statements' do + actionpolicy.stubs(:is_compound?).returns(true) + actionpolicy.expects(:parse_compound).with('foo=bar and bar=foo').returns(true) + actionpolicy.parse_facts('foo=bar and bar=foo').should be_true + end + + it 'should parse all facts' do + actionpolicy.stubs(:is_compound?).returns(false) + actionpolicy.expects(:lookup_fact).twice.returns(true) + actionpolicy.parse_facts('foo=bar bar=foo').should be_true + end + end + + describe '#parse_classes' do + it 'should return true if classes is a wildcard' do + actionpolicy.parse_classes('*').should be_true + end + + it 'should parse compound class statements' do + actionpolicy.stubs(:is_compound?).returns(true) + actionpolicy.expects(:parse_compound).with('foo=bar and bar=foo').returns(true) + actionpolicy.parse_facts('foo=bar and bar=foo').should be_true + end + + it 'should parse all classes' do + actionpolicy.stubs(:is_compound?).returns(false) + actionpolicy.expects(:lookup_fact).times(3).returns(true) + actionpolicy.parse_facts('foo bar baz').should be_true + end + end + + describe '#lookup_fact' do + it 'should return false if a class is found in the fact field' do + Log.expects(:warn).with('Class found where fact was expected') + actionpolicy.lookup_fact('rspec').should be_false + end + + it 'should lookup a fact value and return its true value' do + Util.expects(:get_fact).with('foo').returns('bar') + actionpolicy.lookup_fact('foo=bar').should be_true + end + end + + describe '#lookup_class' do + it 'should return false if a fact is found in the class field' do + Log.expects(:warn).with('Fact found where class was expected') + actionpolicy.lookup_class('foo=bar').should be_false + end + + it 'should lookup a fact value and return its true value' do + Util.expects(:has_cf_class?).with('rspec').returns(true) + actionpolicy.lookup_class('rspec').should be_true + end + end + + describe '#lookup' do + it 'should call #lookup_fact if a fact was passed' do + actionpolicy.expects(:lookup_fact).with('foo=bar').returns(true) + actionpolicy.lookup('foo=bar').should be_true + end + + it 'should call #lookup_class if a class was passed' do + actionpolicy.expects(:lookup_class).with('/rspec/').returns(true) + actionpolicy.lookup('/rspec/').should be_true + end + end + + describe '#lookup_policy_file' do + before do + Log.stubs(:debug) + end + + it 'should return the path of the policyfile is present' do + File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(true) + actionpolicy.lookup_policy_file.should == '/rspecdir/policies/rspec_agent.policy' + end + + it 'should return the default file path if one is specified' do + config.stubs(:pluginconf).returns({'actionpolicy.enable_default' => '1'}) + File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(false) + File.expects(:exist?).with('/rspecdir/policies/default.policy').returns(true) + actionpolicy.lookup_policy_file.should == '/rspecdir/policies/default.policy' + end + + it 'should return a custom default file path if one is specified' do + config.stubs(:pluginconf).returns({'actionpolicy.enable_default' => '1', + 'actionpolicy.default_name' => 'rspec'}) + File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(false) + File.expects(:exist?).with('/rspecdir/policies/rspec.policy').returns(true) + actionpolicy.lookup_policy_file.should == '/rspecdir/policies/rspec.policy' + end + + it 'should return nil if no policy file exists' do + File.expects(:exist?).with('/rspecdir/policies/rspec_agent.policy').returns(false) + actionpolicy.lookup_policy_file.should == nil + end + end + + describe '#eval_statement' do + it 'should return the logical string if param is not an statement or fstatement' do + actionpolicy.eval_statement({'and' => 'and'}).should == 'and' + end + + it 'should lookup the value of a statement if param is a statement' do + actionpolicy.expects(:lookup).with('foo=bar').returns(true) + actionpolicy.eval_statement({'statement' => 'foo=bar'}).should be_true + end + + it 'should lookup the value of a data function if param is a fstatement' do + Matcher.expects(:eval_compound_fstatement).with("rspec('data').value=result").returns(true) + actionpolicy.eval_statement({'fstatement' => "rspec('data').value=result"}).should be_true + end + + it 'should log a failure message and return false if the fstatement cannot be parsed' do + Matcher.expects(:eval_compound_fstatement).with("rspec('data').value=result").raises('error') + Log.expects(:warn).with('Could not call Data function in policy file: error') + actionpolicy.eval_statement({'fstatement' => "rspec('data').value=result"}).should be_false + end + end + + describe '#is_compound?' do + it 'should return false if a compound statement was not identified' do + actionpolicy.is_compound?('not').should be_true + actionpolicy.is_compound?('!rspec').should be_true + actionpolicy.is_compound?('and').should be_true + actionpolicy.is_compound?('or').should be_true + actionpolicy.is_compound?("data('field').value=othervalue").should be_true + end + + it 'should return true if a compound statement was identified' do + actionpolicy.is_compound?('f1=v1 f1=v2').should be_false + actionpolicy.is_compound?('class1 class2 /class*/').should be_false + end + end + + describe '#deny' do + it 'should log the failure and raise an RPCAborted error' do + Log.expects(:debug).with('fail') + expect{ + actionpolicy.deny('fail') + }.to raise_error RPCAborted + end + end + end + end +end diff --git a/spec/actionpolicy/fixtures/default_allow b/spec/actionpolicy/fixtures/default_allow new file mode 100644 index 0000000..9dba188 --- /dev/null +++ b/spec/actionpolicy/fixtures/default_allow @@ -0,0 +1 @@ +policy default allow diff --git a/spec/actionpolicy/fixtures/default_deny b/spec/actionpolicy/fixtures/default_deny new file mode 100644 index 0000000..2d1efaf --- /dev/null +++ b/spec/actionpolicy/fixtures/default_deny @@ -0,0 +1 @@ +policy default deny diff --git a/spec/actionpolicy/fixtures/example1 b/spec/actionpolicy/fixtures/example1 new file mode 100644 index 0000000..956e2b1 --- /dev/null +++ b/spec/actionpolicy/fixtures/example1 @@ -0,0 +1,2 @@ +policy default deny +allow * * * * diff --git a/spec/actionpolicy/fixtures/example10 b/spec/actionpolicy/fixtures/example10 new file mode 100644 index 0000000..2ab44e0 --- /dev/null +++ b/spec/actionpolicy/fixtures/example10 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 rspec * rspec diff --git a/spec/actionpolicy/fixtures/example11 b/spec/actionpolicy/fixtures/example11 new file mode 100644 index 0000000..a7bda99 --- /dev/null +++ b/spec/actionpolicy/fixtures/example11 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 rspec foo=bar rspec diff --git a/spec/actionpolicy/fixtures/example12 b/spec/actionpolicy/fixtures/example12 new file mode 100644 index 0000000..2a088bf --- /dev/null +++ b/spec/actionpolicy/fixtures/example12 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 rspec foo=bar and bar=foo rspec diff --git a/spec/actionpolicy/fixtures/example13 b/spec/actionpolicy/fixtures/example13 new file mode 100644 index 0000000..6b864a7 --- /dev/null +++ b/spec/actionpolicy/fixtures/example13 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 rspec foo=bar one and two or three diff --git a/spec/actionpolicy/fixtures/example14 b/spec/actionpolicy/fixtures/example14 new file mode 100644 index 0000000..3a31e6b --- /dev/null +++ b/spec/actionpolicy/fixtures/example14 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 rspec foo=bar and one or (two and one) diff --git a/spec/actionpolicy/fixtures/example15 b/spec/actionpolicy/fixtures/example15 new file mode 100644 index 0000000..e7ad84e --- /dev/null +++ b/spec/actionpolicy/fixtures/example15 @@ -0,0 +1,5 @@ +policy default deny +allow uid=500 * * * +allow uid=600 * customer=acme acme::devserver +allow uid=600 enable disable status customer=acme * +allow uid=700 restart (puppet().enabled=false and environment=production) or environment=development diff --git a/spec/actionpolicy/fixtures/example2 b/spec/actionpolicy/fixtures/example2 new file mode 100644 index 0000000..ef8f247 --- /dev/null +++ b/spec/actionpolicy/fixtures/example2 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 * * * diff --git a/spec/actionpolicy/fixtures/example3 b/spec/actionpolicy/fixtures/example3 new file mode 100644 index 0000000..a750dcb --- /dev/null +++ b/spec/actionpolicy/fixtures/example3 @@ -0,0 +1,2 @@ +policy default deny +allow * rspec * * diff --git a/spec/actionpolicy/fixtures/example4 b/spec/actionpolicy/fixtures/example4 new file mode 100644 index 0000000..e4f60d0 --- /dev/null +++ b/spec/actionpolicy/fixtures/example4 @@ -0,0 +1,2 @@ +policy default deny +allow * * foo=bar * diff --git a/spec/actionpolicy/fixtures/example5 b/spec/actionpolicy/fixtures/example5 new file mode 100644 index 0000000..7e54e69 --- /dev/null +++ b/spec/actionpolicy/fixtures/example5 @@ -0,0 +1,2 @@ +policy default deny +allow * * * rspec diff --git a/spec/actionpolicy/fixtures/example6 b/spec/actionpolicy/fixtures/example6 new file mode 100644 index 0000000..3615ca5 --- /dev/null +++ b/spec/actionpolicy/fixtures/example6 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 rspec * * diff --git a/spec/actionpolicy/fixtures/example7 b/spec/actionpolicy/fixtures/example7 new file mode 100644 index 0000000..15f5420 --- /dev/null +++ b/spec/actionpolicy/fixtures/example7 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 * foo=bar * diff --git a/spec/actionpolicy/fixtures/example8 b/spec/actionpolicy/fixtures/example8 new file mode 100644 index 0000000..4ac49e3 --- /dev/null +++ b/spec/actionpolicy/fixtures/example8 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 * * rspec diff --git a/spec/actionpolicy/fixtures/example9 b/spec/actionpolicy/fixtures/example9 new file mode 100644 index 0000000..bf1da55 --- /dev/null +++ b/spec/actionpolicy/fixtures/example9 @@ -0,0 +1,2 @@ +policy default deny +allow uid=500 rspec foo=bar * diff --git a/spec/spec.opts b/spec/spec.opts new file mode 100644 index 0000000..fe2ea32 --- /dev/null +++ b/spec/spec.opts @@ -0,0 +1 @@ +--colour --backtrace diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..fa4ce15 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,18 @@ +$: << File.join([File.dirname(__FILE__), "lib"]) + +require 'rubygems' +require 'rspec' +require 'mcollective' +require 'mcollective/test' +require 'rspec/mocks' +require 'mocha' +require 'tempfile' + +RSpec.configure do |config| + config.mock_with :mocha + config.include(MCollective::Test::Matchers) + + config.before :each do + MCollective::PluginManager.clear + end +end diff --git a/util/actionpolicy.ddl b/util/actionpolicy.ddl new file mode 100644 index 0000000..967b9d9 --- /dev/null +++ b/util/actionpolicy.ddl @@ -0,0 +1,9 @@ +metadata :name => "actionpolicy", + :description => "Action Policy simplerpc authorization plugin", + :author => "P.Loubser ", + :license => "ASL 2.0", + :version => "2.0.0", + :url => "https://github.com/puppetlabs/mcollective-actionpolicy-authorization", + :timeout => 1 + +requires :mcollective => "2.2.1" diff --git a/util/actionpolicy.rb b/util/actionpolicy.rb new file mode 100644 index 0000000..903595c --- /dev/null +++ b/util/actionpolicy.rb @@ -0,0 +1,219 @@ +module MCollective + module Util + class ActionPolicy + attr_accessor :config, :allow_unconfigured, :configdir, :agent, :caller, :action + + def self.authorize(request) + ActionPolicy.new(request).authorize_request + end + + def initialize(request) + @config = Config.instance + @agent = request.agent + @caller = request.caller + @action = request.action + @allow_unconfigured = !!(config.pluginconf.fetch('actionpolicy.allow_unconfigured', 'n') =~ /^1|y/i) + @configdir = @config.configdir + end + + def authorize_request + # Lookup the policy file. If none exists and @allow_unconfigured + # is false the request gets denied. + policy_file = lookup_policy_file + + # No policy file exists and allow_unconfigured is false + if !policy_file && !@allow_unconfigured + deny('Could not load any valid policy files. Denying based on allow_unconfigured: %s' % @allow_unconfigured) + # No policy exists but allow_unconfigured is true + elsif !(policy_file) && @allow_unconfigured + Log.debug('Could not load any valid policy files. Allowing based on allow_unconfigured: %s' % @allow_unconfigured) + return true + end + + # A policy file exists + parse_policy_file(policy_file) + end + + def parse_policy_file(policy_file) + Log.debug('Parsing policyfile for %s: %s' % [@agent, policy_file]) + allow = @allow_unconfigured + + File.read(policy_file).each_line do |line| + next if line =~ /^(#.*|\s*)$/ + + if line =~ /^policy\s+default\s+(\w+)/ + if $1 == 'allow' + allow = true + else + allow = false + end + elsif line =~ /^(allow|deny)\t+(.+?)\t+(.+?)\t+(.+?)(\t+(.+?))*$/ + if check_policy($2, $3, $4, $6) + if $1 == 'allow' + return true + else + deny("Denying based on explicit 'deny' policy rule in policyfile: %s" % File.basename(policy_file)) + end + end + else + Log.debug("Cannot parse policy line: %s" % line) + end + end + + allow || deny("Denying based on default policy in %s" % File.basename(policy_file)) + end + + # Check if a request made by a caller matches the state defined in the policy + def check_policy(rpccaller, actions, facts, classes) + # If we have a wildcard caller or the caller matches our policy line + # then continue else skip this policy line\ + if (rpccaller != '*') && (rpccaller != @caller) + return false + end + + # If we have a wildcard actions list or the request action is in the list + # of actions in the policy line continue, else skip this policy line + if (actions != '*') && !(actions.split.include?(@action)) + return false + end + + unless classes + return parse_compound(facts) + else + return parse_facts(facts) && parse_classes(classes) + end + end + + def parse_facts(facts) + return true if facts == '*' + + if is_compound?(facts) + return parse_compound(facts) + else + facts.split.each do |fact| + return false unless lookup_fact(fact) + end + end + + true + end + + def parse_classes(classes) + return true if classes == '*' + + if is_compound?(classes) + return parse_compound(classes) + else + classes.split.each do |klass| + return false unless lookup_class(klass) + end + end + + true + end + + def lookup_fact(fact) + if fact =~ /(.+)(<|>|=|<=|>=)(.+)/ + lv = $1 + sym = $2 + rv = $3 + + sym = '==' if sym == '=' + return eval("'#{Util.get_fact(lv)}'#{sym}'#{rv}'") + else + Log.warn("Class found where fact was expected") + return false + end + end + + def lookup_class(klass) + if klass =~ /(.+)(<|>|=|<=|>=)(.+)/ + Log.warn("Fact found where class was expected") + return false + else + return Util.has_cf_class?(klass) + end + end + + def lookup(token) + if token =~ /(.+)(<|>|=|<=|>=)(.+)/ + return lookup_fact(token) + else + return lookup_class(token) + end + end + + # Here we lookup the full path of the policy file. If the policyfile + # does not exist, we check to see if a default file was set and + # determine its full path. If no default file exists, or default was + # not specified, we return false. + def lookup_policy_file + policy_file = File.join(@configdir, "policies", "#{@agent}.policy") + + Log.debug("Looking for policy in #{policy_file}") + + return policy_file if File.exist?(policy_file) + + if @config.pluginconf.fetch('actionpolicy.enable_default', 'n') =~ /^1|y/i + defaultname = @config.pluginconf.fetch('actionpolicy.default_name', 'default') + default_file = File.join(@configdir, "policies", "#{defaultname}.policy") + + Log.debug("Initial lookup failed: looking for policy in #{default_file}") + + return default_file if File.exist?(default_file) + end + + Log.debug('Could not find any policy files.') + nil + end + + # Evalute a compound statement and return its truth value + def eval_statement(statement) + token_type = statement.keys.first + token_value = statement.values.first + + return token_value if (token_type != 'statement' && token_type != 'fstatement') + + if token_type == 'statement' + return lookup(token_value) + elsif token_type == 'fstatement' + begin + return Matcher.eval_compound_fstatement(token_value) + rescue => e + Log.warn("Could not call Data function in policy file: #{e}") + return false + end + end + end + + def is_compound?(list) + list.split.each do |token| + if token =~ /^!|^not$|^or$|^and$|\(.+\)/ + return true + end + end + + false + end + + def parse_compound(list) + stack = Matcher.create_compound_callstack(list) + + begin + stack.map!{ |item| eval_statement(item) } + rescue => e + Log.debug(e.to_s) + return false + end + + eval(stack.join(' ')) + end + + def deny(logline) + Log.debug(logline) + + raise(RPCAborted, 'You are not authorized to call this agent or action.') + end + end + end +end