Skip to content

Commit

Permalink
Introduce Cucumber Tests for Rotators (#581)
Browse files Browse the repository at this point in the history
This refactor brings integration tests and some refactoring to improve modularity and flexibility for the PostgreSQL rotator.
  • Loading branch information
jonahx authored and jvanderhoof committed Jun 25, 2018
1 parent a40fb01 commit 17848f0
Show file tree
Hide file tree
Showing 19 changed files with 340 additions and 112 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ api.html

brakeman-output.*
.idea
TEMP_NOTES.txt
3 changes: 3 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ pipeline {
stage('API') {
steps { sh 'cd ci && ./test --cucumber-api' }
}
stage('Rotators') {
steps { sh 'cd ci && ./test --cucumber-rotators' }
}
}
post {
always {
Expand Down
11 changes: 9 additions & 2 deletions app/domain/rotation/conjur_facade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ class ConjurFacade

def initialize(rotated_variable:,
secret_model: Secret,
resource_model: Resource,
db: Sequel::Model.db)
@rotated_variable = rotated_variable
@secret_model = secret_model
@resource_model = resource_model
@db = db
end

def current_values(variables)
@secret_model.current_values(variables)
def current_values(variable_ids)
@secret_model.current_values(variable_ids)
end

def annotations
@resource_model.annotations(@rotated_variable.resource_id)
end

# new_values is a Hash of {resource_id: new_value} pairs that represent a
Expand All @@ -38,6 +44,7 @@ def current_values(variables)
#
def update_variables(new_values, &rotator_code)
@db.transaction do

new_values.each do |resource_id, value|
update_secret(resource_id, value)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'base58'
require 'securerandom'

module Rotation

Expand All @@ -7,13 +8,13 @@ module Rotation
#
# This means that your password will have a space of 58 ^ length. Passwords
# are generated using the ruby `SecureRandom` library.
class Base58Password
class Password

# Use the first char of the default alphabet to pad zeros
#
ZERO_PAD_CHAR = Base58::ALPHABETS[:flickr][0]

def self.new(length:)
def self.base58(length:)

valid = length > 0 && length.is_a?(Integer)
raise ArgumentError, "length must be a positive integer" unless valid
Expand Down
1 change: 0 additions & 1 deletion app/domain/rotation/rotated_variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def name
# and returns everything up to th first slash
#
def prefix
puts '@resource_id', @resource_id
@resource_id.match(%r{(.*)/.*})[1]
end

Expand Down
102 changes: 59 additions & 43 deletions app/domain/rotation/rotators/postgresql/password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,31 @@ module Postgresql

class Password

def initialize(password_factory: ::Rotation::Base58Password, pg: ::PG)
PASSWORD_LENGTH_ANNOTATION = 'rotation/postgresql/password/length'

def initialize(password_factory: ::Rotation::Password, pg: ::PG)
@password_factory = password_factory
@pg = pg
end

# 1. Generate new pw
# 2. Update of variable in Conjur.
# 2. Update variable in Conjur.
# 3. Update variable in the DB itself.
#
# NOTE: Both 2 and 3 are executed inside a single transaction, so either
# both updates happen, or neither do.
#
# NOTE: The order matters:
# 1. Capture the *current* credentials
# 2. The Database object `db` then uses those to perform the
# update to the new credentials.
#
def rotate(facade)

resource_id = facade.rotated_variable.resource_id
db = rotated_db(facade)
new_pw = db.new_password
credentials = DbCredentials.new(facade)
new_pw = new_password(facade)
db = Database.new(credentials, @pg)
pw_update = Hash[resource_id, new_pw]

facade.update_variables(pw_update) do
Expand All @@ -31,63 +40,47 @@ def rotate(facade)

private

def rotated_db(facade)
RotatedDb.new(facade, @pg, @password_factory)
def new_password(facade)
@password_factory.base58(length: pw_length(facade))
end

class RotatedDb

def initialize(facade, pg, password_factory)
@facade = facade
@pg = pg
@password_factory = password_factory
end
def pw_length(facade)
@pw_length ||= facade.annotations[PASSWORD_LENGTH_ANNOTATION].to_i || 20
end

def new_password
len = policy_vals[pw_length_id] || 20
@password_factory.new(length: len)
end
class DbCredentials

def update_password(new_pw)
conn = connection
conn.prepare('update_pw', "ALTER ROLE $1 WITH PASSWORD '$2'")
conn.exec_prepared('update_pw', [current.username, new_pw])
conn.close
# NOTE: It's important that @credentials is initialized on
# intialization, because we need to capture the *current*
# credentials so that we can access the db to *update* to
# the new password.
#
def initialize(facade)
@facade = facade
@credentials = current_credentials
end

def connection
connection = @pg.connect(db_uri)
connection.exec('SELECT 1')
connection
def db_uri
url, uname, password = credential_resource_ids.map(&@credentials)
"postgresql://#{uname}:#{password}@#{url}"
end

def db_uri
url, username, password = credential_ids.map { |x| policy_vals[x] }
"postgresql://#{username}:#{password}@#{url}"
def username
@credentials[username_id]
end

private

# Values of the postgres rotator related variables in policy.yml
#
def policy_vals
@policy_vals ||= @facade.current_values(
credential_ids << pw_length_id
)
end

# Variables containing database connection info are expected to exist
#
def credential_ids
@credential_ids ||= [url_id, username_id, password_id]
def current_credentials
@facade.current_values(credential_resource_ids)
end

def rotated_variable
@facade.rotated_variable
end

def pw_length_id
rotated_variable.sibling_id('password/length')
def credential_resource_ids
[url_id, username_id, password_id]
end

def password_id
Expand All @@ -103,6 +96,29 @@ def username_id
end
end

class Database

def initialize(credentials, pg)
@credentials = credentials
@pg = pg
end

def update_password(new_pw)
username = @credentials.username
conn = connection
conn.exec("ALTER ROLE #{username} WITH PASSWORD '#{new_pw}'")
conn.close
end

private

def connection
connection = @pg.connect(@credentials.db_uri)
connection.exec('SELECT 1')
connection
end
end

end

end
Expand Down
9 changes: 9 additions & 0 deletions app/models/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ def find_if_visible role, *a
def [] *args
args.length == 3 ? super(args.join ':') : super
end

def annotations(resource_id)
natural_join(:annotations)
.where(resource_id: resource_id)
.all
.map(&:values)
.reduce([]) { |m,x| m << [x[:name], x[:value]] }
.to_h
end
end

def extended_associations
Expand Down
4 changes: 4 additions & 0 deletions ci/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ services:
pg:
image: postgres:9.3

testdb:
image: postgres:9.3

conjur:
image: "conjur:${TAG}"
environment:
Expand Down Expand Up @@ -42,6 +45,7 @@ services:
links:
- conjur
- pg
- testdb

ldap-server:
image: osixia/openldap
Expand Down
10 changes: 10 additions & 0 deletions ci/test
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# shellcheck disable=SC1091


# Display CLI usage information
function print_help {
cat << EOF
Expand All @@ -17,6 +18,8 @@ GLOBAL OPTIONS
--cucumber-authenticators - Runs Cucumber Authenticator features
--cucumber-rotators - Runs Cucumber Rotator features
--cucumber-policy - Runs Cucumber Policy features
-h | --help - Show this message
Expand Down Expand Up @@ -67,11 +70,13 @@ export COMPOSE_PROJECT_NAME="$(openssl rand -hex 3)"
services="pg conjur"
RUN_ALL=true
RUN_AUTHENTICATORS=false
RUN_ROTATORS=false
RUN_API=false
RUN_POLICY=false
RUN_RSPEC=false
while true ; do
case "$1" in
--cucumber-rotators ) RUN_ALL=false ; RUN_ROTATORS=true ; shift ;;
--cucumber-authenticators ) RUN_ALL=false ; RUN_AUTHENTICATORS=true ; shift ;;
--cucumber-api ) RUN_ALL=false ; RUN_API=true ; shift ;;
--cucumber-policy ) RUN_ALL=false ; RUN_POLICY=true ; shift ;;
Expand All @@ -91,6 +96,11 @@ export TAG="$(version_tag)"
cd ci

# Run tests based on what flags were passed
if [[ $RUN_ROTATORS = true || $RUN_ALL = true ]]; then
services="$services testdb"
run_cucumber_tests 'rotators'
fi

if [[ $RUN_AUTHENTICATORS = true || $RUN_ALL = true ]]; then
services="$services ldap-server"
run_cucumber_tests 'authenticators'
Expand Down
5 changes: 5 additions & 0 deletions cucumber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ authenticators: >
--format pretty
-r cucumber/authenticators/features/support
-r cucumber/authenticators/features/step_definitions cucumber/authenticators
rotators: >
--format pretty
-r cucumber/rotators/features/support
-r cucumber/rotators/features/step_definitions cucumber/rotators
5 changes: 2 additions & 3 deletions cucumber/policy/features/support/env.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require 'aruba'
require 'aruba/cucumber'
require 'conjur-api'
require_relative './world.rb'

Conjur.configuration.appliance_url = ENV['CONJUR_APPLIANCE_URL'] || 'http://conjur'
Conjur.configuration.account = ENV['CONJUR_ACCOUNT'] || 'cucumber'
World(PossumWorld)
7 changes: 5 additions & 2 deletions cucumber/policy/features/support/world.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
require 'conjur-api'

Conjur.configuration.appliance_url = ENV['CONJUR_APPLIANCE_URL'] || 'http://conjur'
Conjur.configuration.account = ENV['CONJUR_ACCOUNT'] || 'cucumber'

module FullId
def make_full_id id, account: Conjur.configuration.account
tokens = id.split(":", 3)
Expand Down Expand Up @@ -91,5 +96,3 @@ def login_as_role login, api_key = nil
@conjur_api = Conjur::API.new_from_key login, api_key
end
end

World(PossumWorld)
34 changes: 34 additions & 0 deletions cucumber/rotators/features/password.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Feature: Postgres password rotation

Background: Configure a postgres rotator
Given I reset my root policy
And I have the root policy:
"""
- !policy
id: db-reports
body:
- &variables
- !variable url
- !variable username
- !variable
id: password
annotations:
rotation/rotator: postgresql/password
rotation/ttl: PT1S
rotation/postgresql/password/length: 32
"""
And I create a db user "test" with password "secret"
And I add the value "testdb" to variable "db-reports/url"
And I add the value "test" to variable "db-reports/username"
And I add the value "secret" to variable "db-reports/password"

# NOTE: To make these tests robust on unreliable Jenkins servers that may
# not be able to respect `sleep` times exactly, we give ourselves
# some leeway, waiting for 3 rotations but allowing for more than
# 3 seconds of time.
#
Scenario: Values are rotated according to the policy
Given I poll "db-reports/password" and db user "test" for 3 rotations in 20 seconds
Then the first 3 db and conjur passwords match
Then the first 3 conjur passwords are distinct
And the generated passwords have length 32
Loading

0 comments on commit 17848f0

Please sign in to comment.