Posted on 03-03-2014 10:13 PM
Hi there,
Has anyone had any success running GUI scripts through the Casper Suite as part of a recurring policy? I'm writing a script to automate the migration from a WPA2/PSK network to a WPA Enterprise network in an environment where the endpoints are not domain joined. The problem I'm running into is the part of the script that's responsible for clicking the AirPort menu bar icon and selecting the preferred SSID is not executing when run as part of a policy. I can make the exception by modifying the TCC.db SQLite database from the script if I run the policy with a manual trigger from ARD, (ARDAgent is given access to control the computer). It also works if I run the script manually from the command line.
I've tried making exceptions to the TCC.db database for com.jamfsoftware.jamf.agent, com.jamfsoftware.task.Every 15 Minutes, com.jamfsoftware.jamf.daemon, and com.jamfsoftware.startupItem. I've also tried wrapping the script in a payload-free package and making the exception for com.apple.installer and /System/Library/PrivateFrameworks/PackageKit.Framework/Resources/installd.
I should also mention that the '-setairportnetwork' option in networksetup has not worked for programmatically connecting to an 802.1X network in my testing.
I'm attaching the script in case anyone has any insight.
Thanks for reading!
#!/usr/bin/python
'''
This script will configure an OS X
client to connect to the specified
802.1X WLAN.
Created by James Barclay on 2014-02-18
'''
import CoreFoundation
import logging
import os
import Quartz
import re
import sqlite3
import subprocess
import sys
import time
from Foundation import *
# Constants
CONFIG_PROFILE_ID = 'com.company.wireless'
COMPANY_DIR = '/Library/Application Support/.company'
PREFERRED_SSID = 'Wireless'
TCC_DIR = '/Library/Application Support/com.apple.TCC'
DBPATH = os.path.join(TCC_DIR, 'TCC.db')
WIRELESS_CONFIG_PROFILE = '/private/tmp/wireless.mobileconfig'
AIRPORT = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
NETWORK_SETUP = '/usr/sbin/networksetup'
PROFILES = '/usr/bin/profiles'
def get_console_user():
'''Returns the currently logged-in user as
a string, even if running as EUID root.'''
if os.geteuid() == 0:
console_user = subprocess.check_output(['/usr/bin/stat',
'-f%Su',
'/dev/console']).strip()
else:
import getpass
console_user = getpass.getuser()
return console_user
def logger(msg):
log_dir = os.path.join(COMPANY_DIR, 'Logs')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
logging.basicConfig(filename=os.path.join(log_dir, 'wireless_connect.log'),
format='%(asctime)s %(message)s',
level=logging.DEBUG)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
logging.debug(msg)
def create_tcc_db(path):
'''Creates the TCC.db SQLite database at
the specified path.'''
conn = sqlite3.connect(path)
c = conn.cursor()
c.execute('''CREATE TABLE admin
(key TEXT PRIMARY KEY NOT NULL, value INTEGER NOT NULL)''')
c.execute("INSERT INTO admin VALUES ('version', 4)")
c.execute('''CREATE TABLE access
(service TEXT NOT NULL,
client TEXT NOT NULL,
client_type INTEGER NOT NULL,
allowed INTEGER NOT NULL,
prompt_count INTEGER NOT NULL,
CONSTRAINT key PRIMARY KEY (service, client, client_type))''')
c.execute('''CREATE TABLE access_times
(service TEXT NOT NULL,
client TEXT NOT NULL,
client_type INTEGER NOT NULL,
last_used_time INTEGER NOT NULL,
CONSTRAINT key PRIMARY KEY (service, client, client_type))''')
c.execute('''CREATE TABLE access_overrides
(service TEXT PRIMARY KEY NOT NULL)''')
conn.commit()
conn.close()
def accessibility_allow(bundle_id, allow, client_type=0):
'''Modifies the access table in the TCC SQLite database.
Takes the bundle ID and a Boolean as arguments.
e.g., accessibility_allow('com.apple.Mail', True)'''
tcc_db_exists = False
if not os.path.exists(TCC_DIR):
os.mkdir(TCC_DIR, int('700', 8))
else:
tcc_db_exists = True
conn = sqlite3.connect(DBPATH)
c = conn.cursor()
# Setup the database if it doesn't already exist
if not tcc_db_exists:
create_tcc_db(DBPATH)
if isinstance(allow, bool):
if allow:
c.execute('''INSERT or REPLACE INTO access values
('kTCCServiceAccessibility', ?, ?, 1, 0, NULL)''', (bundle_id, client_type,))
conn.commit()
else:
c.execute('''INSERT or REPLACE INTO access values
('kTCCServiceAccessibility', ?, ?, 0, 0, NULL)''', (bundle_id, client_type,))
conn.commit()
else:
logger('%s is not of type bool.' % allow)
conn.close()
def screen_is_locked():
'''Returns True if the screen is locked.
Had to fork this with `su -c` because
Quartz.CGSessionCopyCurrentDictionary will
will return None if no UI session exists
for the user that spawned the process,
(root in this case).'''
console_user = get_console_user()
cmd = '/usr/bin/python -c 'import sys, Quartz; d = Quartz.CGSessionCopyCurrentDictionary(); print d''
d = subprocess.check_output(['/usr/bin/sudo',
'/usr/bin/su',
'%s' % console_user,
'-c',
cmd])
locked = 'kCGSSessionOnConsoleKey = 0'
sleeping = 'CGSSessionScreenIsLocked = 1'
logger('Quartz.CGSessionCopyCurrentDictionary returned the following for %s:
%s' % (console_user, d))
if locked in d or sleeping in d:
return True
def at_login_window():
'''Returns True if running at the login window.'''
ps_output = subprocess.check_output(['/bin/ps', 'aux'])
if 'Finder.app' not in ps_output:
return True
def get_pref_val(key, domain):
'''Returns the preference value for the specified key
and preference domain.'''
console_user = get_console_user()
val = None
try:
cmd = '/usr/bin/python -c 'import CoreFoundation; val = CoreFoundation.CFPreferencesCopyAppValue("%s", "%s"); print val'' % (key, domain)
val = subprocess.check_output(['/usr/bin/su',
'%s' % console_user,
'-c',
cmd])
except subprocess.CalledProcessError, e:
logger('An error occurred when attempting to retrieve a value for key %s in %s. Error: %s' % (key, val, e))
return val
def ask_for_password(boolean):
askForPassword = {
'com.apple.screensaver': {
'askForPassword': boolean,
},
}
CoreFoundation.CFPreferencesSetAppValue('askForPassword', askForPassword, 'com.apple.screensaver')
def get_wireless_interface():
'''Returns the wireless interface device
name, (e.g., en0 or en1).'''
hardware_ports = subprocess.check_output([NETWORK_SETUP,
'-listallhardwareports'])
match = re.search("(AirPort|Wi-Fi).*?(en\d)", hardware_ports, re.S)
if match:
wireless_interface = match.group(2)
return wireless_interface
def get_current_wlan(interface):
'''Returns the currently connected WLAN name.'''
wireless_status = subprocess.check_output([AIRPORT,
'-I',
interface]).split('
')
current_wlan_name = None
for line in wireless_status:
match = re.search(r' +SSID:s(.*)', line)
if match:
current_wlan_name = match.group(1)
return current_wlan_name
def remove_old_ssids(interface):
'''Removes previously used SSIDs from
the preferred networks list.'''
old_ssids = ['Test1', 'Test2', 'Test3', 'Guest']
for ssid in old_ssids:
subprocess.check_output([NETWORK_SETUP,
'-removepreferredwirelessnetwork',
interface,
ssid])
def wireless_setup_done():
'''Returns True if setup has been done before.'''
path = os.path.join(COMPANY_DIR, 'wireless_setup_done')
if os.path.exists(path):
return True
def is_preferred_wlan_available():
'''Returns True if the preferred WLAN
is currently available.'''
available_ssids = subprocess.check_output([AIRPORT, '-s'])
if PREFERRED_SSID in available_ssids:
return True
def touch(path):
'''Mimics the behavior of the `touch`
command-line utility.'''
with open(path, 'a'):
os.utime(path, None)
def enable_assistive_devices():
'''Enables access to Assistive Devices if
not currently enabled. Requires root privs.'''
accessibility_api_enabled = '/private/var/db/.AccessibilityAPIEnabled'
if not os.path.isfile(accessibility_api_enabled):
touch(accessibility_api_enabled)
def is_config_profile_installed():
'''Returns True if CONFIG_PROFILE is installed.'''
installed_profiles = subprocess.check_output([PROFILES, '-C'])
if CONFIG_PROFILE_ID in installed_profiles:
return True
def install_8021X_config_profile(path):
'''Installs configuration profile at path.'''
try:
subprocess.check_output([PROFILES,
'-I',
'-F',
path])
except subprocess.CalledProcessError, e:
logger('Unable to install configuration profile %s. Error: %s.' % (WIRELESS_CONFIG_PROFILE, e))
def connect_to_wlan(ssid):
'''Connects to specified SSID via Python
<-> AppleScript <-> Objective-C bridge. It's
a terrible GUI scripting hack.'''
cmd = '''set the_ssid to "%s"
tell application "System Events" to tell process "SystemUIServer"
tell menu bar 1
set menu_extras to value of attribute "AXDescription" of menu bar items
repeat with the_menu from 1 to the count of menu_extras
set menu_extra_strings to item the_menu of menu_extras
if item the_menu of menu_extras contains "Wi-Fi" then tell menu bar item the_menu
click
delay 4
click menu item the_ssid of front menu
delay 0.5
end tell
end repeat
end tell -- menu bar 1
end tell
''' % ssid
s = NSAppleScript.alloc().initWithSource_(cmd)
s.executeAndReturnError_(None)
def prompt_user_to_change_wlan(wireless_interface, current_wlan, old_ss_policy):
'''Displays a dialog with jamfHelper about
the wireless network changing.'''
description = '''You are switching from the %s wireless network to %s. Are you sure you want to continue?
There will be a brief loss of connectivity, so be sure to save your work. After clicking Continue you
will be prompted for your LDAP credentials.''' % (current_wlan, PREFERRED_SSID)
jamf_helper = '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper'
heading = 'Connect to %s?' % PREFERRED_SSID
icon = '/Library/User Pictures/Company/Company.tiff'
title = 'Company Wireless Configuration'
response = None
try:
response = subprocess.check_call(['/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper',
'-windowType',
'utility',
'-icon',
'%s' % icon,
'-title',
'%s' % title,
'-heading',
'%s' % heading,
'-description',
'%s' % description,
'-button1',
'"OK"',
'-button2',
'"Cancel"',
'-cancelButton',
'"2"'])
except subprocess.CalledProcessError, e:
logger('An error occurred when attempting to run jamfHelper.
Return code: %s
Error: %s' % (e.returncode, e))
console_user = get_console_user()
if str(response).isdigit():
response = int(response)
if response == 0:
logger('%s clicked OK.' % console_user)
# Enable Assistive Devices if needed
enable_assistive_devices()
# Install the 802.1X Configuration Profile
install_8021X_config_profile(WIRELESS_CONFIG_PROFILE)
# Sleep for 5 seconds
time.sleep(5)
# Allow controlling of the computer
accessibility_allow('/System/Library/PrivateFrameworks/PackageKit.Framework/Resources/installd', True, client_type=1)
accessibility_allow('org.python.python', True)
accessibility_allow('com.apple.installer', True)
accessibility_allow('com.jamfsoftware.task.Every 15 Minutes', True)
accessibility_allow('com.jamfsoftware.jamf.agent', True)
accessibility_allow('com.jamfsoftware.jamf.daemon', True)
accessibility_allow('com.jamfsoftware.startupItem', True)
# Connect to the WLAN
connect_to_wlan(PREFERRED_SSID)
# Sleep for two minutes
time.sleep(120)
# Disallow controlling of the computer
accessibility_allow('/System/Library/PrivateFrameworks/PackageKit.Framework/Resources/installd', False, client_type=1)
accessibility_allow('org.python.python', False)
accessibility_allow('com.apple.installer', False)
accessibility_allow('com.jamfsoftware.task.Every 15 Minutes', False)
accessibility_allow('com.jamfsoftware.jamf.agent', False)
accessibility_allow('com.jamfsoftware.jamf.daemon', False)
accessibility_allow('com.jamfsoftware.startupItem', False)
# Reset screensaver password prefs
new_ss_policy = get_pref_val('askForPassword', 'com.apple.screensaver')
if old_ss_policy != new_ss_policy:
ask_for_password(old_ss_policy)
if get_current_wlan(wireless_interface) == PREFERRED_SSID:
# Remove old SSIDs from preferred networks
remove_old_ssids(wireless_interface)
# Create dummy receipt so we know this has run successfully
wireless_setup_done = os.path.join(COMPANY_DIR, 'wireless_setup_done')
touch(wireless_setup_done)
elif response == 2:
logger('%s cancelled the operation. Exiting now.' % console_user)
sys.exit(1)
else:
logger('Unknown response: %s. Exiting now.' % response)
sys.exit(1)
else:
logger('Response %s is not a digit. Exiting now.' % response)
sys.exit(1)
def main():
if wireless_setup_done():
logger('The wireless configuration has already been completed. Exiting now.')
sys.exit(1)
if at_login_window():
logger('We appear to be running from the login window. Exiting now.')
sys.exit(1)
if screen_is_locked():
logger('We appear to be running from the lock screen. Exiting now.')
sys.exit(1)
if is_preferred_wlan_available() is not True:
logger('%s is not available. Exiting now.' % PREFERRED_SSID)
sys.exit(1)
if is_config_profile_installed() is not True:
# Get current screensaver policy
old_ss_policy = get_pref_val('askForPassword', 'com.apple.screensaver')
# Disable screensaver password temporarily (if needed)
if old_ss_policy:
ask_for_password(False)
# Get the wireless interface, (e.g., en0)
wireless_interface = get_wireless_interface()
# Get the current wireless network name
current_wlan = get_current_wlan(wireless_interface)
# Prompt the user to continue
prompt_user_to_change_wlan(wireless_interface, current_wlan, old_ss_policy)
else:
logger('The configuration profile with ID '%s' is already installed. Exiting now.' % CONFIG_PROFILE_ID)
sys.exit(1)
if __name__ == '__main__':
main()
Solved! Go to Solution.
Posted on 03-05-2014 01:53 PM
UPDATE: This was resolved by using the pymaKeyboard python module to control the keyboard inputs directly.
https://github.com/pudquick/pymaKeyboard
It now works like this:
Posted on 03-05-2014 01:53 PM
UPDATE: This was resolved by using the pymaKeyboard python module to control the keyboard inputs directly.
https://github.com/pudquick/pymaKeyboard
It now works like this: