Skip to content

refactor: use tailscale api for user checking instead of tsnet#978

Merged
steveiliop56 merged 3 commits into
mainfrom
refactor/tailscale-no-tsnet
Jul 8, 2026
Merged

refactor: use tailscale api for user checking instead of tsnet#978
steveiliop56 merged 3 commits into
mainfrom
refactor/tailscale-no-tsnet

Conversation

@steveiliop56

@steveiliop56 steveiliop56 commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Updated Tailscale configuration: removed experimental environment variables and added a simplified Tailscale section with API token, tailnet, and configurable cache duration.
    • Tailscale identity/status is now refreshed via the Tailscale REST API with periodic caching.
  • Bug Fixes
    • Improved Tailscale login validation by requiring the Tailscale device owner to match the existing user email.
    • Listener and routing behavior no longer switches based on Tailscale, reducing unexpected redirect and startup issues.
  • Build & CI
    • Adjusted build tag handling in release/nightly pipelines and the build configuration.

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces the embedded Tailscale server integration with a REST API client, updates Tailscale configuration and context fields, removes Tailscale listener/AppURL wiring, refactors OAuth request helpers to use context, and removes Tailscale build-tag and dependency references.

Changes

Tailscale REST migration

Layer / File(s) Summary
Tailscale config schema and env defaults
internal/model/config.go, .env.example
Config gains a top-level Tailscale field with Enabled/APIToken/Tailnet/CacheDuration; ExperimentalConfig becomes empty; the example environment variables are updated.
Tailscale REST service implementation
internal/service/tailscale_service.go
TailscaleService now fetches and caches devices and users from the Tailscale REST API, and Whois resolves addresses from cached device/user data.
Context model NodeName rename
internal/model/context.go
TailscaleContext.UserID is renamed to NodeName, and TailscaleNodeName() returns the new field.
Middleware whois flow and device owner validation
internal/middleware/context_middleware.go
tailscaleWhois drops the request context, adds device-owner validation, and populates the tailscale context from the new whois response fields.
Bootstrap listener and AppURL wiring removal
internal/bootstrap/app_bootstrap.go, internal/bootstrap/router_bootstrap.go
Removes the Tailscale-based AppURL/CookieDomain override and the Tailscale listener/serve path.
Build tags and dependency cleanup
Makefile, .github/workflows/nightly.yml, .github/workflows/release.yml, go.mod
Removes Tailscale build-tag composition, drops the Go install step from the release metadata job, and prunes Tailscale-related indirect dependencies.
Shared HTTP request helper extraction
internal/service/simple_req.go, internal/service/oauth_extractors.go
Adds a shared generic HTTP GET/JSON helper and switches the OAuth extractors to use it with context-aware requests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: Rycochet, scottmckendry

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing tsnet-based Tailscale user checks with the Tailscale API.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/tailscale-no-tsnet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
internal/service/simple_req.go (2)

10-30: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider context-based cancellation for this shared HTTP helper.

simpleReq now backs both OAuth calls and the new Tailscale REST polling (getDeviceList/getUsersList). It builds an http.Request without a context.Context, so cancellation/deadlines rely entirely on the caller's *http.Client timeout. If any caller passes a client without a configured timeout, a hung request could block a polling cycle indefinitely.

♻️ Optional refactor: accept a context
-func simpleReq[T any](client *http.Client, url string, headers map[string]string) (*T, error) {
+func simpleReq[T any](ctx context.Context, client *http.Client, url string, headers map[string]string) (*T, error) {
 	var decodedRes T
 
-	req, err := http.NewRequest("GET", url, nil)
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 	if err != nil {
 		return nil, err
 	}

This widens the function signature across all call sites, so weigh it against the current reliance on client-level timeouts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/simple_req.go` around lines 10 - 30, simpleReq currently
creates requests without a context, so long-running OAuth and Tailscale polling
calls can hang if the passed http.Client has no timeout. Update simpleReq to
accept a context and pass it into http.NewRequestWithContext, then thread that
context through the callers like getDeviceList and getUsersList so cancellation
and deadlines are enforced consistently. Keep the existing header handling,
response closing, and status checks intact while updating the signature and all
call sites.

28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Include response body in the non-2xx error for easier debugging.

Currently only the status text is surfaced; the response body (often containing API error details) is discarded.

♻️ Optional improvement
 	if res.StatusCode < 200 || res.StatusCode >= 300 {
-		return nil, fmt.Errorf("request failed with status: %s", res.Status)
+		body, _ := io.ReadAll(res.Body)
+		return nil, fmt.Errorf("request failed with status: %s, body: %s", res.Status, string(body))
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/simple_req.go` around lines 28 - 30, The non-2xx branch in
the simple request helper only returns the HTTP status text and drops the
response body, which hides useful API error details. Update the error handling
around the res.StatusCode check to read and include the response body in the
returned error message, and make sure the response body is still safely
handled/closed after being read.
internal/service/tailscale_service.go (1)

189-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use explicit local copies before storing range item pointers.

This avoids ambiguity around range-variable pointer semantics and matches this repo’s preferred pattern. Based on learnings, when storing a pointer to a Go range value, prefer an explicit local copy pattern.

Proposed refactor
+deviceLoop:
 	for _, d := range devices.Devices {
 		if len(d.Tags) != 0 {
 			continue
 		}
 		for _, a := range d.Addresses {
 			if a == addr {
-				device = &d
-				break
+				result := d
+				device = &result
+				break deviceLoop
 			}
 		}
 	}
 	for _, u := range users.Users {
 		if u.LoginName == device.User {
-			user = &u
+			result := u
+			user = &result
 			break
 		}
 	}

Also applies to: 215-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/tailscale_service.go` around lines 189 - 195, The loops in
tailscale_service.go are storing pointers to Go range items directly, which
should be replaced with an explicit local copy before assignment. In the code
that iterates over devices.Devices and the related block around the other
referenced range loop, create a new local variable from the current range item
and assign the pointer to that copy instead of taking the range variable’s
address. Keep the logic in the existing device-selection code paths unchanged,
but update the pointer capture pattern in the affected loops to match the repo’s
preferred approach.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 226-233: The Tailscale env block in the example env file is out of
dotenv-linter order. Reorder the TINYAUTH_TAILSCALE_* entries so the API token
and cache duration keys come before TINYAUTH_TAILSCALE_ENABLED, while keeping
the rest of the Tailscale settings grouped together and preserving the existing
comments.

In `@internal/service/tailscale_service.go`:
- Line 117: The Tailscale service client is created without any timeout, so
requests can hang indefinitely during startup or cache misses. Update the client
initialization in the Tailscale service setup where s.client is assigned in the
Tailscale service constructor/initializer to use an http.Client with a
reasonable timeout value instead of the zero-value client. Keep the change
localized to the Tailscale client creation path so all API calls inherit the
timeout behavior.

---

Nitpick comments:
In `@internal/service/simple_req.go`:
- Around line 10-30: simpleReq currently creates requests without a context, so
long-running OAuth and Tailscale polling calls can hang if the passed
http.Client has no timeout. Update simpleReq to accept a context and pass it
into http.NewRequestWithContext, then thread that context through the callers
like getDeviceList and getUsersList so cancellation and deadlines are enforced
consistently. Keep the existing header handling, response closing, and status
checks intact while updating the signature and all call sites.
- Around line 28-30: The non-2xx branch in the simple request helper only
returns the HTTP status text and drops the response body, which hides useful API
error details. Update the error handling around the res.StatusCode check to read
and include the response body in the returned error message, and make sure the
response body is still safely handled/closed after being read.

In `@internal/service/tailscale_service.go`:
- Around line 189-195: The loops in tailscale_service.go are storing pointers to
Go range items directly, which should be replaced with an explicit local copy
before assignment. In the code that iterates over devices.Devices and the
related block around the other referenced range loop, create a new local
variable from the current range item and assign the pointer to that copy instead
of taking the range variable’s address. Keep the logic in the existing
device-selection code paths unchanged, but update the pointer capture pattern in
the affected loops to match the repo’s preferred approach.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c0e1dd43-9ad0-40b0-aa0d-b8f9e6bf2f20

📥 Commits

Reviewing files that changed from the base of the PR and between 73cc480 and d408fe9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • .env.example
  • .github/workflows/nightly.yml
  • .github/workflows/release.yml
  • Makefile
  • go.mod
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/middleware/context_middleware.go
  • internal/model/config.go
  • internal/model/context.go
  • internal/service/oauth_extractors.go
  • internal/service/simple_req.go
  • internal/service/tailscale_service.go
💤 Files with no reviewable changes (2)
  • internal/bootstrap/app_bootstrap.go
  • internal/service/oauth_extractors.go

Comment thread .env.example
Comment thread internal/service/tailscale_service.go Outdated
Comment thread internal/model/config.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/service/tailscale_service.go (1)

200-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer an explicit local copy over taking the address of the range variable.

device = &d (Line 206) and user = &u (Line 228) capture the address of the loop variable. On Go 1.25 this is safe, but the project convention is to bind an explicit copy for clarity and to avoid reviewer confusion about loop-variable capture semantics.

Additionally, the inner break on Line 207 only exits the inner Addresses loop; the outer device loop keeps scanning and could overwrite device on a later address match. Consider breaking out of the outer loop once a device is found.

♻️ Proposed refactor
 	for _, d := range devices.Devices {
 		if len(d.Tags) != 0 {
 			continue
 		}
 		for _, a := range d.Addresses {
 			if a == addr {
-				device = &d
+				match := d
+				device = &match
 				break
 			}
 		}
+		if device != nil {
+			break
+		}
 	}
 	for _, u := range users.Users {
 		if u.LoginName == device.User {
-			user = &u
+			match := u
+			user = &match
 			break
 		}
 	}

Based on learnings, prefer the explicit local copy pattern (e.g., result := config; return &result) rather than &<range-variable> directly, even when newer Go versions make per-iteration address taking safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/tailscale_service.go` around lines 200 - 231, The Tailscale
device/user lookup in tailscaleService should avoid taking the address of range
variables and should stop scanning once a match is found. In the device search
loop, bind an explicit local copy before assigning to device instead of using
&d, and make sure the outer device loop exits after the first matching address
rather than only breaking the inner Addresses loop. Apply the same explicit-copy
pattern when setting user from users.Users instead of using &u, so the code is
clear and consistent with the project convention.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/service/tailscale_service.go`:
- Around line 200-231: The Tailscale device/user lookup in tailscaleService
should avoid taking the address of range variables and should stop scanning once
a match is found. In the device search loop, bind an explicit local copy before
assigning to device instead of using &d, and make sure the outer device loop
exits after the first matching address rather than only breaking the inner
Addresses loop. Apply the same explicit-copy pattern when setting user from
users.Users instead of using &u, so the code is clear and consistent with the
project convention.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e9dec8a-8bc7-47af-925e-fa7586146cfc

📥 Commits

Reviewing files that changed from the base of the PR and between 83184db and 8da8fd5.

📒 Files selected for processing (3)
  • .env.example
  • internal/model/config.go
  • internal/service/tailscale_service.go

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 8, 2026
@steveiliop56 steveiliop56 merged commit 0bd2821 into main Jul 8, 2026
5 checks passed
@steveiliop56 steveiliop56 deleted the refactor/tailscale-no-tsnet branch July 8, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants