diff --git a/.kitchen.yml b/.kitchen.yml index 3cbdd73..c64d577 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -10,6 +10,7 @@ driver: provisioner: name: chef_zero require_chef_omnibus: true + data_bags_path: ./test/data_bags # client_rb: # treat_deprecation_warnings_as_errors: true # WiP on some depends @@ -49,7 +50,7 @@ suites: - centos-6 - centos-6.6 - centos-7 - - centos-7.0 + - centos-7.2 - fedora-20 - fedora-21 - opensuse-12.3 @@ -61,3 +62,20 @@ suites: - name: attributes run_list: - recipe[dovecot_test::attributes] +- name: create_pwfile + run_list: + - recipe[dovecot_test::create_pwfile] + excludes: + # tests are not working on older versions of dovecot + - centos-6.7 + - debian-8.2 + - ubuntu-12.04 + - ubuntu-15.10 + - ubuntu-16.04 + - fedora-20 + - fedora-21 + - opensuse-12.3 + - opensuse-13.2 + - oraclelinux-6 + - scientific-6.6 + diff --git a/.rubocop.yml b/.rubocop.yml index 387191b..2574da7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,3 +4,5 @@ AllCops: - vendor/**/* Metrics/ModuleLength: Max: 121 +Metrics/LineLength: + Max: 121 diff --git a/.travis.yml b/.travis.yml index 11bbf7a..636fe47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,9 @@ env: - TESTS="integration[attributes-ubuntu-1510,verify]" - TESTS="integration[attributes-ubuntu-1604,verify]" - TESTS="integration[attributes-scientific-66,verify]" + - TESTS="integration[create-pwfile-centos-72,verify]" + - TESTS="integration[create-pwfile-debian-79,verify]" + - TESTS="integration[create-pwfile-ubuntu-14.04,verify]" before_install: - chef --version &> /dev/null || curl -L https://www.getchef.com/chef/install.sh | sudo bash -s -- -P chefdk -v 1.2.22 diff --git a/README.md b/README.md index cb16ec6..9d67da7 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Table of Contents * [dovecot::ohai_plugin](#dovecotohai_plugin) * [dovecot::from_package](#dovecotfrom_package) * [dovecot::service](#dovecotservice) + * [dovecot::create_pwfile](#dovecotcreate_pwfile) * [Ohai Plugin](#ohai-plugin) * [Usage Examples](#usage-examples) * [Including in a Cookbook Recipe](#including-in-a-cookbook-recipe) @@ -60,6 +61,7 @@ Table of Contents * [Quota-status Service Example](#quota-status-service-example) * [Quota-warning Service Example](#quota-warning-service-example) * [LDAP Example](#ldap-example) + * [Password File Example](#password-file-example) * [A Complete Example](#a-complete-example) * [Testing](#testing) * [Contributing](#contributing) @@ -126,6 +128,9 @@ To see a more complete description of the attributes, go to the [Dovecot wiki2 c | `node['dovecot']['services']` | `{}` | Dovecot Services configuration as a hash of hashes ([see the examples below](#service-examples)). Supported services: anvil, director, imap-login, pop3-login, lmtp, imap, pop3, auth, auth-worker, dict, tcpwrap, managesieve-login managesieve, quota-status, quota-warning and doveadm. | `node['dovecot']['conf']['mail_plugins']` | `[]` | Dovecot default enabled mail_plugins. | `node['dovecot']['ohai_plugin']['build-options']` | `true` | Whether to enable reading build options inside ohai plugin. Can be disabled to be lighter. +| `node['dovecot']['databag_name']` | `dovecot` | The databag to use. +| `node['dovecot']['databag_users_item']` | `users` | The databag item to use for User's database (Passwords). +| `node['dovecot']['conf']['password_file']` | `#{node['dovecot']['conf_path']}/password` | The Password file location ## Main Configuration Attributes @@ -465,6 +470,14 @@ Installs the required packages. Used by the default recipe if `node['dovecot'][' Configures the Dovecot service. Used by the default recipe. +## dovecot::create_pwfile + +Creates and configures a password file from local mailboxes based on a data bag. + + +* `node['dovecot']['databag_name']`: The Databag on which items are stored. +* `node['dovecot']['databag_users_item']`: The databag item to use (under the databag set) + Ohai Plugin =========== @@ -895,6 +908,46 @@ node.default['dovecot']['conf']['ldap']['base'] = node['openldap']['basedn'] include_recipe 'dovecot' ``` +## Password File Example + +This is an example how to use userdb password file. + +```ruby +# Define databag and item inside Databag (default.conf) +node.default['dovecot']'databag_name'] = 'dovecot' +node.default['dovecot']['databag_users_item'] = 'users' + +# Attributes for userdb to function +node.default['dovecot']['auth']['passwdfile'] = { + 'passdb' => { + 'driver' => 'passwd-file', + 'args' => node['dovecot']['conf']['password_file'] + }, + 'userdb' => { + 'driver' => 'passwd-file', + 'args' => "username_format=%u #{node['dovecot']['conf']['password_file']}", + 'default_fields' => 'home=/var/dovecot/vmail/%d/%n' + } +} + + +#include this recipe on your +include_recipe 'dovecot::pwfile-file' +``` + +Databag example. +Two ways of defining an user example included + +```json +{ + "users": { + "dilan": "password1234", + "vassilis": [ + "vassilis1234",null,null,null,null,null,null + ] + } +} +``` ## A Complete Example This is a complete recipe example for installing and configuring Dovecot 2 to work with PostfixAdmin MySQL tables, including IMAP service: diff --git a/attributes/conf_10_ssl.rb b/attributes/conf_10_ssl.rb index 659f57d..c1acc02 100644 --- a/attributes/conf_10_ssl.rb +++ b/attributes/conf_10_ssl.rb @@ -21,11 +21,10 @@ # conf.d/10-ssl.conf -if node['platform_family'] == 'suse' && node['platform_version'].to_i < 13 - default['dovecot']['conf']['ssl'] = false -else - default['dovecot']['conf']['ssl'] = nil -end +default['dovecot']['conf']['ssl'] = \ + if node['platform_family'] == 'suse' && node['platform_version'].to_i < 13 + false + end case node['platform_family'] when 'rhel', 'fedora' diff --git a/attributes/conf_files.rb b/attributes/conf_files.rb index 4ddd62e..ff9c0b7 100644 --- a/attributes/conf_files.rb +++ b/attributes/conf_files.rb @@ -23,6 +23,8 @@ default['dovecot']['conf_files_user'] = 'root' default['dovecot']['conf_files_group'] = node['dovecot']['group'] default['dovecot']['conf_files_mode'] = '00644' +default['dovecot']['conf']['password_file'] = + "#{node['dovecot']['conf_path']}/password" default['dovecot']['sensitive_files'] = %w( *.conf.ext diff --git a/attributes/default.rb b/attributes/default.rb index d06934c..b3a760a 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -34,3 +34,5 @@ default['dovecot']['user'] = 'dovecot' default['dovecot']['group'] = node['dovecot']['user'] default['dovecot']['user_homedir'] = node['dovecot']['lib_path'] +default['dovecot']['databag_name'] = 'dovecot' +default['dovecot']['databag_users_item'] = 'users' diff --git a/libraries/pwfile.rb b/libraries/pwfile.rb new file mode 100644 index 0000000..951c19e --- /dev/null +++ b/libraries/pwfile.rb @@ -0,0 +1,102 @@ +# encoding: UTF-8 +# +# Cookbook Name:: dovecot +# Library:: Pwfile +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2013-2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +module DovecotCookbook + # Helper module to check password file and import it + module Pwfile + extend Chef::Mixin::ShellOut + + def self.exists?(localdata) + ::File.exist?(localdata) + end + + def self.file_to_hash(inputfile) + output_entries = {} + File.open(inputfile, File::RDONLY | File::CREAT, 640) do |passwordfile| + passwordfile.readlines.each do |line| + user, data = fileline_to_userdb_hash(line) + output_entries[user] = data + end + end + output_entries + end + + def self.passfile_read(input) + # Returns Password file in userdb style hash and if exists + [file_to_hash(input), exists?(input)] + end + + def self.fileline_to_userdb_hash(input) + # Returns a hash of details daken from the userdb file line + data = [nil] * 7 + if input.strip.split(':').length == 2 + user, data[0] = input.strip.split(':') + else + user = input.strip.split(':')[0] + data = input.strip.split(':')[1..7] + end + [user, data] + end + + def self.dbentry_to_array(key, value) + # Returns an array with 8 values to use with user copy. + if value.is_a?(Array) + [key] + (value + ([nil] * (7 - value.size))) + else + [key, value] + ([nil] * 6) + end + end + + def self.password_valid?(hashed_pw, plaintext_pw) + shell_out("/usr/bin/doveadm pw -t '#{hashed_pw}' -p '#{plaintext_pw}'") + .exitstatus == 0 + end + + def self.arrays_same?(array1, array2) + true if (array1 - array2).empty? && (array2 - array1).empty? + end + + def self.encrypt_password(plaintextpass) + # Return the password generated by dovecot and remove newline + shell_out("/usr/bin/doveadm pw -s MD5 -p \ + #{plaintextpass}").stdout.tr("\n", '') + end + + def self.generate_userpass(input_creds, plaintextpass, updated, file_exists) + if !input_creds.nil? && file_exists == true + if password_valid?( + input_creds[0], plaintextpass + ) + return [input_creds[0], updated] + end + end + [encrypt_password(plaintextpass), true] + end + + def self.compile_users(databag_users, current_users, pwfile_exists, updated, credentials) + databag_users.each do |username, user_details| + current_user = dbentry_to_array(username, user_details) + current_user[1], updated = generate_userpass(current_users[username], current_user[1], updated, pwfile_exists) + credentials.push(current_user) + end + updated + end + end +end diff --git a/metadata.rb b/metadata.rb index 62f5068..91262a0 100644 --- a/metadata.rb +++ b/metadata.rb @@ -27,7 +27,7 @@ Installs and configures Dovecot, open source IMAP and POP3 email server. EOH long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) -version '3.2.0' # WiP +version '3.2.0' # WIP if respond_to?(:source_url) source_url "https://github.com/zuazo/#{name}-cookbook" @@ -55,6 +55,7 @@ 'Provides an Ohai plugin for reading dovecot install information.' recipe 'dovecot::from_package', 'Installs the required packages.' recipe 'dovecot::service', 'Configures the Dovecot service.' +recipe 'dovecot::create_pwfile', 'Creates a userdb password file from databag.' attribute 'dovecot/install_from', display_name: 'dovecot install method', @@ -79,6 +80,20 @@ required: 'optional', default: '"dovecot"' +attribute 'dovecot/databag_name', + display_name: 'Databag name', + description: 'The name of the databag to use', + type: 'string', + required: 'optional', + default: '"dovecot"' + +attribute 'dovecot/databag_users_item', + display_name: 'Databag users item name', + description: 'The name item to put the users in', + type: 'string', + required: 'optional', + default: '"users"' + attribute 'dovecot/user_homedir', display_name: 'dovecot homedir', description: 'Dovecot system user home directory.', @@ -122,6 +137,13 @@ required: 'optional', default: '00644' +attribute 'dovecot/conf/password_file', + display_name: 'path of password file', + description: 'The path and filename of the password file', + type: 'string', + required: 'optional', + default: 'node["dovecot"]["conf_path"]/password' + attribute 'dovecot/sensitive_files', display_name: 'dovecot sensitve files', description: diff --git a/recipes/create_pwfile.rb b/recipes/create_pwfile.rb new file mode 100644 index 0000000..1988cb5 --- /dev/null +++ b/recipes/create_pwfile.rb @@ -0,0 +1,60 @@ +# +# Cookbook Name:: dovecot +# Recipe:: create_pwfile +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2013-2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# The file credentials should be like: +# user:password:uid:gid:(gecos):home:(shell):extra_fields +# We ignore gecos and shell, all the others are included in the script but +# uid,gid,home,extra_Fields can be nil +# user:pass:::::: + +# Predefined Variables +credentials = [] +update_credentials = false + +ruby_block 'databag_to_dovecot_userdb' do + block do + passwd_file = node['dovecot']['conf']['password_file'] + databag_users = data_bag_item(node['dovecot']['databag_name'], node['dovecot']['databag_users_item'])['users'] + + # Check if passwd file exists + local_creds, pwfile_exists = DovecotCookbook::Pwfile.passfile_read(passwd_file) + # Check if users on both passwd file and databag are the same + # if not, force credentials update + update_credentials = true unless DovecotCookbook::Pwfile.arrays_same?(databag_users.keys, local_creds.keys) + # Check if users has a changed password, if not change it and force update + update_credentials = DovecotCookbook::Pwfile.compile_users(databag_users, + local_creds, + pwfile_exists, + update_credentials, + credentials) + end + action :run +end + +template node['dovecot']['conf']['password_file'] do + source 'password.erb' + owner node['dovecot']['user'] + group node['dovecot']['group'] + mode '0640' + sensitive true + variables( + credentials: credentials + ) + only_if { update_credentials } +end diff --git a/templates/default/password.erb b/templates/default/password.erb new file mode 100644 index 0000000..5cf5b78 --- /dev/null +++ b/templates/default/password.erb @@ -0,0 +1,3 @@ +<% @credentials.each do |user, password, uid, gid, gecos, homedir, shell, extra_fields| -%> +<%= user %>:<%= password %>:<%= uid %>:<%= gid %>::<%= homedir %>::<%= extra_fields %> +<% end -%> diff --git a/test/cookbooks/dovecot_test/recipes/create_pwfile.rb b/test/cookbooks/dovecot_test/recipes/create_pwfile.rb new file mode 100644 index 0000000..ae9d338 --- /dev/null +++ b/test/cookbooks/dovecot_test/recipes/create_pwfile.rb @@ -0,0 +1,79 @@ +# encoding: UTF-8 +# +# Cookbook Name:: dovecot_test +# Recipe:: default +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2013-2015 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +maildir = '/var/dovecot/vmail' + +node.default['dovecot']['auth']['passwdfile'] = { + 'passdb' => { + 'driver' => 'passwd-file', + 'args' => node['dovecot']['conf']['password_file'] + }, + 'userdb' => { + 'driver' => 'passwd-file', + 'args' => "username_format=%u #{node['dovecot']['conf']['password_file']}", + 'default_fields' => "home=#{maildir}/%d/%n" + } +} +node.default['dovecot']['services'] = { + 'auth' => { + 'listeners' => [ + { + 'unix:auth-passdb' => { + 'mode' => '0600', + 'user' => 'dovecot', + 'group' => 'dovecot' + } + } + ] + }, + 'config' => { + 'listeners' => [ + { + 'unix:config' => { + 'user' => 'dovecot' + } + } + ] + } +} + +node.default['dovecot']['protocols']['imap'] = {} +node.default['dovecot']['protocols']['pop3'] = {} +node.default['dovecot']['protocols']['lda'] = + { 'mail_plugins' => %w($mail_plugins) } +node.default['dovecot']['conf']['mail_uid'] = 'dovecot' +node.default['dovecot']['conf']['mail_gid'] = 'dovecot' +node.default['dovecot']['conf']['mail_location'] = 'maildir:~/Maildir' + +include_recipe 'dovecot_test' +include_recipe 'dovecot::create_pwfile' + +['/var/dovecot', maildir].each do |dir_to_create| + directory dir_to_create do + owner node.default['dovecot']['user'] + group node.default['dovecot']['user'] + action :create + end +end + +# Required for integration tests: +package 'lsof' +include_recipe 'netstat' diff --git a/test/data_bags/dovecot/users.json b/test/data_bags/dovecot/users.json new file mode 100644 index 0000000..c591c05 --- /dev/null +++ b/test/data_bags/dovecot/users.json @@ -0,0 +1,8 @@ +{ + "users": { + "dilan": "password1234", + "vassilis": [ + "vassilis1234",null,null,null,null,null,null + ] + } +} diff --git a/test/integration/create_pwfile/bats/default.bats b/test/integration/create_pwfile/bats/default.bats new file mode 100644 index 0000000..af19c3c --- /dev/null +++ b/test/integration/create_pwfile/bats/default.bats @@ -0,0 +1,14 @@ +#!/usr/bin/env bats + +@test "doveconf runs without errors" { + doveconf > /dev/null +} + +@test "ordinary files has the correct mode" { + ! stat -c '%a' /etc/dovecot/*.conf.ext | grep -v -qwF 640 +} + +@test "sensitive files has restricted mode" { + ! stat -c '%a' /etc/dovecot/conf.d/* | grep -v -qwF 644 +} + diff --git a/test/integration/create_pwfile/bats/imap.bats b/test/integration/create_pwfile/bats/imap.bats new file mode 100644 index 0000000..558b860 --- /dev/null +++ b/test/integration/create_pwfile/bats/imap.bats @@ -0,0 +1,10 @@ +#!/usr/bin/env bats + +@test "dovecot listens in the imap port" { + lsof -cdovecot -a -iTCP:imap2 +} + +@test "dovecot listens in the imaps port" { + lsof -cdovecot -a -iTCP:imaps +} + diff --git a/test/integration/create_pwfile/bats/ohai.bats b/test/integration/create_pwfile/bats/ohai.bats new file mode 100644 index 0000000..3bba454 --- /dev/null +++ b/test/integration/create_pwfile/bats/ohai.bats @@ -0,0 +1,15 @@ +#!/usr/bin/env bats + +setup() { + PLUGINS_DIR=/tmp/kitchen/ohai/plugins +} + +@test "ohai gets dovecot version" { + unset BUSSER_ROOT GEM_HOME GEM_PATH GEM_CACHE + ohai -d $PLUGINS_DIR | tr -d $'\n' | grep -q '"dovecot":\s*{\s*"version"' +} + +@test "ohai prints nothing to stderr" { + unset BUSSER_ROOT GEM_HOME GEM_PATH GEM_CACHE + [ -z "`ohai -d $PLUGINS_DIR 2>&1 > /dev/null`" ] +} diff --git a/test/integration/create_pwfile/bats/pop3.bats b/test/integration/create_pwfile/bats/pop3.bats new file mode 100644 index 0000000..62a2f39 --- /dev/null +++ b/test/integration/create_pwfile/bats/pop3.bats @@ -0,0 +1,7 @@ +#!/usr/bin/env bats + +@test "dovecot listens in the imap port" { + lsof -cdovecot -a -iTCP:pop3 +} + + diff --git a/test/integration/create_pwfile/serverspec/Gemfile b/test/integration/create_pwfile/serverspec/Gemfile new file mode 100644 index 0000000..99fd83c --- /dev/null +++ b/test/integration/create_pwfile/serverspec/Gemfile @@ -0,0 +1,7 @@ +# encoding: UTF-8 +# -*- mode: ruby -*- +# vi: set ft=ruby : + +source 'https://rubygems.org' + +gem 'serverspec', '~> 2.0' diff --git a/test/integration/create_pwfile/serverspec/dovecot_spec.rb b/test/integration/create_pwfile/serverspec/dovecot_spec.rb new file mode 100644 index 0000000..b797b5c --- /dev/null +++ b/test/integration/create_pwfile/serverspec/dovecot_spec.rb @@ -0,0 +1,24 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe process('dovecot') do + it { should be_running } +end diff --git a/test/integration/create_pwfile/serverspec/files_spec.rb b/test/integration/create_pwfile/serverspec/files_spec.rb new file mode 100644 index 0000000..12d7799 --- /dev/null +++ b/test/integration/create_pwfile/serverspec/files_spec.rb @@ -0,0 +1,133 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +def absolute_path(f) + f[0] == '/' ? f : "/etc/dovecot/#{f}" +end + +platform = os[:family].downcase +platform_version = os[:release].to_f + +ssl_cert, ssl_key = + case platform + when 'redhat', 'centos', 'scientific', 'fedora', 'amazon' + %w(/etc/pki/dovecot/certs/dovecot.pem /etc/pki/dovecot/private/dovecot.pem) + when 'debian' + platform_version >= 8 ? [nil, nil] : %w(dovecot.pem private/dovecot.pem) + when 'ubuntu' + if platform_version >= 15.10 + [nil, nil] + elsif platform_version >= 14 + %w(dovecot.pem private/dovecot.pem) + else + %w(/etc/ssl/certs/dovecot.pem /etc/ssl/private/dovecot.pem) + end + when 'suse', 'opensuse' + [nil, nil] + else + %w(dovecot.pem private/dovecot.pem) + end + +normal_files = %w( + dovecot.conf + conf.d/10-mail.conf + conf.d/10-logging.conf + conf.d/auth-vpopmail.conf.ext + conf.d/auth-static.conf.ext + conf.d/auth-dict.conf.ext + conf.d/15-lda.conf + conf.d/10-tcpwrapper.conf + conf.d/10-director.conf + conf.d/90-plugin.conf + conf.d/90-quota.conf + conf.d/15-mailboxes.conf + conf.d/auth-passwdfile.conf.ext + conf.d/auth-checkpassword.conf.ext + conf.d/10-master.conf + conf.d/auth-deny.conf.ext + conf.d/90-acl.conf + conf.d/10-auth.conf + conf.d/auth-system.conf.ext + conf.d/10-ssl.conf + conf.d/auth-sql.conf.ext + conf.d/20-imap.conf + conf.d/auth-master.conf.ext +) + +sensitive_files = %w( + dovecot-sql.conf.ext + dovecot-dict-auth.conf.ext + dovecot-db.conf.ext + dovecot-dict-sql.conf.ext +) + +normal_files.each do |f| + describe file(absolute_path(f)) do + it { should be_file } + it { should be_mode 644 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'dovecot' } + it { should be_readable.by_user('dovecot') } + it { should_not be_writable.by_user('dovecot') } + end +end + +sensitive_files.each do |f| + describe file(absolute_path(f)) do + it { should be_file } + it { should be_mode 640 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'dovecot' } + it { should be_readable.by_user('dovecot') } + it { should_not be_writable.by_user('dovecot') } + end +end + +describe file('/etc/dovecot/password') do + it { should be_file } + it { should be_mode 640 } + it { should be_owned_by 'dovecot' } + it { should be_grouped_into 'dovecot' } + it { should be_readable.by_user('dovecot') } + it { should be_writable.by_user('dovecot') } +end + +ssl_key_files = [ssl_key].compact +ssl_cert_files = [ssl_cert].compact + +ssl_cert_files.each do |f| + describe file(absolute_path(f)) do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_readable.by_user('root') } + end +end + +ssl_key_files.each do |f| + describe file(absolute_path(f)) do + it { should be_file } + it { should be_mode 600 } + it { should be_owned_by 'root' } + it { should be_readable.by_user('root') } + it { should_not be_readable.by_user('dovecot') } + it { should_not be_writable.by_user('dovecot') } + end +end diff --git a/test/integration/create_pwfile/serverspec/imap_spec.rb b/test/integration/create_pwfile/serverspec/imap_spec.rb new file mode 100644 index 0000000..4b3998b --- /dev/null +++ b/test/integration/create_pwfile/serverspec/imap_spec.rb @@ -0,0 +1,27 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe imap_server('localhost') do + it { should connect } + it { should authenticate 'vassilis', 'vassilis1234' } + it { should authenticate 'dilan', 'password1234' } + it { should_not authenticate 'vassilis', 'vassilis123' } +end diff --git a/test/integration/create_pwfile/serverspec/spec_helper.rb b/test/integration/create_pwfile/serverspec/spec_helper.rb new file mode 100644 index 0000000..546c0cc --- /dev/null +++ b/test/integration/create_pwfile/serverspec/spec_helper.rb @@ -0,0 +1,24 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'serverspec' +require 'type/imap_server' + +# Set backend type +set :backend, :exec diff --git a/test/integration/create_pwfile/serverspec/type/imap_server.rb b/test/integration/create_pwfile/serverspec/type/imap_server.rb new file mode 100644 index 0000000..006c4c5 --- /dev/null +++ b/test/integration/create_pwfile/serverspec/type/imap_server.rb @@ -0,0 +1,89 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2015 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'net/imap' + +module Serverspec + # Serverspec resource types. + module Type + # Serverspec IMAP resource type. + class ImapServer < Base + # TODO: Silence stderr with IMAP exceptions + + def initialize(server, port) + @host = port.nil? ? server : "#{server}:#{port}" + super(@host) + end + + def to_s + %(IMAP "imap://#{@host}") + end + + protected + + def imap + @imap ||= ::Net::IMAP.new(@host) + end + + def imap_close + return if @imap.nil? + @imap.close + rescue ::Net::IMAP::BadResponseError + nil + ensure + @imap = nil + end + + public + + def connects? + imap + imap_close + true + rescue ::Net::IMAP::NoResponseError + false + end + + def authenticates?(user, pass, auth_type) + imap.authenticate(auth_type, user, pass) + imap.examine('INBOX') + imap_close + true + rescue ::Net::IMAP::NoResponseError + false + end + end + + def imap_server(server, port = nil) + ::Serverspec::Type::ImapServer.new(server, port) + end + end +end + +include Serverspec::Type + +RSpec::Matchers.define :connect do + match(&:connects?) +end + +RSpec::Matchers.define :authenticate do |user, pass, auth_type = 'PLAIN'| + match do |subject| + subject.authenticates?(user, pass, auth_type) + end +end diff --git a/test/integration/create_pwfile/serverspec/user_spec.rb b/test/integration/create_pwfile/serverspec/user_spec.rb new file mode 100644 index 0000000..282a4d4 --- /dev/null +++ b/test/integration/create_pwfile/serverspec/user_spec.rb @@ -0,0 +1,43 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +platform = os[:family].downcase + +home = + case platform + when 'redhat', 'centos', 'scientific', 'fedora', 'amazon' + '/usr/libexec/dovecot' + when 'suse', 'opensuse' + '/var/run/dovecot' + else + '/usr/lib/dovecot' + end + +describe user('dovecot') do + it { should exist } + it { should belong_to_group 'dovecot' } + it { should have_home_directory home } + it { should have_login_shell '/bin/false' } +end + +describe group('dovecot') do + it { should exist } +end diff --git a/test/unit/recipes/conf_files_spec.rb b/test/unit/recipes/conf_files_spec.rb index 1f90639..141e91a 100644 --- a/test/unit/recipes/conf_files_spec.rb +++ b/test/unit/recipes/conf_files_spec.rb @@ -131,6 +131,7 @@ conf.d/10-tcpwrapper.conf conf.d/15-lda.conf conf.d/15-mailboxes.conf + conf.d/15-replication.conf conf.d/90-acl.conf conf.d/90-plugin.conf conf.d/90-quota.conf diff --git a/test/unit/recipes/create_pwfile_spec.rb b/test/unit/recipes/create_pwfile_spec.rb new file mode 100644 index 0000000..84721d1 --- /dev/null +++ b/test/unit/recipes/create_pwfile_spec.rb @@ -0,0 +1,72 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2014 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require_relative '../spec_helper' + +describe 'dovecot::create_pwfile', order: :random do + let(:chef_runner) { ChefSpec::SoloRunner.new } + let(:chef_run) { chef_runner.converge(described_recipe) } + let(:node) { chef_runner.node } + + it 'runs databag_to_dovecot_userdb ruby block' do + expect(chef_run).to run_ruby_block('databag_to_dovecot_userdb') + end + + describe 'inside ruby_block[databag_to_dovecot_userdb] resource' do + let(:chef_runner) { ChefSpec::SoloRunner.new(step_into: %w(ruby_block)) } + let(:data_bag_users) do + { + 'users' => { + 'dilan' => 'password1234', + 'vassilis' => ['vassilis1234', nil, nil, nil, nil, nil, nil] + } + } + end + let(:pwfile) { instance_double('File', readlines: [], close: nil) } + let(:encrypted_password) { 'encrypted_password' } + before do + # Stub ::File.open: + allow(::File).to receive(:open).and_call_original + allow(::File).to receive(:open).with('/etc/dovecot/password', any_args) + .and_return(pwfile) + + # Stub Data Bag: + stub_data_bag_item('dovecot', 'users').and_return(data_bag_users) + + # Stub some commands excuted inside the ruby_block: + # Using: https://github.com/zuazo/filesystem_resize-cookbook/blob/master/test/unit/support/stubs.rb + stub_shell_out( + '/usr/bin/doveadm pw -s MD5 -p password1234' + ).and_return_stdout(encrypted_password) + stub_shell_out( + '/usr/bin/doveadm pw -s MD5 -p vassilis1234' + ).and_return_stdout(encrypted_password) + end + + it 'creates password file template' do + expect(chef_run).to create_template('/etc/dovecot/password') + .with_path('/etc/dovecot/password') + .with_source('password.erb') + .with_owner('dovecot') + .with_group('dovecot') + .with_mode('0640') + .with_sensitive(true) + end + end +end diff --git a/test/unit/spec_helper.rb b/test/unit/spec_helper.rb index c7ac38c..8a5b255 100644 --- a/test/unit/spec_helper.rb +++ b/test/unit/spec_helper.rb @@ -25,6 +25,7 @@ require_relative 'support/coverage' require_relative 'support/conf_requirements' +require_relative 'support/stubs' RSpec.configure do |config| # Prohibit using the should syntax diff --git a/test/unit/support/stubs.rb b/test/unit/support/stubs.rb new file mode 100644 index 0000000..2ad65d1 --- /dev/null +++ b/test/unit/support/stubs.rb @@ -0,0 +1,62 @@ +# encoding: UTF-8 +# +# Author:: Xabier de Zuazo () +# Copyright:: Copyright (c) 2015 Onddo Labs, SL. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Class to stub Chef::Mixin::ShellOut#shell_out result. +class FakeShellOut + # Class to stub Chef::Mixin::ShellOut#shell_out status result. + class FakeShellOutStatus + def initialize(status) + @status = status + end + + def success? + @status == true + end + end + + attr_reader :status, :stdout + + def initialize + and_return_status(true) + and_return_stdout('') + end + + def and_return_status(status) + @status = FakeShellOutStatus.new(status) + self + end + + def and_return_stdout(stdout) + @stdout = stdout + self + end +end + +def stub_shell_out(cmd) + output = FakeShellOut.new + allow_any_instance_of(Chef::Mixin::ShellOut).to receive(:shell_out) + .with(cmd) + .and_return(output) + output +end + +def data_file(file) + data_dir = ::File.join(::File.dirname(__FILE__), '..', 'data') + ::IO.read(::File.join(data_dir, file)) +end