Skip to content

Commit

Permalink
Adddd support for modification of TCC.db in user space.
Browse files Browse the repository at this point in the history
* Added support for `-u` or `--user` so that user specific TCC database can be modified.
* `-u` without parameters will attempt modifications of the current user.
* Username is checked and, if not existing in the system, raised as an error with a hint.
* Renamed service to default_service for clarity (as `global service` is set from arguments).
* Normalised all help strings to NOT end in ".", matching the default `-h` string format.
* Moved from `str.format()` and modulo operator (`%`) to f-strings.
* Error output now goes to stderr.
* Fixed behaviour for ACTION = reset, by ALLOWING unknown arguments to be passthrough.
* Fixed behaviour for ACTION = reset, by using the absolute path to /usr/bin/tccutil, so that there is no clash if using wrapper scripts (such as Homebrew does).
* Updated README.md information
  • Loading branch information
tnarik authored and jacobsalmela committed Dec 10, 2023
1 parent e7f12b5 commit 57bf5db
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 61 deletions.
48 changes: 35 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Discussions on this topic can be found here: https://github.com/jacobsalmela/tcc

- `tccutil.py` can be installed without any additional software.
- it has an easy to use syntax
- it supports both system wide and user scope TCC manipulation
- it wraps the native `/usr/bin/tccutil` tool
- there are other solutions out there, but there were some things I did not like about them:

+ [Privacy Manager Services](https://github.com/univ-of-utah-marriott-library-apple/privacy_services_manager) has other dependencies that need to be installed (it has also gone over five years without any updates)
Expand Down Expand Up @@ -58,34 +60,36 @@ Clone this repo and manually copy `tccutil.py` to `/usr/local/bin` or run from a

**This utility needs super-user priveleges for most operations.** It is important that you either run this as root or use `sudo`, otherwise it won't work and you will end up with “permission denied” errors.


```
usage: tccutil.py [-h] [--service SERVICE] [--list] [--insert INSERT] [-v]
[-r REMOVE] [-e ENABLE] [-d DISABLE] [--version]
usage: tccutil.py [-h] [--service SERVICE] [--list] [--digest] [--insert INSERT] [-v]
[-r REMOVE] [-e ENABLE] [-d DISABLE] [--user [USER]] [--version]
[ACTION]
Modify Accesibility Preferences
positional arguments:
ACTION This option is only used to perform a reset.
ACTION This option is only used to perform a reset, using "/usr/bin/tccutil". See
`man tccutil` for additional syntax
optional arguments:
-h, --help show this help message and exit
--service SERVICE, -s SERVICE
Set TCC service
--list, -l List all entries in the accessibility database.
--list, -l List all entries in the accessibility database
--digest Print the digest hash of the accessibility database
--insert INSERT, -i INSERT
Adds the given bundle ID or path to the accessibility
database.
-v, --verbose Outputs additional info for some commands.
Adds the given bundle ID or path to the accessibility database
-v, --verbose Outputs additional info for some commands
-r REMOVE, --remove REMOVE
Removes a given Bundle ID or Path from the
Accessibility Database.
Removes a given Bundle ID or Path from the Accessibility Database
-e ENABLE, --enable ENABLE
Enables Accessibility Access for the given Bundle ID
or Path.
Enables Accessibility Access for the given Bundle ID or Path
-d DISABLE, --disable DISABLE
Disables Accessibility Access for the given Bundle ID
or Path.
Disables Accessibility Access for the given Bundle ID or Path
--user [USER], -u [USER]
Modify accessibility database for a given user (defaults to current,
if no additional parameter is provided)
--version Show the version of this script
```

Expand All @@ -98,12 +102,24 @@ List existing Entries in the Accessibility Database
sudo tccutil.py --list
```

List existing Entries in the Accessibility Database specific to the current user

```bash
sudo tccutil.py --list -u
```

Add `/usr/bin/osascript` to the Accessibility Database (using UNIX-Style Option)

```bash
sudo tccutil.py -i /usr/bin/osascript
````

Add `/usr/bin/osascript` to the Accessibility Database specific to user 'myuser' (using UNIX-Style Option)

```bash
sudo tccutil.py -i /usr/bin/osascript -u myuser
````
Add *Script Editor* to the Accessibility Database (using Long Option)
```bash
Expand All @@ -128,6 +144,12 @@ Disable `/usr/bin/osascript` (must already exist in the Database)
sudo tccutil.py -d /usr/bin/osascript
```

Reset system wide accessibility database

```bash
sudo tccutil.py reset ALL
```

## Contributing

Many people have contributed already, so feel free to make a PR and we'll get it merged in.
108 changes: 60 additions & 48 deletions tccutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import sqlite3
import sys
import os
import pwd
import hashlib
from platform import mac_ver
from packaging.version import Version as version
Expand All @@ -29,7 +30,7 @@
# Current OS X version
osx_version = version(mac_ver()[0]) # mac_ver() returns 10.16 for Big Sur instead 11.+

# Database Path
# Database Path (System by default)
tcc_db = '/Library/Application Support/com.apple.TCC/TCC.db'

# Set "sudo" to True if called with Admin-Privileges.
Expand All @@ -39,49 +40,57 @@
verbose = False

# TCC Service
service = "kTCCServiceAccessibility"

default_service = "kTCCServiceAccessibility"

parser = argparse.ArgumentParser(description='Modify Accesibility Preferences')
parser.add_argument(
'action',
metavar='ACTION',
type=str,
nargs='?',
help='This option is only used to perform a reset.',
help='This option is only used to perform a reset, using "/usr/bin/tccutil".\
See `man tccutil` for additional syntax',
)
parser.add_argument(
'--service', '-s',
default=service,
default=default_service,
help="Set TCC service"
)

parser.add_argument(
'--list', '-l', action='store_true',
help="List all entries in the accessibility database."
help="List all entries in the accessibility database"
)
parser.add_argument(
'--digest', action='store_true',
help="Print the digest hash of the accessibility database."
help="Print the digest hash of the accessibility database"
)
parser.add_argument(
'--insert', '-i', action='append', default=[],
help="Adds the given bundle ID or path to the accessibility database.",
help="Adds the given bundle ID or path to the accessibility database",
)
parser.add_argument(
"-v", "--verbose", action='store_true',
help="Outputs additional info for some commands.",
help="Outputs additional info for some commands",
)
parser.add_argument(
"-r", "--remove", action='append', default=[],
help="Removes a given Bundle ID or Path from the Accessibility Database.",
help="Removes a given Bundle ID or Path from the Accessibility Database",
)
parser.add_argument(
"-e", "--enable", action='append', default=[],
help="Enables Accessibility Access for the given Bundle ID or Path.",
help="Enables Accessibility Access for the given Bundle ID or Path",
)
parser.add_argument(
"-d", "--disable", action='append', default=[],
help="Disables Accessibility Access for the given Bundle ID or Path."
help="Disables Accessibility Access for the given Bundle ID or Path"
)
parser.add_argument(
'--user', '-u',
nargs="?",
const='',
default=None,
help="Modify accessibility database for a given user (defaults to current, if no additional parameter is provided)",
)
parser.add_argument(
'--version', action='store_true',
Expand All @@ -90,15 +99,15 @@

def display_version():
"""Print the version of this utility."""
print("%s %s" % (util_name, util_version))
print(f"{util_name} {util_version}")
sys.exit(0)


def sudo_required():
"""Check if user has root priveleges to access the database."""
if not sudo:
print("Error:")
print(" When accessing the Accessibility Database, %s needs to be run with admin-privileges.\n" % (util_name))
print("Error:", file=sys.stderr)
print(f" When accessing the Accessibility Database, {util_name} needs to be run with admin-privileges.\n", file=sys.stderr)
display_help(1)


Expand Down Expand Up @@ -153,12 +162,12 @@ def open_database(digest=False):
(osx_version >= version('14.0') and
accessTableDigest in ["34abf99d20"])
):
print("TCC Database structure is unknown (%s)" % accessTableDigest)
print(f"TCC Database structure is unknown ({accessTableDigest})", file=sys.stderr)
sys.exit(1)

verbose_output("Database opened.\n")
except TypeError:
print("Error opening Database. You probably need to disable SIP for this to work.")
print("Error opening Database. You probably need to disable SIP for this to work.", file=sys.stderr)
sys.exit(1)


Expand All @@ -167,7 +176,7 @@ def display_help(error_code=None):
parser.print_help()
if error_code is not None:
sys.exit(error_code)
print("%s %s" % (util_name, util_version))
print(f"{util_name} {util_version}")
sys.exit(0)


Expand All @@ -183,7 +192,7 @@ def close_database():
except:
verbose_output("Database closed.")
except:
print("Error closing Database.")
print("Error closing Database.", file=sys.stderr)
sys.exit(1)
except:
pass
Expand All @@ -208,7 +217,7 @@ def verbose_output(*args):
def list_clients():
"""List items in the database."""
open_database()
c.execute("SELECT client from access WHERE service is '%s'" % service)
c.execute(f"SELECT client from access WHERE service is '{service}'")
verbose_output("Fetching Entries from Database...\n")
for row in c.fetchall():
# print each entry in the Accessibility pane.
Expand All @@ -223,12 +232,12 @@ def cli_util_or_bundle_id(client):
# GUI so you can manually click the checkbox.
if client[0] == '/':
client_type = 1
verbose_output("Detected \"%s\" as Command Line Utility." % (client))
verbose_output(f'Detected "{client}" as Command Line Utility.')
# Otherwise, the app will be a bundle ID, which starts
# with a com., net., or org., etc.
else:
client_type = 0
verbose_output("Detected \"%s\" as Bundle ID." % (client))
verbose_output(f'Detected "{client}" as Bundle ID.')
return client_type


Expand All @@ -238,74 +247,69 @@ def insert_client(client):
# Check if it is a command line utility or a bundle ID
# as the default value to enable it is different.
client_type = cli_util_or_bundle_id(client)
verbose_output("Inserting \"%s\" into Database..." % (client))
verbose_output(f'Inserting "{client}" into Database...')
# Sonoma
if osx_version >= version('10.16'):
try:
c.execute("INSERT or REPLACE INTO access VALUES('%s','%s',%s,2,4,1,NULL,NULL,0,'UNUSED',NULL,0, NULL, NULL, NULL,'UNUSED', NULL)"
% (service, client, client_type))
c.execute(f"INSERT or REPLACE INTO access VALUES('{service}','{client}',{client_type},2,4,1,NULL,NULL,0,'UNUSED',NULL,0, NULL, NULL, NULL,'UNUSED', NULL)")
except sqlite3.OperationalError:
print("Attempting to write a readonly database. You probably need to disable SIP.")
print("Attempting to write a readonly database. You probably need to disable SIP.", file=sys.stderr)
# Big Sur and later
elif osx_version >= version('10.16'):
try:
c.execute("INSERT or REPLACE INTO access VALUES('%s','%s',%s,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,0)"
% (service, client, client_type))
c.execute(f"INSERT or REPLACE INTO access VALUES('{service}','{client}',{client_type},2,4,1,NULL,NULL,0,'UNUSED',NULL,0,0)")
except sqlite3.OperationalError:
print("Attempting to write a readonly database. You probably need to disable SIP.")
print("Attempting to write a readonly database. You probably need to disable SIP.", file=sys.stderr)
# Mojave through Big Sur
elif osx_version >= version('10.14'):
c.execute("INSERT or REPLACE INTO access VALUES('%s','%s',%s,1,1,NULL,NULL,NULL,'UNUSED',NULL,0,0)"
% (service, client, client_type))
c.execute(f"INSERT or REPLACE INTO access VALUES('{service}','{client}',{client_type},1,1,NULL,NULL,NULL,'UNUSED',NULL,0,0)")
# El Capitan through Mojave
elif osx_version >= version('10.11'):
c.execute("INSERT or REPLACE INTO access VALUES('%s','%s',%s,1,1,NULL,NULL)"
% (service, client, client_type))
c.execute(f"INSERT or REPLACE INTO access VALUES('{service}','{client}',{client_type},1,1,NULL,NULL)")
# Yosemite or lower
else:
c.execute("INSERT or REPLACE INTO access VALUES('%s','%s',%s,1,1,NULL)"
% (service, client, client_type))
c.execute(f"INSERT or REPLACE INTO access VALUES('{service}','{client}',{client_type},1,1,NULL)")
commit_changes()


def delete_client(client):
"""Remove a client from the database."""
open_database()
verbose_output("Removing \"%s\" from Database..." % (client))
verbose_output(f'Removing "{client}" from Database...')
try:
c.execute("DELETE from access where client IS '%s' AND service IS '%s'" % (client, service))
c.execute(f"DELETE from access where client IS '{client}' AND service IS '{service}'")
except sqlite3.OperationalError:
print("Attempting to write a readonly database. You probably need to disable SIP.")
print("Attempting to write a readonly database. You probably need to disable SIP.", file=sys.stderr)
commit_changes()


def enable(client):
"""Add a client from the database."""
open_database()
verbose_output("Enabling %s..." % (client,))
verbose_output(f'Enabling {client}...')
# Setting typically appears in System Preferences
# right away (without closing the window).
# Set to 1 to enable the client.
enable_mode_name = 'auth_value' if osx_version >= version('10.16') else 'allowed'
try:
c.execute("UPDATE access SET %s='1' WHERE client='%s' AND service IS '%s'" % (enable_mode_name, client, service))
c.execute(f"UPDATE access SET {enable_mode_name}='1' WHERE client='{client}' AND service IS '{service}'")
except sqlite3.OperationalError:
print("Attempting to write a readonly database. You probably need to disable SIP.")
print("Attempting to write a readonly database. You probably need to disable SIP.", file=sys.stderr)
commit_changes()


def disable(client):
"""Disable a client in the database."""
open_database()
verbose_output("Disabling %s..." % (client,))
verbose_output(f"Disabling {client}...")
# Setting typically appears in System Preferences
# right away (without closing the window).
# Set to 0 to disable the client.
enable_mode_name = 'auth_value' if osx_version >= version('10.16') else 'allowed'
try:
c.execute("UPDATE access SET %s='0' WHERE client='%s' AND service IS '%s'" % (enable_mode_name, client, service))
c.execute(f"UPDATE access SET {enable_mode_name}='0' WHERE client='{client}' AND service IS '{service}'")
except sqlite3.OperationalError:
print("Attempting to write a readonly database. You probably need to disable SIP.")
print("Attempting to write a readonly database. You probably need to disable SIP.", file=sys.stderr)
commit_changes()


Expand All @@ -317,19 +321,27 @@ def main():
print(" No arguments.\n")
display_help(2)

args = parser.parse_args()
args, rest = parser.parse_known_args()

if args.version:
display_version()
return

global tcc_db
if args.user != None:
try:
if (len(args.user) > 0): pwd.getpwnam(args.user)
tcc_db = os.path.abspath(os.path.expanduser(f'~{args.user}/{tcc_db}'))
except KeyError:
print(f'User "{args.user}" does not exist. Do you mean to use "{args.user}" as ACTION?', file=sys.stderr)
sys.exit(1)

if args.action:
if args.action == 'reset':
exit_status = os.system("tccutil \
{}".format(' '.join(sys.argv[1:])))
exit_status = os.system(f'/usr/bin/tccutil -v {" ".join(rest)}')
sys.exit(exit_status / 256)
else:
print("Error\n Unrecognized command {}".format(args.action))
print(f'Error\n Unrecognized command "{args.action}"', file=sys.stderr)

global service
service = args.service
Expand Down

0 comments on commit 57bf5db

Please sign in to comment.