DataTalksClub / DataTalksClub/website
Deliver secret-safe Slack access grants, reveal, and durable email
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Parent epic: #108
Outcome
After the first accepted version-1 profile completion of an eligible verified member, atomically create or confirm one accounts-owned SlackAccessGrant and, for the new-account onboarding sequence, one #49 logical delivery plus durable job for the current non-secret invite version. Expose the current shared DataTalks.Club Slack join URL only after an authenticated member deliberately reveals it at /accounts/community/slack/.
The profile/grant remain saved when the worker, Relay, or runtime secret is unavailable. The MVP has no Slack API, SCIM/member synchronization, membership lookup/revocation, public join URL, or manual review queue.
Normative authority
_docs/PROCESS.md— role-separated lifecycle and versioned verification._docs/specs/01-platform-architecture.md, “Member signup, profile, Slack, and course registration” — atomic completion/grant/delivery and secret-safe reveal._docs/specs/05-events-registration-email.md, “Slack-access transactional delivery” and “Durable delivery model” — one-time purpose, invite rotation, #49 state/retry/suppression boundary, and no-secret retention._docs/specs/06-studio-and-admin-api.md, “Member self service” — exact private member surface and no self-service resend API._docs/specs/07-security-privacy-operations.md— identity denial, private caching, redaction, deletion, retention, accessibility, and observability._docs/specs/10-verification-strategy.md, “Member onboarding and Slack” — required fault, browser, privacy, and artifact canaries._docs/specs/open-decisions.md, resolved decisions 10, 11, 12, 19, and 21 — Relay ownership; purpose availability; unified retention; immediate shared-link eligibility; email-verification-first ordering; Slack is a one-time requested-service message rather than a recurring preference.- #247 owns the MemberProfile model/service and #248 owns verified completion and account-settings onboarding. #49 owns
EmailDelivery, durable jobs, Relay submission, projections, callbacks, reconciliation, retry, ambiguity, and suppression.
Trigger reconciliation for this child: specification 01 and resolved decision 19 make the first member-confirmed version-1 completion the mutation boundary. Email verification is a mandatory prerequisite and ordering gate, not a separate verification-callback grant/send trigger; #248 itself creates no Slack or email state. The legacy/imported bypass applies to the automatic delivery only, not to the verified-complete profile requirement for reveal eligibility.
Hard dependencies and start gate
This issue is groomed but BLOCKED on accepted, integrated, pushed, and on-call-green #248 plus an accepted, integrated, pushed, and on-call-green ordinary-delivery slice of #49. #248 must carry the accepted #247/#224/#231/#234/#51 artifacts described in its refreshed gate; #249 must not substitute issue prose, a frozen candidate, or already-present source for any accepted interface.
Before engineering starts, record in the engineer handoff:
- the accepted merge and migration identities for #247 and #248, including the exact completion service boundary and #234-derived eligible/survivor resolver;
- the accepted #49 merge identity and exact public service/model symbols implementing the semantic interfaces below;
- the immutable Relay
slack_accesspurpose route: template key/version, approved sender ID, typed context schema, scoped website credential contract, and exact pinned Relay OpenAPI/deployment evidence required by #49; - the configured initial positive
invite_version, with a synthetic secret resolver in tests. No real secret or provider access is part of engineering or automated verification.
#64 remains the owner of the global rights/erasure/restore workflow. This issue supplies the Slack-specific deny/cancel hook that #64 later invokes; #64 is not a blocker for synthetic implementation. #9/runtime infrastructure is a blocker only for the deployed HUMAN gate, not local engineering.
The required #49 artifact is the bounded ordinary-delivery contract, not closure of every management or restore concern in the #49 parent. It must include the exact atomic intent/job service, model/state projection, leased/fenced handler seam, stable idempotency/request-hash rules, and an immutable Relay slack_access route whose late-bound sensitive scalar can be resolved only in the worker and transmitted without entering the website persisted context, operator-visible hash material, logs, traces, audits, or evidence. The artifact must carry its own accepted Relay/template/credential prerequisites. #249 grants no Relay, provider, secret-management, infrastructure, or production authority.
The dependency direction is one-way: #249 supplies the accepted grant/reveal/deny interfaces later consumed by #251 privacy integration and, through #251 plus the management foundations and owner decisions, #250 support correction/manual resend. #249 does not depend on #250, #251, #32, #33, or global privacy/recovery closure, so no member-management/privacy cycle exists.
Ownership and exact grant contract
accounts owns one SlackAccessGrant per durable account. Add only the minimum secret-free state:
- UUID primary key;
- required one-to-one owner
CustomUser, cascading only with the owning account; - required MemberProfile UUID/reference, which must belong to that same owner;
completion_versionfixed to the accepted profile schema version that established eligibility;- positive
granted_invite_version, recording the active non-secret invite version at first grant; granted_at,created_at, andupdated_attimestamps.
Database and service constraints enforce one grant per account, profile/owner equality, completion_version >= 1, and granted_invite_version >= 1. The grant stores no email, profile scalar, join URL, secret reference/name, rendered content, provider payload, Relay credential, delivery status copy, or external Slack membership claim. Current eligibility is computed through the accepted accounts identity/profile service; quarantine, disablement, deletion, broken/cyclic alias, collision, unverified ownership, or profile mismatch never become a permissive grant state.
The active invite version is a positive, non-secret runtime configuration value paired operationally with the separately stored secret. Changing the URL without advancing the version is invalid and fails closed. A version may never be reused for different URL bytes. Grant rows preserve the version at which access was established; each delivery pins its own invite version. Ordinary profile edits/revisions do not modify, revoke, or recreate a grant.
Accounts service and atomic completion hook
#249 adds one accounts-owned semantic command, called directly from #248's application service rather than a signal, model hook, view, form, or job:
ensure_slack_access_for_completion(owner, profile, completion_version, onboarding_classification)
The command runs inside the same outer database transaction only on the first successful completion_version: 0 -> 1 member-confirmed transition. Verified ownership is already a prerequisite of that transition. Later edits, reconfirmations, login, verification callbacks, GETs, retries, or rotation do not invoke an automatic-send path. It:
- re-resolves the canonical eligible owner through the accepted #234/#247 identity boundary and locks the owner/profile/grant uniqueness boundary;
- requires current verified ownership, matching completed profile, and the submitted completion version; otherwise it returns the same generic denial and creates nothing;
- creates or confirms the one grant using the current non-secret invite version;
- for a newly verified/new-account onboarding sequence, calls #49's atomic
create logical delivery + durable jobinterface exactly once; - for an explicitly legacy/imported contact, creates/confirms reveal eligibility but creates no automatic Slack delivery, matching the resolved bypass rule;
- returns a secret-free result containing only grant UUID, completion version, invite version, and a bounded outcome code.
The #49 call uses:
- classification
transactional, purposeslack_access, the immutable accepted template key/version and approved Relay sender ID; - the grant/account/profile scalar references allowed by #49;
- the verified recipient snapshot/reference permitted by the unified retention contract;
- secret-free context containing only the schema/invite version and any accepted fixed scalar template fields;
- logical key
slack_access:<grant-uuid>:completion:<completion-version>:invite:<invite-version>and the corresponding #49 canonical request hash; - a worker resolver key/callback, not a stored URL or secret value.
The delivery and durable job are created by #49 in the same transaction; no transaction.on_commit substitute may create one without the other. Rollback of completion/grant/delivery/job leaves none of the new state. Unique constraints plus conflict recovery make duplicate and concurrent completion return the original grant/delivery/job. A same-key changed recipient, template, sender, context, completion version, or invite version is a conflict and sends nothing.
The verified recipient snapshot is immutable for that logical delivery. A later profile edit or email change does not mutate/replay it and does not automatically create another Slack message.
Secret resolver and #49 worker boundary
Only two application boundaries may resolve the current shared join URL:
- the leased/fenced #49 Slack delivery handler after database commit; and
- the authenticated reveal POST described below.
Both call one narrow runtime resolver equivalent to:
resolve_slack_join_url(expected_invite_version) -> secret value | unavailable/version_mismatch
The resolver verifies that the configured active version equals the expected version. It returns the secret only in memory to the immediate Relay request or eligible reveal response. The value must not be returned by diagnostic/status methods, interpolated into exceptions, request/job/audit representations, retained request/context/body records, request hashes visible to operators, traces, or test artifacts.
The leased handler rechecks owner/profile/grant eligibility immediately before resolving the secret and immediately before #49 submission. It then submits the exact accepted immutable request through #49. Exact retries preserve the same version and request hash. Missing secret, version mismatch, or Relay unavailability is safe retryable/delayed work under #49. An uncertain acknowledgement is ambiguous and is never automatically resent. Suppressed, dead, hard-bounced, or complained delivery does not revoke reveal eligibility and never causes a direct Relay/SES/Datamailer fallback.
No domain service, transaction, callback, page GET, model save, migration, or test contacts Relay. The website never calls a Slack API.
Invite-version rotation
Rotation is an operational configuration/deployment action outside this issue's member UI:
- publish/accept the new immutable Relay route if required, provision the new shared URL in the approved secret channel, and advance the positive invite version together;
- never log/read back the URL, reuse an old version, rewrite an existing delivery, or send a bulk/automatic replacement message;
- existing eligible members immediately reveal the currently configured active version without editing/reconfirming profile data; reveal authorization is based on current eligibility plus the grant, not equality with the grant’s historical
granted_invite_version; each worker submission remains pinned to its delivery’s immutable expected version and fails closed on mismatch; - resolved decision 21 remains controlling: rotation alone does not automatically send Slack email again;
- a later authorized resend uses the separate member-management child, creates a new audited #49 manual-resend intent pinned to the current version, and is not implemented here.
Automated rotation tests use only two synthetic canary URLs and prove that the old expected version fails closed after rotation, current reveal uses only the new canary, existing initial deliveries remain immutable, and no new delivery/job/network call is created by rotation or GET.
Exact member route, states, and copy
GET|POST /accounts/community/slack/ is HTML-only. There is no self API representation or member-facing resend command.
Common response policy for every status and redirect: authenticated/private zero-TTL, Cache-Control: private, no-store, X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer, no sitemap/search/public serializer entry, no analytics containing account/grant/delivery identifiers, and no shared cache storage.
- Signed out: follow the accepted safe login/path-intent flow; Location contains no email, grant ID, token, or secret.
- Authenticated but incomplete: preserve the local Slack intent and continue through
/accounts/profile/; completion returns here once. Existing non-Slack account access remains unchanged. - Inactive, quarantined, absorbed without one valid survivor, colliding, unverified, deleted, broken/mismatched, or otherwise unavailable identity: generic private
404; no existence or missing-field detail and no mutation. - Eligible
GET: contains no join URL. HeadingJoin the DataTalks.Club Slack, bodyYour community access is ready., and buttonReveal Slack invitationposting to the same route with CSRF. - Eligible
POST: re-resolve eligibility, require CSRF, resolve the current secret only after authorization, and render headingJoin the DataTalks.Club Slack, bodyYour invitation is ready. This link is shared—please keep it private., and actionOpen Slack invitation. The external anchor usesrel="noopener noreferrer"; the response remains no-store/referrer-safe. - Secret missing/version mismatch/resolver failure: no link and no cause detail. Heading
Slack invitation temporarily unavailable; bodyYour access is saved. Please try again later or contact support.
The page may show only these folded delivery messages, never raw #49 reason/provider data:
pending,queued,leased,retryable:Your email may be delayed. You can still reveal the invitation here.provider_accepted,delivered:Your Slack access email has been processed.(provider_acceptedis never called delivered.)ambiguous:We could not confirm the email status. You can still reveal the invitation here.suppressed,dead,hard_bounced,complained:We could not deliver the email. You can still reveal the invitation here or contact support.- no automatic delivery (legacy/imported) or no projection: omit delivery copy; do not imply an email was sent.
Refresh/back/duplicate GET or POST may repeat the same authorized reveal response but creates no grant, delivery, job, resend, profile change, or external membership. Logout/session flush and account switch must prevent browser history/back-forward cache from revealing the prior member's response.
Disablement, quarantine, deletion, and cancellation
Provide one idempotent accounts service hook for the owning identity/privacy lifecycle:
deny_slack_access_and_cancel_unsubmitted(owner, safe_reason_code)
Within its owning transaction it fences all not-yet-submitted Slack jobs for the account and makes every future reveal/handler eligibility check fail. It does not resolve the secret or make a network call. A leased/submitted/ambiguous/terminal Relay message is never falsely labelled cancelled or rolled back; #49 reconciliation/suppression owns its truthful projection, and any external propagation is queued only after commit through the accepted durable mechanism.
Disablement or quarantine preserves the minimum grant/delivery metadata required by policy but denies reveal/resend. Full account erasure removes the grant/profile PII and future eligibility, cancels safe unsubmitted work, and lets #49 retain only its approved redacted non-PII reconciliation metadata for the approved 180-day window. #64 owns processor propagation and restored-backup tombstone replay. Neither this hook nor UI claims that an already used Slack URL or external Slack membership was revoked.
Acceptance criteria
- Accepted/integrated/pushed/on-call-green #248 (carrying #247 and its prerequisites) and the bounded ordinary #49 artifact identities are recorded, including the final accounts migration leaf and exact late-bound Relay route/consumer contract, before engineering starts.
- One secret-free grant per eligible durable owner enforces owner/profile/completion/version constraints and has zero
content.Personread/write/link/authority side effect. - First eligible new-account completion atomically commits completion, grant, one logical delivery, and one durable job; rollback creates none and no network/secret read occurs in the transaction.
- Legacy/imported completion creates reveal eligibility without an automatic Slack email; Slack remains outside recurring preference toggles and is never resent automatically.
- Duplicate, replayed, concurrent, restarted, and lease-expired work converges to one grant/delivery/job per logical key; changed work conflicts; ambiguity is never automatically resent.
- Only the leased handler and authorized reveal POST resolve the version-matched secret in memory; every persisted/logged/audited/metric/API/OpenAPI/screenshot/issue artifact is secret-free.
- The exact GET/POST authentication, completion resume, CSRF, generic denial, private headers, copy, folded delivery states, external-link attributes, logout/history, and no-side-effect behavior pass.
- Worker/Relay/secret failure preserves profile and eligibility; suppression/failure affects email status only and leaves reveal available when the account remains eligible.
- Invite rotation advances version without profile re-entry, old-version reveal, delivery mutation, automatic/bulk email, duplicate work, or version reuse.
- Disablement/quarantine/erasure immediately deny reveal and fence safe unsubmitted work without falsely claiming submitted email or external Slack membership was revoked.
- #49 focused fault/security tests, migration drift, OpenAPI/cache-route parity, accessibility, container checks, and the graph-selected versioned verification plan pass with no required skip.
- Independent tester Playwright passes desktop/mobile, reads every required screenshot, and proves screenshots/artifacts contain no join URL, email, account/profile/grant ID, session/CSRF value, provider payload, or real member data.
- [HUMAN] In an authorized deployed environment, an operator configures the real versioned secret through the approved channel without reading it into evidence; one synthetic verified new member completes onboarding, receives the one-time message through the normal #49 path, and reveals a working current link. Evidence records only version, safe state/outcome, deployment identity, and timestamps.
Required Django/service/security scenarios
- First completion, DB failure at each write, duplicate HTML/API completion, two concurrent completions, replay/restart, mismatched owner/profile/version, and exact unique-race recovery.
- Active verified owner; valid absorbed-source session resolving to survivor; unverified/expired evidence; inactive; quarantine; normalized collision; missing/broken/cyclic alias; profile/user mismatch; deleted account. Assert generic denial and zero unauthorized mutation/secret resolution.
- New-account versus explicit legacy/imported onboarding classification; verify exactly one automatic delivery only for the former and no preference toggle can suppress or trigger Slack access.
- #49 handler before/after job claim, lease expiry, secret unavailable, version mismatch, timeout before Relay commit, response loss after commit, exact replay, request conflict, callback loss/reorder, retryable, provider-accepted, delivered, ambiguous, suppressed, dead, hard-bounced, and complained.
- Resolver and artifact canaries across database rows, serialized job arguments/results, request hashes visible to operators, exception text, logs, metrics, audits, traces, email retained context/body, OpenAPI/examples, screenshots, test reports, and issue-ready evidence.
- GET side-effect freedom; POST absent/invalid CSRF; another account/session; logout; account switch; refresh/back/duplicate POST; safe login/profile intent; every status/header/copy fold; no raw reason/provider/recipient exposure.
- Synthetic invite version 1 -> 2 rotation, stale/missing/mismatched version, attempted version reuse/change without bump, current reveal, immutable old delivery, and zero rotation-triggered send.
- Disable/quarantine/delete before job claim, while leased, after Relay acceptance, during ambiguity, and after terminal status. Assert only safe unsubmitted work is fenced, truthful #49 projection retained, future reveal/resend denied, and no external membership-revocation claim.
- Retention expiry and #64 hook contract with only synthetic rows: grant deletion/anonymization, redacted delivery metadata boundary, idempotent replay, and no secret/provider call.
Browser, no-JavaScript, accessibility, and screenshot scenarios
The independent tester uses synthetic identities and synthetic secret canaries, captures under .tmp/screenshots/ at about 1440×900 and 390×844, and reads every image:
- Signed-out Slack route follows the safe login flow; verified incomplete member resumes the first missing profile field and returns once after completion.
- Eligible GET shows the exact heading/body/reveal button and folded pending state with keyboard-visible focus, usable target size, zoom/reflow, contrast, and JavaScript disabled.
- CSRF-protected reveal succeeds with a synthetic link and the expected copy/anchor policy. Assert the link in DOM and navigation without capturing it in a screenshot; screenshots are taken before reveal or with the link absent.
- Missing secret/version mismatch shows the exact delayed/support state without leaking whether the secret, worker, Relay, delivery, or account caused it.
- Provider-accepted is not described as delivered; ambiguous and suppression/terminal folds use the exact safe copy and retain reveal eligibility.
- Inactive/quarantined/colliding/broken identities and another account receive the same generic private denial with no object/member detail.
- Refresh/back/duplicate POST creates nothing; logout and account switch followed by browser back do not reveal the prior response.
- Rotation makes the current synthetic link available without profile re-entry or auto-email; no image/report contains either old or new synthetic URL canary.
HUMAN evidence and delivery convention
Automated engineering/tester/PM acceptance uses synthetic secrets, simulation/allowlisting, and no real provider call. After all automated gates pass, add human; the accepted engineering commit uses Refs #249, and #249 stays open until the deployed HUMAN criterion passes. The HUMAN operator records only the exact deployment identity, invite version, safe delivery/reveal outcomes, and time window—never the URL, email, profile value, credential, provider payload, screenshot of the revealed link, or production member data.
After that redacted HUMAN evidence, PM may close #249. Parent #108 remains open until all five child lanes and its own final HUMAN coordination criterion are complete.
Explicit non-goals
No MemberProfile schema/catalog/reconciliation change; no course registration/enrollment/snapshot; no Studio/admin member correction or resend UI/API; no member-facing resend; no #64 global rights engine, provider propagation, or restore implementation; no Slack API/SCIM/member directory/membership lookup/revocation; no manual review; no public /slack join link change; no recurring preference toggle/unsubscribe for Slack; no automatic resend on profile/email edit or invite rotation; no template editor/renderer/store; no second outbox/provider-attempt/event stack; no direct Relay call in a transaction; no direct Amazon SES/Datamailer fallback; no production secret inspection, provider mutation, infrastructure apply, protected-data access, or real send during automated acceptance.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with _docs/PROCESS.md and the referenced architecture, delivery, API, security, and verification specifications, then review the accepted #247, #248, and #49 interfaces required by the handoff. The implementation entry point is #248's application service calling accounts' ensure_slack_access_for_completion, with #49 providing the atomic delivery and worker boundary. Done means the secret-free grant, reveal behavior, durable delivery contract, failure handling, and required verification artifacts satisfy the stated constraints; engineering is blocked until the specified dependencies are accepted and integrated.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, authentication, backend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100