Skip to content
Draft
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,74 @@ install packages api key: 12345
See [discussion here](https://github.com/SuffolkLITLab/docassemble-AssemblyLine/issues/69)


# Answer Set Import Safety Configuration

Answer set JSON imports are intentionally restricted to reduce risk from malformed and malicious payloads.

Default behavior:
- Plain JSON values are imported by default, and object reconstruction is allowed only for allowlisted DAObject classes.
- Top-level variable names must match `^[A-Za-z][A-Za-z0-9_]*$`.
- Internal/protected variable names are blocked.
- If `answer set import allowed variables` is not set, imports use a denylist-only policy for backwards compatibility.
- Object payloads can be imported when classes are allowlisted; by default, known `docassemble.base` and `docassemble.AssemblyLine` DAObject descendants are allowed.
Comment thread
nonprofittechy marked this conversation as resolved.
Outdated

Default import limits (`assembly line: answer set import limits`):
- `max bytes`: `1048576` (1 MB)
- `max depth`: `40`
- `max keys`: `20000`
- `max list items`: `5000`
- `max string length`: `200000`
- `max number abs`: `1000000000000000` (`10**15`)

Final allowlist/config policy:
- Default allowlist: unset (`answer set import allowed variables` omitted), to avoid breaking existing interviews unexpectedly.
- Recommended production policy: set an explicit allowlist to only shared/reusable variables in your jurisdiction.
- `answer set import allow objects` defaults to `true`; set it to `false` if you want strict plain-JSON-only imports.
- `answer set import allowed object classes` can extend the default DAObject class allowlist with explicit additional class paths.
- Additional classes in `answer set import allowed object classes` apply to object envelopes at any depth (top-level variables and nested descendants).
- `answer set import remap known classes` defaults to `true`; this safely maps known class basenames from other packages (such as playground exports) onto official allowlisted classes.
- `answer set import class remap` can define explicit basename-to-class mappings for additional controlled remaps.

Example hardened configuration:

```yaml
assembly line:
enable answer sets: true
enable answer set imports: true
answer set import require signed: false
answer set import allow objects: true
answer set import remap known classes: true
answer set import limits:
max bytes: 1048576
max depth: 40
max keys: 20000
max list items: 5000
max string length: 200000
max number abs: 1000000000000000
answer set import allowed variables:
- users_name
- users_address
- users_phone_number
- users_email
- household_size
answer set import allowed object classes:
- docassemble.AssemblyLine.al_general.ALIndividual
- docassemble.AssemblyLine.al_general.ALPeopleList
- docassemble.AssemblyLine.al_general.ALAddress
answer set import class remap:
ALIndividual: docassemble.AssemblyLine.al_general.ALIndividual
ALPeopleList: docassemble.AssemblyLine.al_general.ALPeopleList
```

Notes:
- Keeping `answer set import require signed: false` matches current compatibility-first behavior; unsigned imports still pass strict structural validation.
- If your environment can manage signing keys, set `answer set import require signed: true` to require signed payloads.
- Class allowlisting uses full dotted class names (exact match), not wildcard patterns.
- Playground-authored classes usually need explicit allowlisting, e.g. `docassemble.playground1.al_general.ALIndividual`.
- If a playground package name changes across environments (for example `playground1` to `playground2`), update `answer set import allowed object classes` to match the runtime class path.
- With `answer set import remap known classes: true`, exports that use known class basenames (for example `docassemble.playground1.al_general.ALIndividual`) can be remapped to official allowlisted classes without instantiating the playground class.


# ALDocument class

## Purpose
Expand Down
8 changes: 4 additions & 4 deletions docassemble/AssemblyLine/data/questions/al_document.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ code: |
key=action_argument("key"),
preferred_formats=preferred_formats,
)
email_arg = action_argument('email')
email_arg = action_argument("email")
if isinstance(email_arg, list):
email_str = ', '.join(email_arg)
email_str = ", ".join(email_arg)
else:
email_str = str(email_arg)
if email_success:
Expand Down Expand Up @@ -72,9 +72,9 @@ code: |
key=action_argument("key"),
preferred_formats=preferred_formats,
)
email_arg = action_argument('email')
email_arg = action_argument("email")
if isinstance(email_arg, list):
email_str = ', '.join(email_arg)
email_str = ", ".join(email_arg)
else:
email_str = str(email_arg)
if email_success:
Expand Down
32 changes: 26 additions & 6 deletions docassemble/AssemblyLine/data/questions/al_saved_sessions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ code: |
# HACK
# Create a placeholder value to avoid playground errors
al_sessions_snapshot_results = DAEmpty()
al_sessions_last_import_report = {"accepted": [], "rejected": [], "warnings": []}
---
initial: True
code: |
Expand Down Expand Up @@ -217,18 +218,39 @@ back button: False
---
id: al sessions load status
continue button field: al_sessions_load_status
comment: |
#TODO There's no error handling yet so this might be a lie
question: |
% if al_sessions_snapshot_results:
Your answer set was loaded
% else:
Your answer set was not loaded. You can try again.
% endif
subquestion: |
% if defined('al_sessions_last_import_report'):
% if al_sessions_last_import_report.get('warnings'):
${ collapse_template(al_sessions_import_warnings_template) }
% endif
% if al_sessions_last_import_report.get('rejected'):
${ collapse_template(al_sessions_import_rejected_template) }
% endif
% endif

Tap "next" to keep answering any unanswered questions and finish the interview.
back button: False
---
template: al_sessions_import_warnings_template
subject: Import warnings
content: |
% for warning in al_sessions_last_import_report.get('warnings', []):
* ${ warning }
% endfor
---
template: al_sessions_import_rejected_template
subject: Variables skipped during import
content: |
% for item in al_sessions_last_import_report.get('rejected', []):
* `${ item.get('path', '?') }`: ${ item.get('reason', 'unknown reason') }
% endfor
Comment thread
nonprofittechy marked this conversation as resolved.
Outdated
---
question: |
Upload a JSON file
subquestion: |
Expand All @@ -239,11 +261,9 @@ fields:
accept: |
"application/json, text/json, text/*, .json"
validation code: |
try:
json.loads(al_sessions_json_file.slurp())
except:
validation_error("Upload a file with valid JSON")
is_valid_json(al_sessions_json_file.slurp())
---
code: |
al_sessions_snapshot_results = load_interview_json(al_sessions_json_file.slurp())
al_sessions_last_import_report = get_last_import_report()
al_sessions_import_json = True
8 changes: 7 additions & 1 deletion docassemble/AssemblyLine/data/questions/al_settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,10 @@ code: |
---
code: |
# Can be an exact path or just a name, in which case we will search /usr/share/fonts and /var/www/.fonts for a matching file ending in .ttf
al_typed_signature_font = "/usr/share/fonts/truetype/google-fonts/BadScript-Regular.ttf"
al_typed_signature_font = "/usr/share/fonts/truetype/google-fonts/BadScript-Regular.ttf"
---
code: |
# Allow users to import answer sets from JSON files.
# The global config 'enable answer set imports' is checked first; this variable allows
# interview authors to disable imports at the interview level even if global config permits them.
al_allow_answer_set_imports = True
1 change: 1 addition & 0 deletions docassemble/AssemblyLine/data/questions/al_visual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ data from code:
(
get_config('assembly line',{}).get('enable answer sets')
and get_config('assembly line',{}).get('enable answer set imports')
and al_allow_answer_set_imports
)
or (user_logged_in() and user_has_privilege('admin'))
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"users_name": "Alex",
"city": "Boston",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"users_name": "Alex",
"__class__": "builtins.object",
"city": "Boston"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"users_name": "Alex",
"_internal": {
"steps": 99
},
"city": "Boston"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"users": {
"_class": "docassemble.playground1.al_general.ALPeopleList",
"instanceName": "users",
"object_type": {
"_class": "type",
"name": "docassemble.playground1.al_general.ALIndividual"
},
"elements": [
{
"_class": "docassemble.playground1.al_general.ALIndividual",
"instanceName": "users[0]",
"name": {
"_class": "docassemble.playground1.al_general.IndividualName",
"instanceName": "users[0].name",
"first": "Client",
"last": "Example"
}
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"users": {
"_class": "docassemble.bad.Actor",
"instanceName": "users"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"users": {
"_class": "docassemble.AssemblyLine.al_general.ALPeopleList",
"instanceName": "users",
"object_type": {
"_class": "type",
"name": "docassemble.AssemblyLine.al_general.ALIndividual"
},
"elements": [
{
"_class": "docassemble.AssemblyLine.al_general.ALIndividual",
"instanceName": "users[0]",
"name": {
"_class": "docassemble.base.util.IndividualName",
"instanceName": "users[0].name",
"first": "Client",
"last": "Example"
},
"agent": {
"_class": "docassemble.AssemblyLine.al_general.ALIndividual",
"instanceName": "spouse"
},
"custom_text": "notes",
"custom_float": 1.25,
"custom_dict": {
"_class": "docassemble.base.util.DADict",
"instanceName": "users[0].custom_dict",
"elements": {
"case": "A123"
}
}
},
{
"_class": "docassemble.AssemblyLine.al_general.ALIndividual",
"instanceName": "spouse",
"name": {
"_class": "docassemble.base.util.IndividualName",
"instanceName": "spouse.name",
"first": "Spouse",
"last": "Example"
}
}
]
}
}
Loading
Loading