#!/usr/bin/perl # # update-user-is-whd-asset-admin.pl # # JAMF policy script to make clients admin of their own asset. Fetches asset # clients from WHD based on the computer serial number. If $user_name is # already part of the admin group, then do nothing. If $user_name matches one # of the WHD asset clients (found using the hardware serial number), then # $user_name is added to the admin group. # # Note that older MacOS versions did not include the JSON perl module, nor do # they have Xcode installed to use the module from CPAN, so we must check for # the existance of the JSON perl module to manage the error. # # JAMF script parameters: # # Parameter 4: WHD hostname? # Parameter 5: WHD API username? # Parameter 6: WHD API key? # Parameter 7: Always admin usernames (optional)? # # Copyright 2023 JS Morisset and Sunshine Coast # School District 46 . # # Authored by JS Morisset . # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # See for the GNU General Public License. # use strict; use warnings; use Data::Dumper; use LWP::UserAgent; use IO::Socket::SSL; use URI; # # Extra required modules. # # The JSON module is not available in old MacOS versions. If missing, we cannot # install it from CPAN as MacOS does not have Xcode installed by default. The # BEGIN block tells perl to run the code inside the BEGIN block as soon as that # part of the script has finished compiling. # BEGIN { foreach my $mod ( ( 'JSON' ) ) { my $mod_fname = "$mod.pm"; $mod_fname =~ s!::!/!g; my $mod_found = 0; while ( < @INC > ) { if ( -s "$_/$mod_fname" ) { $mod_found = 1; eval "require $mod"; } } if ( ! $mod_found ) { my $mount_point = $ARGV[ 0 ] || ''; my $computer_name = $ARGV[ 1 ] || ''; my $user_name = $ARGV[ 2 ] || ''; my $whd_server = $ARGV[ 3 ] || ''; # WHD hostname? my $whd_api_user = $ARGV[ 4 ] || ''; # WHD API username? my $whd_api_key = $ARGV[ 5 ] || ''; # WHD API key? my $admin_users = $ARGV[ 6 ] || ''; # Always admin usernames (optional)? my $macos_version = `/usr/bin/sw_vers -productVersion`; print "\n"; print "mount_point = $mount_point\n"; print "computer_name = $computer_name\n"; print "user_name = $user_name\n"; print "whd_server = $whd_server\n"; print "whd_api_user = $whd_api_user\n"; print "whd_api_key = ********\n"; print "admin_users = $admin_users\n"; print "macos_version = $macos_version\n"; print "\n"; print "error: $mod perl module is required and missing.\n\n"; exit 1; } } JSON->import( qw( decode_json ) ); } # # Global variables. # # Note that WHD API connections require a tech account name. Any tech account # name will do so long as the tech account name exists. # my $admin_node = '.'; my $admin_group = 'admin'; my $mount_point = $ARGV[ 0 ] || ''; my $computer_name = $ARGV[ 1 ] || ''; my $user_name = $ARGV[ 2 ] || ''; my $whd_server = $ARGV[ 3 ] || ''; # WHD hostname? my $whd_api_user = $ARGV[ 4 ] || ''; # WHD API username? my $whd_api_key = $ARGV[ 5 ] || ''; # WHD API key? my $admin_users = $ARGV[ 6 ] || ''; # Always admin usernames (optional)? my $whd_asset_url = "https://$whd_server/helpdesk/WebObjects/Helpdesk.woa/ra/Assets/"; my $whd_client_url = "https://$whd_server/helpdesk/WebObjects/Helpdesk.woa/ra/Clients/"; my $home_dir = "/Users/$user_name"; my $cacert_pem = "$home_dir/.cacert.pem"; my $macos_version = `/usr/bin/sw_vers -productVersion`; print "\n"; print "mount_point = $mount_point\n"; print "computer_name = $computer_name\n"; print "user_name = $user_name\n"; print "whd_server = $whd_server\n"; print "whd_api_user = $whd_api_user\n"; print "whd_api_key = ********\n"; print "admin_users = $admin_users\n"; print "macos_version = $macos_version\n"; print "\n"; # # Basic requirement checks. # if ( ! length( $whd_api_key ) ) { if ( ! length( $user_name ) ) { print "error: user name parameter is required.\n\n"; exit 1; } elsif ( ! length( $whd_server ) ) { print "error: WHD hostname parameter is required.\n\n"; exit 1; } elsif ( ! length( $whd_api_user ) ) { print "error: WHD API username parameter is required.\n\n"; exit 1; } elsif ( ! length( $whd_api_key ) ) { print "error: WHD API key parameter is required.\n\n"; exit 1; } } elsif ( ! -d $home_dir ) { print "error: home folder $home_dir does not exist.\n\n"; exit 1; } # # Main section. # # If $user_name is already part of the admin group, then do nothing. # # If $user_name is part of the always admin parameter, then add $user_name to # the admin group. # # If $user_name matches one of the WHD asset clients (found using the hardware # serial number), then add $user_name to the admin group. # my $hw_serial_no = get_hardware_serial_number(); if ( user_is_admin() ) { print "user $user_name is already admin of $computer_name ($hw_serial_no).\n"; } elsif ( user_is_always_admin() ) { print "user $user_name can always admin $computer_name ($hw_serial_no).\n"; add_user_admin(); } elsif ( user_can_admin_asset() ) { print "user $user_name can admin $computer_name ($hw_serial_no).\n"; add_user_admin(); } else { print "user $user_name cannot admin $computer_name ($hw_serial_no).\n"; } print "\n"; exit 0; # Stop here. # # Export the root certificates keychain to a .pem file in the user's folder for # LWP::UserAgent. # sub update_user_cacert_pem { `/usr/bin/security export -t certs -f pemseq -k /System/Library/Keychains/SystemRootCertificates.keychain -o "$cacert_pem" 2>/dev/null`; if ( ! -s $cacert_pem ) { print "error: failed to export system root certificates to $cacert_pem.\n\n"; exit 1; } } # # Get the hardware serial number for this computer. # # The hardware serial number should match the asset serial number in WHD. # sub get_hardware_serial_number { # # decode_json() is provided by the JSON module. # my $hw_data = decode_json( `system_profiler -json SPHardwareDataType` ); if ( ! length( $hw_data->{ SPHardwareDataType }[ 0 ]->{ serial_number } ) ) { print "error: failed to get the hardware serial number.\n\n"; exit 1; } return $hw_data->{ SPHardwareDataType }[ 0 ]->{ serial_number }; } # # Check if the user name is in the local admin group. # # Returns 0 (false) or 1 (true). # sub user_is_admin { my $group_membership = `/usr/bin/dscl -q "$admin_node" read "/Groups/$admin_group" GroupMembership`; my @user_names = split( /[, ]+/, $group_membership ); shift( @user_names ) if $user_names[ 0 ] eq 'GroupMembership:'; return grep( /^$user_name$/, @user_names ) ? 1 : 0; } # # Check if the user name is part of the optional $admin_users parameter. # sub user_is_always_admin { my @user_names = split( /[, ]+/, $admin_users ); return grep( /^$user_name$/, @user_names ) ? 1 : 0; } # # Check if the user is a client of the asset in WHD using the hardware serial # number. # # Returns 0 (false) or 1 (true). # sub user_can_admin_asset { update_user_cacert_pem(); my @user_names = get_whd_asset_client_user_names(); return grep( /^$user_name$/, @user_names ) ? 1 : 0; } # # Add the user to the local admin group. # sub add_user_admin { print "adding $user_name to the $admin_group group...\n"; `/usr/sbin/dseditgroup -o edit -n "$admin_node" -a "$user_name" -t user "$admin_group"`; if ( user_is_admin( $user_name ) ) { # Double check, just in case. print "success: user $user_name is now admin of $computer_name ($hw_serial_no).\n"; } else { print "error: failed to add user $user_name to the $admin_group group.\n\n"; exit 1; } } # # Get all client user names for an asset in WHD using the hardware serial # number. # # Returns an array of client names. # sub get_whd_asset_client_user_names { my @client_ids = get_whd_asset_client_ids(); my @client_user_names; foreach ( @client_ids ) { my $url = URI->new( $whd_client_url . $_ ); $url->query_form( 'username' => $whd_api_user, 'apiKey' => $whd_api_key, ); my $ua = LWP::UserAgent->new(); $ua->ssl_opts( SSL_ca_file => $cacert_pem ); my $res = $ua->get( $url ); if ( ! $res->is_success ) { print $res->status_line; exit 1; } # # decode_json() is provided by the JSON module. # my $data = decode_json( $res->decoded_content() ); my $client_id = $data->{ id }; my $client_username = $data->{ username }; print "retrieved client id $client_id user name $client_username.\n"; push @client_user_names, $client_username; } return @client_user_names; } # # Get all client IDs for an asset in WHD using the hardware serial number. # # Returns an array of client IDs. # sub get_whd_asset_client_ids { my $data = get_whd_asset_data(); my $asset_id = $data->{ id }; my $asset_no = $data->{ assetNumber }; my @clients = @{ $data->{ clients } }; print "retrieved asset id $asset_id tag $asset_no serial number $hw_serial_no.\n"; if ( @clients < 1 ) { print "error: asset id $asset_id tag $asset_no serial number $hw_serial_no has no clients.\n\n"; exit 1; } my @client_ids; foreach ( @clients ) { push @client_ids, $_->{ id }; } return @client_ids; } # # Get an asset from WHD using the hardware serial number. # # Returns a single asset array. # # If there is 0 or more than 1 asset(s) returned by the WHD API query, then # exit with an error. # sub get_whd_asset_data { my $whd_serial_no = $_[ 0 ] || $hw_serial_no; my $is_try_again = $_[ 1 ] || 0; my $url = URI->new( $whd_asset_url ); $url->query_form( 'username' => $whd_api_user, 'apiKey' => $whd_api_key, 'qualifier' => "( serialNumber = '${whd_serial_no}' )", 'style' => 'details' ); my $ua = LWP::UserAgent->new(); $ua->ssl_opts( SSL_ca_file => $cacert_pem ); my $res = $ua->get( $url ); if ( ! $res->is_success ) { print $res->status_line; exit 1; } # # decode_json() is provided by the JSON module. # my $data = decode_json( $res->decoded_content() ); if ( @{ $data } < 1 ) { print "error: no asset for serial number $whd_serial_no.\n"; if ( $is_try_again ) { print "\n"; exit 1; } # # The barcode scanner adds an extra "S", so if we do not find # the asset, try again with a leading "S". # return get_whd_asset_data( "S$whd_serial_no", 1 ); } elsif ( @{ $data } > 1 ) { print "error: more than one asset for serial number $whd_serial_no.\n\n"; exit 1; } return @{ $data }[ 0 ]; }