10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [22.2.0](https://github.com/theforeman/puppet-foreman/tree/22.2.0) (2023-02-21)

[Full Changelog](https://github.com/theforeman/puppet-foreman/compare/22.1.2...22.2.0)

**Implemented enhancements:**

- Fixes [\#36037](https://projects.theforeman.org/issues/36037) - Manage Redis service for Redis cache [\#1109](https://github.com/theforeman/puppet-foreman/pull/1109) ([ekohl](https://github.com/ekohl))
- Add basic external auth for API [\#1108](https://github.com/theforeman/puppet-foreman/pull/1108) ([ofedoren](https://github.com/ofedoren))
- bump puppet/systemd to \< 5.0.0 [\#1104](https://github.com/theforeman/puppet-foreman/pull/1104) ([jhoblitt](https://github.com/jhoblitt))

## [22.1.2](https://github.com/theforeman/puppet-foreman/tree/22.1.2) (2023-02-01)

[Full Changelog](https://github.com/theforeman/puppet-foreman/compare/22.1.1...22.1.2)
Expand Down
20 changes: 20 additions & 0 deletions manifests/config.pp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
}
}

if $foreman::rails_cache_store['type'] == 'redis' {
if $foreman::rails_cache_store['urls'] {
$redis_cache_urls = prefix($foreman::rails_cache_store['urls'], 'redis://')
} else {
include redis
$redis_cache_urls = ["redis://localhost:${redis::port}/0"]
}
} else {
$redis_cache_urls = undef
}

# Used in the settings template
$websockets_ssl_cert = pick($foreman::websockets_ssl_cert, $foreman::server_ssl_cert)
$websockets_ssl_key = pick($foreman::websockets_ssl_key, $foreman::server_ssl_key)
Expand Down Expand Up @@ -204,6 +215,10 @@
ssl_content => template('foreman/auth_gssapi.conf.erb'),
}

foreman::config::apache::fragment { 'external_auth_api':
ssl_content => template('foreman/external_auth_api.conf.erb'),
}

if $foreman::ipa_manage_sssd {
$sssd = pick(fact('foreman_sssd'), {})
$sssd_services = join(unique(pick($sssd['services'], []) + ['ifp']), ', ')
Expand All @@ -228,6 +243,11 @@
content => template('foreman/settings-external-auth.yaml.erb'),
order => '02',
}

foreman::settings_fragment { 'authorize_login_delegation_api.yaml':
content => template('foreman/settings-external-auth-api.yaml.erb'),
order => '03',
}
}
} else {
$foreman_socket_override = undef
Expand Down
1 change: 1 addition & 0 deletions manifests/config/apache.pp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@

if $ipa_authentication {
include apache::mod::authnz_pam
include apache::mod::auth_basic
include apache::mod::intercept_form_submit
include apache::mod::lookup_identity
include apache::mod::auth_gssapi
Expand Down
6 changes: 6 additions & 0 deletions manifests/init.pp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
#
# $ipa_authentication:: Enable configuration for external authentication via IPA
#
# $ipa_authentication_api:: Enable configuration for external authentication via IPA for API
#
# === Advanced parameters:
#
# $foreman_url:: URL on which foreman is going to run
Expand Down Expand Up @@ -249,6 +251,7 @@
Optional[String] $initial_organization = undef,
Optional[String] $initial_location = undef,
Boolean $ipa_authentication = false,
Boolean $ipa_authentication_api = false,
Optional[Stdlib::Absolutepath] $http_keytab = undef,
Boolean $gssapi_local_name = true,
String $pam_service = 'foreman',
Expand Down Expand Up @@ -321,6 +324,9 @@
if $ipa_authentication and $keycloak {
fail("${facts['networking']['hostname']}: External authentication via IPA and Keycloak are mutually exclusive.")
}
if !$ipa_authentication and $ipa_authentication_api {
fail("${facts['networking']['hostname']}: External authentication for API via IPA requires general external authentication to be enabled.")
}
} elsif $ipa_authentication {
fail("${facts['networking']['hostname']}: External authentication via IPA can only be enabled when Apache is used.")
} elsif $keycloak {
Expand Down
4 changes: 2 additions & 2 deletions metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "theforeman-foreman",
"version": "22.1.2",
"version": "22.2.0",
"author": "theforeman",
"summary": "Foreman server configuration",
"license": "GPL-3.0+",
Expand All @@ -14,7 +14,7 @@
"dependencies": [
{
"name": "puppet/systemd",
"version_requirement": ">= 3.1.0 < 4.0.0"
"version_requirement": ">= 3.1.0 < 5.0.0"
},
{
"name": "puppetlabs/apache",
Expand Down
55 changes: 47 additions & 8 deletions spec/acceptance/foreman_basic_spec.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
require 'spec_helper_acceptance'

describe 'Scenario: install foreman' do
describe 'Foreman' do
before(:context) { purge_foreman }

it_behaves_like 'an idempotent resource' do
let(:manifest) { 'include foreman' }
describe 'with default options' do
it_behaves_like 'an idempotent resource' do
let(:manifest) { 'include foreman' }
end

it_behaves_like 'the foreman application'

describe package('foreman-journald') do
it { is_expected.not_to be_installed }
end

describe package('foreman-telemetry') do
it { is_expected.not_to be_installed }
end
end

it_behaves_like 'the foreman application'
describe 'with Redis caching' do
it_behaves_like 'an idempotent resource' do
let(:manifest) do
<<~PUPPET
class { 'foreman':
rails_cache_store => { 'type' => 'redis' },
}
PUPPET
end
end

describe package('foreman-journald') do
it { is_expected.not_to be_installed }
it_behaves_like 'the foreman application'

# TODO: verify it works using the /api/ping endpoint
# https://projects.theforeman.org/issues/36113
end

describe package('foreman-telemetry') do
it { is_expected.not_to be_installed }
context 'GSSAPI auth enabled' do
before { on default, 'mkdir -p /etc/httpd && touch /etc/httpd/conf.keytab' }

it_behaves_like 'an idempotent resource' do
let(:manifest) do
<<-PUPPET
class { 'foreman':
ipa_authentication => true,
ipa_authentication_api => true,
# Stub out ipa enrollment
http_keytab => '/etc/httpd/conf.keytab',
ipa_manage_sssd => false,
}
PUPPET
end
end

it_behaves_like 'the foreman application', { expected_login_url_path: '/users/extlogin' }
end
end
26 changes: 19 additions & 7 deletions spec/acceptance/foreman_cli_plugins_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,26 @@ class { 'foreman::cli':
}
if $facts['os']['family'] == 'RedHat' {
include foreman::cli::ansible
include foreman::cli::azure
include foreman::cli::kubevirt
include foreman::cli::openscap
}
include foreman::cli::ansible
include foreman::cli::discovery
include foreman::cli::google
include foreman::cli::puppet
include foreman::cli::remote_execution
include foreman::cli::ssh
include foreman::cli::tasks
include foreman::cli::templates
include foreman::cli::webhooks
include foreman::cli::puppet
include foreman::cli::google
PUPPET
end
end

it_behaves_like 'hammer'

['discovery', 'remote_execution', 'ssh', 'tasks', 'templates', 'webhooks', 'puppet'].each do |plugin|
['ansible', 'discovery', 'google', 'puppet', 'remote_execution', 'ssh', 'tasks', 'templates', 'webhooks'].each do |plugin|
package_name = case fact('os.family')
when 'RedHat'
"rubygem-hammer_cli_foreman_#{plugin}"
Expand All @@ -46,6 +48,14 @@ class { 'foreman::cli':
it { is_expected.to be_installed }
end
end

if fact('os.family') == 'RedHat'
['azure_rm', 'kubevirt', 'openscap'].each do |plugin|
describe package("rubygem-hammer_cli_foreman_#{plugin}") do
it { is_expected.to be_installed }
end
end
end
end

if fact('os.family') == 'RedHat'
Expand Down Expand Up @@ -73,6 +83,7 @@ class { 'foreman::cli':
}
include foreman::cli::katello
include foreman::cli::virt_who_configure
Yumrepo['katello'] -> Class['foreman::cli::katello']
PUPPET
Expand All @@ -81,9 +92,10 @@ class { 'foreman::cli':

it_behaves_like 'hammer'

package_name = "rubygem-hammer_cli_katello"
describe package(package_name) do
it { is_expected.to be_installed }
['katello', 'foreman_virt_who_configure'].each do |plugin|
describe package("rubygem-hammer_cli_#{plugin}") do
it { is_expected.to be_installed }
end
end
end
end
Expand Down
24 changes: 24 additions & 0 deletions spec/classes/foreman_config_ipa_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
it { should contain_class('apache::mod::intercept_form_submit') }
it { should contain_class('apache::mod::lookup_identity') }
it { should contain_class('apache::mod::auth_gssapi') }
it { should contain_class('apache::mod::auth_basic') }

it 'should contain Apache fragments' do
should contain_foreman__config__apache__fragment('intercept_form_submit')
Expand All @@ -41,6 +42,13 @@
.with_ssl_content(%r{^\s*GssapiCredStore keytab:#{keytab_path}$})
.with_ssl_content(/^\s*GssapiLocalName On$/)
.with_ssl_content(/^\s*require pam-account foreman$/)

should contain_foreman__config__apache__fragment('external_auth_api')
.with_ssl_content(/^\s*AuthType Basic$/)
.with_ssl_content(/^\s*AuthBasicProvider PAM$/)
.with_ssl_content(/^\s*AuthPAMService foreman$/)
.with_ssl_content(/^\s*require pam-account foreman$/)
.without_ssl_content(/^\s*AuthType GSSAPI/)
end

context 'with gssapi_local_name=false' do
Expand All @@ -52,6 +60,22 @@
end
end

context 'with GSSAPI auth for API' do
let(:params) { super().merge(ipa_authentication_api: true) }

it 'should contain GSSAPI and Basic coniguration in API fragment' do
should contain_foreman__config__apache__fragment('external_auth_api')
.with_ssl_content(/^\s*AuthType Basic$/)
.with_ssl_content(/^\s*AuthBasicProvider PAM$/)
.with_ssl_content(/^\s*AuthPAMService foreman$/)
.with_ssl_content(/^\s*require pam-account foreman$/)
.with_ssl_content(/^\s*AuthType GSSAPI/)
.with_ssl_content(/^\s*GssapiCredStore keytab:#{keytab_path}$/)
.with_ssl_content(/^\s*GssapiLocalName On$/)
.with_ssl_content(/^\s*require pam-account foreman$/)
end
end

context 'with SELinux' do
let(:facts) { override_facts(super(), os: {'selinux' => {'enabled' => selinux}}) }

Expand Down
21 changes: 21 additions & 0 deletions spec/classes/foreman_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,33 @@
end

describe 'with rails_cache_store redis' do
let(:params) { super().merge(rails_cache_store: { type: "redis" }) }
it 'should set rails_cache_store config' do
should contain_concat__fragment('foreman_settings+01-header.yaml')
.with_content(%r{^:rails_cache_store:\n\s+:type:\s*redis\n\s+:urls:\n\s*- redis://localhost:6379/0\n\s+:options:\n\s+:compress:\s*true\n\s+:namespace:\s*foreman$})
end
it { is_expected.to contain_package('foreman-redis') }

describe 'without dynflow managing redis' do
let(:params) { super().merge(dynflow_manage_services: false) }

it { is_expected.to contain_class('redis') }
end
end

describe 'with rails_cache_store redis with explicit URL' do
let(:params) { super().merge(rails_cache_store: { type: "redis", urls: [ "redis.example.com/0" ]}) }
it 'should set rails_cache_store config' do
should contain_concat__fragment('foreman_settings+01-header.yaml')
.with_content(/^:rails_cache_store:\n\s+:type:\s*redis\n\s+:urls:\n\s*- redis:\/\/redis.example.com\/0\n\s+:options:\n\s+:compress:\s*true\n\s+:namespace:\s*foreman$/)
end
it { is_expected.to contain_package('foreman-redis') }

describe 'without dynflow managing redis' do
let(:params) { super().merge(dynflow_manage_services: false) }

it { is_expected.not_to contain_class('redis') }
end
end

describe 'with rails_cache_store redis with options' do
Expand Down
4 changes: 2 additions & 2 deletions spec/support/acceptance/examples.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
shared_examples 'the foreman application' do
shared_examples 'the foreman application' do |params = {}|
[
['debian', 'ubuntu'].include?(os[:family]) ? 'apache2' : 'httpd',
'dynflow-sidekiq@orchestrator',
Expand All @@ -24,7 +24,7 @@
end

describe command("curl -s --cacert /etc/foreman-certs/certificate.pem https://#{host_inventory['fqdn']} -w '\%{redirect_url}' -o /dev/null") do
its(:stdout) { is_expected.to eq("https://#{host_inventory['fqdn']}/users/login") }
its(:stdout) { is_expected.to eq("https://#{host_inventory['fqdn']}#{params.fetch(:expected_login_url_path, '/users/login')}") }
its(:exit_status) { is_expected.to eq 0 }
end
end
Expand Down
2 changes: 1 addition & 1 deletion templates/auth_gssapi.conf.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

<LocationMatch ^(/api(/v2)?)?/users/extlogin/?$>
<LocationMatch ^/users/extlogin/?$>
SSLRequireSSL
AuthType GSSAPI
AuthName "GSSAPI Single Sign On Login"
Expand Down
28 changes: 28 additions & 0 deletions templates/external_auth_api.conf.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

<LocationMatch ^/api(/v2)?/users/extlogin/?$>
SSLRequireSSL
<% if scope.lookupvar('foreman::ipa_authentication_api') %>
<If "%{HTTP:Authorization} =~ /^Basic/">
AuthType Basic
AuthName "PAM Authentication"
AuthBasicProvider PAM
AuthPAMService <%= scope.lookupvar('foreman::pam_service') %>
</If>
<Else>
AuthType GSSAPI
AuthName "GSSAPI Single Sign On Login"
GssapiCredStore keytab:<%= @http_keytab %>
GssapiSSLonly On
GssapiLocalName <%= @gssapi_local_name %>
</Else>
<% else %>
AuthType Basic
AuthName "PAM Authentication"
AuthBasicProvider PAM
AuthPAMService <%= scope.lookupvar('foreman::pam_service') %>
<% end %>
require pam-account <%= scope.lookupvar('foreman::pam_service') %>
ErrorDocument 401 '{ "error": "External authentication did not pass." }'
# The following is needed as a workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1020087
ErrorDocument 500 '{ "error": "External authentication did not pass." }'
</LocationMatch>
1 change: 1 addition & 0 deletions templates/settings-external-auth-api.yaml.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:authorize_login_delegation_api: true
1 change: 0 additions & 1 deletion templates/settings-external-auth.yaml.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
:authorize_login_delegation: true
:authorize_login_delegation_api: true
:authorize_login_delegation_auth_source_user_autocreate: External
8 changes: 2 additions & 6 deletions templates/settings.yaml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,8 @@
:type: <%= scope["foreman::rails_cache_store"]["type"] %>
<% if scope["foreman::rails_cache_store"]["type"] == "redis" -%>
:urls:
<% if scope["foreman::rails_cache_store"].key?("urls") -%>
<% scope["foreman::rails_cache_store"]["urls"].each do |url| -%>
- redis://<%= url %>
<% end -%>
<% else -%>
- redis://localhost:8479/0
<% @redis_cache_urls.each do |url| -%>
- <%= url %>
<% end -%>
:options:
<% if scope["foreman::rails_cache_store"].key?("options") -%>
Expand Down