feat: add lf-staff/lf-contractor LDAP → OpenFGA global team sync script#45
feat: add lf-staff/lf-contractor LDAP → OpenFGA global team sync script#45
Conversation
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>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a new Go CLI that syncs LDAP group members ( Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.goimplementing LDAP member fetch (via OAuth2 client_credentials) and OpenFGA tuple diff + batched write/delete. - Adds
DRYRUN/DEBUGenv var support and JSON structured logging viaslog.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
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
toAddandtoRemovecan 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
📒 Files selected for processing (1)
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>
There was a problem hiding this comment.
♻️ Duplicate comments (3)
scripts/sync_global_groups/main.go (3)
414-426:⚠️ Potential issue | 🟠 MajorROOT-project auditor enforcement is missing.
Per PR objectives, this script must ensure
lf-staffandlf-contractorareauditoron 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 | 🟠 MajorAvoid logging individual usernames at info level.
Logging each
auth0|usernameat 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 | 🟠 MajorNormalize Auth0 subjects before diffing.
The current logic strips
user:but leaves theauth0|prefix intact. LDAP members are keyed asalicewhile OpenFGA members becomeauth0|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
📒 Files selected for processing (3)
scripts/sync_global_groups/cronjob.yamlscripts/sync_global_groups/deploy-configmap.shscripts/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>
|
Addressed CodeRabbit review comments:
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
scripts/sync_global_groups/main.go (1)
358-363:⚠️ Potential issue | 🟠 MajorMove 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
📒 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>
|
Addressing Copilot review comments:
|
… 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>
|
Addressing latest Copilot review comments (566c460):
|
There was a problem hiding this comment.
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.
| body, _ := io.ReadAll(resp.Body) | ||
| resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return nil, fmt.Errorf("OpenFGA returned %d", resp.StatusCode) |
There was a problem hiding this comment.
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.
| 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))) |
There was a problem hiding this comment.
Already addressed in 6c17ed7 — OpenFGA read error now includes the response body, and io.ReadAll errors are handled explicitly.
| _, _ = io.ReadAll(resp.Body) | ||
| resp.Body.Close() | ||
| if resp.StatusCode != http.StatusOK { | ||
| return fmt.Errorf("OpenFGA write returned %d", resp.StatusCode) |
There was a problem hiding this comment.
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.
| _, _ = 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) |
There was a problem hiding this comment.
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>
|
Addressing latest Copilot review comments (6c17ed7):
|
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>
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Already addressed in 6c17ed7 — resp.Body.Close() is called after io.ReadAll with explicit error handling.
| 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) |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
Intentional — per-user mutations are logged at Info consistently with other sync handlers in this repo.
| image: golang:1.26-alpine | ||
| command: | ||
| - go | ||
| - run | ||
| - /script/main.go |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed to golang:1.24-alpine to match the go.mod toolchain.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Already addressed in 6c17ed7 — io.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>
There was a problem hiding this comment.
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.
| image: golang:1.26-alpine | ||
| command: | ||
| - go | ||
| - run | ||
| - /script/main.go |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed to golang:1.24-alpine to match the go.mod toolchain.
| # Apply with: kubectl apply -f cronjob.yaml -n lfx | ||
| apiVersion: batch/v1 | ||
| kind: CronJob | ||
| metadata: | ||
| name: sync-global-groups-ad-hoc | ||
| namespace: lfx |
There was a problem hiding this comment.
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.
| # 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 |
There was a problem hiding this comment.
Intentional — this manifest is prod-only; the namespace is not expected to vary.
| openfgaStoreID = os.Getenv("OPENFGA_STORE_ID") | ||
| debug = isTruthy(os.Getenv("DEBUG")) | ||
| dryRun = isTruthy(os.Getenv("DRYRUN")) | ||
| ) | ||
|
|
There was a problem hiding this comment.
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.
| 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) | |
| } |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
| resp.Body.Close() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("read token response body: %w", err) | ||
| } |
There was a problem hiding this comment.
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).
| 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) | |
| } |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
| defer resp.Body.Close() | |
| defer func() { | |
| if err := resp.Body.Close(); err != nil { | |
| slog.Error("close LDAP response body", "group", group, "error", err) | |
| } | |
| }() |
There was a problem hiding this comment.
Same as above — ignored close errors are not flagged by our errcheck config, consistent with the rest of the repo. 0 lint issues locally.
| resp.Body.Close() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("read OpenFGA response body: %w", err) | ||
| } |
There was a problem hiding this comment.
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.
| 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) | |
| } |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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).
| resp.Body.Close() | |
| if closeErr := resp.Body.Close(); closeErr != nil { | |
| slog.Warn("failed to close OpenFGA write response body", "error", closeErr) | |
| } |
There was a problem hiding this comment.
Same as above — ignored close errors are not flagged by our errcheck config. 0 lint issues locally.
| openfgaStoreID = os.Getenv("OPENFGA_STORE_ID") | ||
| debug = isTruthy(os.Getenv("DEBUG")) | ||
| dryRun = isTruthy(os.Getenv("DRY_RUN")) | ||
| ) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
Summary
scripts/sync_global_groups/main.go— a standalone Go script that syncs thelf-staffandlf-contractorLDAP groups into OpenFGA asteam:lf-staffandteam:lf-contractormember tuplesDRY_RUNandDEBUGenv vars; usesslogfor JSON structured loggingcontext.WithTimeoutthrough all HTTP requests to bound total execution timeROOT project auditor relation
The
team:lf-staffandteam:lf-contractorteams have already been written asauditoron 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
Kubernetes
A
CronJob(every 10 minutes) andConfigMapare deployed to thelfxnamespace in prod. Seescripts/sync_global_groups/cronjob.yamlanddeploy-configmap.sh.Notes
go.mod), consistent with other scripts in this repo.envfile 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)