Skip to content

SpaceWorks 0.8.0 - #16

Merged
Shaan-Shoukath merged 7 commits into
mainfrom
dev
Sep 1, 2026
Merged

SpaceWorks 0.8.0#16
Shaan-Shoukath merged 7 commits into
mainfrom
dev

Conversation

@Shaan-Shoukath

Copy link
Copy Markdown
Collaborator
  • fix(scripts): record the executable bit in git so the tarball ships it
  • fix(requests): let public request submission work with the membership module off
  • refactor(makerspaces): move the encrypted-secret accessors to a mixin
  • feat(makerspaces): anonymous-request principal and its credential refusals
  • feat(requests): opt-in account-less public borrow requests
  • feat: outbound-only Telegram, account-less requests, staff dock, guard fixes
  • fix(requests): make account-less borrowing usable, visible and audited

Shaan-Shoukath and others added 7 commits August 30, 2026 18:21
This repository has core.fileMode = false, so chmod +x never reaches a commit.
The mode guard measured os.access() on the working tree, which reports a local
bit no clone and no release tarball ever sees.

That blind spot voided a production fix. scripts/restore.sh was chmod'd 0755 and
guarded on 2026-08-25, the guard went green, and `git archive HEAD` still shipped
it 0644 - so import-backup.sh:134 still died at `exec .../restore.sh` with
"Permission denied", after recording the restore intent and releasing its lock.
install.sh, import-backup.sh and verify-release-images.sh shipped 0644 too; only
spaceworks-compose.sh was ever fixed, via git update-index.

Modes are now set with git update-index --chmod=+x, and the guard reads
`git ls-files -s` because for every script install.sh does not chmod at unpack
time, git's mode IS the shipped mode. Verified by clearing the bit in the index
while the working tree stayed 755: the guard fails, where before it passed.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… module off

setup.sh ships MSPROFILE="recommended", whose extras include member_accounts but
not membership. request_workflow is core and cannot be turned off. But
RequestSubmitView called require_active_member_presence unconditionally, and that
guard hard-requires an active MakerspaceMembership row without ever consulting the
module registry. So a fresh self-host install that keeps the default profile
shipped unable to accept a public borrow request: a makerspace built from
profile_modules('recommended') returned 403 membership_required to an
authenticated user. The cloud and full profiles include membership and were
unaffected, as is any operator who ticked it on in the setup checklist.

Only RequestSubmitView changes. The membership row is the ONLY thing binding a
user to a makerspace in that guard - is_servable, is_authenticated, is_active and
access_status are all global - so relaxing every caller would let any account act
at any membership-off space. Submitting a request is a proposal staff must
accept; the other surfaces move hardware or reserve capacity, and tests assert
they still refuse.

Waivers cannot be enforced with membership off: acceptance is stored on
MakerspaceMembership and accepting one requires an active membership. In that
configuration the flow is public request -> staff accept.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
models_makerspace.py was at 292 lines against the repo's ~300 hard ceiling, and the
next phase adds fields to Makerspace. CLAUDE.md requires splitting an at-ceiling
file in its own commit before adding to it.

The ten encrypt-on-set/decrypt-on-get accessors over five credential columns are
one repeated shape with no field definitions, so moving them changes no schema:
`makemigrations --check` reports no changes. A mixin rather than free functions
because every method reads and writes an attribute of the row it is called on.

260 lines now, and nothing outside the mixin touches the ciphertext columns, which
keeps API_CLIENT_ENC_KEY the single decryption door.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…usals

Substrate for opt-in account-less borrow requests. No endpoint changes yet.

HardwareRequest.requester is a non-null PROTECT FK, so an account-less request
still needs a User row - but AuditLog.actor is nullable and record(None, ...) is
supported, so anonymous actions will record actor=None rather than be attributed
to this principal. One User per anonymous request was rejected outright: both FKs
are PROTECT, so every such row would be permanently undeletable, making an
unauthenticated endpoint an unbounded undeletable-PII growth path.

So: one inert principal per makerspace, created lazily under the makerspace
select_for_update lock (same order as walk-in creation, for the deadlock its
comment describes), with an unusable password, is_active=False and no contact
columns. It is identified by a DB-unique OneToOneField, never a username prefix -
the invariants pin walkin_/member_ and a backfill that reads them.

anonymous_requests_enabled defaults False and is deliberately NOT inferred from
the membership module: recommended installs omit that module, so inferring would
silently open an unauthenticated write path on every deployment at upgrade.

principal_guards refuses the principal for password set/reset, phone and social
binding, and access mutation/restore, in shared services that both the REST paths
and the Django admin call - a check in one leaves the other open. Without this the
principal was convertible into a real account.

Four registries had to be fed, one cascading after the next: the Lane D field
snapshot, the catalog digest (updated after diffing catalog_schema, which showed
exactly the two new fields and no other model), data_export's relational user
edges plus classification, and the deployment-global uniqueness policy - the
OneToOne creates a global constraint, and the target must create its own
principal rather than import an inert source User.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
RequestSubmitView becomes AllowAny plus an in-view rule, so behaviour is
unchanged for every deployment that has not opted in: an anonymous POST to a
makerspace without anonymous_requests_enabled still returns exactly today's 401.
With the flag on, a stranger submits with contact details and gets a public token.

Identity: requester points at the makerspace's inert principal, the human's
details go in the existing requester_name/contact_email/contact_phone snapshot
columns, and the audit row records actor=None rather than attributing the act to
the principal. The principal's username is never copied into the snapshot.
Authenticated submissions ignore the contact fields entirely, so a signed-in user
cannot use them to impersonate someone.

This is an unauthenticated write path, so the limits are part of the feature, not
hardening to follow. Each accepted payload creates a request, items, an audit row
and a notification fan-out, and a supplied address would otherwise trigger both
requester and staff mail - an email-bomb and staff-amplification surface. Hence
2/min and 10/hour per IP, 3/day per email keyed on a fingerprint rather than the
address, a ceiling on outstanding anonymous requests, a required Idempotency-Key,
and caps on item count, quantity and every text field.

Nothing proves a supplied address belongs to the sender and staff acceptance does
not prove it either, so the contact is marked unverified and requester lifecycle
mail is suppressed until it is.

Verified on the running stack, not only in tests: requester is the principal, the
principal is inactive with an unusable password, the contact snapshot is correct,
no username leaks into the snapshot, and the audit actor is None.

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6 <noreply@openai.com>
…d fixes

Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shaan-Shoukath <shaanshoukath4522@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Shaan-Shoukath
Shaan-Shoukath merged commit b15c4e1 into main Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant