From 1ccfd13194b259c3712fa9e83b9f4a4fe5487925 Mon Sep 17 00:00:00 2001 From: Pinny Date: Tue, 30 Jun 2026 13:01:22 +0000 Subject: [PATCH 01/10] Bump dependencies --- Dockerfile | 4 +- go.mod | 46 +++++------ go.sum | 96 +++++++++++----------- hack/Dockerfile_fips | 4 +- internal/githubclient/githubclient.go | 2 +- internal/githubclient/githubclient_test.go | 2 +- 6 files changed, 77 insertions(+), 77 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9c9c9f4eb..df0245270 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ # Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -ARG BUILD_IMAGE=golang:1.26.3@sha256:2981696eed011d747340d7252620932677929cce7d2d539602f56a8d7e9b660b -ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:e3f945647ffb95b5839c07038d64f9811adf17308b9121d8a2b87b6a22a80a39 +ARG BUILD_IMAGE=golang:1.26.4@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609 +ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:963fa6c544fe5ce420f1f54fb88b6fb01479f054c8056d0f74cc2c6000df5240 # Prepare to cross-compile by always running the build stage in the build platform, not the target platform. FROM --platform=$BUILDPLATFORM $BUILD_IMAGE AS build-env diff --git a/go.mod b/go.mod index f0e92c635..83ac3a8cd 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc github.com/chromedp/chromedp v0.15.1 - github.com/coreos/go-oidc/v3 v3.18.0 + github.com/coreos/go-oidc/v3 v3.19.0 github.com/coreos/go-semver v0.3.1 github.com/creack/pty v1.1.24 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/felixge/httpsnoop v1.0.4 + github.com/felixge/httpsnoop v1.1.0 github.com/go-jose/go-jose/v3 v3.0.5 github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-ldap/ldap/v3 v3.4.13 @@ -18,9 +18,9 @@ require ( github.com/go-logr/stdr v1.2.2 github.com/go-logr/zapr v1.3.0 github.com/gofrs/flock v0.13.0 - github.com/google/cel-go v0.28.0 + github.com/google/cel-go v0.28.1 github.com/google/go-cmp v0.7.0 - github.com/google/go-github/v86 v86.0.0 + github.com/google/go-github/v87 v87.0.0 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 @@ -37,24 +37,24 @@ require ( github.com/tdewolff/minify/v2 v2.24.13 go.uber.org/mock v0.6.0 go.uber.org/zap v1.28.0 - golang.org/x/crypto v0.51.0 - golang.org/x/net v0.54.0 + golang.org/x/crypto v0.53.0 + golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sync v0.20.0 - golang.org/x/term v0.43.0 - golang.org/x/text v0.37.0 - k8s.io/api v0.36.0 - k8s.io/apiextensions-apiserver v0.36.0 - k8s.io/apimachinery v0.36.0 - k8s.io/apiserver v0.36.0 - k8s.io/client-go v0.36.0 - k8s.io/component-base v0.36.0 + golang.org/x/sync v0.21.0 + golang.org/x/term v0.44.0 + golang.org/x/text v0.38.0 + k8s.io/api v0.36.2 + k8s.io/apiextensions-apiserver v0.36.2 + k8s.io/apimachinery v0.36.2 + k8s.io/apiserver v0.36.2 + k8s.io/client-go v0.36.2 + k8s.io/component-base v0.36.2 k8s.io/gengo v0.0.0-20260408192533-25e2208e0dc3 k8s.io/klog/v2 v2.140.0 - k8s.io/kube-aggregator v0.36.0 - k8s.io/kube-openapi v0.0.0-20260509192518-b540ad9def2b - k8s.io/streaming v0.36.0 - k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 + k8s.io/kube-aggregator v0.36.2 + k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821 + k8s.io/streaming v0.36.2 + k8s.io/utils v0.0.0-20260626114624-be93311217bd sigs.k8s.io/yaml v1.6.0 ) @@ -167,10 +167,10 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/sys v0.46.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect @@ -181,7 +181,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/kms v0.36.0 // indirect + k8s.io/kms v0.36.2 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index 8b15eeb27..3daf7ec4e 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-oidc v2.5.0+incompatible h1:6W0vGJR3Tu0r0PwfmjOrRZSlfxeEln8dsejt3ZWIvwo= github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= -github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= +github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I= +github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -120,8 +120,8 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -253,8 +253,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= -github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= +github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -272,8 +272,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24= github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw= -github.com/google/go-github/v86 v86.0.0 h1:S/6aANJhwRm8EQmGKVML3j41yq0h2BsTP8FnDkO7kcA= -github.com/google/go-github/v86 v86.0.0/go.mod h1:zKv1l4SwDXNFMGByi2FWkq71KwSXqj/eQRZuqtmcot8= +github.com/google/go-github/v87 v87.0.0 h1:9Ck3dcOxWJyfsN8tzdah4YvmqB/7ZsstMglv/PkOsl0= +github.com/google/go-github/v87 v87.0.0/go.mod h1:hGUoT5pwm/ck5uLL+wroSVQfg8mpe+buxllCcGV4VaM= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -698,8 +698,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -738,8 +738,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -779,8 +779,8 @@ golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -805,8 +805,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -860,10 +860,10 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE= +golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -872,8 +872,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -885,8 +885,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -950,8 +950,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1087,33 +1087,33 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= -k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= -k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= -k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= -k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= -k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= -k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE= -k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho= -k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= -k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= -k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo= -k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4= +k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/apiserver v0.36.2 h1:6vMnkmHZPeBloNkHUhmZYq7Ylv8WIB8xjyEl+eSt26E= +k8s.io/apiserver v0.36.2/go.mod h1:9PoQ2ikCytrZyZg11mGhLEF5m8Rgsb5FJmYJ4Wvnl1k= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w= +k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA= k8s.io/gengo v0.0.0-20260408192533-25e2208e0dc3 h1:rf5GFLj5SnELuHVF2S6RVkUGJzC1KEiQvWFbxmgI2mM= k8s.io/gengo v0.0.0-20260408192533-25e2208e0dc3/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kms v0.36.0 h1:DPy0VDWi6hCgFMgzV5cNuSDrIROMRcJpTZ1GnB+D368= -k8s.io/kms v0.36.0/go.mod h1:g91diTD9h0oJCCHkTb00krlF+Qm5HTnkWLi9Q/TpRoc= -k8s.io/kube-aggregator v0.36.0 h1:yrR+aw41p4/Wur55FCcfozPSBHH70HIs5j+J6ZphExg= -k8s.io/kube-aggregator v0.36.0/go.mod h1:2CkdUvPZjEbKnlhn+wxj6z3yity7H4xsTrFX+M/t1UE= -k8s.io/kube-openapi v0.0.0-20260509192518-b540ad9def2b h1:uMjR3gZY5tZj+VduN5sj0nsvWUxuvoMVU0yQ0xk6yNA= -k8s.io/kube-openapi v0.0.0-20260509192518-b540ad9def2b/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= -k8s.io/streaming v0.36.0 h1:agnTxU+NFulUrtYzXUGKO3ndEa8jKwht1Kwn9nu9x+4= -k8s.io/streaming v0.36.0/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/kms v0.36.2 h1:o0l8zMRecm38k5NyRnO/3+2YA/MR6TMGcc3KQZA76hI= +k8s.io/kms v0.36.2/go.mod h1:g91diTD9h0oJCCHkTb00krlF+Qm5HTnkWLi9Q/TpRoc= +k8s.io/kube-aggregator v0.36.2 h1:zfeH9Fs16oDquNfBZef3M27dGG3QtJ9/TwkWfBlw1lo= +k8s.io/kube-aggregator v0.36.2/go.mod h1:UMrB5DfEhznFTf0bqYW2SV26GDy8HNaxoYakvKVWZ8M= +k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821 h1:m2wZhD5+vJZyCVkTvUHIfaiXc/mdt3Pxyx3vUnGsKzU= +k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= +k8s.io/streaming v0.36.2 h1:NSKthPPg9UFSKsRauVJUVGH2Dvn8fhKmY4qrMkw/p98= +k8s.io/streaming v0.36.2/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= +k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs= +k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/hack/Dockerfile_fips b/hack/Dockerfile_fips index 7360f518b..8cb6b5690 100644 --- a/hack/Dockerfile_fips +++ b/hack/Dockerfile_fips @@ -16,8 +16,8 @@ # See https://go.googlesource.com/go/+/dev.boringcrypto/README.boringcrypto.md # and https://kupczynski.info/posts/fips-golang/ for details. -ARG BUILD_IMAGE=golang:1.26.3@sha256:2981696eed011d747340d7252620932677929cce7d2d539602f56a8d7e9b660b -ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:e3f945647ffb95b5839c07038d64f9811adf17308b9121d8a2b87b6a22a80a39 +ARG BUILD_IMAGE=golang:1.26.4@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609 +ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:963fa6c544fe5ce420f1f54fb88b6fb01479f054c8056d0f74cc2c6000df5240 # This is not currently using --platform to prepare to cross-compile because we use gcc below to build # platform-specific GCO code. This makes multi-arch builds slow due to target platform emulation. diff --git a/internal/githubclient/githubclient.go b/internal/githubclient/githubclient.go index c426ce93e..dfa8bfd51 100644 --- a/internal/githubclient/githubclient.go +++ b/internal/githubclient/githubclient.go @@ -12,7 +12,7 @@ import ( "slices" "strings" - "github.com/google/go-github/v86/github" + "github.com/google/go-github/v87/github" "k8s.io/apimachinery/pkg/util/sets" "go.pinniped.dev/internal/plog" diff --git a/internal/githubclient/githubclient_test.go b/internal/githubclient/githubclient_test.go index 718d3115c..f97936730 100644 --- a/internal/githubclient/githubclient_test.go +++ b/internal/githubclient/githubclient_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/google/go-github/v86/github" + "github.com/google/go-github/v87/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/require" "k8s.io/client-go/util/cert" From d14722396af7dd207ec82fa0f221d8383051827d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20O=CC=88zc=CC=A7elik?= Date: Tue, 23 Jun 2026 15:10:39 -0700 Subject: [PATCH 02/10] Fixed API changes based on new dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "github.com/google/go-github/v87/github" has API shape changes, therefore a mere go.mod dependency update was inadequate; this PR adds necessary changes for feature parity and tests. Co-authored-by: Ryan Richard Signed-off-by: Volkan Özçelik --- internal/githubclient/githubclient.go | 14 ++++++++++-- internal/githubclient/githubclient_test.go | 26 +++++++++++++--------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/internal/githubclient/githubclient.go b/internal/githubclient/githubclient.go index dfa8bfd51..046638162 100644 --- a/internal/githubclient/githubclient.go +++ b/internal/githubclient/githubclient.go @@ -71,8 +71,18 @@ func NewGitHubClient(httpClient *http.Client, apiBaseURL, token string) (GitHubI return nil, fmt.Errorf("%s: token cannot be empty string", errorPrefix) } - client := github.NewClient(httpClient).WithAuthToken(token) - client.BaseURL = parsedURL + // go-github's WithEnterpriseURLs requires a non-empty upload URL even though + // Pinniped only calls read endpoints (Users.Get, Organizations.List, + // Teams.ListUserTeams), all of which use the base URL. The upload URL is never + // exercised today. It should be updated if an upload-style call is ever used. + client, err := github.NewClient( + github.WithHTTPClient(httpClient), + github.WithAuthToken(token), + github.WithEnterpriseURLs(parsedURL.String(), parsedURL.String()), + ) + if err != nil { + return nil, fmt.Errorf("%s: %w", errorPrefix, err) + } return &githubClient{ client: client, diff --git a/internal/githubclient/githubclient_test.go b/internal/githubclient/githubclient_test.go index f97936730..3926c0cc0 100644 --- a/internal/githubclient/githubclient_test.go +++ b/internal/githubclient/githubclient_test.go @@ -106,8 +106,8 @@ func TestNewGitHubClient(t *testing.T) { require.NotNil(t, actualI) actual, ok := actualI.(*githubClient) require.True(t, ok) - require.NotNil(t, actual.client.BaseURL) - require.Equal(t, test.wantBaseURL, actual.client.BaseURL.String()) + require.NotNil(t, actual.client) + require.Equal(t, test.wantBaseURL, actual.client.BaseURL()) // Force the githubClient's httpClient roundTrippers to run and add the Authorization header @@ -227,9 +227,9 @@ func TestGetUser(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - githubClient := &githubClient{ - client: github.NewClient(test.httpClient).WithAuthToken(test.token), - } + c, err := github.NewClient(github.WithHTTPClient(test.httpClient), github.WithAuthToken(test.token)) + require.NoError(t, err) + githubClient := &githubClient{client: c} ctx := context.Background() if test.ctx != nil { @@ -361,9 +361,9 @@ func TestGetOrgMembership(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - githubClient := &githubClient{ - client: github.NewClient(test.httpClient).WithAuthToken(test.token), - } + c, err := github.NewClient(github.WithHTTPClient(test.httpClient), github.WithAuthToken(test.token)) + require.NoError(t, err) + githubClient := &githubClient{client: c} ctx := context.Background() if test.ctx != nil { @@ -700,6 +700,7 @@ func TestGetTeamMembership(t *testing.T) { }, ), ), + token: "fake-token", wantErr: `error fetching team membership for authenticated user: missing the "organization" attribute for a team`, }, { @@ -716,6 +717,7 @@ func TestGetTeamMembership(t *testing.T) { }, ), ), + token: "fake-token", wantErr: `error fetching team membership for authenticated user: missing the organization's "login" attribute for a team`, }, { @@ -733,6 +735,7 @@ func TestGetTeamMembership(t *testing.T) { }, ), ), + token: "fake-token", wantErr: `error fetching team membership for authenticated user: the "name" attribute is missing for a team`, }, { @@ -750,6 +753,7 @@ func TestGetTeamMembership(t *testing.T) { }, ), ), + token: "fake-token", wantErr: `error fetching team membership for authenticated user: the "slug" attribute is missing for a team`, }, { @@ -808,9 +812,9 @@ func TestGetTeamMembership(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - githubClient := &githubClient{ - client: github.NewClient(test.httpClient).WithAuthToken(test.token), - } + c, err := github.NewClient(github.WithHTTPClient(test.httpClient), github.WithAuthToken(test.token)) + require.NoError(t, err) + githubClient := &githubClient{client: c} ctx := context.Background() if test.ctx != nil { From a0ee4b945e0f9d7913f01ca6b6f3df4a57d55d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20O=CC=88zc=CC=A7elik?= Date: Thu, 25 Jun 2026 11:41:58 -0700 Subject: [PATCH 03/10] fix unit test failure in celformer_test. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ryan Richard Signed-off-by: Volkan Özçelik --- internal/celtransformer/celformer_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/celtransformer/celformer_test.go b/internal/celtransformer/celformer_test.go index d9ea59961..7694416c0 100644 --- a/internal/celtransformer/celformer_test.go +++ b/internal/celtransformer/celformer_test.go @@ -596,7 +596,7 @@ func TestTransformer(t *testing.T) { &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, }, ctx: alreadyCancelledContext, - wantEvaluationErr: `identity transformation at index 0: context canceled`, + wantEvaluationErr: `identity transformation at index 0: operation interrupted: context canceled`, }, { name: "slow groups transformation expressions are canceled by the cancel context after partial evaluation", @@ -606,7 +606,7 @@ func TestTransformer(t *testing.T) { &GroupsTransformation{Expression: `groups.filter(x, groups.all(x, true))`}, }, ctx: alreadyCancelledContext, - wantEvaluationErr: `identity transformation at index 0: context canceled`, + wantEvaluationErr: `identity transformation at index 0: operation interrupted: context canceled`, }, { name: "slow policy expressions are canceled by the cancel context after partial evaluation", @@ -617,7 +617,7 @@ func TestTransformer(t *testing.T) { &AllowAuthenticationPolicy{Expression: `groups.all(x, groups.all(x, true))`}, // this is the slow one }, ctx: alreadyCancelledContext, - wantEvaluationErr: `identity transformation at index 1: context canceled`, + wantEvaluationErr: `identity transformation at index 1: operation interrupted: context canceled`, }, { name: "slow transformation expressions are canceled and the rest of the expressions do not run", @@ -630,7 +630,7 @@ func TestTransformer(t *testing.T) { &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, }, ctx: alreadyCancelledContext, - wantEvaluationErr: `identity transformation at index 1: context canceled`, + wantEvaluationErr: `identity transformation at index 1: operation interrupted: context canceled`, }, { name: "slow username transformation expressions are canceled after a maximum allowed duration", @@ -640,7 +640,7 @@ func TestTransformer(t *testing.T) { // On my laptop, evaluating this expression would take ~20 seconds if we allowed it to evaluate to completion. &UsernameTransformation{Expression: `groups.filter(x, groups.all(x, true))[0]`}, }, - wantEvaluationErr: `identity transformation at index 0: context deadline exceeded`, + wantEvaluationErr: `identity transformation at index 0: operation interrupted: context deadline exceeded`, }, { name: "slow groups transformation expressions are canceled after a maximum allowed duration", @@ -650,7 +650,7 @@ func TestTransformer(t *testing.T) { // On my laptop, evaluating this expression would take ~20 seconds if we allowed it to evaluate to completion. &GroupsTransformation{Expression: `groups.filter(x, groups.all(x, true))`}, }, - wantEvaluationErr: `identity transformation at index 0: context deadline exceeded`, + wantEvaluationErr: `identity transformation at index 0: operation interrupted: context deadline exceeded`, }, { name: "slow policy transformation expressions are canceled after a maximum allowed duration", @@ -660,7 +660,7 @@ func TestTransformer(t *testing.T) { // On my laptop, evaluating this expression would take ~20 seconds if we allowed it to evaluate to completion. &AllowAuthenticationPolicy{Expression: `groups.all(x, groups.all(x, true))`}, }, - wantEvaluationErr: `identity transformation at index 0: context deadline exceeded`, + wantEvaluationErr: `identity transformation at index 0: operation interrupted: context deadline exceeded`, }, { name: "compile errors are returned by the compile step for a username transform", From a1a5019e3f6c10fe8d4f98277a2ec87fac1fdbf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20O=CC=88zc=CC=A7elik?= Date: Thu, 25 Jun 2026 12:13:15 -0700 Subject: [PATCH 04/10] otel version bump for CVE fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ryan Richard Signed-off-by: Volkan Özçelik --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 83ac3a8cd..4e85e0adc 100644 --- a/go.mod +++ b/go.mod @@ -153,15 +153,15 @@ require ( go.opentelemetry.io/contrib/propagators/b3 v1.32.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.32.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.26.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/zipkin v1.32.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/go.sum b/go.sum index 3daf7ec4e..acf4298ce 100644 --- a/go.sum +++ b/go.sum @@ -639,8 +639,8 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.32.0 h1:K/fOyTMD6GELKTIJBaJ9k3 go.opentelemetry.io/contrib/propagators/jaeger v1.32.0/go.mod h1:ISE6hda//MTWvtngG7p4et3OCngsrTVfl7c6DjN17f8= go.opentelemetry.io/contrib/samplers/jaegerremote v0.26.0 h1:/SKXyZLAnuj981HVc8G5ZylYK3qD2W6AYR6cJx5kIHw= go.opentelemetry.io/contrib/samplers/jaegerremote v0.26.0/go.mod h1:cOEzME0M2OKeHB45lJiOKfvUCdg/r75mf7YS5w0tbmE= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= @@ -651,14 +651,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJK go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/exporters/zipkin v1.32.0 h1:6O8HgLHPXtXE9QEKEWkBImL9mEKCGEl+m+OncVO53go= go.opentelemetry.io/otel/exporters/zipkin v1.32.0/go.mod h1:+MFvorlowjy0iWnsKaNxC1kzczSxe71mw85h4p8yEvg= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= From fce9b9ed1b75e74d412a6633aa532976b4c331b8 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 25 Jun 2026 15:56:45 -0700 Subject: [PATCH 05/10] fix a parsing mistake in ActiveDirectory code Signed-off-by: Ryan Richard --- .../active_directory_upstream_watcher.go | 30 ++++++++++---- .../active_directory_upstream_watcher_test.go | 41 ++++++++++++++++--- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 4511b8407..eafae48aa 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package activedirectoryupstreamwatcher implements a controller which watches ActiveDirectoryIdentityProviders. @@ -8,7 +8,6 @@ import ( "context" "encoding/base64" "fmt" - "regexp" "strconv" "strings" @@ -454,14 +453,29 @@ func groupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) { return sAMAccountName + "@" + domain, nil } -var domainComponentsRegexp = regexp.MustCompile(",DC=|,dc=") - func getDomainFromDistinguishedName(distinguishedName string) (string, error) { - domainComponents := domainComponentsRegexp.Split(distinguishedName, -1) - if len(domainComponents) == 1 { - return "", fmt.Errorf("did not find domain components in group dn: %s", distinguishedName) + parsedDN, err := ldap.ParseDN(distinguishedName) + if err != nil { + return "", fmt.Errorf("could not parse DN %q: %w", distinguishedName, err) } - return strings.Join(domainComponents[1:], "."), nil + + var dcValues []string + // Iterate through the Relative Distinguished Names (RDNs) + for _, rdn := range parsedDN.RDNs { + // Each RDN can contain multiple attribute-value pairs (usually just one) + for _, attr := range rdn.Attributes { + // Compare case-insensitively since LDAP attributes are case-insensitive + if strings.EqualFold(attr.Type, "dc") { + dcValues = append(dcValues, attr.Value) + } + } + } + + if len(dcValues) == 0 { + return "", fmt.Errorf("did not find domain components in group DN %q", distinguishedName) + } + + return strings.Join(dcValues, "."), nil } //nolint:gochecknoglobals // this needs to be a global variable so that tests can check pointer equality diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index ca6a28915..64dee7ced 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -2481,14 +2481,35 @@ func TestGroupSAMAccountNameWithDomainSuffix(t *testing.T) { wantResult: "Mammals@mycompany.example.com", }, { - name: "no domain components in DN", + name: "unusual CN with DN and valid sAMAccountName", entry: &ldap.Entry{ - DN: "no-domain-components", + // A backslash within a CN escapes the next character, so we want to ignore the letters "DC=" there. + DN: `CN=animals\,DC=should-be-ignored-because-it-is-part-of-cn,OU=Users,OU=pinniped-ad,DC=mycompany,DC=example,DC=com`, Attributes: []*ldap.EntryAttribute{ ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}), }, }, - wantErr: "did not find domain components in group dn: no-domain-components", + wantResult: "Mammals@mycompany.example.com", + }, + { + name: "invalid DN", + entry: &ldap.Entry{ + DN: "invalid-dn", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}), + }, + }, + wantErr: `could not parse DN "invalid-dn": DN ended with incomplete type, value pair`, + }, + { + name: "no domain components in DN", + entry: &ldap.Entry{ + DN: "CN=foo", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}), + }, + }, + wantErr: `did not find domain components in group DN "CN=foo"`, }, { name: "multiple values for sAMAccountName attribute", @@ -2576,9 +2597,19 @@ func TestGetDomainFromDistinguishedName(t *testing.T) { wantDomain: "activedirectory.mycompany.example.com", }, { - name: "no domain components", + name: "unusual CN", + distinguishedName: `CN=animals\,DC=should-be-ignored-because-it-is-part-of-cn,OU=Users,OU=pinniped-ad,dc=activedirectory,DC=mycompany,DC=example,DC=com`, + wantDomain: "activedirectory.mycompany.example.com", + }, + { + name: "invalid DN", distinguishedName: "not-a-dn", - wantErr: "did not find domain components in group dn: not-a-dn", + wantErr: `could not parse DN "not-a-dn": DN ended with incomplete type, value pair`, + }, + { + name: "no domain components", + distinguishedName: "CN=foo", + wantErr: `did not find domain components in group DN "CN=foo"`, }, } From 414af00ff4e1732519f0146d07631f62851a905e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 25 Jun 2026 16:39:00 -0700 Subject: [PATCH 06/10] limit req body size in login_handler.go Signed-off-by: Ryan Richard --- .../endpoints/login/login_handler.go | 4 ++- .../endpoints/login/login_handler_test.go | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/federationdomain/endpoints/login/login_handler.go b/internal/federationdomain/endpoints/login/login_handler.go index 04e077c38..9d99842e0 100644 --- a/internal/federationdomain/endpoints/login/login_handler.go +++ b/internal/federationdomain/endpoints/login/login_handler.go @@ -1,4 +1,4 @@ -// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login @@ -52,6 +52,8 @@ func NewHandler( auditLogger plog.AuditLogger, ) http.Handler { loginHandler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB limit + if err := auditLogger.AuditRequestParams(r, paramsSafeToLog()); err != nil { plog.DebugErr("error parsing callback request params", err) return httperr.New(http.StatusBadRequest, "error parsing request params") diff --git a/internal/federationdomain/endpoints/login/login_handler_test.go b/internal/federationdomain/endpoints/login/login_handler_test.go index a1a0deb6f..c70e6132e 100644 --- a/internal/federationdomain/endpoints/login/login_handler_test.go +++ b/internal/federationdomain/endpoints/login/login_handler_test.go @@ -4,6 +4,7 @@ package login import ( + "io" "net/http" "net/http/httptest" "net/url" @@ -116,6 +117,8 @@ func TestLoginEndpoint(t *testing.T) { method string path string csrfCookie string + contentType string + formParams url.Values getHandlerErr error postHandlerErr error @@ -350,6 +353,17 @@ func TestLoginEndpoint(t *testing.T) { wantEncodedState: happyState, wantDecodedState: expectedHappyDecodedUpstreamStateParam(), }, + { + name: "POST with body too large", + method: http.MethodPost, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + contentType: "application/x-www-form-urlencoded", + formParams: map[string][]string{"k": {strings.Repeat("v", 1_048_576)}}, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: error parsing request params\n", + }, { name: "happy GET request with err param which can be set by the real POST handler on redirects back to the GET handler", method: http.MethodGet, @@ -461,10 +475,19 @@ func TestLoginEndpoint(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - req := httptest.NewRequestWithContext(t.Context(), test.method, test.path, nil) + var body io.Reader + if test.method == http.MethodPost && len(test.formParams) > 0 { + body = strings.NewReader(test.formParams.Encode()) + } + + req := httptest.NewRequestWithContext(t.Context(), test.method, test.path, body) + if test.csrfCookie != "" { req.Header.Set("Cookie", test.csrfCookie) } + if test.contentType != "" { + req.Header.Set("Content-Type", test.contentType) + } req, _ = auditid.NewRequestWithAuditID(req, func() string { return "fake-audit-id" }) rsp := httptest.NewRecorder() From a2d1ae52565df3cef611d4366828f416be1ce3cd Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 1 Jul 2026 12:12:47 -0700 Subject: [PATCH 07/10] update NetworkPolicies for new Kind/k8s version Signed-off-by: Ryan Richard --- hack/prepare-for-integration-tests.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 88ee3f90c..ce7464f87 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +# Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -431,6 +431,10 @@ spec: podSelector: matchLabels: app: proxy + # Allowed to talk to the API Server (and other kube-system pods) which Kind puts in this IP range. + - to: + - ipBlock: + cidr: 172.18.0.0/16 # Default internal KIND Docker bridge network --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy @@ -457,6 +461,10 @@ spec: podSelector: matchLabels: app: proxy + # Allowed to talk to the API Server (and other kube-system pods) which Kind puts in this IP range. + - to: + - ipBlock: + cidr: 172.18.0.0/16 # Default internal KIND Docker bridge network EOF fi From d8e5f4315ce54b6b2c30c7c568919d2b83de1b60 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 2 Jul 2026 07:03:39 -0700 Subject: [PATCH 08/10] tests clear old GitHub totp code before new one Signed-off-by: Ryan Richard --- test/testlib/browsertest/browsertest.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/testlib/browsertest/browsertest.go b/test/testlib/browsertest/browsertest.go index 761cdeb78..b929d3714 100644 --- a/test/testlib/browsertest/browsertest.go +++ b/test/testlib/browsertest/browsertest.go @@ -271,6 +271,11 @@ func (b *Browser) SendKeysToFirstMatch(t *testing.T, cssSelector string, runesTo b.runWithTimeout(t, b.timeout(), chromedp.SendKeys(cssSelector, runesToType, chromedp.NodeVisible, chromedp.NodeEnabled, chromedp.ByQuery)) } +func (b *Browser) ClearFirstMatch(t *testing.T, cssSelector string) { + t.Helper() + b.runWithTimeout(t, b.timeout(), chromedp.Clear(cssSelector, chromedp.NodeVisible, chromedp.NodeEnabled, chromedp.ByQuery)) +} + func (b *Browser) ClickFirstMatch(t *testing.T, cssSelector string) string { t.Helper() var text string @@ -449,6 +454,10 @@ func handleGithubOTPLoginPage(t *testing.T, b *Browser, upstream testlib.TestGit t.Logf("waiting for GitHub MFA page") b.WaitForVisibleElements(t, otpSelector) + // We may be in a loop where we are going to re-try entering the OTP code again on the same page, + // so first clear out any old OTP code that might be there from a previous attempt. + b.ClearFirstMatch(t, otpSelector) + // Sleep for a bit to make it less likely that we use the same OTP code twice when multiple tests are run in serial. // GitHub gets upset when the same OTP code gets reused. // GitHub seems to also get upset when any OTP codes are used often, like when all our GitHub tests run sequentially, From 6223b41286b1746b88c0d50609cf69aacf9abde3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 2 Jul 2026 13:43:45 -0700 Subject: [PATCH 09/10] retry when github user api 401's during login Signed-off-by: Ryan Richard --- hack/header.txt | 2 +- .../endpoints/token/token_handler_test.go | 7 +- .../resolved_github_provider.go | 10 +- .../resolved_github_provider_test.go | 42 +++--- .../upstreamprovider/upstream_provider.go | 19 ++- internal/githubclient/githubclient.go | 51 ++++++- internal/githubclient/githubclient_test.go | 125 +++++++++++++++++- .../mocks/mockcachevalue/mockcachevalue.go | 2 +- .../mockcredentialrequest.go | 2 +- .../mockgithubclient/mockgithubclient.go | 10 +- internal/mocks/mockissuer/mockissuer.go | 2 +- internal/mocks/mockkeyset/mockkeyset.go | 2 +- .../mockkubecertagent/mockdynamiccert.go | 2 +- .../mockpodcommandexecutor.go | 2 +- internal/mocks/mockldapconn/mockldapconn.go | 2 +- .../mockoidcclientoptions.go | 2 +- .../mockresponsewriter/mockresponsewriter.go | 2 +- .../mocksecrethelper/mocksecrethelper.go | 2 +- .../mockupstreamoidcidentityprovider.go | 2 +- .../oidctestutil/testgithubprovider.go | 18 +-- internal/upstreamgithub/upstreamgithub.go | 6 +- .../upstreamgithub/upstreamgithub_test.go | 48 +++++-- 22 files changed, 281 insertions(+), 79 deletions(-) diff --git a/hack/header.txt b/hack/header.txt index c45500693..649ffa377 100644 --- a/hack/header.txt +++ b/hack/header.txt @@ -1,2 +1,2 @@ -Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 091ca93a9..9e2a0cb39 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -2149,9 +2149,10 @@ func TestRefreshGrant(t *testing.T) { return &expectedGithubUpstreamRefresh{ performedByUpstreamName: githubUpstreamName, args: &oidctestutil.GetUserArgs{ - Ctx: nil, // this will be filled in with the actual request context by the test below - AccessToken: githubUpstreamAccessToken, - IDPDisplayName: githubUpstreamName, + Ctx: nil, // this will be filled in with the actual request context by the test below + AccessToken: githubUpstreamAccessToken, + IDPDisplayName: githubUpstreamName, + RetryOnUnauthorized: upstreamprovider.DoNotRetryOnUnauthorized, }, } } diff --git a/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider.go b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider.go index 79ccc08f8..f2dd5a6b7 100644 --- a/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider.go +++ b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider.go @@ -1,4 +1,4 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package resolvedgithub @@ -108,7 +108,7 @@ func (p *FederationDomainResolvedGitHubIdentityProvider) LoginFromCallback( ) } - user, err := p.Provider.GetUser(ctx, accessToken, p.GetDisplayName()) + user, err := p.Provider.GetUser(ctx, accessToken, p.GetDisplayName(), upstreamprovider.RetryOnUnauthorized) if errors.As(err, &upstreamprovider.GitHubLoginDeniedError{}) { // We specifically want errors of type GitHubLoginDeniedError to have a user-displayed message. @@ -153,7 +153,11 @@ func (p *FederationDomainResolvedGitHubIdentityProvider) UpstreamRefresh( } // Get the user's GitHub identity and groups again using the cached access token. - refreshedUserInfo, err := p.Provider.GetUser(ctx, githubSessionData.UpstreamAccessToken, p.GetDisplayName()) + refreshedUserInfo, err := p.Provider.GetUser(ctx, + githubSessionData.UpstreamAccessToken, + p.GetDisplayName(), + upstreamprovider.DoNotRetryOnUnauthorized, + ) if err != nil { return nil, p.refreshErr(err) } diff --git a/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider_test.go b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider_test.go index 82e8980a3..f62941af5 100644 --- a/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider_test.go +++ b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider_test.go @@ -153,9 +153,10 @@ func TestLoginFromCallback(t *testing.T) { }, wantGetUserCall: true, wantGetUserArgs: &oidctestutil.GetUserArgs{ - Ctx: uniqueCtx, - AccessToken: "fake-access-token", - IDPDisplayName: "fake-display-name", + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + RetryOnUnauthorized: upstreamprovider.RetryOnUnauthorized, }, wantIdentity: &resolvedprovider.Identity{ UpstreamUsername: "fake-username", @@ -205,9 +206,10 @@ func TestLoginFromCallback(t *testing.T) { }, wantGetUserCall: true, wantGetUserArgs: &oidctestutil.GetUserArgs{ - Ctx: uniqueCtx, - AccessToken: "fake-access-token", - IDPDisplayName: "fake-display-name", + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + RetryOnUnauthorized: upstreamprovider.RetryOnUnauthorized, }, wantIdentity: nil, wantExtras: nil, @@ -232,9 +234,10 @@ func TestLoginFromCallback(t *testing.T) { }, wantGetUserCall: true, wantGetUserArgs: &oidctestutil.GetUserArgs{ - Ctx: uniqueCtx, - AccessToken: "fake-access-token", - IDPDisplayName: "fake-display-name", + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + RetryOnUnauthorized: upstreamprovider.RetryOnUnauthorized, }, wantIdentity: nil, wantExtras: nil, @@ -324,9 +327,10 @@ func TestUpstreamRefresh(t *testing.T) { idpDisplayName: "fake-display-name", wantGetUserCall: true, wantGetUserArgs: &oidctestutil.GetUserArgs{ - Ctx: uniqueCtx, - AccessToken: "fake-access-token", - IDPDisplayName: "fake-display-name", + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + RetryOnUnauthorized: upstreamprovider.DoNotRetryOnUnauthorized, }, wantRefreshedIdentity: &resolvedprovider.RefreshedIdentity{ UpstreamUsername: "refreshed-username", @@ -349,9 +353,10 @@ func TestUpstreamRefresh(t *testing.T) { idpDisplayName: "fake-display-name", wantGetUserCall: true, wantGetUserArgs: &oidctestutil.GetUserArgs{ - Ctx: uniqueCtx, - AccessToken: "fake-access-token", - IDPDisplayName: "fake-display-name", + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + RetryOnUnauthorized: upstreamprovider.DoNotRetryOnUnauthorized, }, wantRefreshedIdentity: nil, wantWrappedErr: "fake github GetUser error message", @@ -407,9 +412,10 @@ func TestUpstreamRefresh(t *testing.T) { idpDisplayName: "fake-display-name", wantGetUserCall: true, wantGetUserArgs: &oidctestutil.GetUserArgs{ - Ctx: uniqueCtx, - AccessToken: "fake-access-token", - IDPDisplayName: "fake-display-name", + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + RetryOnUnauthorized: upstreamprovider.DoNotRetryOnUnauthorized, }, wantRefreshedIdentity: nil, wantWrappedErr: `user's calculated downstream subject at initial login was "https://fake-downstream-subject" ` + diff --git a/internal/federationdomain/upstreamprovider/upstream_provider.go b/internal/federationdomain/upstreamprovider/upstream_provider.go index 070de4594..4a1743272 100644 --- a/internal/federationdomain/upstreamprovider/upstream_provider.go +++ b/internal/federationdomain/upstreamprovider/upstream_provider.go @@ -1,4 +1,4 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package upstreamprovider @@ -26,6 +26,18 @@ const ( AccessTokenType RevocableTokenType = "access_token" ) +// UnauthorizedRetryBehavior controls whether GetUser retries after a 401 from the GitHub user API. +type UnauthorizedRetryBehavior bool + +const ( + // RetryOnUnauthorized retries a bounded number of times after a short delay, since GitHub + // occasionally returns a transient 401 for a token that was just issued. + RetryOnUnauthorized UnauthorizedRetryBehavior = true + // DoNotRetryOnUnauthorized fails immediately on a 401 (e.g. during a refresh, where a 401 + // indicates a genuinely revoked or invalid token). + DoNotRetryOnUnauthorized UnauthorizedRetryBehavior = false +) + // LDAPRefreshAttributes contains information about the user from the original login request // and previous refreshes to be used during an LDAP session refresh. type LDAPRefreshAttributes struct { @@ -189,5 +201,8 @@ type UpstreamGithubIdentityProviderI interface { // GetUser calls the user, orgs, and teams APIs of GitHub using the accessToken. // It validates any required org memberships. It returns a User or an error. // The IDP display name is passed to aid in building a suitable downstream subject string. - GetUser(ctx context.Context, accessToken string, idpDisplayName string) (*GitHubUser, error) + // If retryOnUnauthorized is true, a 401 from the user API will be retried a bounded number of + // times after a short delay, to work around GitHub occasionally returning a transient 401 for a + // token that was just issued. + GetUser(ctx context.Context, accessToken string, idpDisplayName string, retryOnUnauthorized UnauthorizedRetryBehavior) (*GitHubUser, error) } diff --git a/internal/githubclient/githubclient.go b/internal/githubclient/githubclient.go index 046638162..4f1f16f22 100644 --- a/internal/githubclient/githubclient.go +++ b/internal/githubclient/githubclient.go @@ -11,6 +11,7 @@ import ( "net/url" "slices" "strings" + "time" "github.com/google/go-github/v87/github" "k8s.io/apimachinery/pkg/util/sets" @@ -22,6 +23,13 @@ import ( const ( emptyUserMeansTheAuthenticatedUser = "" pageSize = 100 + + // defaultUnauthorizedRetryDelay and defaultUnauthorizedMaxRetries control how GetUserInfo + // retries when the GitHub user API returns a 401 for a token that was just issued. GitHub + // sometimes returns a transient 401 for a valid, freshly issued token before it has fully + // propagated; hopefully waiting briefly and retrying will resolve it. + defaultUnauthorizedRetryDelay = 1 * time.Second + defaultUnauthorizedMaxRetries = 2 ) type UserInfo struct { @@ -36,13 +44,18 @@ type TeamInfo struct { } type GitHubInterface interface { - GetUserInfo(ctx context.Context) (*UserInfo, error) + GetUserInfo(ctx context.Context, retryOnUnauthorized bool) (*UserInfo, error) GetOrgMembership(ctx context.Context) ([]string, error) GetTeamMembership(ctx context.Context, allowedOrganizations *setutil.CaseInsensitiveSet) ([]TeamInfo, error) } type githubClient struct { client *github.Client + + // unauthorizedRetryDelay and unauthorizedMaxRetries configure GetUserInfo's retry-on-401 + // behavior. They are fields so that unit tests can adjust them. + unauthorizedRetryDelay time.Duration + unauthorizedMaxRetries int } var _ GitHubInterface = (*githubClient)(nil) @@ -85,15 +98,43 @@ func NewGitHubClient(httpClient *http.Client, apiBaseURL, token string) (GitHubI } return &githubClient{ - client: client, + client: client, + unauthorizedRetryDelay: defaultUnauthorizedRetryDelay, + unauthorizedMaxRetries: defaultUnauthorizedMaxRetries, }, nil } -// GetUserInfo returns the "Login" and "ID" attributes of the logged-in user. -func (g *githubClient) GetUserInfo(ctx context.Context) (*UserInfo, error) { +// isUnauthorized returns true only when err is a *github.ErrorResponse for an HTTP 401. GitHub's +// rate-limiting conditions are surfaced by go-github as the distinct *github.RateLimitError and +// *github.AbuseRateLimitError types (HTTP 403/429), so this check can never match those and cannot +// interfere with go-github's rate-limit handling. +func isUnauthorized(err error) bool { + var errResp *github.ErrorResponse + return errors.As(err, &errResp) && + errResp.Response != nil && + errResp.Response.StatusCode == http.StatusUnauthorized +} + +// GetUserInfo returns the "Login" and "ID" attributes of the logged-in user. If retryOnUnauthorized +// is true and GitHub responds with a 401, the request is retried a bounded number of times after a +// short delay, since GitHub sometimes returns a transient 401 for a token that was just issued. +func (g *githubClient) GetUserInfo(ctx context.Context, retryOnUnauthorized bool) (*UserInfo, error) { const errorPrefix = "error fetching authenticated user" - user, _, err := g.client.Users.Get(ctx, emptyUserMeansTheAuthenticatedUser) + var user *github.User + var err error + for attempt := 0; ; attempt++ { + user, _, err = g.client.Users.Get(ctx, emptyUserMeansTheAuthenticatedUser) + if err == nil || !retryOnUnauthorized || !isUnauthorized(err) || attempt >= g.unauthorizedMaxRetries { + break + } + plog.Debug("got 401 from GitHub user endpoint", "attempt", attempt+1) + select { + case <-ctx.Done(): + return nil, fmt.Errorf("%s: %w", errorPrefix, ctx.Err()) + case <-time.After(g.unauthorizedRetryDelay): + } + } if err != nil { return nil, fmt.Errorf("%s: %w", errorPrefix, err) } diff --git a/internal/githubclient/githubclient_test.go b/internal/githubclient/githubclient_test.go index 3926c0cc0..c77648d47 100644 --- a/internal/githubclient/githubclient_test.go +++ b/internal/githubclient/githubclient_test.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" "testing" + "time" "github.com/google/go-github/v87/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -125,12 +126,13 @@ func TestGetUser(t *testing.T) { t.Parallel() tests := []struct { - name string - httpClient *http.Client - token string - ctx context.Context - wantErr string - wantUserInfo UserInfo + name string + httpClient *http.Client + token string + ctx context.Context + retryOnUnauthorized bool + wantErr string + wantUserInfo UserInfo }{ { name: "happy path", @@ -236,7 +238,7 @@ func TestGetUser(t *testing.T) { ctx = test.ctx } - actual, err := githubClient.GetUserInfo(ctx) + actual, err := githubClient.GetUserInfo(ctx, test.retryOnUnauthorized) if test.wantErr != "" { rt, ok := test.httpClient.Transport.(*mock.EnforceHostRoundTripper) require.True(t, ok) @@ -251,6 +253,115 @@ func TestGetUser(t *testing.T) { } } +func TestGetUserInfoRetryOnUnauthorized(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + retryOnUnauthorized bool + retryDelay time.Duration // zero means no artificial delay + ctxTimeout time.Duration // zero means context.Background() with no timeout + handlerStatus int + handlerMessage string + succeedOnAttempt int // returns success instead of handlerStatus after this many attempts; 0 means never succeed + wantErrContains string + wantErrIs error + wantUserInfo *UserInfo + wantAttempts int + }{ + { + name: "retries once on a 401 then succeeds, when retryOnUnauthorized is true", + retryOnUnauthorized: true, + handlerStatus: http.StatusUnauthorized, + handlerMessage: "bad credentials", + succeedOnAttempt: 2, + wantUserInfo: &UserInfo{Login: "some-username", ID: "12345678"}, + wantAttempts: 2, + }, + { + name: "exhausts retries and fails when every attempt returns a 401, when retryOnUnauthorized is true", + retryOnUnauthorized: true, + handlerStatus: http.StatusUnauthorized, + handlerMessage: "bad credentials", + wantErrContains: "401 bad credentials", + wantAttempts: 1 + defaultUnauthorizedMaxRetries, + }, + { + name: "does not retry a 401 when retryOnUnauthorized is false", + retryOnUnauthorized: false, + handlerStatus: http.StatusUnauthorized, + handlerMessage: "bad credentials", + wantErrContains: "401 bad credentials", + wantAttempts: 1, + }, + { + name: "does not retry a non-401 error even when retryOnUnauthorized is true", + retryOnUnauthorized: true, + handlerStatus: http.StatusForbidden, + handlerMessage: "rate limited", + wantErrContains: "403 rate limited", + wantAttempts: 1, + }, + { + name: "returns promptly when the context is canceled during the retry wait", + retryOnUnauthorized: true, + retryDelay: time.Hour, // long enough that a real wait would fail the test + ctxTimeout: 50 * time.Millisecond, + handlerStatus: http.StatusUnauthorized, + handlerMessage: "bad credentials", + wantErrIs: context.DeadlineExceeded, + wantAttempts: 1, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + attempts := new(int) + + httpClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler(mock.GetUser, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + *attempts++ + if *attempts == test.succeedOnAttempt { + _, err := w.Write([]byte(`{"login":"some-username","id":12345678}`)) + require.NoError(t, err) + return + } + mock.WriteError(w, test.handlerStatus, test.handlerMessage) + })), + ) + + c, err := github.NewClient(github.WithHTTPClient(httpClient), github.WithAuthToken("some-token")) + require.NoError(t, err) + + githubClient := &githubClient{ + client: c, + unauthorizedRetryDelay: test.retryDelay, + unauthorizedMaxRetries: defaultUnauthorizedMaxRetries, + } + + ctx := context.Background() + if test.ctxTimeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, test.ctxTimeout) + defer cancel() + } + + actual, err := githubClient.GetUserInfo(ctx, test.retryOnUnauthorized) + require.Equal(t, test.wantAttempts, *attempts) + switch { + case test.wantErrContains != "": + require.ErrorContains(t, err, test.wantErrContains) + case test.wantErrIs != nil: + require.ErrorIs(t, err, test.wantErrIs) + default: + require.NoError(t, err) + require.Equal(t, test.wantUserInfo, actual) + } + }) + } +} + func TestGetOrgMembership(t *testing.T) { t.Parallel() diff --git a/internal/mocks/mockcachevalue/mockcachevalue.go b/internal/mocks/mockcachevalue/mockcachevalue.go index 5aa817f7a..13dc68aa1 100644 --- a/internal/mocks/mockcachevalue/mockcachevalue.go +++ b/internal/mocks/mockcachevalue/mockcachevalue.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockcredentialrequest/mockcredentialrequest.go b/internal/mocks/mockcredentialrequest/mockcredentialrequest.go index fbe51490b..31ff558ea 100644 --- a/internal/mocks/mockcredentialrequest/mockcredentialrequest.go +++ b/internal/mocks/mockcredentialrequest/mockcredentialrequest.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockgithubclient/mockgithubclient.go b/internal/mocks/mockgithubclient/mockgithubclient.go index 7f7ee6f20..b8e42d8a1 100644 --- a/internal/mocks/mockgithubclient/mockgithubclient.go +++ b/internal/mocks/mockgithubclient/mockgithubclient.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // @@ -77,16 +77,16 @@ func (mr *MockGitHubInterfaceMockRecorder) GetTeamMembership(ctx, allowedOrganiz } // GetUserInfo mocks base method. -func (m *MockGitHubInterface) GetUserInfo(ctx context.Context) (*githubclient.UserInfo, error) { +func (m *MockGitHubInterface) GetUserInfo(ctx context.Context, retryOnUnauthorized bool) (*githubclient.UserInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserInfo", ctx) + ret := m.ctrl.Call(m, "GetUserInfo", ctx, retryOnUnauthorized) ret0, _ := ret[0].(*githubclient.UserInfo) ret1, _ := ret[1].(error) return ret0, ret1 } // GetUserInfo indicates an expected call of GetUserInfo. -func (mr *MockGitHubInterfaceMockRecorder) GetUserInfo(ctx any) *gomock.Call { +func (mr *MockGitHubInterfaceMockRecorder) GetUserInfo(ctx, retryOnUnauthorized any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInfo", reflect.TypeOf((*MockGitHubInterface)(nil).GetUserInfo), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInfo", reflect.TypeOf((*MockGitHubInterface)(nil).GetUserInfo), ctx, retryOnUnauthorized) } diff --git a/internal/mocks/mockissuer/mockissuer.go b/internal/mocks/mockissuer/mockissuer.go index 28f711fe7..594969aad 100644 --- a/internal/mocks/mockissuer/mockissuer.go +++ b/internal/mocks/mockissuer/mockissuer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockkeyset/mockkeyset.go b/internal/mocks/mockkeyset/mockkeyset.go index a444be834..a2d4dcfed 100644 --- a/internal/mocks/mockkeyset/mockkeyset.go +++ b/internal/mocks/mockkeyset/mockkeyset.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockkubecertagent/mockdynamiccert.go b/internal/mocks/mockkubecertagent/mockdynamiccert.go index c417a4f74..a8456d22c 100644 --- a/internal/mocks/mockkubecertagent/mockdynamiccert.go +++ b/internal/mocks/mockkubecertagent/mockdynamiccert.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockkubecertagent/mockpodcommandexecutor.go b/internal/mocks/mockkubecertagent/mockpodcommandexecutor.go index 66b4e93fb..206dcd5e8 100644 --- a/internal/mocks/mockkubecertagent/mockpodcommandexecutor.go +++ b/internal/mocks/mockkubecertagent/mockpodcommandexecutor.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockldapconn/mockldapconn.go b/internal/mocks/mockldapconn/mockldapconn.go index 90a4e1264..599cdb816 100644 --- a/internal/mocks/mockldapconn/mockldapconn.go +++ b/internal/mocks/mockldapconn/mockldapconn.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockoidcclientoptions/mockoidcclientoptions.go b/internal/mocks/mockoidcclientoptions/mockoidcclientoptions.go index ce4438664..4dc4356be 100644 --- a/internal/mocks/mockoidcclientoptions/mockoidcclientoptions.go +++ b/internal/mocks/mockoidcclientoptions/mockoidcclientoptions.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockresponsewriter/mockresponsewriter.go b/internal/mocks/mockresponsewriter/mockresponsewriter.go index 1d14f37ac..e6f1edf71 100644 --- a/internal/mocks/mockresponsewriter/mockresponsewriter.go +++ b/internal/mocks/mockresponsewriter/mockresponsewriter.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mocksecrethelper/mocksecrethelper.go b/internal/mocks/mocksecrethelper/mocksecrethelper.go index 65acf06ca..c3c976016 100644 --- a/internal/mocks/mocksecrethelper/mocksecrethelper.go +++ b/internal/mocks/mocksecrethelper/mocksecrethelper.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go index d41a01b6b..db009669d 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/testutil/oidctestutil/testgithubprovider.go b/internal/testutil/oidctestutil/testgithubprovider.go index dea7fa9ff..7c8575afe 100644 --- a/internal/testutil/oidctestutil/testgithubprovider.go +++ b/internal/testutil/oidctestutil/testgithubprovider.go @@ -1,4 +1,4 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidctestutil @@ -25,9 +25,10 @@ type ExchangeAuthcodeArgs struct { // GetUserArgs is used to spy on calls to // TestUpstreamGitHubIdentityProvider.GetUserFunc(). type GetUserArgs struct { - Ctx context.Context - AccessToken string - IDPDisplayName string + Ctx context.Context + AccessToken string + IDPDisplayName string + RetryOnUnauthorized upstreamprovider.UnauthorizedRetryBehavior } type TestUpstreamGitHubIdentityProviderBuilder struct { @@ -239,15 +240,16 @@ func (u *TestUpstreamGitHubIdentityProvider) ExchangeAuthcodeArgs(call int) *Exc return u.exchangeAuthcodeArgs[call] } -func (u *TestUpstreamGitHubIdentityProvider) GetUser(ctx context.Context, accessToken string, idpDisplayName string) (*upstreamprovider.GitHubUser, error) { +func (u *TestUpstreamGitHubIdentityProvider) GetUser(ctx context.Context, accessToken string, idpDisplayName string, retryOnUnauthorized upstreamprovider.UnauthorizedRetryBehavior) (*upstreamprovider.GitHubUser, error) { if u.getUserArgs == nil { u.getUserArgs = make([]*GetUserArgs, 0) } u.getUserCallCount++ u.getUserArgs = append(u.getUserArgs, &GetUserArgs{ - Ctx: ctx, - AccessToken: accessToken, - IDPDisplayName: idpDisplayName, + Ctx: ctx, + AccessToken: accessToken, + IDPDisplayName: idpDisplayName, + RetryOnUnauthorized: retryOnUnauthorized, }) return u.GetUserFunc(ctx, accessToken) } diff --git a/internal/upstreamgithub/upstreamgithub.go b/internal/upstreamgithub/upstreamgithub.go index dc8e2e56b..722bcc9aa 100644 --- a/internal/upstreamgithub/upstreamgithub.go +++ b/internal/upstreamgithub/upstreamgithub.go @@ -1,4 +1,4 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package upstreamgithub implements an abstraction of upstream GitHub provider interactions. @@ -115,7 +115,7 @@ func (p *Provider) ExchangeAuthcode(ctx context.Context, authcode string, redire // If the user's information meets the AllowedOrganization criteria specified on the GitHubIdentityProvider, // they will be allowed to log in. // Note that errors from the githubclient package already have helpful error prefixes, so there is no need for additional prefixes here. -func (p *Provider) GetUser(ctx context.Context, accessToken string, idpDisplayName string) (*upstreamprovider.GitHubUser, error) { +func (p *Provider) GetUser(ctx context.Context, accessToken string, idpDisplayName string, retryOnUnauthorized upstreamprovider.UnauthorizedRetryBehavior) (*upstreamprovider.GitHubUser, error) { githubClient, err := p.buildGitHubClient(p.c.HttpClient, p.c.APIBaseURL, accessToken) if err != nil { return nil, err @@ -123,7 +123,7 @@ func (p *Provider) GetUser(ctx context.Context, accessToken string, idpDisplayNa githubUser := upstreamprovider.GitHubUser{} - userInfo, err := githubClient.GetUserInfo(ctx) + userInfo, err := githubClient.GetUserInfo(ctx, bool(retryOnUnauthorized)) if err != nil { return nil, err } diff --git a/internal/upstreamgithub/upstreamgithub_test.go b/internal/upstreamgithub/upstreamgithub_test.go index 7b72fa4d1..391633c0d 100644 --- a/internal/upstreamgithub/upstreamgithub_test.go +++ b/internal/upstreamgithub/upstreamgithub_test.go @@ -207,6 +207,7 @@ func TestGetUser(t *testing.T) { tests := []struct { name string providerConfig ProviderConfig + retryOnUnauthorized upstreamprovider.UnauthorizedRetryBehavior buildGitHubClientError error buildMockResponses func(hubInterface *mockgithubclient.MockGitHubInterface) wantUser *upstreamprovider.GitHubUser @@ -221,7 +222,28 @@ func TestGetUser(t *testing.T) { UsernameAttribute: idpv1alpha1.GitHubUsernameLoginAndID, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, gomock.Any()).Return(nil, nil) + }, + wantUser: &upstreamprovider.GitHubUser{ + Username: "some-github-login:some-github-id", + DownstreamSubject: fmt.Sprintf("https://some-url?idpName=%s&login=some-github-login&id=some-github-id", encodedIDPDisplayName), + }, + }, + { + name: "happy path with retryOnUnauthorized", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: idpv1alpha1.GitHubUsernameLoginAndID, + }, + retryOnUnauthorized: upstreamprovider.RetryOnUnauthorized, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext, true).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -241,7 +263,7 @@ func TestGetUser(t *testing.T) { UsernameAttribute: idpv1alpha1.GitHubUsernameLogin, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -261,7 +283,7 @@ func TestGetUser(t *testing.T) { UsernameAttribute: idpv1alpha1.GitHubUsernameID, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -282,7 +304,7 @@ func TestGetUser(t *testing.T) { AllowedOrganizations: setutil.NewCaseInsensitiveSet("ALLOWED-ORG1", "ALLOWED-ORG2"), }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -303,7 +325,7 @@ func TestGetUser(t *testing.T) { AllowedOrganizations: setutil.NewCaseInsensitiveSet("allowed-org"), }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -321,7 +343,7 @@ func TestGetUser(t *testing.T) { GroupNameAttribute: idpv1alpha1.GitHubUseTeamNameForGroupName, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -360,7 +382,7 @@ func TestGetUser(t *testing.T) { GroupNameAttribute: idpv1alpha1.GitHubUseTeamSlugForGroupName, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -405,7 +427,7 @@ func TestGetUser(t *testing.T) { HttpClient: someHttpClient, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(nil, errors.New("error from githubClient.GetUserInfo")) + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(nil, errors.New("error from githubClient.GetUserInfo")) }, wantErrMsg: "error from githubClient.GetUserInfo", }, @@ -417,7 +439,7 @@ func TestGetUser(t *testing.T) { UsernameAttribute: idpv1alpha1.GitHubUsernameLoginAndID, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{}, nil) + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{}, nil) mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, errors.New("error from githubClient.GetOrgMembership")) }, wantErrMsg: "error from githubClient.GetOrgMembership", @@ -430,7 +452,7 @@ func TestGetUser(t *testing.T) { UsernameAttribute: idpv1alpha1.GitHubUsernameLoginAndID, }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{}, nil) + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{}, nil) mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil) mockGitHubInterface.EXPECT().GetTeamMembership(someContext, gomock.Any()).Return(nil, errors.New("error from githubClient.GetTeamMembership")) }, @@ -444,7 +466,7 @@ func TestGetUser(t *testing.T) { UsernameAttribute: "this-is-not-legal-value-from-the-enum", }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -460,7 +482,7 @@ func TestGetUser(t *testing.T) { GroupNameAttribute: "this-is-not-legal-value-from-the-enum", }, buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { - mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + mockGitHubInterface.EXPECT().GetUserInfo(someContext, false).Return(&githubclient.UserInfo{ Login: "some-github-login", ID: "some-github-id", }, nil) @@ -498,7 +520,7 @@ func TestGetUser(t *testing.T) { return mockGitHubInterface, test.buildGitHubClientError } - actualUser, actualErr := p.GetUser(context.Background(), accessToken, idpDisplayName) + actualUser, actualErr := p.GetUser(context.Background(), accessToken, idpDisplayName, test.retryOnUnauthorized) switch { case test.wantErrMsg != "": From f82bf07a6eba450666584677eaaed8cc8c07168e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 2 Jul 2026 14:30:13 -0700 Subject: [PATCH 10/10] try to reduce integration test flake after pod restart Signed-off-by: Ryan Richard --- .../integration/limited_ciphers_utils_test.go | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/integration/limited_ciphers_utils_test.go b/test/integration/limited_ciphers_utils_test.go index 3b6682bcb..aecaf3c4c 100644 --- a/test/integration/limited_ciphers_utils_test.go +++ b/test/integration/limited_ciphers_utils_test.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2024-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -14,6 +14,8 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" + apiregistrationv1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper" "sigs.k8s.io/yaml" "go.pinniped.dev/internal/config/concierge" @@ -216,6 +218,23 @@ func restartAllPodsOfApp( requireEventually.Equal(len(newPods), int(originalScale), "wanted pods to return to original scale") requireEventually.True(allPodsReady(newPods), "wanted all new pods to be ready") }, 2*time.Minute, 200*time.Millisecond) + + // Wait for all the APIServices belonging to this app to become available again. + aggregatedClient := testlib.NewAggregatedClientset(t) + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + listCtx, listCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer listCancel() + apiServices, err := aggregatedClient.ApiregistrationV1().APIServices().List(listCtx, metav1.ListOptions{ + LabelSelector: "app=" + appName, + }) + requireEventually.NoError(err) + for _, apiService := range apiServices.Items { + requireEventually.True( + apiregistrationv1helper.IsAPIServiceConditionTrue(&apiService, apiregistrationv1.Available), + "wanted APIService %q to have condition Available=True", apiService.Name, + ) + } + }, 2*time.Minute, 200*time.Millisecond) } // Even if the test fails due to the below assertions, still try to scale back to original scale,