#!/usr/bin/perl -w # # $Id$ # # The author disclaims all copyrights and releases this script into the # public domain. # # Interface to modify group memberships. Alters NetInfo database on # Mac OS X. Written for Mac OS X 10.3, untested on other releases. use strict; # group members must match the following my $group_re = qr/[A-Za-z0-9]+/; # Local NetInfo domain. Untested on Mac OS X server, or where NetInfo # running over network. my $ni_domain = '/'; # property NetInfo stores group members under my $ni_members = 'users'; my @groupcheck_cmd = ( qw(/usr/bin/niutil -list), $ni_domain ); my @grouplist_cmd = ( qw(/usr/bin/niutil -readprop), $ni_domain ); my @groupadd_cmd = ( qw(/usr/bin/niutil -mergeprop), $ni_domain ); my @groupdel_cmd = ( qw(/usr/bin/niutil -destroyval), $ni_domain ); my $groupname = shift; print_help() unless @ARGV; unless ( $groupname =~ m/$group_re/ ) { remark( 'error', 'group fails regex check', { name => $groupname } ); exit 102; } # where NetInfo stores group data my $ni_group = "/groups/$groupname"; # abort if group does not exist, as mergeprop will create a new group by # name without a gid! unless ( runs_ok( @groupcheck_cmd, $ni_group ) ) { remark( 'error', 'exiting as group does not exist', { name => $groupname } ); exit 103; } # find out existing members of group, figure out what users (if any) # need to be added my ( $output, $details ) = get_output( @grouplist_cmd, $ni_group, $ni_members ); # this error check disabled as group entry may not have 'users' property #unless ( defined $output ) { # remark( 'warning', 'problem listing group members', $details ); # exit 101; #} my %seen_members; for my $line (@$output) { # niutil lists groups on one line, space delimited my @sys_groups = split / /, $line; @seen_members{@sys_groups} = (); } # pull member data out of remaining arguments my ( @required, @new_members ); for my $entry (@ARGV) { for my $member ( $entry =~ m/($group_re)/g ) { push @required, $member; next if exists $seen_members{$member}; $seen_members{$member}++; push @new_members, $member; } } # find members to remove and evict delete @seen_members{@required}; if ( keys %seen_members ) { remark( 'info', 'members to remove', { names => join( ':', sort keys %seen_members ) } ); for my $member ( keys %seen_members ) { my ( $status, $details ) = runs_ok( @groupdel_cmd, $ni_group, $ni_members, $member ); unless ($status) { remark( 'warning', 'non-zero exit from group delete command', $details ); } } } unless (@new_members) { remark( 'info', 'no new members to add' ); } else { remark( 'info', 'members to add', { names => join( ':', sort @new_members ) } ); my ( $status, $details ) = runs_ok( @groupadd_cmd, $ni_group, $ni_members, @new_members ); unless ($status) { remark( 'warning', 'non-zero exit from groupadd command', $details ); } } # is this resync needed? runs_ok( qw(/usr/bin/niutil -resync), $ni_domain ); exit; sub print_help { print "usage: $0 groupname member [member ..]\n"; exit 100; } sub runs_ok { my $param = {}; if ( @_ and ref $_[0] eq 'HASH' ) { $param = { %$param, %{ $_[0] } }; shift @_; } my @command = @_; return unless @command; remark( 'info', 'running command', { command => "@command" } ); my $timeout = $param->{timeout} || 60; eval { local $SIG{ALRM} = sub { die "alarm\n" }; alarm $timeout; system @command; alarm 0; }; if ($@) { unless ( $@ eq "alarm\n" ) { remark( 'error', 'unexpected command error', { command => "@command" } ); return; } remark( 'warning', 'command timed out', { command => "@command", timeout => $timeout } ); return; } # TODO use POSIX for portable exit status checking? my %result = ( exit_value => $? >> 8, signal_num => $? & 127, ); # invert exit logic (0 == good) to work with standard Perl checks; # real exit in hash if needed my $status = $result{exit_value} ? 0 : 1; wantarray ? ( $status, \%result ) : $status; } sub get_output { my $param = {}; if ( @_ and ref $_[0] eq 'HASH' ) { $param = { %$param, %{ $_[0] } }; shift @_; } my @command = @_; return unless @command; remark( 'info', 'running command', { command => "@command" } ); my $timeout = $param->{timeout} || 60; my @results; eval { local $SIG{ALRM} = sub { die "alarm\n" }; alarm $timeout; unless ( open LISTING, '-|' or exec @command ) { remark( 'error', 'could not run', { command => "@command", errno => $! } ); } else { while (