Skip to content

feat(shortener): custom domains#74

Open
PenguinzTech wants to merge 4 commits into
release/v0.1.xfrom
fix/tenant-team-isolation
Open

feat(shortener): custom domains#74
PenguinzTech wants to merge 4 commits into
release/v0.1.xfrom
fix/tenant-team-isolation

Conversation

@PenguinzTech

Copy link
Copy Markdown
Contributor

Summary

Implements custom domain registration, DNS TXT verification, and per-domain redirect routing for the shortener service. Enables Free tier users to add custom domains, route requests through them, and track TLS provisioning status.

  • Domain model with tenant scoping and unique (tenant_id, domain_name) constraint
  • DNS TXT challenge-based verification (dnspython)
  • Per-domain redirect routing: same short_code resolves differently per domain/tenant
  • Feature flag gating: current.custom-domains (Free tier, default OFF)
  • All redirect hardening preserved (rate-limit, bot-filter, GeoIP, validation)
  • Alembic migration with clean upgrade/downgrade on SQLite

Test plan

  • 20 tests: domain registration, validation, verification, listing, deletion, routing
  • 75% coverage on domains.py
  • Feature flag behavior verified (enabled/disabled states)
  • Tenant isolation enforced
  • DNS resolution with mocking (success/failure cases)
  • Redirect hardening regression tests
  • Reserved domain rejection
  • Hostname format validation

🤖 Generated with Claude Code

PenguinzTech and others added 4 commits July 13, 2026 20:09
…startup

Fixes two critical findings:
- C1: JWT_SECRET_KEY no longer falls back to the dev SECRET_KEY default in
  production. ProductionConfig now requires it via env var and validate()
  rejects the dev default, closing an admin-token forgery vector.
- C2: create_app() now calls ProductionConfig.validate() at startup (guarded
  by hasattr so Dev/Testing configs are unaffected), so the dead secret-guard
  actually runs and the app boot-fails on default/missing secrets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tokens off URL

Fixes account-takeover and token-leak findings in the OIDC/SSO consumer:
- OC1: callback now validates the id_token (JWKS signature, iss/aud/exp/nonce)
  instead of blindly trusting the userinfo response; identity derives from
  validated claims. Adds PKCE (S256) + nonce to authorize/callback.
- OC2: auto-link/auto-provision gated on email_verified (+ OIDC_AUTO_LINK_
  VERIFIED_EMAIL flag, default off); no more silent email-based account linking.
- OH4: access/refresh tokens returned as httpOnly Secure SameSite cookies, not
  in the redirect URL query string.
- OH5: identity linking only via authenticated OIDC round-trip; removed
  body-supplied external_sub/external_email linking.

Adds nullable columns oidc_providers.issuer, user_oidc_links.email_verified.
_encrypt_secret fail-open and update_provider gating deferred to A5.

Full backend suite: 403 passed, 1 skipped, 90.16% coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…H3/H1)

Closes IDOR: a global viewer holds global tenants:read/teams:read, so the
scope gate passed for everyone and routes then loaded any object by id with
no membership check — any user could read any org's tenants/teams + member
emails across tenants.

- Adds membership helpers in rbac.py; each object-scoped tenant/team route now
  requires the caller to be a global admin OR a member of that specific
  tenant/team (admin ops require tenant/team admin scope, resolved with the id
  so tenant/team-scoped roles are actually consulted).
- create_team validates the body tenant_id against caller membership (no more
  cross-tenant writes).
- Regression tests: non-member viewer 403, member 200, global admin 200,
  foreign-tenant create 403.

Also gitignore /worktrees/ (repo-local worktree convention).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…follow-up)

The initial isolation fix guarded only read routes; write/admin routes still had
just a global scope gate, so a user with global teams:write/tenants:write (e.g. a
global maintainer) could modify/delete any team/tenant and grant roles across orgs
— privilege escalation. (Also restores object-scoped authz that had regressed when
team_id_param was dropped from the decorators.)

- Adds _is_team_admin / _is_tenant_admin helpers (global admin scope OR team/tenant-
  scoped admin role for THAT object).
- update/delete/add-member/remove-member on both teams.py and tenants.py now require
  the caller to be an admin of that specific team/tenant (or global admin).
- add_team_member/add_tenant_member especially: they grant roles incl. *_admin, so
  non-admins of the object are now blocked.
- Regression tests: global maintainer (no admin role) → 403 on every write route;
  team/tenant admin → 200/201.

Full suite: 423 passed, 90.10% coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @PenguinzTech, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

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