Skip to content
Draft
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
12 changes: 12 additions & 0 deletions .github/workflows/pr_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ jobs:
- name: 'Test-build the Puppet module'
run: 'bundle exec rake pupmod:build'

reference-docs:
name: 'REFERENCE.md is up to date'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: 'Install Ruby 3.4'
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.9
bundler-cache: true
- run: 'bundle exec rake validate:strings'

spec-tests:
name: 'Puppet Spec'
needs: [puppet-syntax]
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
* Fri Jul 24 2026 Steven Pritchard <steve@sicura.us> - 2.0.1
- Document the gpasswd provider inline (puppet-strings/YARD comments)
- Add REFERENCE.md and a CI job that keeps it validated
(`rake validate:strings`); openvox-strings is temporarily sourced
from a fork branch that adds provider rendering to the markdown
generator, pending its release upstream

* Thu Jul 16 2026 Steven Pritchard <steve@sicura.us> - 2.0.0
- Migrate to the OpenVox test stack (openvox gem, voxpupuli-test,
simp-rake-helpers 6.x, simp-beaker-helpers 3.x) and add a Ruby 4.0 /
Expand Down
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ group :test do
openvox_version = ENV.fetch('OPENVOX_VERSION', puppet_version)
gem 'hiera-puppet-helper'
gem 'openvox', openvox_version
gem 'openvox-strings'
# TODO: revert to the released gem once provider rendering is merged upstream
gem 'openvox-strings', git: 'https://github.com/silug/openvox-strings', branch: 'add-provider-markdown'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gem 'rake'
gem 'rspec'
gem 'rspec-puppet'
Expand Down
40 changes: 40 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Reference

<!-- DO NOT EDIT: This document was generated by Puppet Strings -->

## Table of Contents

### Providers

* [`gpasswd`](#provider_group_gpasswd): Group management via `gpasswd`. Extends the standard `groupadd` provider with support for the `manages_members` feature so that the members

## Providers

### <a name="provider_group_gpasswd"></a>`gpasswd`

Provider for the `group` resource type.

Group management via `gpasswd`.

Extends the standard `groupadd` provider with support for the
`manages_members` feature so that the members of a local group can be
managed with the native `group` type, even when the users themselves are
defined in a remote system such as LDAP:

group { 'test':
members => ['foo', 'bar', 'baz'],
}

The group's `auth_membership` parameter selects exclusive membership
(`gpasswd -M`, membership matches the catalog exactly) or additive
membership (`gpasswd -a` per user, existing members are preserved).

Members that the system cannot resolve (via `getpwnam`/`getpwuid`) are
skipped with a warning instead of failing the resource, and errors from
individual membership changes are downgraded to warnings so that one bad
member does not abort management of the rest of the group.

#### Commands

* `addmember`: `gpasswd`
* `modmember`: `gpasswd`
67 changes: 63 additions & 4 deletions lib/puppet/provider/group/gpasswd.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
require 'puppet/provider/group/groupadd'

# @summary Local group membership management via `gpasswd`
Puppet::Type.type(:group).provide :gpasswd, parent: Puppet::Type::Group::ProviderGroupadd do
require 'shellwords'

desc <<~EOM
Group management via `gpasswd`. This allows for local group
management when the users exist in a remote system.
Group management via `gpasswd`.

Extends the standard `groupadd` provider with support for the
`manages_members` feature so that the members of a local group can be
managed with the native `group` type, even when the users themselves are
defined in a remote system such as LDAP:

group { 'test':
members => ['foo', 'bar', 'baz'],
}

The group's `auth_membership` parameter selects exclusive membership
(`gpasswd -M`, membership matches the catalog exactly) or additive
membership (`gpasswd -a` per user, existing members are preserved).

Members that the system cannot resolve (via `getpwnam`/`getpwuid`) are
skipped with a warning instead of failing the resource, and errors from
individual membership changes are downgraded to warnings so that one bad
member does not abort management of the rest of the group.
EOM

commands addmember: 'gpasswd',
Expand All @@ -15,11 +33,22 @@
has_feature :libuser if Puppet.features.libuser?
has_feature :system_groups unless ['HP-UX', 'Solaris'].include? Facter.value(:operatingsystem)

# Whether the running Puppet represents the `members` property as a
# `Puppet::Property::List` (comma-joined string) rather than a plain Array
#
# @return [Boolean]
def is_new_format? # rubocop:disable Style/PredicatePrefix
defined?(Puppet::Property::List) &&
@resource.parameter('members').class.ancestors.include?(Puppet::Property::List)
end

# Create the group and add any initial members with `gpasswd -a`
#
# The parent provider's group creation command and the per-member `gpasswd`
# commands are executed here (see `mod_group`) rather than returned, because
# the Nameservice layer can only execute a single command.
#
# @return [String] a no-op command for the Nameservice layer to execute
def addcmd
# This pulls in the main group add command should the group need
# to be added from scratch.
Expand All @@ -38,9 +67,15 @@ def addcmd
'/bin/true'
end

# Build the command that modifies a group property
#
# This is a repeat from puppet/provider/nameservice/objectadd.
# The self.class.name matches are hard coded so cannot be easily
# overridden.
#
# @param param [Symbol] the property being modified
# @param value [String] the new value
# @return [Array<String>] the command to execute
def modifycmd(param, value)
cmd_type = (param.to_s =~ %r{password_.+_age}) ? :password : :modify
cmd = [command(cmd_type)]
Expand All @@ -62,6 +97,14 @@ def modifycmd(param, value)
cmd
end

# Current group members, as read from the system group database
#
# When `auth_membership` is false and all desired members are already
# present, the desired value is returned instead so that the property is
# seen as in sync.
#
# @return [Array<String>, String, nil] the membership in the format the
# running Puppet's `members` property expects (see `is_new_format?`)
def members
members_to_set = @resource.parameter('members').shouldorig

Expand Down Expand Up @@ -91,6 +134,11 @@ def members
retval
end

# Whether the current membership matches the desired membership
#
# @param is [Array<String>] the current members
# @param should [Array<String>, String] the desired members
# @return [Boolean]
def members_insync?(is, should)
# We need to remove any user that the system doesn't recognize, otherwise
# the add and/or remove commands will fail.
Expand All @@ -115,6 +163,14 @@ def members_insync?(is, should)
Array(is).sort.uniq == sorted_should
end

# Set the group membership with `gpasswd`
#
# Uses `gpasswd -M` (exclusive) when `auth_membership` is true and
# per-member `gpasswd -a` (additive) otherwise.
#
# @param to_set [Array<String>, String] the desired members, in the format
# the running Puppet's `members` property provides (see `is_new_format?`)
# @return [void]
def members=(to_set)
cmd = []

Expand All @@ -140,8 +196,8 @@ def members=(to_set)

private

# This define takes an array of commands to run and executes them in
# order to modify the group memberships on the system.
# Execute a list of group modification commands in order
#
# A useful warning message is output if there is an issue modifying
# the group but all members that can be added are added. This is an
# attempt to do the "right thing" without actually breaking a run
Expand All @@ -150,6 +206,9 @@ def members=(to_set)
#
# The run of the type *will* succeed in all cases and present warnings to the
# user.
#
# @param cmds [Array<String>] shell commands to execute
# @return [void]
def mod_group(cmds)
cmds.each do |run_cmd|
output = execute(run_cmd, custom_environment: @custom_environment, failonfail: false, combine: true)
Expand Down
2 changes: 1 addition & 1 deletion metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simp-gpasswd",
"version": "2.0.0",
"version": "2.0.1",
"author": "SIMP Team",
"summary": "Adds support for :manages_members to the Linux group native type",
"license": "Apache-2.0",
Expand Down
Loading