Skip to content

Commit

Permalink
ldap: fixed a couple of bugs around SSL support
Browse files Browse the repository at this point in the history
This commit fixes a couple of bugs present in both master and 2.3:

1. We didn't implement some options that needed to be passed to the LDAP
   backend to fully support SSL connections. This has been addressed
   also in the configuration, but without breaking existing
   installations (e.g. the `method` attribute from 2.3 has been left
   untouched). This will be addressed in later commits of the master
   branch (so in 2.4 users should adapt to this change).
2. We were relying on Devise's translations for failures, but some of
   them were not available. This has been addressed and improved: the
   error message will be more on point and more informative to end
   users.

There is still room for improvement, but we can do it in later commits:
let's keep this commit to the point so it can be cherry-picked into the
2.3 branch.

Fixes SUSE#1746
Fixes SUSE#1774

bsc#1073232

Signed-off-by: Miquel Sabaté Solà <[email protected]>
  • Loading branch information
mssola committed May 10, 2018
1 parent 48aafec commit 2c559f4
Show file tree
Hide file tree
Showing 11 changed files with 654 additions and 71 deletions.
59 changes: 59 additions & 0 deletions bin/check_services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

##
# TODO: this should be re-purposed once we support health for LDAP

require "net/ldap"

puts case Portus::DB.ping
when :ready
"DB_READY"
when :empty
"DB_EMPTY"
when :missing
"DB_MISSING"
when :down
"DB_DOWN"
else
"DB_UNKNOWN"
end

params = { host: APP_CONFIG["ldap"]["hostname"], port: APP_CONFIG["ldap"]["port"] }

# Fill authentication details.
if APP_CONFIG.enabled?("ldap.authentication")
params[:auth] = {
method: :simple,
username: APP_CONFIG["ldap"]["authentication"]["bind_dn"],
password: APP_CONFIG["ldap"]["authentication"]["password"]
}
end

# Fill TLS options with the given env. variables or assume defaults.
if APP_CONFIG["ldap"]["encryption"]["method"].present?
params[:encryption] = { method: APP_CONFIG["ldap"]["encryption"]["method"].to_sym }

if APP_CONFIG["ldap"]["encryption"]["options"]["ca_file"].present?
params[:encryption][:tls_options] = {
ca_file: APP_CONFIG["ldap"]["encryption"]["options"]["ca_file"],
ssl_version: APP_CONFIG["ldap"]["encryption"]["options"]["ssl_version"]
}
else
params[:encryption][:tls_options] = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
end
end

if APP_CONFIG.disabled?("ldap")
puts "LDAP_DISABLED"
else
ldap = Net::LDAP.new(params)
begin
if ldap.bind
puts "LDAP_OK"
else
puts "LDAP_FAIL"
end
rescue Net::LDAP::Error
puts "LDAP_FAIL"
end
end
68 changes: 63 additions & 5 deletions bin/integration/integration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,22 @@
require "portus/cmd"
require "portus/test"

# Returns the file to be used for the given profile.
def compose_file
profile = case ENV["PORTUS_INTEGRATION_PROFILE"]
when /^ldap/
".ldap"
else
".clair"
end
"docker-compose#{profile}.yml"
end

##
# Configurable variables.

SOURCE_DIR = Rails.root.join("examples", "compose")
SOURCE_CONFIG = SOURCE_DIR.join("docker-compose.clair.yml")
SOURCE_CONFIG = SOURCE_DIR.join(compose_file)

##
# Auxiliar methods.
Expand Down Expand Up @@ -129,12 +140,11 @@ def cn
puts yml.to_yaml if ENV["CI"]

##
# Create build directory and spit the output there.
# Create build directory if needed and spit the output there.

FileUtils.rm_rf(Rails.root.join("build"))
FileUtils.mkdir_p(Rails.root.join("build", "secrets"), mode: 0o755)
FileUtils.mkdir_p(Rails.root.join("build", "secrets", "ldap"), mode: 0o755)

dst = Rails.root.join("build", "docker-compose.yml")
dst = Rails.root.join("build", compose_file)
log :info, "File to be used: #{dst}"
File.open(dst, "w+") { |f| f.write(yml.to_yaml) }

Expand Down Expand Up @@ -180,3 +190,51 @@ def cn
secrets = Rails.root.join("build", "secrets")
File.open(secrets.join("portus.key"), "w+") { |f| f.write(key.to_pem) }
File.open(secrets.join("portus.crt"), "w+") { |f| f.write(cert.to_pem) }

##
# Certificates for LDAP
# TODO: join if possible with the ones above
# TODO: DIY

root_key = OpenSSL::PKey::RSA.new 2048 # the CA's public/private key
root_ca = OpenSSL::X509::Certificate.new
root_ca.version = 2 # cf. RFC 5280 - to make it a "v3" certificate
root_ca.serial = 1
root_ca.subject = OpenSSL::X509::Name.parse "/C=DE/ST=Bayern/L=Nürnberg/O=SUSE/OU=Org/CN=ldap"
root_ca.issuer = root_ca.subject # root CA's are "self-signed"
root_ca.public_key = root_key.public_key
root_ca.not_before = Time.zone.now
root_ca.not_after = root_ca.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = root_ca
ef.issuer_certificate = root_ca
root_ca.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
root_ca.add_extension(ef.create_extension("keyUsage", "keyCertSign, cRLSign", true))
root_ca.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
root_ca.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false))
root_ca.sign(root_key, OpenSSL::Digest::SHA256.new)
ldap_secrets = Rails.root.join("build", "secrets", "ldap")
File.open(ldap_secrets.join("ca.crt"), "wb") { |f| f.print root_ca.to_pem }
File.open(ldap_secrets.join("ca.key"), "wb") { |f| f.print root_key.to_s }
File.open(ldap_secrets.join("ca.pem"), "wb") { |f| f.print(root_ca.to_pem + root_key.to_s) }

key = OpenSSL::PKey::RSA.new 2048
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 2
cert.subject = OpenSSL::X509::Name.parse "/C=DE/ST=Bayern/L=Nürnberg/O=SUSE/OU=Org/CN=ldap"
cert.issuer = root_ca.subject # root CA is the issuer
cert.public_key = key.public_key
cert.not_before = Time.zone.now
cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60 # 1 years validity
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = root_ca
cert.add_extension(ef.create_extension("keyUsage", "digitalSignature", true))
cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
cert.add_extension(ef.create_extension("subjectAltName", "DNS:ldap", false))
cert.sign(root_key, OpenSSL::Digest::SHA256.new)

File.open(ldap_secrets.join("ldap.crt"), "wb") { |f| f.print cert.to_pem }
File.open(ldap_secrets.join("ldap.key"), "wb") { |f| f.print key.to_s }
File.open(ldap_secrets.join("ldap.pem"), "wb") { |f| f.print(cert.to_pem + key.to_s) }
174 changes: 117 additions & 57 deletions bin/test-integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,85 +22,145 @@ if [ ! -f "$ROOT_DIR/bin/integration/init" ]; then
fi
chmod +x $ROOT_DIR/bin/integration/init

# Generate the build directory.
bundle exec rails runner $ROOT_DIR/bin/integration/integration.rb

##
# Start containers.
# Functions for starting/cleaning containers and running tests.

# It will kill and remove all containers related to integration testing.
cleanup_containers() {
pushd "$ROOT_DIR/build"
docker-compose kill
docker-compose rm -f
docker-compose -f $1 kill
docker-compose -f $1 rm -f
popd
}

if [[ ! "$SKIP_ENV_TESTS" ]]; then
cleanup_containers
pushd "$ROOT_DIR/build"
docker-compose up -d
popd
# Start the containers depending on the profile to be picked.
start_containers() {
if [[ ! "$SKIP_ENV_TESTS" ]]; then
cleanup_containers $1
pushd "$ROOT_DIR/build"
docker-compose -f $1 up -d
popd

# We will wait 10 minutes until everything is properly set up.
TIMEOUT=600
COUNT=0
RETRY=1

DB=0
LDAP=0

while [ $RETRY -ne 0 ]; do
msg=$(SKIP_MIGRATION=1 docker exec $CNAME portusctl exec rails r /srv/Portus/bin/check_services.rb)
case $(echo "$msg" | grep DB) in
"DB_READY")
DB=1
;;
*)
echo "Database is not ready yet:"
echo $msg
;;
esac

case $(echo "$msg" | grep LDAP) in
"LDAP_DISABLED"|"LDAP_OK")
LDAP=1
;;
*)
echo "LDAP is not ready yet"
;;
esac

if (( "$DB" == "1" )) && (( "$LDAP" == "1" )); then
echo "Let's go!"
break
fi

# We will wait 10 minutes until everything is properly set up.
TIMEOUT=600
COUNT=0
RETRY=1
if [ "$COUNT" -ge "$TIMEOUT" ]; then
echo "[integration] Timeout reached, exiting with error"
cleanup_containers $1
exit 1
fi

while [ $RETRY -ne 0 ]; do
msg=$(SKIP_MIGRATION=1 docker exec $CNAME portusctl exec rails r /srv/Portus/bin/check_db.rb)
case $(echo "$msg" | grep DB) in
"DB_READY")
echo "Database ready"
break
;;
*)
echo "Database is not ready yet:"
echo $msg
;;
esac

if [ "$COUNT" -ge "$TIMEOUT" ]; then
echo "[integration] Timeout reached, exiting with error"
cleanup_containers
exit 1
fi
sleep 5
COUNT=$((COUNT+5))
done

sleep 5
COUNT=$((COUNT+5))
done
echo "You may want to set the 'SKIP_ENV_TESTS' env. variable for successive runs..."

echo "You may want to set the 'SKIP_ENV_TESTS' env. variable for successive runs..."
# Travis oddities...
if [ ! -z "$CI" ]; then
sleep 10
fi
fi
}

# Travis oddities...
if [ ! -z "$CI" ]; then
sleep 10
# Run tests.
run_tests() {
tests=()
if [[ -z "$TESTS" ]]; then
tests=($ROOT_DIR/spec/integration/$1*.bats)
else
for f in $TESTS; do
tests+=("$ROOT_DIR/spec/integration/$1$f.bats")
done
fi
fi
echo "Running: ${tests[*]}"
bats -t ${tests[*]}
}

##
# Setup environment

# We build the development image only once.
export PORTUS_INTEGRATION_BUILD_IMAGE=false
pushd $ROOT_DIR
docker rmi -f opensuse/portus:development
docker build -t opensuse/portus:development .
popd

# Integration tests will play with the following images
export DEVEL_NAME="busybox"
export DEVEL_IMAGE="$DEVEL_NAME:latest"
docker pull $DEVEL_IMAGE

# Run tests.
tests=()
if [[ -z "$TESTS" ]]; then
tests=($ROOT_DIR/spec/integration/*.bats)
# Remove current build directory
rm -rf "$ROOT_DIR/build"

##
# Profiles

profiles=()
if [[ -z "$PROFILES" ]]; then
profiles=("clair" "ldap")
else
for f in $TESTS; do
tests+=("$ROOT_DIR/spec/integration/$f.bats")
for p in $PROFILES; do
profiles+=("$p")
done
fi
set +e
echo "Running: ${tests[*]}"
bats -t ${tests[*]}
status=$?
set -e

# Tear down
if [[ "$TEARDOWN_TESTS" ]]; then
cleanup_containers
fi
##
# Actual run.

for p in ${profiles[*]}; do
export PORTUS_INTEGRATION_PROFILE="$p"
bundle exec rails runner $ROOT_DIR/bin/integration/integration.rb
start_containers "docker-compose.$p.yml"

prefix=""
if [ "$p" != "clair" ]; then
prefix="$p/"
fi

set +e
run_tests $prefix
status=$?
set -e

cleanup_containers "docker-compose.$p.yml"

if [ $status -ne 0 ]; then
exit $status
fi
done

exit $status
exit 0
16 changes: 14 additions & 2 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,22 @@ ldap:
hostname: "ldap_hostname"
port: 389

# Available options: "plain", "simple_tls" and "starttls". The default is
# "plain", the recommended is "starttls".
# Available options: "plain", "simple_tls" and "start_tls".
# TODO: deprecated in favor of `encryption.method`.
method: "plain"

# Encryption options
encryption:
# Available methods: "plain", "simple_tls" and "start_tls".
method: ""
options:
# The CA file to be accepted by the LDAP server. If none is provided, then
# the default parameters from the host will be sent.
ca_file: ""

# Protocol version.
ssl_version: "TLSv1_2"

# The base where users are located (e.g. "ou=users,dc=example,dc=com").
base: ""

Expand Down
Loading

0 comments on commit 2c559f4

Please sign in to comment.