Skip to content

Commit 1cf27f9

Browse files
committed
feat: expose metrics and harden release paths
1 parent 0466a7a commit 1cf27f9

12 files changed

Lines changed: 574 additions & 30 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,6 @@ blog/
194194
/cmd/ws/ws
195195
/playground
196196
/docs
197+
!/docs/
198+
!/docs/metrics.md
197199
/CONTEXT.md

docs/metrics.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Prometheus metrics
2+
3+
Windshift exposes Prometheus metrics from the public `GET /metrics` endpoint.
4+
When `WINDSHIFT_CONTEXT_PATH` is set, the endpoint uses the same prefix as the
5+
rest of the application, for example `/windshift/metrics`.
6+
7+
The endpoint is intentionally unauthenticated so an infrastructure scraper can
8+
reach it in the same way as `/healthz` and `/readyz`. Restrict it at the reverse
9+
proxy or network boundary when metrics should not be available publicly.
10+
11+
The registry includes:
12+
13+
- Go runtime metrics (`go_*`), including goroutines, heap use, and GC pauses.
14+
- Process metrics (`windshift_process_*`), including CPU, memory, and open file
15+
descriptors where the operating system supports them.
16+
- Database pool metrics (`go_sql_*`) for open, in-use, and idle connections,
17+
connection waits, and connection lifetime closures.
18+
- HTTP request totals and duration histograms (`windshift_http_*`). The `route`
19+
label contains the registered route pattern, such as `/api/items/{id}`, and
20+
never the raw request path. Both metric families also include the response
21+
status code.
22+
- Agent run queue depth, in-flight runs, retained outcomes, and average duration
23+
(`windshift_agent_*`).
24+
- Retained webhook delivery outcomes (`windshift_webhook_dispatches`).
25+
- Scheduled SCM poll totals, failures, and duration (`windshift_scm_*`).
26+
27+
Example Prometheus configuration:
28+
29+
```yaml
30+
scrape_configs:
31+
- job_name: windshift
32+
static_configs:
33+
- targets: [windshift:8080]
34+
```

frontend/src/lib/dialogs/LoginDialog.svelte

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import Input from '../components/Input.svelte';
1212
import Label from '../components/Label.svelte';
1313
import AlertBox from '../components/AlertBox.svelte';
14+
import Spinner from '../components/Spinner.svelte';
1415
import {
1516
isWebAuthnSupported
1617
} from '../utils/webauthn-utils.js';
@@ -37,6 +38,7 @@
3738
let showFidoOption = $state(false);
3839
let ssoError = $state(null);
3940
let ssoRequiredMessage = $state(null);
41+
let loginOptionsReady = $state(false);
4042
4143
// Auth policy status (fetched on mount)
4244
let policyStatus = $state({
@@ -50,12 +52,16 @@
5052
5153
// Initialize SSO status and auth policy on mount
5254
onMount(async () => {
53-
await Promise.all([
54-
ssoStore.initStatus(),
55-
loadPolicyStatus()
56-
]);
57-
// Check for SSO error in URL (after callback redirect)
58-
ssoError = ssoStore.checkForError();
55+
try {
56+
await Promise.all([
57+
ssoStore.initStatus(),
58+
loadPolicyStatus()
59+
]);
60+
// Check for SSO error in URL (after callback redirect)
61+
ssoError = ssoStore.checkForError();
62+
} finally {
63+
loginOptionsReady = true;
64+
}
5965
});
6066
6167
// Load public policy status
@@ -252,6 +258,15 @@
252258
<AlertBox variant="error" message={$authStore.error} class="mb-4" />
253259
{/if}
254260
261+
{#if !loginOptionsReady}
262+
<div
263+
class="flex min-h-40 items-center justify-center gap-3 text-sm text-[var(--ds-text-subtle)]"
264+
data-testid="login-options-loading"
265+
>
266+
<Spinner size="sm" />
267+
<span>{t('common.loading')}</span>
268+
</div>
269+
{:else}
255270
<!-- Keep the session choice in front of SSO so it also remains available
256271
when the authentication policy hides the password form. -->
257272
{#if $ssoStore.enabled && !$ssoStore.statusLoading}
@@ -367,7 +382,11 @@
367382
{:else}
368383
369384
<!-- Login Form -->
370-
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
385+
<form
386+
onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}
387+
class="space-y-4"
388+
data-testid="login-password-form"
389+
>
371390
<!-- Email/Username Field -->
372391
<div>
373392
<Label for="emailOrUsername" color="default" class="mb-1">
@@ -479,6 +498,7 @@
479498
</Button>
480499
</form>
481500
{/if}
501+
{/if}
482502
</div>
483503
</div>
484504
</div>

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/emersion/go-message v0.18.2
2121
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
2222
github.com/extism/go-sdk v1.7.1
23+
github.com/felixge/httpsnoop v1.1.0
2324
github.com/getkin/kin-openapi v0.144.0
2425
github.com/go-ldap/ldap/v3 v3.4.12
2526
github.com/go-playground/validator/v10 v10.28.0
@@ -36,6 +37,7 @@ require (
3637
github.com/microcosm-cc/bluemonday v1.0.27
3738
github.com/modelcontextprotocol/go-sdk v1.4.1
3839
github.com/openai/openai-go/v3 v3.46.0
40+
github.com/prometheus/client_golang v1.24.1
3941
github.com/sahilm/fuzzy v0.1.3
4042
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
4143
github.com/spf13/cobra v1.8.0
@@ -82,6 +84,7 @@ require (
8284
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
8385
github.com/aymerick/douceur v0.2.0 // indirect
8486
github.com/beevik/etree v1.6.0 // indirect
87+
github.com/beorn7/perks v1.0.1 // indirect
8588
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
8689
github.com/cespare/xxhash/v2 v2.3.0 // indirect
8790
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab // indirect
@@ -106,7 +109,6 @@ require (
106109
github.com/dustin/go-humanize v1.0.1 // indirect
107110
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
108111
github.com/fatih/color v1.17.0 // indirect
109-
github.com/felixge/httpsnoop v1.1.0 // indirect
110112
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
111113
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
112114
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
@@ -151,11 +153,15 @@ require (
151153
github.com/muesli/cancelreader v0.2.2 // indirect
152154
github.com/muesli/termenv v0.16.0 // indirect
153155
github.com/muhlemmer/gu v0.3.1 // indirect
156+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
154157
github.com/ncruces/go-strftime v1.0.0 // indirect
155158
github.com/oasdiff/yaml v0.1.1 // indirect
156159
github.com/oasdiff/yaml3 v0.0.14 // indirect
157160
github.com/philhofer/fwd v1.2.0 // indirect
158161
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
162+
github.com/prometheus/client_model v0.6.2 // indirect
163+
github.com/prometheus/common v0.70.1 // indirect
164+
github.com/prometheus/procfs v0.21.1 // indirect
159165
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
160166
github.com/rivo/uniseg v0.4.7 // indirect
161167
github.com/russellhaering/goxmldsig v1.6.0 // indirect

go.sum

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
8585
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
8686
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
8787
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
88+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
89+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
8890
github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI=
8991
github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
9092
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
@@ -347,6 +349,8 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
347349
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
348350
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
349351
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
352+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
353+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
350354
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
351355
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
352356
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -365,6 +369,14 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
365369
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
366370
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
367371
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
372+
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
373+
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
374+
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
375+
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
376+
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
377+
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
378+
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
379+
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
368380
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
369381
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
370382
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -459,6 +471,8 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
459471
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
460472
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
461473
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
474+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
475+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
462476
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
463477
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
464478
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
@@ -475,17 +489,13 @@ golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
475489
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
476490
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
477491
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
478-
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
479-
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
480492
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
481493
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
482494
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
483495
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
484496
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
485497
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
486498
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
487-
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
488-
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
489499
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
490500
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
491501
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -553,8 +563,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
553563
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
554564
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
555565
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
556-
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
557-
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
558566
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
559567
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
560568
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
@@ -565,8 +573,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
565573
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
566574
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
567575
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
568-
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
569-
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
570576
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
571577
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
572578
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

internal/handlers/items_roadmap_hierarchy.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ func (h *ItemHandler) GetRoadmapHierarchyDates(w http.ResponseWriter, r *http.Re
2626
ctx, cancel := h.requestDBContext(r)
2727
defer cancel()
2828

29-
items, truncated, err := repository.NewItemRepository(h.db).GetRoadmapHierarchyDates(ctx, req.RootIDs)
29+
itemRepo := repository.NewItemRepository(h.db)
30+
rootWorkspaceIDs, err := itemRepo.GetRoadmapHierarchyRootWorkspaceIDs(ctx, req.RootIDs)
3031
if err != nil {
3132
if errors.Is(err, repository.ErrRoadmapHierarchyRootLimit) {
3233
respondBadRequest(w, r, err.Error())
@@ -35,6 +36,19 @@ func (h *ItemHandler) GetRoadmapHierarchyDates(w http.ResponseWriter, r *http.Re
3536
}
3637
return
3738
}
39+
authorizedRootIDs, err := authorizedRoadmapHierarchyRootIDs(req.RootIDs, rootWorkspaceIDs, func(workspaceID int) (bool, error) {
40+
return h.canViewItem(user.ID, workspaceID)
41+
})
42+
if err != nil {
43+
respondInternalError(w, r, err)
44+
return
45+
}
46+
47+
items, truncated, err := itemRepo.GetRoadmapHierarchyDates(ctx, authorizedRootIDs)
48+
if err != nil {
49+
respondInternalError(w, r, err)
50+
return
51+
}
3852

3953
allowedByWorkspace := make(map[int]bool)
4054
filtered := make([]models.RoadmapHierarchyDate, 0, len(items))
@@ -55,3 +69,35 @@ func (h *ItemHandler) GetRoadmapHierarchyDates(w http.ResponseWriter, r *http.Re
5569

5670
respondJSONOK(w, map[string]any{"items": filtered, "truncated": truncated})
5771
}
72+
73+
func authorizedRoadmapHierarchyRootIDs(rootIDs []int, workspaceIDs map[int]int, canView func(int) (bool, error)) ([]int, error) {
74+
allowedByWorkspace := make(map[int]bool)
75+
seen := make(map[int]struct{}, len(rootIDs))
76+
authorized := make([]int, 0, len(rootIDs))
77+
for _, itemID := range rootIDs {
78+
if itemID <= 0 {
79+
continue
80+
}
81+
if _, duplicate := seen[itemID]; duplicate {
82+
continue
83+
}
84+
seen[itemID] = struct{}{}
85+
workspaceID, exists := workspaceIDs[itemID]
86+
if !exists {
87+
continue
88+
}
89+
allowed, known := allowedByWorkspace[workspaceID]
90+
if !known {
91+
var err error
92+
allowed, err = canView(workspaceID)
93+
if err != nil {
94+
return nil, err
95+
}
96+
allowedByWorkspace[workspaceID] = allowed
97+
}
98+
if allowed {
99+
authorized = append(authorized, itemID)
100+
}
101+
}
102+
return authorized, nil
103+
}

0 commit comments

Comments
 (0)