Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for selinux login #356

Merged
merged 10 commits into from
Jul 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions examples/login.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Groups
selinux::login { '%cn_cegbu_aconex_fr-dev-ops-priv_staff_u':
ensure => 'present',
selinux_login_name => '%cn_cegbu_aconex_fr-dev-ops-priv',
selinux_user => 'staff_u',
}

# Individual
selinux::login { 'localuser_staff_u':
ensure => 'present',
selinux_login_name => 'localuser',
selinux_user => 'staff_u',
}
119 changes: 119 additions & 0 deletions lib/puppet/provider/selinux_login/semanage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Puppet::Type.type(:selinux_login).provide(:semanage) do
desc 'Support managing SELinux login definitions via semanage'

defaultfor kernel: 'Linux'
# semanage fails when SELinux is disabled, so let's not pretend to work in that situation.
confine selinux: true

# Determine the appropriate python command
def self.python_command
@python_command ||= nil
return @python_command if @python_command

# Find the correct version of python on the system
python_paths = [
'/usr/libexec/platform-python',
'python',
'python3',
'python2'
]

valid_paths = []

python_paths.each do |pypath|
candidate = Puppet::Util.which(pypath)

next unless candidate
valid_paths << candidate

if Puppet::Util::Execution.execute("#{candidate} -c 'import semanage'", failonfail: false).exitstatus.zero?
@python_command = candidate
break
end
end

return @python_command if @python_command

# Since this is used in 'instances', we have to shrug and hope for the
# best unless we want runs to fail until the system is 100% correct.
# So far, it does not appear to hurt anything in practice and preserves the
# behavior from previous releases that hard coded the path into the python
# script.
valid_paths.first
end

# current file path is lib/puppet/provider/selinux_login/semanage.rb
# semanage_users.py is lib/puppet_x/voxpupuli/selinux/semanage_users.py
USERS_HELPER = File.expand_path('../../../../puppet_x/voxpupuli/selinux/semanage_users.py', __FILE__)
commands semanage: 'semanage',
python: python_command

mk_resource_methods

def self.parse_helper_lines(lines)
ret = {}
lines.each do |line|
split = line.split(%r{\s+})
# helper format is:
# root unconfined_u
# system_u system_u
# __default__ unconfined_u
# %cn_cegbu_aconex_fr-dev-ops-priv unconfined_u
# %cn_cegbu_aconex_fr-dev-platform-priv unconfined_u
selinux_login_name, selinux_user = split

key = selinux_login_name.to_s
ret[key] = {
ensure: :present,
name: key,
selinux_login_name: selinux_login_name,
selinux_user: selinux_user
}
end
ret
end

def self.instances
# no way to do this with one call as far as I know
policy = parse_helper_lines(python(USERS_HELPER).split("\n"))
policy.values.map do |item|
new(item)
end
end

def self.prefetch(resources)
# is there a better way to do this? map port/protocol pairs to the provider regardless of the title
# and make sure all system resources have ensure => :present so that we don't try to remove them
instances.each do |provider|
resource = resources[provider.name]
if resource
unless resource[:selinux_user].to_s == provider.selinux_user && resource[:selinux_login_name].to_s == provider.selinux_login_name || resource.purging?
raise Puppet::ResourceError, "Selinux_port['#{resource[:name]}']: title does not match its port and protocol, and a conflicting resource exists"
end
resource.provider = provider
resource[:ensure] = :present if provider.source == :policy
else
resources.values.each do |res|
next unless res[:selinux_user] == provider.selinux_user && res[:selinux_login_name] == provider.selinux_login_name
warning("Selinux_login['#{resource[:name]}']: title does not match format selinux_login_name_selinux_user")
resource.provider = provider
resource[:ensure] = :present if provider.source == :policy
end
end
end
end

def create
args = ['login', '-a', '-s', @resource[:selinux_user], @resource[:selinux_login_name]]
semanage(*args)
end

def destroy
args = ['login', '-d', @property_hash[:selinux_login_name]]
semanage(*args)
end

def exists?
@property_hash[:ensure] == :present
end
end
30 changes: 30 additions & 0 deletions lib/puppet/type/selinux_login.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Puppet::Type.newtype(:selinux_login) do
@doc = 'Manage SELinux login definitions. You should use selinux::login instead of this directly.'

ensurable

newparam(:title, namevar: true) do
desc 'Should be of the form "linuxuser_selinuxuser" or the type may misbehave'
end

newproperty(:selinux_login_name) do
desc 'The name of the linux user or group to map.'
isrequired
end

newproperty(:selinux_user) do
desc 'The selinux user to map to.'
isrequired
end

autorequire(:package) do
[
'policycoreutils',
'policycoreutils-python',
'policycoreutils-python-utils',
'python3-policycoreutils',
'selinux-policy-dev',
'selinux-policy-devel'
]
end
end
37 changes: 37 additions & 0 deletions lib/puppet_x/voxpupuli/selinux/semanage_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This script uses libsemanage directly to access the ports list
# it is *much* faster than semanage port -l

# will work with python 2.6+
from __future__ import print_function
from sys import exit
try:
import semanage
except ImportError:
# The semanage python library does not exist, so let's assume SELinux is disabled...
# In this case, the correct response is to return no ports when puppet does a
# prefetch, to avoid an error. We depend on the semanage binary anyway, which
# is uses the library
exit(0)


handle = semanage.semanage_handle_create()

if semanage.semanage_is_managed(handle) < 0:
exit(1)
if semanage.semanage_connect(handle) < 0:
exit(1)

def print_seuser(seuser):
seuser_login = semanage.semanage_seuser_get_name(seuser)
selinux_user = semanage.semanage_seuser_get_sename(seuser)
print("{} {}".format(seuser_login, selinux_user))


(status, seusers) = semanage.semanage_seuser_list(handle)

for seuser in seusers:
print_seuser(seuser)


semanage.semanage_disconnect(handle)
semanage.semanage_handle_destroy(handle)
7 changes: 7 additions & 0 deletions manifests/init.pp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
# @param permissive Hash of selinux::module resource parameters
# @param port Hash of selinux::port resource parameters
# @param exec_restorecon Hash of selinux::exec_restorecon resource parameters
# @param login Hash of selinux::login resource parameters
#
class selinux (
Variant[String[1], Array[String[1]]] $package_name,
Expand All @@ -54,6 +55,7 @@
Optional[Hash] $permissive = undef,
Optional[Hash] $port = undef,
Optional[Hash] $exec_restorecon = undef,
Hash[String[1],Hash[String[1],String[1]]] $login = {},
) {
class { 'selinux::package':
manage_package => $manage_package,
Expand Down Expand Up @@ -89,6 +91,11 @@
if $exec_restorecon {
create_resources ( 'selinux::exec_restorecon', $exec_restorecon )
}
$login.each |$login_name, $login_attributes| {
selinux::login { $login_name:
* => $login_attributes,
}
}

# Ordering
anchor { 'selinux::start': }
Expand Down
44 changes: 44 additions & 0 deletions manifests/login.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This method will manage a selinux login, and will
# persist it across reboots.
#
# @summary Manage a SELinux login
#
# @example Add a map for the localuser to staff_u
# selinux::login { 'localuser_staff_u':
# ensure => 'present',
# selinux_login_name => 'localuser',
# selinux_user => 'staff_u',
# }
#
# @param ensure Set to present to add or absent to remove a selinux login.
# @param selinux_login_name A Linux user or group
# @param selinux_user The selinux user to map to
#
define selinux::login (
String[1] $selinux_login_name,
String[1] $selinux_user,
Enum['present', 'absent'] $ensure = 'present',
) {
include selinux

if $ensure == 'present' {
Anchor['selinux::module post']
-> Selinux::Login[$title]
-> Anchor['selinux::end']
} elsif $ensure == 'absent' {
Class['selinux::config']
-> Selinux::Login[$title]
-> Anchor['selinux::module pre']
} else {
fail('Unexpected $ensure value')
}

# Do nothing unless SELinux is enabled
if $facts['os']['selinux']['enabled'] {
selinux_login { "${selinux_login_name}_${selinux_user}":
ensure => $ensure,
selinux_login_name => $selinux_login_name,
selinux_user => $selinux_user,
}
}
}