Skip to content

Commit

Permalink
Add credentials plugin
Browse files Browse the repository at this point in the history
Its purpose is to avoid storing cleartext passwords in .couchapprc.

The plugin replaces specific URI userinfo strings (e.g. KEYCHAIN_CREDENTIALS)
in the .couchapprc with username and password retrieved from a credentials
store like Keychain Services in Mac OS X.

The plugin is designed to be extensible to support different
credentials stores and takes into account that not all stores
are available on every platform.
  • Loading branch information
afh committed Jan 28, 2012
1 parent f804548 commit 3ac653f
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Expand Up @@ -18,3 +18,7 @@ end
group :test do group :test do
gem "shoulda", ">= 0" gem "shoulda", ">= 0"
end end

group :darwin do
gem 'keychain_services', '~>0.1.1'
end
4 changes: 4 additions & 0 deletions lib/soca/plugin.rb
Expand Up @@ -25,5 +25,9 @@ def app_dir
pusher.app_dir pusher.app_dir
end end


def config
pusher.config
end

end end
end end
75 changes: 75 additions & 0 deletions lib/soca/plugins/credentials.rb
@@ -0,0 +1,75 @@
module Soca
module Plugins
class Credentials < Soca::Plugin

name 'credentials'

# Credentials plugin
# This plugin is run after the couchapprc is loaded,
# it checks the db field in every environment,
# searches for strings ending with '_CREDENTIALS' in the URI userinfo field,
# passes the URI host to a method handling the requested credentials,
# and replaces the userinfo with the username and password from the credentials method.
#
# When adding a new credentials method, please make sure the platform
# specific requirements are met (e.g. external tools or gems)
# and configure its platform availability in the credentials_supported? method

def after_load_couchapprc
config['couchapprc']['env'].each do |env, cfg|
next unless cfg['db'] =~ /^(https?:\/\/)([^@]+_CREDENTIALS)@(.*)$/i
scheme = $1
userinfo = $2
host = $3

unless credentials_supported?(userinfo)
Soca.logger.error "#{userinfo} are not supported on the #{RUBY_PLATFORM} platform"
puts 'skip'
next
end

(username, password) = send(userinfo.downcase, host)
unless username and password
Soca.logger.warn "#{userinfo} returned empty credentials for #{host}"
else
credentials = "#{username}:#{password}@"
config['couchapprc']['env'][env]['db'] = "#{scheme}#{credentials}#{host}"
Soca.logger.debug "Replacing #{userinfo} with #{credentials} in #{cfg['db']}"
end
end
end

private
def credentials_supported?(type)
Soca.logger.debug "Checking support for #{type} on #{RUBY_PLATFORM}"
available_credentials = {
'darwin' => %w[keychain_credentials],
#'linux' => %w[]
}
supported_credentials = available_credentials.map { |os, list| list if RUBY_PLATFORM =~ /#{os}/i }
supported_credentials.flatten.compact.include?(type.downcase)
end

# This methods retrieves the user credentials from the Mac OS X Keychain Services
def keychain_credentials(host)
begin
require 'keychain'
rescue LoadError
warn "Please install the keychain_services gem, in order to retrieve user credentials from your keychain"
return
end

Soca.logger.debug "Searching for #{host} in keychain"
item = Keychain.items.find { |item| item if item.label == host }
unless item
# strip url-path from host url and search again
(host, db) = host.split('/', 2)
Soca.logger.debug "Searching for #{host} in keychain"
item = Keychain.items.find { |item| item if item.label == host }
end
[item.account, item.password] if item
end

end
end
end
1 change: 1 addition & 0 deletions lib/soca/pusher.rb
Expand Up @@ -26,6 +26,7 @@ def load_config
def load_couchapprc def load_couchapprc
@config ||= {} @config ||= {}
@config['couchapprc'] = JSON.parse(File.read(File.join(app_dir, '.couchapprc'))) @config['couchapprc'] = JSON.parse(File.read(File.join(app_dir, '.couchapprc')))
run_hooks!(:after_load_couchapprc)
end end


def build def build
Expand Down
1 change: 1 addition & 0 deletions test/helper.rb
Expand Up @@ -8,6 +8,7 @@
$LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.dirname(__FILE__))
require 'soca' require 'soca'
require 'soca/plugins/compass' require 'soca/plugins/compass'
require 'soca/plugins/credentials'


class Test::Unit::TestCase class Test::Unit::TestCase


Expand Down
40 changes: 40 additions & 0 deletions test/test_credentials_plugin.rb
@@ -0,0 +1,40 @@
require 'helper'

class TestCredentialsPlugin < Test::Unit::TestCase
def app_path(relative='')
File.expand_path(@test_app_dir + '/' + relative)
end

context 'credentials plugin' do
setup do
@pusher = Soca::Pusher.new(app_path)
@plugin = Soca::Plugins::Credentials.new(@pusher)
end

if RUBY_PLATFORM =~ /darwin/i
require 'keychain'
context 'given keychain credentials' do
should 'check for platform availability' do
@plugin.expects(:credentials_supported?).
with('KEYCHAIN_CREDENTIALS').
returns(true)
@plugin.after_load_couchapprc
end

# Note this test requires you have
# an application password item
# in your keychain with the
# name localhost:5894/testapp
# username admin
# password admin
should 'return username and password for host from keychain' do
@plugin.expects(:keychain_credentials).
with('localhost:5984/testapp').
returns(%w[admin admin])
@plugin.after_load_couchapprc
end
end
end

end
end
3 changes: 3 additions & 0 deletions test/testapp/.couchapprc
Expand Up @@ -5,6 +5,9 @@
}, },
"production": { "production": {
"db": "http://admin:admin@c.ixxr.net/testapp" "db": "http://admin:admin@c.ixxr.net/testapp"
},
"keychain_credentials": {
"db": "http://KEYCHAIN_CREDENTIALS@localhost:5984/testapp"
} }
} }
} }

0 comments on commit 3ac653f

Please sign in to comment.