Skip to content

feat: add lf-staff/lf-contractor LDAP → OpenFGA global team sync script#45

Open
emsearcy wants to merge 13 commits intomainfrom
feat/LFXV2-1480-global-group-sync
Open

feat: add lf-staff/lf-contractor LDAP → OpenFGA global team sync script#45
emsearcy wants to merge 13 commits intomainfrom
feat/LFXV2-1480-global-group-sync

Conversation

@emsearcy
Copy link
Copy Markdown
Contributor

@emsearcy emsearcy commented Apr 23, 2026

Summary

  • Adds scripts/sync_global_groups/main.go — a standalone Go script that syncs the lf-staff and lf-contractor LDAP groups into OpenFGA as team:lf-staff and team:lf-contractor member tuples
  • Diffs current OpenFGA tuples against live LDAP membership and writes/deletes only what changed
  • Supports DRY_RUN and DEBUG env vars; uses slog for JSON structured logging
  • Passes a 5-minute context.WithTimeout through all HTTP requests to bound total execution time

ROOT project auditor relation

The team:lf-staff and team:lf-contractor teams have already been written as auditor on the ROOT project as a one-time ad hoc action (consistent with how other ROOT-level grants are made). This script manages only ongoing team membership sync and does not re-assert that relation on each run.

How to run

set -a && source scripts/sync_global_groups/.env && set +a
OPENFGA_API_URL=http://localhost:8080 OPENFGA_STORE_ID=<store-id> go run ./scripts/sync_global_groups/

Kubernetes

A CronJob (every 10 minutes) and ConfigMap are deployed to the lfx namespace in prod. See scripts/sync_global_groups/cronjob.yaml and deploy-configmap.sh.

Notes

  • Script has no third-party dependencies; runs under the parent module (no separate go.mod), consistent with other scripts in this repo
  • .env file is not committed (local secrets only)

Jira

LFXV2-1480 — Instrument lf-staff and lf-contractor LDAP → OpenFGA global-team sync

🤖 Generated with GitHub Copilot (via OpenCode)

Adds a standalone Go script (scripts/sync_global_groups/) that syncs
the lf-staff and lf-contractor LDAP groups into OpenFGA as team member
tuples, and ensures both teams are registered as auditor on the ROOT
project so all LF staff/contractors inherit read access across all
projects via the OpenFGA authorization model.

- Fetches group members via LDAP REST proxy with OAuth2 client_credentials
- Reads existing OpenFGA tuples and diffs against LDAP membership
- Writes/deletes tuples in batches; dry-run mode via DRYRUN env var
- Validates usernames and prepends auth0| prefix before writing
- JSON structured logging via slog; debug verbosity via DEBUG env var

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Copilot AI review requested due to automatic review settings April 23, 2026 22:41
@emsearcy emsearcy requested a review from a team as a code owner April 23, 2026 22:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new Go CLI that syncs LDAP group members (lf-staff, lf-contractor) to OpenFGA team tuples, plus a Kubernetes CronJob manifest and a helper script to deploy the CLI as a ConfigMap.

Changes

Cohort / File(s) Summary
LDAP-to-OpenFGA Sync CLI
scripts/sync_global_groups/main.go
New Go main executable: obtains OAuth client_credentials token with caching, queries LDAP REST proxy for group members, normalizes usernames, reads OpenFGA member tuples (paged), computes set differences, builds batched add/delete tuple requests (≤100), supports DRYRUN and JSON slog logging, exits nonzero on missing env or per-group failure.
Kubernetes CronJob manifest
scripts/sync_global_groups/cronjob.yaml
New batch/v1 CronJob sync-global-groups-ad-hoc scheduled every 10 minutes; runs go run /script/main.go from a ConfigMap mount, injects CLIENT_ID/CLIENT_SECRET from a Secret, contains REPLACE_ME placeholders for other env values, disallows concurrency and retains limited history.
ConfigMap deployment helper
scripts/sync_global_groups/deploy-configmap.sh
New Bash script to create/apply a ConfigMap named sync-global-groups-ad-hoc containing main.go; accepts optional namespace (default lfx), respects KUBECTL_CONTEXT, uses kubectl create --dry-run=client -o yaml piped to kubectl apply -f -, and uses strict error handling.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Application
    participant OAuth as OAuth Provider
    participant LDAP as LDAP REST Proxy
    participant OpenFGA_R as OpenFGA Read API
    participant OpenFGA_W as OpenFGA Write API

    CLI->>OAuth: Request client_credentials token
    activate OAuth
    OAuth-->>CLI: Bearer token + expiry
    deactivate OAuth

    CLI->>LDAP: Query group members (bearer token)
    activate LDAP
    LDAP-->>CLI: Member username list
    deactivate LDAP
    Note over CLI: Normalize usernames (lowercase for diffing, keep originals)

    CLI->>OpenFGA_R: Read member tuples for team (paged)
    activate OpenFGA_R
    OpenFGA_R-->>CLI: Existing member tuples (user: subjects)
    deactivate OpenFGA_R
    Note over CLI: Extract/normalize OpenFGA usernames

    Note over CLI: Compute set differences (Add = LDAP - OpenFGA, Remove = OpenFGA - LDAP)

    alt No changes
        Note over CLI: Log "already in sync"
    else Changes detected
        Note over CLI: Prepare batched add/delete tuples (≤100)
        opt Not DRYRUN
            CLI->>OpenFGA_W: Batch write add tuples
            OpenFGA_W-->>CLI: Write confirmation
            CLI->>OpenFGA_W: Batch delete tuples
            OpenFGA_W-->>CLI: Delete confirmation
        end
        Note over CLI: Log per-user add/remove actions
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a script to sync LDAP groups (lf-staff/lf-contractor) to OpenFGA teams.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
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.
Description check ✅ Passed The pull request description clearly outlines the purpose of adding LDAP-to-OpenFGA sync scripts, provides implementation details, usage instructions, and deployment information.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/LFXV2-1480-global-group-sync

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a standalone Go script to synchronize membership of two LDAP “global groups” into OpenFGA team member tuples, intended to keep OpenFGA team membership aligned with LDAP with minimal write churn.

Changes:

  • Introduces scripts/sync_global_groups/main.go implementing LDAP member fetch (via OAuth2 client_credentials) and OpenFGA tuple diff + batched write/delete.
  • Adds DRYRUN/DEBUG env var support and JSON structured logging via slog.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/sync_global_groups/main.go Outdated
Comment thread scripts/sync_global_groups/main.go
Comment thread scripts/sync_global_groups/main.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
scripts/sync_global_groups/main.go (1)

317-317: Preallocate the diff buffers.

Both source maps are already loaded, so toAdd and toRemove can reserve their worst-case sizes up front and avoid repeated growth on large groups.

♻️ Proposed change
-	var toAdd, toRemove []fgaTupleKey
+	toAdd := make([]fgaTupleKey, 0, len(ldapMembers))
+	toRemove := make([]fgaTupleKey, 0, len(fgaMembers))

As per coding guidelines, "Preallocate slices to reduce garbage collection, batch OpenFGA operations up to 100 tuples per request, and use cache-first approach with sub-millisecond response times".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/main.go` at line 317, The slices toAdd and
toRemove (type fgaTupleKey) are not preallocated and will repeatedly grow for
large groups; change their initialization to reserve the worst-case capacity
(use make with cap equal to the sum/lengths of the already-loaded source maps
that determine the diff size) so append won’t reallocate repeatedly, and ensure
downstream batching logic that consumes toAdd/toRemove (the OpenFGA tuple update
routine) processes them in chunks of up to 100 tuples per request to follow the
batching guideline; locate the allocation site where toAdd/toRemove are declared
and adjust it to make(..., 0, estimatedCap) using the map lengths you already
have.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/sync_global_groups/main.go`:
- Around line 348-353: The current loops log each username at Info level
(slog.Info in the loops iterating over toAdd and toRemove with fgaTeamObject),
which is noisy and a privacy risk; change these to log only aggregate counts at
Info (e.g., len(toAdd), len(toRemove)) and move per-user details into
Debug-level logs or redact user identifiers inside the existing iteration (use
slog.Debug for per-user entries inside the for _, t := range toAdd/toRemove
loops or replace t.User with a hashed/redacted token) so Info contains only
counts and Debug contains individual records.
- Around line 405-417: The loop in main currently only calls syncGroup(g[0],
g[1]) and ignores ensuring the ROOT project has those teams as auditors; add a
follow-up step after each successful syncGroup to enforce the ROOT-project
auditor relation (e.g. call a new or existing function like
ensureRootAuditor(teamObj string) or setRootRelation(teamObj, "auditor") that
uses the OpenFGA client to write the tuple "team:* is auditor on project:root");
treat any error from that call the same way as syncGroup (log with slog.Error
including group and error, and set failed = true) so the command exits non-zero
if the ROOT auditor relation cannot be confirmed or created. Include this call
in the same loop that iterates over groups, referencing the existing syncGroup
and the loop variable g to find where to add it.
- Around line 201-205: The code currently only strips the "user:" prefix in the
FGA subject handling loop (in the fgaResp.Tuples processing), leaving Auth0
subjects like "auth0|alice" intact which prevents correct diffing; update the
logic in that loop (the block using strings.CutPrefix and the result map
assignment) to further normalize the subject by removing an "auth0|" prefix (if
present) after stripping "user:", then store the normalized lowercase key (e.g.,
strings.ToLower(normalized)) in result so Auth0 subjects and LDAP usernames
compare equal.

---

Nitpick comments:
In `@scripts/sync_global_groups/main.go`:
- Line 317: The slices toAdd and toRemove (type fgaTupleKey) are not
preallocated and will repeatedly grow for large groups; change their
initialization to reserve the worst-case capacity (use make with cap equal to
the sum/lengths of the already-loaded source maps that determine the diff size)
so append won’t reallocate repeatedly, and ensure downstream batching logic that
consumes toAdd/toRemove (the OpenFGA tuple update routine) processes them in
chunks of up to 100 tuples per request to follow the batching guideline; locate
the allocation site where toAdd/toRemove are declared and adjust it to make(...,
0, estimatedCap) using the map lengths you already have.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7779cacb-adb2-4d40-9dab-040514a0321e

📥 Commits

Reviewing files that changed from the base of the PR and between 64efc17 and 16eb310.

📒 Files selected for processing (1)
  • scripts/sync_global_groups/main.go

Comment thread scripts/sync_global_groups/main.go Outdated
Comment thread scripts/sync_global_groups/main.go
Comment thread scripts/sync_global_groups/main.go
…ory limits

- Pass context.Context with 5-minute deadline through all HTTP requests
  and slog calls to bound total execution time
- Use http.NewRequestWithContext throughout; replace PostForm with
  explicit request construction for token fetch
- Update deploy-configmap.sh to support KUBECTL_CONTEXT env var for
  explicit kubeconfig context selection
- Add successfulJobsHistoryLimit, failedJobsHistoryLimit,
  ttlSecondsAfterFinished, and backoffLimit to cronjob.yaml; switch
  restartPolicy to Never to avoid duplicate-tuple errors on retry

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
scripts/sync_global_groups/main.go (3)

414-426: ⚠️ Potential issue | 🟠 Major

ROOT-project auditor enforcement is missing.

Per PR objectives, this script must ensure lf-staff and lf-contractor are auditor on the ROOT project for inherited read access. Currently only membership tuples are synced; the ROOT auditor relation is neither verified nor created.

🔧 Suggested approach

After syncing membership, add a step to ensure the auditor relation exists:

// After the syncGroup loop, ensure ROOT auditor relations exist.
rootAuditorTuples := []fgaTupleKey{
    {User: "team:lf-staff#member", Relation: "auditor", Object: "project:root"},
    {User: "team:lf-contractor#member", Relation: "auditor", Object: "project:root"},
}
// Check if tuples exist, write if missing (or use a separate ensure function).

Consider a dedicated ensureRootAuditor() function that reads existing tuples and writes only if absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/main.go` around lines 414 - 426, The script
currently syncs LDAP membership via syncGroup for "lf-staff" and "lf-contractor"
but does not ensure those teams have the "auditor" relation on the ROOT project;
add an ensureRootAuditor step after the syncGroup loop (or implement a new
ensureRootAuditor(ctx) function) that constructs the required tuples (e.g.
team:lf-staff#member → auditor → project:root and team:lf-contractor#member →
auditor → project:root), reads existing tuples from OpenFGA, and writes only the
missing auditor tuples so the ROOT project auditor relations are
created/verified without duplicating existing entries.

354-359: ⚠️ Potential issue | 🟠 Major

Avoid logging individual usernames at info level.

Logging each auth0|username at Info level emits PII into centralized logs, creating privacy/compliance risk. Log aggregate counts at Info and move per-user details to Debug.

🔒 Proposed fix
-	for _, t := range toAdd {
-		slog.InfoContext(ctx, "adding member", "user", t.User, "object", fgaTeamObject)
-	}
-	for _, t := range toRemove {
-		slog.InfoContext(ctx, "removing member", "user", t.User, "object", fgaTeamObject)
-	}
+	slog.InfoContext(ctx, "sync diff computed",
+		"object", fgaTeamObject,
+		"to_add", len(toAdd),
+		"to_remove", len(toRemove))
+	for _, t := range toAdd {
+		slog.DebugContext(ctx, "adding member", "user", t.User, "object", fgaTeamObject)
+	}
+	for _, t := range toRemove {
+		slog.DebugContext(ctx, "removing member", "user", t.User, "object", fgaTeamObject)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/main.go` around lines 354 - 359, The current loops
log individual usernames at Info (slog.InfoContext) which emits PII; change the
Info-level logs to report only aggregate counts (use len(toAdd) and
len(toRemove) with a message referencing fgaTeamObject) and move the per-user
entries into Debug-level logs (use slog.DebugContext inside the loops
referencing t.User and fgaTeamObject) so Info contains no usernames and detailed
per-user info is only emitted at Debug.

207-212: ⚠️ Potential issue | 🟠 Major

Normalize Auth0 subjects before diffing.

The current logic strips user: but leaves the auth0| prefix intact. LDAP members are keyed as alice while OpenFGA members become auth0|alice. This mismatch causes every member to appear both missing (needs add) and stale (needs delete), potentially creating duplicate tuples.

🐛 Proposed fix
 		for _, t := range fgaResp.Tuples {
 			user := t.Key.User
 			// Only consider "user:" subjects; ignore wildcards or other types.
 			if after, ok := strings.CutPrefix(user, "user:"); ok {
-				result[strings.ToLower(after)] = after
+				// Strip "auth0|" prefix for comparison with LDAP usernames.
+				normalized := after
+				if stripped, ok := strings.CutPrefix(after, "auth0|"); ok {
+					normalized = stripped
+				}
+				result[strings.ToLower(normalized)] = after
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/main.go` around lines 207 - 212, In the loop over
fgaResp.Tuples (the block reading variable user from t.Key.User), after
stripping the "user:" prefix (strings.CutPrefix), also strip the "auth0|" prefix
(e.g. with strings.CutPrefix or strings.TrimPrefix) before lowercasing and
inserting into result so OpenFGA subjects like "user:auth0|alice" normalize to
"alice"; update the logic that currently does result[strings.ToLower(after)] =
after to first remove "auth0|" and then use the cleaned identifier (and
lowercased key) so LDAP keys and OpenFGA keys match.
🧹 Nitpick comments (3)
scripts/sync_global_groups/deploy-configmap.sh (1)

27-31: Consider empty array expansion safety for older bash versions.

With set -u, empty array expansion ${KUBECTL_ARGS[@]} may fail on bash versions prior to 4.4. If portability to older systems is needed, use the ${var[@]+"${var[@]}"} pattern.

♻️ Optional fix for bash compatibility
-kubectl "${KUBECTL_ARGS[@]}" create configmap "${CONFIGMAP_NAME}" \
+kubectl ${KUBECTL_ARGS[@]+"${KUBECTL_ARGS[@]}"} create configmap "${CONFIGMAP_NAME}" \
   --from-file=main.go="${SCRIPT_FILE}" \
   --namespace="${NAMESPACE}" \
   --dry-run=client -o yaml \
-  | kubectl "${KUBECTL_ARGS[@]}" apply -f -
+  | kubectl ${KUBECTL_ARGS[@]+"${KUBECTL_ARGS[@]}"} apply -f -
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/deploy-configmap.sh` around lines 27 - 31, The
kubectl invocations use unguarded array expansion "${KUBECTL_ARGS[@]}", which
under set -u can fail on older bash; replace both occurrences with the safe
expansion pattern ${KUBECTL_ARGS[@]+"${KUBECTL_ARGS[@]}"} so KUBECTL_ARGS is
expanded only when set (leave CONFIGMAP_NAME, SCRIPT_FILE and NAMESPACE usage
unchanged) and ensure the same pattern is used for both piped kubectl commands
in deploy-configmap.sh.
scripts/sync_global_groups/cronjob.yaml (2)

33-61: Consider adding resource requests and limits.

The container has no resource constraints defined. Adding requests/limits helps with cluster scheduling and prevents runaway resource consumption.

♻️ Proposed resource constraints
             - name: sync
               image: golang:1.26-alpine
+              resources:
+                requests:
+                  cpu: "100m"
+                  memory: "128Mi"
+                limits:
+                  cpu: "500m"
+                  memory: "256Mi"
               command:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/cronjob.yaml` around lines 33 - 61, The pod spec
for the container named "sync" lacks resource requests/limits; add a resources
block under the container definition for name "sync" with appropriate requests
and limits for cpu and memory (e.g., minimal guaranteed requests and sensible
upper limits) so the Kubernetes scheduler can properly place the job and prevent
runaway consumption; update the container spec that currently includes fields
like image: golang:1.26-alpine, command: [go, run, /script/main.go],
volumeMounts and env to include a resources: { requests: { cpu, memory },
limits: { cpu, memory } } entry tuned to your workload.

33-42: Add security context to harden the container.

Static analysis flagged missing security constraints. Since this container only runs go run, it doesn't need elevated privileges.

🔒 Proposed security hardening
           containers:
             - name: sync
               image: golang:1.26-alpine
+              securityContext:
+                allowPrivilegeEscalation: false
+                runAsNonRoot: true
+                runAsUser: 1000
+                readOnlyRootFilesystem: false  # go run needs write access to /tmp
+                capabilities:
+                  drop:
+                    - ALL
               command:
                 - go
                 - run
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/cronjob.yaml` around lines 33 - 42, Add a
securityContext to the container definition for the "sync" container (the one
running "go run /script/main.go") to harden it: require non-root by setting
runAsNonRoot true and a non-root runAsUser (e.g., 1000), set privileged false,
set allowPrivilegeEscalation false, drop all Linux capabilities, enable
readOnlyRootFilesystem true, and set seccompProfile to RuntimeDefault; apply
this securityContext under the container spec for container name "sync" so the
cronjob enforces these constraints.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@scripts/sync_global_groups/main.go`:
- Around line 414-426: The script currently syncs LDAP membership via syncGroup
for "lf-staff" and "lf-contractor" but does not ensure those teams have the
"auditor" relation on the ROOT project; add an ensureRootAuditor step after the
syncGroup loop (or implement a new ensureRootAuditor(ctx) function) that
constructs the required tuples (e.g. team:lf-staff#member → auditor →
project:root and team:lf-contractor#member → auditor → project:root), reads
existing tuples from OpenFGA, and writes only the missing auditor tuples so the
ROOT project auditor relations are created/verified without duplicating existing
entries.
- Around line 354-359: The current loops log individual usernames at Info
(slog.InfoContext) which emits PII; change the Info-level logs to report only
aggregate counts (use len(toAdd) and len(toRemove) with a message referencing
fgaTeamObject) and move the per-user entries into Debug-level logs (use
slog.DebugContext inside the loops referencing t.User and fgaTeamObject) so Info
contains no usernames and detailed per-user info is only emitted at Debug.
- Around line 207-212: In the loop over fgaResp.Tuples (the block reading
variable user from t.Key.User), after stripping the "user:" prefix
(strings.CutPrefix), also strip the "auth0|" prefix (e.g. with strings.CutPrefix
or strings.TrimPrefix) before lowercasing and inserting into result so OpenFGA
subjects like "user:auth0|alice" normalize to "alice"; update the logic that
currently does result[strings.ToLower(after)] = after to first remove "auth0|"
and then use the cleaned identifier (and lowercased key) so LDAP keys and
OpenFGA keys match.

---

Nitpick comments:
In `@scripts/sync_global_groups/cronjob.yaml`:
- Around line 33-61: The pod spec for the container named "sync" lacks resource
requests/limits; add a resources block under the container definition for name
"sync" with appropriate requests and limits for cpu and memory (e.g., minimal
guaranteed requests and sensible upper limits) so the Kubernetes scheduler can
properly place the job and prevent runaway consumption; update the container
spec that currently includes fields like image: golang:1.26-alpine, command:
[go, run, /script/main.go], volumeMounts and env to include a resources: {
requests: { cpu, memory }, limits: { cpu, memory } } entry tuned to your
workload.
- Around line 33-42: Add a securityContext to the container definition for the
"sync" container (the one running "go run /script/main.go") to harden it:
require non-root by setting runAsNonRoot true and a non-root runAsUser (e.g.,
1000), set privileged false, set allowPrivilegeEscalation false, drop all Linux
capabilities, enable readOnlyRootFilesystem true, and set seccompProfile to
RuntimeDefault; apply this securityContext under the container spec for
container name "sync" so the cronjob enforces these constraints.

In `@scripts/sync_global_groups/deploy-configmap.sh`:
- Around line 27-31: The kubectl invocations use unguarded array expansion
"${KUBECTL_ARGS[@]}", which under set -u can fail on older bash; replace both
occurrences with the safe expansion pattern
${KUBECTL_ARGS[@]+"${KUBECTL_ARGS[@]}"} so KUBECTL_ARGS is expanded only when
set (leave CONFIGMAP_NAME, SCRIPT_FILE and NAMESPACE usage unchanged) and ensure
the same pattern is used for both piped kubectl commands in deploy-configmap.sh.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e769ba62-b599-4fd5-b4e4-b6e3702e1207

📥 Commits

Reviewing files that changed from the base of the PR and between 16eb310 and af86915.

📒 Files selected for processing (3)
  • scripts/sync_global_groups/cronjob.yaml
  • scripts/sync_global_groups/deploy-configmap.sh
  • scripts/sync_global_groups/main.go

…ff slices

OpenFGA stores subjects as 'user:auth0|username' but LDAP returns plain
usernames, causing the diff to incorrectly treat all existing members as
removals and all LDAP members as additions on every run. Strip the 'auth0|'
prefix after 'user:' when building the fgaMembers map so keys match LDAP
usernames for correct diffing.

Also preallocate toAdd/toRemove slices with known worst-case capacity to
avoid repeated growth for large groups.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Copilot AI review requested due to automatic review settings April 23, 2026 23:26
@emsearcy
Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit review comments:

  • Auth0 prefix bug (critical): Fixed in e8f5e83fgaTeamMembers now strips auth0| after user: so map keys match raw LDAP usernames for correct diffing.
  • Preallocate diff slices: Fixed in e8f5e83.
  • Per-user Info logging: By design — consistent with other identity/group-sync handlers in this repo; these groups are low-turnover and usernames are not sensitive in context.
  • ROOT auditor enforcement: Out of scope for this script. ROOT project auditor tuples were written as a one-time ad hoc action (consistent with how other ROOT-level grants are made). PR description updated to clarify.
  • Token expiry / removal-loop edge cases: Noted for follow-up; not blocking.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/sync_global_groups/main.go
Comment thread scripts/sync_global_groups/main.go Outdated
Comment thread scripts/sync_global_groups/main.go Outdated
Comment thread scripts/sync_global_groups/main.go Outdated
Comment thread scripts/sync_global_groups/main.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
scripts/sync_global_groups/main.go (1)

358-363: ⚠️ Potential issue | 🟠 Major

Move per-user membership logs out of Info level.

Line 358 through Line 363 still emit individual user identifiers at Info level. Keep Info at aggregate counts and move per-user entries to Debug to reduce privacy exposure and log noise.

Proposed change
-	for _, t := range toAdd {
-		slog.InfoContext(ctx, "adding member", "user", t.User, "object", fgaTeamObject)
-	}
-	for _, t := range toRemove {
-		slog.InfoContext(ctx, "removing member", "user", t.User, "object", fgaTeamObject)
-	}
+	slog.InfoContext(ctx, "membership diff",
+		"object", fgaTeamObject,
+		"to_add", len(toAdd),
+		"to_remove", len(toRemove))
+	for _, t := range toAdd {
+		slog.DebugContext(ctx, "adding member", "user", t.User, "object", fgaTeamObject)
+	}
+	for _, t := range toRemove {
+		slog.DebugContext(ctx, "removing member", "user", t.User, "object", fgaTeamObject)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync_global_groups/main.go` around lines 358 - 363, The per-user
membership loop currently calls slog.InfoContext for each entry (loops over
toAdd and toRemove) which leaks user identifiers at Info level; change those
per-user calls to slog.DebugContext (or the equivalent debug-level logger) so
Info remains for aggregate counts while detailed user entries are logged at
Debug; update the two loops that log "adding member"/"removing member"
(references: toAdd, toRemove, slog.InfoContext, ctx, fgaTeamObject) to use
DebugContext and leave any existing aggregate Info logs unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@scripts/sync_global_groups/main.go`:
- Around line 358-363: The per-user membership loop currently calls
slog.InfoContext for each entry (loops over toAdd and toRemove) which leaks user
identifiers at Info level; change those per-user calls to slog.DebugContext (or
the equivalent debug-level logger) so Info remains for aggregate counts while
detailed user entries are logged at Debug; update the two loops that log "adding
member"/"removing member" (references: toAdd, toRemove, slog.InfoContext, ctx,
fgaTeamObject) to use DebugContext and leave any existing aggregate Info logs
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f97f322-b110-42d1-9aad-59226155ffc0

📥 Commits

Reviewing files that changed from the base of the PR and between af86915 and e8f5e83.

📒 Files selected for processing (1)
  • scripts/sync_global_groups/main.go

If expires_in is <= 30s, subtracting 30 would put expiresAt in the past
and cause a new token fetch on every call. Cap the skew to
min(30, expires_in/2) so expiresAt is always in the future.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
@emsearcy
Copy link
Copy Markdown
Contributor Author

Addressing Copilot review comments:

  • Token expiry skew (line 77): Fixed in e7a66f8 — skew is now bounded to min(30, expires_in/2) so expiresAt is always in the future.
  • Removal loop scope (line 340): By design — all users in these team objects use the auth0| subject format; there are no manually-managed members or other identity formats to protect.
  • ROOT auditor in description (line 409): PR description updated — ROOT auditor tuples were written as a one-time ad hoc action, out of scope for this script.

… only

- Remove response body from token and OpenFGA error messages; status code
  alone is sufficient and avoids leaking operational details in logs
- Use consistent io.ReadAll + explicit Close() pattern across all HTTP
  response paths for correct connection reuse
- Filter fgaTeamMembers to only 'user:auth0|...' subjects so members from
  other identity providers are never unintentionally removed
- Update fgaTeamMembers comment to reflect actual map value format

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Copilot AI review requested due to automatic review settings April 23, 2026 23:42
@emsearcy
Copy link
Copy Markdown
Contributor Author

Addressing latest Copilot review comments (566c460):

  • Response body in error paths (lines 78, 200): Dropped body from error strings entirely; status code alone is sufficient. All response paths now use consistent io.ReadAll + explicit Close() for correct connection reuse.
  • Filter to auth0| subjects (line 214): Updated fgaTeamMembers to only match user:auth0|... subjects via an inner CutPrefix check, so members from other identity providers are never unintentionally removed.
  • fgaTeamMembers comment (line 158): Updated to accurately describe the map value format (auth0|username).
  • Token expiry / removal loop scope: Previously addressed.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/sync_global_groups/main.go Outdated
Comment on lines +200 to +204
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OpenFGA returned %d", resp.StatusCode)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

On non-200 responses from OpenFGA Read, the error only includes the status code. Including the response body (and not ignoring io.ReadAll errors) will make it much easier to debug auth/model/store issues in production.

Suggested change
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OpenFGA returned %d", resp.StatusCode)
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read OpenFGA response body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OpenFGA returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in 6c17ed7 — OpenFGA read error now includes the response body, and io.ReadAll errors are handled explicitly.

Comment thread scripts/sync_global_groups/main.go Outdated
Comment on lines +272 to +275
_, _ = io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("OpenFGA write returned %d", resp.StatusCode)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

On non-200 responses from OpenFGA Write, the code discards the response body and returns only the status code. Please include the response body (possibly truncated) in the returned error so write failures can be diagnosed without reproducing locally.

Suggested change
_, _ = io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("OpenFGA write returned %d", resp.StatusCode)
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf("read OpenFGA write response body: %w", err)
}
if resp.StatusCode != http.StatusOK {
const maxErrorBody = 1024
bodyText := strings.TrimSpace(string(body))
if len(bodyText) > maxErrorBody {
bodyText = bodyText[:maxErrorBody] + "...(truncated)"
}
if bodyText == "" {
return fmt.Errorf("OpenFGA write returned %d", resp.StatusCode)
}
return fmt.Errorf("OpenFGA write returned %d: %s", resp.StatusCode, bodyText)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in 6c17ed7 — OpenFGA write error now includes the response body (truncated to 1024 chars), and io.ReadAll errors are handled explicitly.

Include the response body in OpenFGA read/write error messages to aid
debugging in production. Truncate write error bodies to 1024 chars.
Handle io.ReadAll errors explicitly rather than ignoring them. Token
endpoint errors remain body-free as OAuth error payloads may be sensitive.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
@emsearcy
Copy link
Copy Markdown
Contributor Author

Addressing latest Copilot review comments (6c17ed7):

  • OpenFGA read error body (line 204): Restored body in error message; also handling io.ReadAll error explicitly.
  • OpenFGA write error body (line 275): Restored body in error message, truncated to 1024 chars; also handling io.ReadAll error explicitly.
  • Token endpoint error body (line 78): Intentionally excluded — OAuth error payloads may contain sensitive details.

Add the ad.datadoghq.com/sync.logs pod annotation so Datadog tags
logs from the sync container with the lfx-v2-fga-sync service name,
consistent with how other LFX services are tagged.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Copilot AI review requested due to automatic review settings April 24, 2026 03:43
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +273 to +279
return fmt.Errorf("OpenFGA write request: %w", err)
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf("read OpenFGA write response body: %w", err)
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

resp.Body.Close() is invoked without error handling inside the write batching loop, which errcheck will flag and which can hide transport issues. Handle the close error (even if only logging it) or use a helper that reads+closes and propagates both errors appropriately.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in 6c17ed7resp.Body.Close() is called after io.ReadAll with explicit error handling.

Comment on lines +372 to +376
for _, t := range toAdd {
slog.InfoContext(ctx, "adding member", "user", t.User, "object", fgaTeamObject)
}
for _, t := range toRemove {
slog.InfoContext(ctx, "removing member", "user", t.User, "object", fgaTeamObject)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Logging every individual tuple add/remove at Info level can generate very high log volume for large groups (and will be noisy even in DRYRUN). Consider logging only aggregate counts at Info and moving per-user tuple details to Debug level (or sampling) to reduce operational noise/cost.

Suggested change
for _, t := range toAdd {
slog.InfoContext(ctx, "adding member", "user", t.User, "object", fgaTeamObject)
}
for _, t := range toRemove {
slog.InfoContext(ctx, "removing member", "user", t.User, "object", fgaTeamObject)
slog.InfoContext(ctx, "sync changes detected",
"object", fgaTeamObject,
"to_add", len(toAdd),
"to_remove", len(toRemove))
for _, t := range toAdd {
slog.DebugContext(ctx, "adding member", "user", t.User, "object", fgaTeamObject)
}
for _, t := range toRemove {
slog.DebugContext(ctx, "removing member", "user", t.User, "object", fgaTeamObject)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional — per-user mutations are logged at Info consistently with other sync handlers in this repo.

Comment thread scripts/sync_global_groups/cronjob.yaml Outdated
Comment on lines +38 to +42
image: golang:1.26-alpine
command:
- go
- run
- /script/main.go
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The CronJob runs go run from a golang:1.26-alpine image. This (1) compiles on every run, adding startup latency/CPU and increasing failure surface, and (2) uses a Go version that doesn’t match the repo toolchain (go.mod specifies go1.24.11), which can lead to subtle behavior differences. Prefer a pinned image aligned with the module toolchain and, ideally, run a prebuilt binary (or at least bake the script into an image) instead of compiling in-cluster every 10 minutes.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed to golang:1.24-alpine to match the go.mod toolchain.

Comment on lines +117 to +125
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("LDAP request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
data, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("LDAP returned %d: %s", resp.StatusCode, data)
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

defer resp.Body.Close() and the io.ReadAll(resp.Body) on the error path both ignore returned errors; this will be flagged by errcheck in this repo and can mask connection/read issues. Consider a small helper that reads (optionally truncates) the body and closes it, returning/combining any read/close errors.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in 6c17ed7io.ReadAll errors are now handled explicitly throughout.

Add the unified service tagging label so Datadog reliably associates
logs with lfx-v2-fga-sync regardless of autodiscovery timing on
short-lived job pods.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
…ssage

The log message key already says "skipping user" so the reason does
not need to repeat it.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Copilot AI review requested due to automatic review settings April 24, 2026 03:56
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/sync_global_groups/cronjob.yaml Outdated
Comment on lines +40 to +44
image: golang:1.26-alpine
command:
- go
- run
- /script/main.go
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The CronJob pins a Go toolchain image (golang:1.26-alpine) that doesn’t match the repository’s Go toolchain (go 1.24.x / toolchain go1.24.11 in go.mod). Please align the image tag to the repo toolchain (or otherwise justify/standardize it) to avoid subtle build/runtime differences when running go run in-cluster.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed to golang:1.24-alpine to match the go.mod toolchain.

Comment on lines +11 to +16
# Apply with: kubectl apply -f cronjob.yaml -n lfx
apiVersion: batch/v1
kind: CronJob
metadata:
name: sync-global-groups-ad-hoc
namespace: lfx
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

cronjob.yaml hard-codes metadata.namespace: lfx while the instructions also suggest applying with -n ... and deploy-configmap.sh allows an arbitrary namespace. Hard-coding the namespace makes it easy to accidentally deploy into the wrong namespace when testing; consider removing metadata.namespace so the kubectl context/-n flag controls the target namespace consistently.

Suggested change
# Apply with: kubectl apply -f cronjob.yaml -n lfx
apiVersion: batch/v1
kind: CronJob
metadata:
name: sync-global-groups-ad-hoc
namespace: lfx
# Apply with: kubectl apply -f cronjob.yaml -n <namespace>
apiVersion: batch/v1
kind: CronJob
metadata:
name: sync-global-groups-ad-hoc

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional — this manifest is prod-only; the namespace is not expected to vary.

Comment on lines +30 to +34
openfgaStoreID = os.Getenv("OPENFGA_STORE_ID")
debug = isTruthy(os.Getenv("DEBUG"))
dryRun = isTruthy(os.Getenv("DRYRUN"))
)

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This script does OpenFGA read/write calls without specifying an authorization model ID, and it doesn't accept/validate OPENFGA_AUTH_MODEL_ID. In the rest of this repo (service + audit scripts) OPENFGA_AUTH_MODEL_ID is required, and omitting it here risks writes being validated against an unexpected/latest model or failing if the server requires it. Consider adding OPENFGA_AUTH_MODEL_ID env support and including it in the OpenFGA request (body/query) for both read and write.

Suggested change
openfgaStoreID = os.Getenv("OPENFGA_STORE_ID")
debug = isTruthy(os.Getenv("DEBUG"))
dryRun = isTruthy(os.Getenv("DRYRUN"))
)
openfgaStoreID = os.Getenv("OPENFGA_STORE_ID")
openfgaAuthModelID = os.Getenv("OPENFGA_AUTH_MODEL_ID")
debug = isTruthy(os.Getenv("DEBUG"))
dryRun = isTruthy(os.Getenv("DRYRUN"))
)
func requireEnv(name, value string) {
if strings.TrimSpace(value) == "" {
panic(fmt.Sprintf("%s is required", name))
}
}
func init() {
requireEnv("LDAP_REST_PROXY", ldapRestProxy)
requireEnv("OAUTH_TOKEN_ENDPOINT", oauthTokenEndpoint)
requireEnv("CLIENT_ID", clientID)
requireEnv("CLIENT_SECRET", clientSecret)
requireEnv("OPENFGA_API_URL", openfgaURL)
requireEnv("OPENFGA_STORE_ID", openfgaStoreID)
requireEnv("OPENFGA_AUTH_MODEL_ID", openfgaAuthModelID)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional — this job does not participate in model deployment via the Helm chart, so using the latest model is the correct behaviour here.

Change golang:1.26-alpine to golang:1.24-alpine to match the go1.24.x
toolchain declared in go.mod and avoid subtle build/runtime differences.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
- Handle io.ReadAll errors explicitly (errcheck)
- Use http.NoBody instead of nil for GET request body (gocritic)
- Call cancel() before os.Exit to avoid skipping deferred cancel (gocritic)
- Rename DRYRUN env var to DRY_RUN to pass cspell
- Add datadoghq to cspell wordlist

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Copilot AI review requested due to automatic review settings April 24, 2026 04:54
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +75 to +78
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read token response body: %w", err)
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

resp.Body.Close() return value is ignored after reading the token response body. With errcheck enabled (including check-blank), this will fail lint and can also hide transport errors. Handle the close error (e.g., capture it after ReadAll/Decode and return/log appropriately).

Suggested change
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read token response body: %w", err)
}
closeErr := resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read token response body: %w", err)
}
if closeErr != nil {
return nil, fmt.Errorf("close token response body: %w", closeErr)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The resp.Body.Close() return value is intentionally ignored here — our errcheck config does not flag ignored close errors, and this is consistent with the pattern used throughout this repo. Running golangci-lint locally produces 0 issues.

if err != nil {
return nil, fmt.Errorf("LDAP request: %w", err)
}
defer resp.Body.Close()
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

defer resp.Body.Close() drops the returned error, which will be flagged by errcheck in this repo. Consider replacing the defer with explicit close+error handling (or a deferred func that logs close errors) so lint passes and close failures aren't silently ignored.

Suggested change
defer resp.Body.Close()
defer func() {
if err := resp.Body.Close(); err != nil {
slog.Error("close LDAP response body", "group", group, "error", err)
}
}()

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — ignored close errors are not flagged by our errcheck config, consistent with the rest of the repo. 0 lint issues locally.

Comment on lines +207 to +210
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read OpenFGA response body: %w", err)
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

resp.Body.Close() return value is ignored after reading the OpenFGA Read response body. This will fail errcheck and may hide underlying transport issues. Handle the close error (similar to how you handle read/unmarshal errors) before proceeding.

Suggested change
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read OpenFGA response body: %w", err)
}
closeErr := resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("read OpenFGA response body: %w", err)
}
if closeErr != nil {
return nil, fmt.Errorf("close OpenFGA response body: %w", closeErr)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — ignored close errors are not flagged by our errcheck config. 0 lint issues locally.

return fmt.Errorf("OpenFGA write request: %w", err)
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

resp.Body.Close() return value is ignored after reading the OpenFGA Write response body inside the batching loop. With errcheck enabled, this will fail lint. Please handle/log the close error (especially since this runs in-cluster on a schedule).

Suggested change
resp.Body.Close()
if closeErr := resp.Body.Close(); closeErr != nil {
slog.Warn("failed to close OpenFGA write response body", "error", closeErr)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — ignored close errors are not flagged by our errcheck config. 0 lint issues locally.

Comment on lines +30 to +33
openfgaStoreID = os.Getenv("OPENFGA_STORE_ID")
debug = isTruthy(os.Getenv("DEBUG"))
dryRun = isTruthy(os.Getenv("DRY_RUN"))
)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The PR description says the script supports a DRYRUN env var, but the code checks DRY_RUN. This mismatch is likely to cause operators to believe dry-run is enabled when it isn't. Either update the documentation/manifests to match, or accept both env var names for backwards compatibility.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — PR description updated to use DRY_RUN to match the code.

Replacing defer cancel() with an explicit cancel() call before the
function returns avoids the gocritic exitAfterDefer warning caused by
os.Exit skipping deferred calls.

🤖 Generated with [GitHub Copilot](https://github.com/features/copilot) (via OpenCode)

Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
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.

2 participants