Commit graph

1,722 commits

Author SHA1 Message Date
7bb29708c6 Add usescases to handle checkers 2026-03-16 23:08:55 +07:00
3bfd68c51d Write plugin technical documentation 2026-03-16 23:08:55 +07:00
4ed76a8a11 Load checks plugins 2026-03-16 23:08:55 +07:00
c384c10a88 New custom flag parser: ArrayArgs 2026-03-16 23:08:55 +07:00
f16ae2991e fix: refresh ButtonZonePublish after zone apply
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-16 21:00:35 +07:00
72fa2b3904 fix: wire ActionOnEditableZone into all zone mutation facades
All checks were successful
continuous-integration/drone/push Build is passing
Add/update/delete service calls in the Service facade were bypassing
ActionOnEditableZone, so mutations could silently target a committed or
published zone instead of deriving a new editable snapshot first.

Wraps AddServiceToZone, RemoveServiceFromZone, and UpdateZoneService
with ActionOnEditableZone so the decorator is applied consistently.

Fixes regression introduced by b2b6467575.
2026-03-16 20:07:47 +07:00
a7b225b9df Rework zone diff/apply flow: separate diff from provider API, support partial apply
Decouple diff computation from executable provider closures by fetching
provider records and computing diffs locally via DNSControlDiffByRecord.
On apply, build a target record set from user-selected corrections using
BuildTargetRecords, then ask the provider for executable corrections
against that target. A published snapshot is inserted at ZoneHistory[1]
while the WIP zone at position 0 remains unchanged.
2026-03-16 19:46:09 +07:00
8a2a28e4be providers: Mark secret fields with secret tag; add eye toggle for secret inputs
All checks were successful
continuous-integration/drone/push Build is passing
Also fix a typo in oracle.go label ("Private hey" → "Private key").
2026-03-16 19:44:14 +07:00
e341ea6beb chore(deps): lock file maintenance 2026-03-16 19:44:14 +07:00
69c9ba1d8d Expand authuser test coverage: hash functions, validation, recovery, and bcrypt limit 2026-03-16 19:44:14 +07:00
50ff2a1c7a Replace nil mailer checks with LogMailer fallback
Add a LogMailer that prints emails to stdout when no mail transport is
configured, eliminating the reflect-based nil interface checks that were
scattered across the authuser package. The App now always injects a
non-nil Mailer, so the usecase layer no longer needs to guard against it.
2026-03-16 19:44:14 +07:00
fece9cc4a5 Improve password validation performance and email format checking 2026-03-16 19:44:14 +07:00
9203e71494 web: Rename /join route to /register for clarity 2026-03-16 19:44:14 +07:00
36a7d8e9d3 Fix email validation HMAC weakness and prevent user enumeration on registration 2026-03-16 19:44:14 +07:00
ae675d6451 Refactor provider usecase: fix ownership bug, use decorator pattern, enforce service layer
- Fix DeleteProvider skipping ownership check by adding getUserProvider call
- Replace RestrictedService struct embedding with decorator pattern to prevent
  silent method promotion bypassing restriction checks
- Make providerSettingsUsecase delegate to ProviderUsecase instead of accessing
  storage directly, ensuring validation and ownership are enforced
- Accept ProviderValidator as constructor parameter, removing SetValidator mutator
- Add instantiate() helper for consistent provider instantiation error handling
- Wrap ListUserProviders storage errors in InternalError for consistency
- Add Test_DeleteProvider_WrongUser test and reduce test boilerplate
2026-03-16 19:44:14 +07:00
c850cfb0db Refactor orchestrator: add context.Context, fix error handling, use interfaces
- Handle AppendDomainLog errors with log.Printf instead of silently discarding
- Add NoopDomainLogAppender for null object pattern
2026-03-16 19:44:14 +07:00
07b5553369 Add public DNS record generator pages at /generator
Expose service editors publicly (no auth required) at /generator for
SEO discoverability. Each page shows an interactive editor alongside
a live DNS zone record preview powered by a new POST
/service_specs/:ssid/records backend endpoint.
2026-03-16 19:44:13 +07:00
572b4ea167 web: New helper domainJoin, fix OpenPGPKEY and SMIMECERT records when dn is empty 2026-03-15 21:35:45 +07:00
0fb2f048f7 Add missing documentation to some usecases 2026-03-15 17:40:15 +07:00
0dd7135781 Refactor ZoneCorrectionApplierUsecase: fix bugs and improve structure
Extract List method into a dedicated ZoneCorrectionListerUsecase to
separate concerns, and fix several bugs in Apply:

- Fix early-break condition: track appliedCount instead of using the
  correction index, which incorrectly compared against the position in
  all corrections rather than applied ones.
- Stop mutating form.WantedCorrections in-place; use a matched slice
  to track applied corrections without side effects.
- Fix misleading UserMessage strings that all said "unable to create
  the zone" regardless of which step failed.
- Use a single clock call for CommitDate, Published, and LastModified
  instead of two separate time.Now() calls producing different timestamps.
- Inject a clock function for testability.
- Improve error messages to include applied/total correction counts.
2026-03-15 17:40:15 +07:00
89362f473f ci: fix yarn v1 vite hoisting issue for vitest on amd64
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-15 17:40:15 +07:00
943d9b2a0c web: Add drag-and-drop domain group reassignment in ZoneList
When display_by_groups is enabled, domains are now draggable and group
containers act as drop targets. Dropping a domain onto a different group
updates its group via the API and refreshes the domain list.
2026-03-15 17:40:15 +07:00
d4090f983a Add a security policy 2026-03-15 17:40:15 +07:00
94806782e1 ci: Add SBOM generation in SPDX format 2026-03-15 17:40:15 +07:00
1be73506cb Reformat long function signatures 2026-03-14 12:02:00 +07:00
2572e8c319 Preserve service metadata across zone re-analyses
After AnalyzeZone rebuilds services from raw DNS records, metadata that
cannot be derived from DNS (Id, UserComment, OwnerId, Aliases, TTL, and
service-specific fields like OpenPGP/SMimeCert Username) was lost.

Add a post-processing function ReassociateMetadata that matches new
services to old ones by type and subdomain (using RDATA hashing for
disambiguation) and transfers metadata. Services opt in to body-level
transfer via the new MetadataEnricher interface.
2026-03-14 11:06:49 +07:00
f4bcb1c9cf refactor: decompose Analyzer into recordPool and serviceAccumulator
Restructure the service analyzer architecture to improve maintainability:

- Extract recordPool (zone records + mark-delete claiming) and
  serviceAccumulator (service registry + domain normalization) as
  embedded structs in Analyzer
- Replace swap-delete with mark-delete to eliminate mutation-during-iteration
- Centralize domain normalization using helpers.DomainRelative
- Make Comment/NbResources lazy via Service.MarshalJSON instead of
  eager assignment at three separate call sites
- Extract SPF merging from usecase layer into services.CollectAndMergeSPF
- Add GetDefaultTTL accessor and comprehensive Analyzer doc comments
- Add round-trip test infrastructure covering MX, CNAME, CAA, TXT, SPF,
  DMARC, GSuite, Origin, Server and more
2026-03-14 11:06:49 +07:00
6de814a247 docs: add comments to all functions and types in analyzer.go
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 10:36:53 +07:00
2a00d69ebb refactor: use random identifiers instead of SHA1 hashes for service IDs
Replace SHA1-based service ID generation with happydns.NewRandomIdentifier()
for consistency with the rest of the codebase.
2026-03-14 10:36:53 +07:00
3d196088c2 fix: prevent duplicate results in SearchRR when multiple filters match
When a record matched more than one AnalyzerRecordFilter, it was
appended to the result slice multiple times. Break after the first
matching filter to include each record at most once.
2026-03-14 10:36:53 +07:00
c7f309b867 fix: break after finding record in UseRR swap-delete loop
The swap-and-shrink deletion inside a range loop skipped the element
swapped into position k. Since there should only be one matching
record (pointer equality), breaking immediately is both correct and
clearer.
2026-03-14 10:36:53 +07:00
31950811c0 Merge SPF records from multiple services into single TXT record
RFC 7208 requires exactly one SPF record per domain. Previously, the
standalone SPF service and provider services like GSuite each emitted
their own SPF TXT record, producing invalid DNS when both existed.

Introduce SPFContributor interface so services can declare SPF
directives independently. At zone generation time, all contributions
for the same domain are merged into a single SPF record with the
strictest "all" policy winning. During zone import, GSuite claims its
directive via ClaimSPFDirective so the SPF analyzer excludes it from
the standalone SPF service.
2026-03-14 10:36:53 +07:00
fff3c29876 docs: add AI disclaimer section to README
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-12 17:47:12 +07:00
f96f894168 CI: github_url is required to publish a release 2026-03-12 15:59:27 +07:00
e73b6df40a ci: add Codeberg, GitHub and local forgejo release publishing steps on tag
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
v0.6.0
2026-03-12 15:03:56 +07:00
d4970a109d fix: defer OIDC session key deletion until successful authentication
Previously the CSRF state, PKCE verifier, nonce, and next-path were
deleted and the session saved before the token exchange. A failure during
exchange or verification left the user with no way to retry without
restarting the whole flow.

Remove the intermediate session.Save(): the in-memory deletions are
discarded on any error so the session keys remain available for a retry.
On success, SessionLoginOK calls session.Clear() + Save() which atomically
consumes all keys. PKCE ensures the authorization code cannot be replayed
independently of the session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
b4ad9f8092 fix: preserve post-login redirect destination through OIDC flow
The next query parameter was silently dropped when users chose OIDC
login, always redirecting to / after authentication. Forward the
validated next value to /auth/oidc, store it in the session during
redirect, and use it for the final redirect in the callback, matching
the behaviour of password-based login.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
4bab6644b0 security: add nonce validation to OIDC flow to prevent ID token replay
Generate a cryptographically random nonce at redirect time, store it in
the session, and include it in the authorization request. After token
verification, reject the callback if the ID token's nonce claim does not
match the session value, preventing replayed or stolen ID tokens from
being accepted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
c6a2a8cea3 security: add PKCE (S256) to OIDC authorization code flow
Generate a cryptographic code verifier at redirect time, store it in the
session, and send the S256 code_challenge in the authorization request.
Use the verifier during token exchange to bind the code to the session
that initiated the flow, protecting against authorization code
interception attacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
8e90d7be94 security: redact internal OIDC error details from HTTP responses
Log the underlying error server-side and return a generic message to
the client, preventing information leakage of library internals, error
details, and internal URLs through the OIDC callback error responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
cae068c4e9 security: replace SHA-1 with SHA-256 for OIDC user ID derivation
SHA-1 has known collision vulnerabilities. Switch to SHA-256 when
deriving a deterministic user identifier from the email address in the
OIDC callback, eliminating the risk of crafted email collision attacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
d979ccefe6 security: validate next redirect parameter to prevent open redirect
Decode and validate the next query parameter before navigating,
ensuring it is a same-origin relative path (starts with / but not //)
to prevent attackers from redirecting users to external sites after login.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
68a783b7bb security: 15-day session lifetime with 7-day auto-renewal
- Reduce SESSION_MAX_DURATION from 365 days to 15 days
- Add SESSION_RENEWAL_THRESHOLD (7 days): sessions are only extended
  when fewer than 7 days remain, instead of refreshing on every request
- Align cookie MaxAge with SESSION_MAX_DURATION (derived from the constant)
- Enforce expiry in load(): expired sessions are deleted on first use
  and the caller receives an error, preventing Bearer-token replay of
  stale sessions that the securecookie age check would not catch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:56 +07:00
ca206cf24e fix: make updateSession reject calls without an id
The function silently fell back to creating a new session when session.id
was falsy, which could create unintended API tokens from a partial object.
Session creation is already handled by addSession(); updateSession() now
throws early when no id is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:49 +07:00
28ac875585 fix: return updated session from UpdateSession endpoint
The handler fetched the session before applying the update and returned
that pre-update snapshot. The client therefore never saw the new
Description or ExpiresOn values. Fetch the session after the update
so the response reflects the persisted state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:49 +07:00
c682e463d3 fix: set session.IsNew=false only on successful load
Previously IsNew was unconditionally set to false after s.load() even
when load returned an error. Callers that branch on IsNew could treat
a broken/missing session as a pre-existing authenticated one.

Only mark the session as not-new when the load actually succeeded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:49 +07:00
f9d66bf53e security: validate session ID format from Authorization header
Bearer tokens and Basic Auth usernames were used as session IDs without
any format validation, allowing arbitrary strings (including crafted or
very long values) to reach the storage layer as untrusted session IDs.

Restrict accepted session IDs to the exact format produced by
NewSessionID(): standard base32 alphabet [A-Z2-7], exactly 103 chars.
Any token that does not match is ignored, resulting in a new anonymous
session instead of a storage lookup with attacker-controlled input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:49 +07:00
8ab02dffa8 security: propagate DeleteSession error in Save when MaxAge < 0
Previously, if session deletion failed (e.g. storage error), the error
was silently swallowed. The stale session could still be replayed via
Bearer token even after the client-side cookie was cleared.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:49 +07:00
90f07a215c security: rotate session ID on login to prevent session fixation
The server-side session store (gorilla/sessions backed by DB) reused the
same session ID across login: session.Clear() only zeroed the values map
but left session.ID unchanged. An attacker who planted a known session ID
before authentication retained access after the victim logged in.

Fix with a two-phase save:
1. Delete the old session from the DB (MaxAge=-1 save), expiring the cookie.
2. Reset the underlying gorilla Session.ID to "" so the store generates a
   fresh ID, then save the authenticated session with original cookie options
   (Secure, Path, MaxAge) preserved via a duck-typed interface assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:49 +07:00
b0b79efceb security: decouple failure tracking from captcha provider
Previously, RecordFailure/RecordSuccess were only called when a captcha
provider was configured, making brute-force tracking entirely inactive
on deployments without one.

- Always track login failures and successes regardless of captcha config
- When threshold is crossed with a captcha provider: 401 + captcha_required (existing behaviour)
- When threshold is crossed without a captcha provider: 429 + rate_limited flag
- Frontend: show a rate-limited message and disable the submit button on 429
- Add errors.rate-limited translation key to all locales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:03:49 +07:00