Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Tag autocomplete suggestions are scoped to objects the current user can access
* Additional inline JavaScript values and activity-log rich-text previews are escaped or sanitized for their output context
* Added matching Django and Hasura validation for domain and static server names while preserving user access to create and manage shared inventory
* Restricted Django Q scheduled tasks to a server-controlled allowlist (Closes #911)
* The admin panel now exposes only approved functions, Slack notification hooks, and validated task arguments
* Optional system commands must be configured as fixed argument vectors on the server and run without a shell
* Schedule saves, queue submissions, result hooks, and worker execution enforce the policy independently
* Added `audit_django_q_policy` to identify or pause existing schedules that do not satisfy the configured policy

## [7.2.4] - 21 July 2026

Expand Down
2 changes: 1 addition & 1 deletion DOCS/development/modifying-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: "Modifying and then deploying code changes"

While running Ghostwriter using `local.yml`, you can make changes on the fly without needing to run any Docker commands. Django smartly restarts the server when it detects a file change. Also, the local deployment mounts Ghostwriter's directory on your host, so changes to the code (Python or templates) will be live once the file is saved.

The `build, stop, rm, up -d` commands need to be run only if you add a new library or if you add a new task to **tasks.py** **(only after initially adding the new function, not each time you change the function).
The `build, stop, rm, up -d` commands need to be run only if you add a new library or if you add a new task to **tasks.py** **(only after initially adding the new function, not each time you change the function).** A new queued task must also be added to `GHOSTWRITER_DJANGO_Q_INTERNAL_TASKS` or `GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS`, as appropriate.

`docker-compose -f local.yml stop; docker-compose -f local.yml rm -f; docker-compose -f local.yml build; docker-compose -f local.yml up -d`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Ghostwriter also tracks JWT sessions created by the GraphQL `login` mutation. Th

It is recommended you clear expired sessions on a regular basis to keep the session tables tidy. Use the `clear_expired_sessions` management command for this cleanup. The command wraps Django's built-in `clearsessions` command and also deletes expired GraphQL login sessions from Ghostwriter's `UserSession` table.

For a scheduled task, call `django.core.management.call_command` and pass `"clear_expired_sessions"` as its only argument. Set up a task like this one that runs daily with the cron scheduler. For example, `0 5 * * *` will run it every day at 5:00 AM.
For a scheduled task, select the allowlisted `ghostwriter.home.django_q_tasks.clear_expired_sessions` function. The wrapper accepts no arguments and runs only the `clear_expired_sessions` management command. Set up a task like this one with the cron scheduler. For example, `0 5 * * *` will run it every day at 5:00 AM.

<Frame>
<img src="/images/features/clear_sessions.png" alt=""/>
Expand Down
3 changes: 1 addition & 2 deletions DOCS/features/background-tasks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Configuring and scheduling background tasks

Ghostwriter uses the [Django Q project](https://django-q.readthedocs.io/en/latest/) for queuing and managing background tasks. Django Q hands off tasks to the Redis server (already installed and running in Docker).

Tasks are defined in the `tasks.py` file. These tasks can be executed on demand or on a schedule.
Tasks are Python functions, usually defined in a `tasks.py` module. Ghostwriter executes some tasks on demand from the application. Only functions approved by the server-side Django Q policy can be added to a schedule in the admin panel.

Tasks can be queued in a few different ways:

Expand All @@ -14,4 +14,3 @@ Tasks can be queued in a few different ways:
* Use the buttons (various) in Ghostwriter's web interface.

* Use a REST API endpoint (not yet available).

11 changes: 5 additions & 6 deletions DOCS/features/background-tasks/prebuilt-tasks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The following information is for Ghostwriter's provided tasks

The `ghostwriter.shepherd.tasks.release_domains` function checks if the currently checked-out domain names are due to be released. If Slack is enabled, it sends a Slack message when the domain's release date is tomorrow or today. If the release date is today, the domain is also released back into the pool.

The task accepts an optional `reset_dns` argument that defaults to `False`. If this argument is set to `True`, the task will use the Namecheap API to reset the domain's DNS records upon release. This feature requires:
The task accepts an optional `no_action` argument that defaults to `False`. Set it to `True` to preview the domains due for release without changing their availability. Domains configured to reset their DNS records will use the Namecheap API during a normal release. DNS reset requires:

* Namecheap API enabled and configured

Expand Down Expand Up @@ -43,25 +43,24 @@ If you will be using this task, the Q cluster needs to be started using an admin

## Namecheap Synchronization

The `tasks.fetch_namecheap_domains` uses the Namecheap API to fetch all domains from the registrar for the configured account and synchronizes that information with the domain library. If a domain name is found in the library already, the task will update that record.
The `ghostwriter.shepherd.tasks.fetch_namecheap_domains` task uses the Namecheap API to fetch all domains from the registrar for the configured account and synchronizes that information with the domain library. If a domain name is found in the library already, the task will update that record.

If a Namecheap-registered domain in the Ghostwriter library is not found in the fresh list of domains, the task will mark that domain as expired.

## Cloud Monitoring

The `review_cloud_infrastructure` task uses the Amazon Web Services and Digital Ocean APIs to check all running server instances and compare those to cloud servers attached to projects. If a project is marked as complete, the task will flag any running cloud servers attached to that project for review.
The `ghostwriter.shepherd.tasks.review_cloud_infrastructure` task uses the Amazon Web Services and Digital Ocean APIs to check all running server instances and compare those to cloud servers attached to projects. If a project is marked as complete, the task will flag any running cloud servers attached to that project for review.

The task will send a Slack message with all relevant server data to the project's channel if Slack is enabled.

## Update Expiration

The `check_expiration` task checks the expiration dates of all domains in the domain library and compares them to the current date. If the domain is set to auto-renew, the task will increment the expiration date by one year. Otherwise, the task will mark the domain as expired.
The `ghostwriter.shepherd.tasks.check_expiration` task checks the expiration dates of all domains in the domain library and compares them to the current date. If the domain is set to auto-renew, the task will increment the expiration date by one year. Otherwise, the task will mark the domain as expired.

## Operation Log Monitor

The `ghostwriter.modules.oplog_monitors.review_activity_logs` task checks operation logs for activity. It reviews all logs for all active projects. By default, the task looks for an entry created in the past 24 hours. You can change the hours by setting the `hours` argument when scheduling the task.
The `ghostwriter.modules.oplog_monitors.review_active_logs` task checks operation logs for activity. It reviews all logs for all active projects. By default, the task looks for an entry created in the past 24 hours. You can change the hours by setting the `hours` argument when scheduling the task.

The task outputs JSON detailing all activity logs that appear to be idle. This is useful for catching automated activity logging that may have started failing.

If a Slack webhook is enabled and configured, the task will also send a Slack message to the project's Slack channel (if configured) or the global Slack channel.

122 changes: 117 additions & 5 deletions DOCS/features/background-tasks/scheduled-tasks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,127 @@ description: "Configuring tasks to run repeatedly on a schedule"

## Scheduling a Task

Tasks are scheduled in the Django admin panel under _Django Q_ and _Scheduled Tasks_. Add a new task, give the task a name, and tell it which function to run. Functions must be prefixed with `tasks.` to tell Django Q to pull the functions from `tasks.py`. The final requirement is a schedule. Tasks can be run once or repeatedly (e.g. minutes, hourly, daily, weekly).
Tasks are scheduled in the Django admin panel under _Django Q_ and _Scheduled Tasks_. Add a new task, give the task a name, select one of the server-approved
functions, and configure its schedule. Tasks can run once or repeatedly (e.g. minutes, hourly, daily, weekly). You can schedule them by time or using `cron`.

<Info>
To schedule the `release_servers()` function inside `tasks.py`, you would enter the function name as: `tasks.release_servers`
The function and hook lists are controlled by a server-side allowlist. An administrator who only has access to the admin panel cannot add Python functions
or system commands to these lists.
</Info>
You can provide arguments for the functions as needed. Ghostwriter's scheduled tasks do not require arguments, but some have optional arguments. For example, the `scan_servers()` function accepts an `active_only` argument to restrict scanning only to servers that have been checked out for a project.

You can provide approved arguments for functions that accept them. Ghostwriter rejects unknown arguments and values with the wrong type. For example,
`ghostwriter.shepherd.tasks.scan_servers` accepts `only_active=True` to restrict scanning to servers that are in use.

### Example Scheduled Task

Visit the Django Q database from the admin panel to access the _Scheduled Tasks_. You may wish to create a scheduled task to automatically release domains at the end of a project. There is a task for this already in `tasks.py`, `ghostwriter.shepherd.tasks.release_domains`.
Visit the Django Q database from the admin panel to access the _Scheduled Tasks_. For example, you may wish to create a scheduled task to automatically release domains
at the end of a project. There is a task for this already in `tasks.py`, `ghostwriter.shepherd.tasks.release_domains`. It appears in the dropdown as _Release Domains_.

### Available Task Arguments

Arguments may be supplied positionally in the _Arguments_ field or by name in
the _Keyword arguments_ field. Both fields use Python literal syntax, so string
values must be quoted. For example, enter `'nightly-backup'` in _Arguments_ or
`command_name='nightly-backup'` in _Keyword arguments_. Do not provide the same
parameter in both fields.

| Function | Available parameters |
| --- | --- |
| `ghostwriter.reporting.tasks.archive_projects` | None |
| `ghostwriter.rolodex.tasks.check_project_freshness` | None |
| `ghostwriter.shepherd.tasks.check_domains` | `domain_id`: positive integer or `None`; omit it to check every domain |
| `ghostwriter.shepherd.tasks.check_expiration` | None |
| `ghostwriter.shepherd.tasks.fetch_namecheap_domains` | None |
| `ghostwriter.shepherd.tasks.release_domains` | `no_action`: boolean; `True` previews without releasing |
| `ghostwriter.shepherd.tasks.release_servers` | `no_action`: boolean; `True` previews without releasing |
| `ghostwriter.shepherd.tasks.review_cloud_infrastructure` | `aws_only_running`: boolean; `do_only_running`: boolean |
| `ghostwriter.shepherd.tasks.scan_servers` | `only_active`: boolean; `True` scans only servers in use |
| `ghostwriter.shepherd.tasks.update_dns` | `domain`: positive integer or `None`; omit it to update every domain |
| `ghostwriter.modules.oplog_monitors.review_active_logs` | `hours`: integer from 1 through 8,760; defaults to 24 |
| `ghostwriter.home.django_q_tasks.clear_expired_sessions` | None |
| Fixed system-command runner | `command_name`: one of the names in `GHOSTWRITER_DJANGO_Q_COMMANDS` |

## Configuring the Server Allowlist

Ghostwriter includes its prebuilt scheduled tasks in the default policy. Server operators can replace or extend `GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS` from a settings
fragment such as `settings/10-django-q-policy.py`. The Ghostwriter CLI mounts the `settings` directory read-only into the web and queue containers.

```python
GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS = {
**GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS,
"organization.tasks.refresh_inventory": {
"label": "Refresh Organization Inventory",
"args": [],
"kwargs": {
"active_only": {"type": "bool"},
},
},
}
```

Task entries use exact dotted callable paths. Prefixes and wildcards are not supported. Argument policies support `bool`, `float`, `int`, and `str` values, along with
`name`, `required`, `required_parameters`, `nullable`, `choices`, `min`, and `max` restrictions. A named positional argument can also appear in `kwargs`, allowing
administrators to use either input style. Set `allow_any_arguments` only for a trusted function that performs its own strict input validation.

Result hooks use the separate `GHOSTWRITER_DJANGO_Q_SCHEDULE_HOOKS` mapping. Ghostwriter exposes its built-in `ghostwriter.modules.notifications_slack.send_slack_complete_msg`
hook by default. Generic execution functions such as `os.system`, `subprocess.run`, and `django.core.management.call_command` should never be allowlisted.

### Fixed System Commands

Server operators can expose a fixed command by adding it to `GHOSTWRITER_DJANGO_Q_COMMANDS`:

```python
GHOSTWRITER_DJANGO_Q_COMMANDS = {
"nightly-backup": {
"argv": ["/usr/local/bin/ghostwriter-backup", "--quiet"],
"timeout": 1800,
"cwd": "/app",
"env": {},
},
}
```

The executable path and all arguments are fixed in the server configuration. Commands run without a shell and with only the explicitly configured environment. The admin
panel permits selecting the command name but does not permit editing the executable, arguments, working directory, or environment.

Restart both the Ghostwriter web and queue services after changing the policy. Changes are intentionally not loaded from the database or admin panel.

### Auditing Existing Schedules

The task allowlist is deployment-specific, so Ghostwriter does not use a Django
data migration to modify existing schedules. Instead, use the policy audit
command as an operational migration when upgrading or enabling a more
restrictive policy.

For the safest rollout:

1. Stop the Django Q cluster so an old scheduler or worker cannot execute tasks
during the upgrade.
2. Deploy the new Ghostwriter code and the intended server-side allowlist.
3. Audit the existing schedules:

```sh
python manage.py audit_django_q_policy --check
```

4. Add any legitimate custom tasks reported by the audit to the server-side
allowlist, or pause every schedule that does not satisfy the current policy:

```sh
python manage.py audit_django_q_policy --pause-disallowed
```

5. Restart the Ghostwriter web and Django Q services.

Disallowed schedules are paused by setting `repeats=0`; they are not deleted.
This preserves their configuration for later review. If a disallowed schedule
is missed by the audit, the restricted scheduler pauses it when it becomes due.
A task already present in the queue is checked again by the worker and recorded
as a failed task without importing or executing the denied function. Historical
successful and failed task records remain available, but the admin panel cannot
resubmit them unless they satisfy the current policy.

This example executes `ghostwriter.shepherd.tasks.release_domains` under the name `Domain Release` every day at 15:39 UTC with no additional args or kwargs.
<Warning>
The allowlist limits administrators who only control the application or Django admin panel. Anyone who can modify the server configuration, application code, queue,
database, or application secrets is already inside the server trust boundary.
</Warning>
Loading
Loading