Skip to content

Commit

Permalink
Merge pull request #257 from bdclark/acls
Browse files Browse the repository at this point in the history
Add ACL support
  • Loading branch information
johnbellone committed Dec 22, 2015
2 parents eedc49d + b5551e3 commit 6e6c96b
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 2 deletions.
14 changes: 14 additions & 0 deletions .kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,17 @@ suites:
excludes:
- centos-7.1
- centos-6.6
- name: acl
run_list:
- recipe[consul::default]
- recipe[consul::client_gem]
- recipe[consul_spec::acl]
attributes:
consul:
config:
bootstrap: true
server: true
datacenter: fortmeade
acl_master_token: doublesecret
acl_datacenter: fortmeade
acl_default_policy: deny
1 change: 1 addition & 0 deletions Berksfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ end

group :integration do
cookbook 'consul_spec', path: 'test/cookbooks/consul_spec'
cookbook 'apt'
end
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,39 @@ times we need to tell Consul to actually reload configurations. If
there are several definitions this may save a little time off your
Chef run.

### ACLs
The `consul_acl` resource allows management of [Consul ACL rules][15]. Supported
actions are `:create` and `:delete`. The `:create` action will update/insert
as necessary.

The `consul_acl` resource requires the [Diplomat Ruby API][16] gem to be
installed and available to Chef before using the resource. This can be
accomplished by including `consul::client_gem` recipe in your run list.

In order to make the resource idempotent and only notify when necessary, the
`id` field is always required (defaults to the name of the resource).
If `type` is not provided, it will default to "client". The `acl_name`
and `rules` attributes are also optional; if not included they will be empty
in the resulting ACL.

The example below will create a client ACL token with an `ID` of the given UUID,
`Name` of "AwesomeApp Token", and `Rules` of the given string.
```ruby
consul_acl '49f06aa9-782f-465a-becf-44f0aaefd335' do
acl_name 'AwesomeApp Token'
type 'client'
rules <<-EOS.gsub(/^\s{4}/, '')
key "" {
policy = "read"
}
service "" {
policy = "write"
}
EOS
auth_token node['consul']['config']['acl_master_token']
end
```

### Execute
The command-line agent provides a mechanism to facilitate remote
execution. For example, this can be used to run the `uptime` command
Expand All @@ -143,7 +176,7 @@ nature of this command it is _impossible_ for it to be idempotent.

### UI

`consul_ui` resource can be used to download and extract the
`consul_ui` resource can be used to download and extract the
[consul web UI](https://www.consul.io/intro/getting-started/ui.html).
It can be done with a block like this:

Expand All @@ -158,7 +191,7 @@ end
Assuming consul version `0.5.2` above block would create `/srv/consul-ui/0.5.2`
and symlink `/srv/consul-ui/current`.

It does not change agent's configuration by itself.
It does not change agent's configuration by itself.
`consul_config` resource should be modified explicitly in order to host the web page.

```ruby
Expand Down Expand Up @@ -186,3 +219,5 @@ This is optional, because consul UI can be hosted by any web server.
[12]: https://consul.io/docs/commands/exec.html
[13]:https://en.wikipedia.org/wiki/Quorum_(distributed_computing)
[14]: https://github.com/johnbellone/consul-cluster-cookbook
[15]: https://www.consul.io/docs/internals/acl.html
[16]: https://github.com/WeAreFarmGeek/diplomat
2 changes: 2 additions & 0 deletions attributes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
'server' => 8300
}

default['consul']['diplomat_version'] = nil

default['consul']['service']['install_method'] = 'binary'
default['consul']['service']['config_dir'] = '/etc/consul'
default['consul']['service']['binary_url'] = "https://releases.hashicorp.com/consul/%{version}/%{filename}.zip" # rubocop:disable Style/StringLiterals
Expand Down
94 changes: 94 additions & 0 deletions libraries/consul_acl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#
# Cookbook: consul
# License: Apache 2.0
#
# Copyright (C) 2014, 2015 Bloomberg Finance L.P.
#
require 'poise'

module ConsulCookbook
module Resource
# Resource for managing Consul ACLs.
class ConsulAcl < Chef::Resource
include Poise
provides(:consul_acl)
actions(:create, :delete)
default_action(:create)

# @!attribute url
# @return [String]
attribute(:url, kind_of: String, default: 'http://localhost:8500')

# @!attribute auth_token
# @return [String]
attribute(:auth_token, kind_of: String, required: true)

# @!attribute id
# @return [String]
attribute(:id, kind_of: String, name_attribute: true)

# @!attribute acl_name
# @return [String]
attribute(:acl_name, kind_of: String, default: '')

# @!attribute type
# @return [String]
attribute(:type, equal_to: %w{client management}, default: 'client')

# @!attribute rules
# @return [String]
attribute(:rules, kind_of: String, default: '')

def to_acl
{ 'ID' => id, 'Type' => type, 'Name' => acl_name, 'Rules' => rules }
end
end
end

module Provider
# Provider for managing Consul ACLs.
class ConsulAcl < Chef::Provider
include Poise
provides(:consul_acl)

def action_create
configure_diplomat
unless up_to_date?
Diplomat::Acl.create(new_resource.to_acl)
new_resource.updated_by_last_action(true)
end
end

def action_delete
configure_diplomat
unless Diplomat::Acl.info(new_resource.id).empty?
Diplomat::Acl.destroy(new_resource.id)
new_resource.updated_by_last_action(true)
end
end

protected

def configure_diplomat
begin
require 'diplomat'
rescue LoadError
raise RunTimeError, 'The diplomat gem is required; ' \
'include recipe[consul::client_gem] to install.'
end
Diplomat.configure do |config|
config.url = new_resource.url
config.acl_token = new_resource.auth_token
config.options = { request: { timeout: 10 } }
end
end

def up_to_date?
old_acl = Diplomat::Acl.info(new_resource.to_acl['ID']).first
return false if old_acl.nil?
old_acl.select! { |k, _v| %w(ID Type Name Rules).include?(k) }
old_acl == new_resource.to_acl
end
end
end
end
11 changes: 11 additions & 0 deletions recipes/client_gem.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#
# Cookbook: consul
# License: Apache 2.0
#
# Copyright 2014, 2015 Bloomberg Finance L.P.
#

chef_gem 'diplomat' do
version node['consul']['diplomat_version'] if node['consul']['diplomat_version']
action :install
end
46 changes: 46 additions & 0 deletions test/cookbooks/consul_spec/recipes/acl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package 'curl'

consul_acl 'anonymous' do
acl_name 'Anonymous Token'
type 'client'
url "http://localhost:#{node['consul']['config']['ports']['http']}"
auth_token node['consul']['config']['acl_master_token']
notifies :create, 'file[/tmp/anonymous-notified]', :immediately
end

file '/tmp/anonymous-notified' do
action :nothing
end

consul_acl 'management_token' do
acl_name 'Management Token'
type 'management'
auth_token node['consul']['config']['acl_master_token']
notifies :create, 'file[/tmp/management_token-notified]', :immediately
end

file '/tmp/management_token-notified' do
action :nothing
end

consul_acl 'delete_management_token' do
id 'management_token'
auth_token node['consul']['config']['acl_master_token']
action :delete
end

consul_acl 'non_existing_token' do
auth_token node['consul']['config']['acl_master_token']
action :delete
end

consul_acl 'reader_token' do
type 'client'
rules <<-EOS.gsub(/^\s{4}/, '')
dummyrule_line1
dummyrule_line2
EOS
auth_token node['consul']['config']['acl_master_token']
end


30 changes: 30 additions & 0 deletions test/integration/acl/serverspec/localhost/default_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'spec_helper'

describe command('curl -s "http://localhost:8500/v1/acl/info/anonymous"') do
its(:exit_status) { should eq 0 }
its(:stdout) { should match('"ID":"anonymous"') }
its(:stdout) { should match('"Name":"Anonymous Token"') }
its(:stdout) { should match('"Type":"client"') }
its(:stdout) { should match('"Rules":""') }
end

describe file('/tmp/anonymous-notified') do
it { should_not exist }
end

describe command('curl -s "http://localhost:8500/v1/acl/info/management_token"') do
its(:exit_status) { should eq 0 }
its(:stdout) { should match('[]') }
end

describe file('/tmp/management_token-notified') do
it { should exist }
end

describe command('curl -s "http://localhost:8500/v1/acl/info/reader_token"') do
its(:exit_status) { should eq 0 }
its(:stdout) { should match('"ID":"reader_token"') }
its(:stdout) { should match('"Name":""') }
its(:stdout) { should match('"Type":"client"') }
its(:stdout) { should match /\"Rules\":\"dummyrule_line1\\ndummyrule_line2\\n\"/ }
end

0 comments on commit 6e6c96b

Please sign in to comment.