Skip to content

Commit b4e4169

Browse files
committed
add OIDC datagatherer
Signed-off-by: Tim Ramlot <[email protected]>
1 parent 58098f8 commit b4e4169

File tree

7 files changed

+278
-1
lines changed

7 files changed

+278
-1
lines changed

deploy/charts/disco-agent/templates/configmap.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ data:
1919
{{- . | toYaml | nindent 6 }}
2020
{{- end }}
2121
data-gatherers:
22+
- kind: oidc
23+
name: ark/oidc
2224
- kind: k8s-discovery
2325
name: ark/discovery
2426
- kind: k8s-dynamic

deploy/charts/disco-agent/templates/rbac.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,18 @@ subjects:
9595
- kind: ServiceAccount
9696
name: {{ include "disco-agent.serviceAccountName" . }}
9797
namespace: {{ .Release.Namespace }}
98-
98+
---
99+
apiVersion: rbac.authorization.k8s.io/v1
100+
kind: ClusterRoleBinding
101+
metadata:
102+
name: {{ include "disco-agent.fullname" . }}-oidc-discovery
103+
labels:
104+
{{- include "disco-agent.labels" . | nindent 4 }}
105+
roleRef:
106+
kind: ClusterRole
107+
name: system:service-account-issuer-discovery
108+
apiGroup: rbac.authorization.k8s.io
109+
subjects:
110+
- kind: ServiceAccount
111+
name: {{ include "disco-agent.serviceAccountName" . }}
112+
namespace: {{ .Release.Namespace }}

deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ custom-cluster-description:
77
cluster_description: "A cloud hosted Kubernetes cluster hosting production workloads.\n\nteam: team-1\nemail: [email protected]\npurpose: Production workloads\n"
88
period: "12h0m0s"
99
data-gatherers:
10+
- kind: oidc
11+
name: ark/oidc
1012
- kind: k8s-discovery
1113
name: ark/discovery
1214
- kind: k8s-dynamic
@@ -114,6 +116,8 @@ custom-cluster-name:
114116
cluster_description: ""
115117
period: "12h0m0s"
116118
data-gatherers:
119+
- kind: oidc
120+
name: ark/oidc
117121
- kind: k8s-discovery
118122
name: ark/discovery
119123
- kind: k8s-dynamic
@@ -221,6 +225,8 @@ custom-period:
221225
cluster_description: ""
222226
period: "1m"
223227
data-gatherers:
228+
- kind: oidc
229+
name: ark/oidc
224230
- kind: k8s-discovery
225231
name: ark/discovery
226232
- kind: k8s-dynamic
@@ -328,6 +334,8 @@ defaults:
328334
cluster_description: ""
329335
period: "12h0m0s"
330336
data-gatherers:
337+
- kind: oidc
338+
name: ark/oidc
331339
- kind: k8s-discovery
332340
name: ark/discovery
333341
- kind: k8s-dynamic

examples/one-shot-oidc.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# one-shot-oidc.yaml
2+
#
3+
# An example configuration file which can be used for local testing.
4+
# For example:
5+
#
6+
# go run . agent \
7+
# --agent-config-file examples/one-shot-oidc.yaml \
8+
# --one-shot \
9+
# --output-path output.json
10+
#
11+
organization_id: "my-organization"
12+
cluster_id: "my_cluster"
13+
period: 1m
14+
data-gatherers:
15+
- kind: "oidc"
16+
name: "ark/oidc"

pkg/agent/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/jetstack/preflight/pkg/datagatherer/k8sdiscovery"
2323
"github.com/jetstack/preflight/pkg/datagatherer/k8sdynamic"
2424
"github.com/jetstack/preflight/pkg/datagatherer/local"
25+
"github.com/jetstack/preflight/pkg/datagatherer/oidc"
2526
"github.com/jetstack/preflight/pkg/kubeconfig"
2627
"github.com/jetstack/preflight/pkg/logs"
2728
"github.com/jetstack/preflight/pkg/version"
@@ -901,6 +902,8 @@ func (dg *DataGatherer) UnmarshalYAML(unmarshal func(any) error) error {
901902
cfg = &k8sdynamic.ConfigDynamic{}
902903
case "k8s-discovery":
903904
cfg = &k8sdiscovery.ConfigDiscovery{}
905+
case "oidc":
906+
cfg = &oidc.OIDCDiscovery{}
904907
case "local":
905908
cfg = &local.Config{}
906909
// dummy dataGatherer is just used for testing

pkg/datagatherer/oidc/oidc.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package oidc
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"k8s.io/client-go/rest"
9+
10+
"github.com/jetstack/preflight/pkg/datagatherer"
11+
"github.com/jetstack/preflight/pkg/kubeconfig"
12+
)
13+
14+
// OIDCDiscovery contains the configuration for the k8s-discovery data-gatherer
15+
type OIDCDiscovery struct {
16+
// KubeConfigPath is the path to the kubeconfig file. If empty, will assume it runs in-cluster.
17+
KubeConfigPath string `yaml:"kubeconfig"`
18+
}
19+
20+
// UnmarshalYAML unmarshals the Config resolving GroupVersionResource.
21+
func (c *OIDCDiscovery) UnmarshalYAML(unmarshal func(any) error) error {
22+
aux := struct {
23+
KubeConfigPath string `yaml:"kubeconfig"`
24+
}{}
25+
err := unmarshal(&aux)
26+
if err != nil {
27+
return err
28+
}
29+
30+
c.KubeConfigPath = aux.KubeConfigPath
31+
32+
return nil
33+
}
34+
35+
func (c *OIDCDiscovery) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer, error) {
36+
cl, err := kubeconfig.NewDiscoveryClient(c.KubeConfigPath)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
return &DataGathererOIDC{
42+
cl: cl.RESTClient(),
43+
}, nil
44+
}
45+
46+
// DataGathererOIDC stores the config for a k8s-discovery datagatherer
47+
type DataGathererOIDC struct {
48+
cl rest.Interface
49+
}
50+
51+
func (g *DataGathererOIDC) Run(ctx context.Context) error {
52+
return nil
53+
}
54+
55+
func (g *DataGathererOIDC) WaitForCacheSync(ctx context.Context) error {
56+
// no async functionality, see Fetch
57+
return nil
58+
}
59+
60+
// Fetch will fetch discovery data from the apiserver, or return an error
61+
func (g *DataGathererOIDC) Fetch() (any, int, error) {
62+
ctx := context.Background()
63+
64+
oidcResponse, oidcErr := g.fetchOIDCConfig(ctx)
65+
jwksResponse, jwksErr := g.fetchJWKS(ctx)
66+
67+
errToString := func(err error) string {
68+
if err != nil {
69+
return err.Error()
70+
}
71+
return ""
72+
}
73+
74+
return OIDCDiscoveryData{
75+
OIDCConfig: oidcResponse,
76+
OIDCConfigError: errToString(oidcErr),
77+
JWKS: jwksResponse,
78+
JWKSError: errToString(jwksErr),
79+
}, 1, nil
80+
}
81+
82+
type OIDCDiscoveryData struct {
83+
OIDCConfig map[string]any `json:"openid_configuration,omitempty"`
84+
OIDCConfigError string `json:"openid_configuration_error,omitempty"`
85+
JWKS map[string]any `json:"jwks,omitempty"`
86+
JWKSError string `json:"jwks_error,omitempty"`
87+
}
88+
89+
func (g *DataGathererOIDC) fetchOIDCConfig(ctx context.Context) (map[string]any, error) {
90+
bytes, err := g.cl.Get().AbsPath("/.well-known/openid-configuration").Do(ctx).Raw()
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to get OIDC discovery document: %v", err)
93+
}
94+
95+
var oidcResponse map[string]any
96+
if err := json.Unmarshal(bytes, &oidcResponse); err != nil {
97+
return nil, fmt.Errorf("failed to unmarshal OIDC discovery document: %v", err)
98+
}
99+
100+
return oidcResponse, nil
101+
}
102+
103+
func (g *DataGathererOIDC) fetchJWKS(ctx context.Context) (map[string]any, error) {
104+
bytes, err := g.cl.Get().AbsPath("/openid/v1/jwks").Do(ctx).Raw()
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to get JWKS from jwks_uri: %v", err)
107+
}
108+
109+
var jwksResponse map[string]any
110+
if err := json.Unmarshal(bytes, &jwksResponse); err != nil {
111+
return nil, fmt.Errorf("failed to unmarshal JWKS response: %v", err)
112+
}
113+
114+
return jwksResponse, nil
115+
}

pkg/datagatherer/oidc/oidc_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package oidc
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"net/url"
7+
"testing"
8+
9+
"k8s.io/client-go/discovery"
10+
"k8s.io/client-go/rest"
11+
)
12+
13+
func makeRESTClient(t *testing.T, ts *httptest.Server) rest.Interface {
14+
t.Helper()
15+
u, err := url.Parse(ts.URL)
16+
if err != nil {
17+
t.Fatalf("parse server url: %v", err)
18+
}
19+
20+
cfg := &rest.Config{
21+
Host: u.Host,
22+
}
23+
24+
discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, ts.Client())
25+
if err != nil {
26+
t.Fatalf("new discovery client: %v", err)
27+
}
28+
29+
return discoveryClient.RESTClient()
30+
}
31+
32+
func TestFetch_Success(t *testing.T) {
33+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34+
switch r.URL.Path {
35+
case "/.well-known/openid-configuration":
36+
w.Header().Set("Content-Type", "application/json")
37+
_, _ = w.Write([]byte(`{"issuer":"https://example"}`))
38+
case "/openid/v1/jwks":
39+
w.Header().Set("Content-Type", "application/json")
40+
_, _ = w.Write([]byte(`{"keys":[]}`))
41+
default:
42+
http.NotFound(w, r)
43+
}
44+
}))
45+
defer ts.Close()
46+
47+
rc := makeRESTClient(t, ts)
48+
g := &DataGathererOIDC{cl: rc}
49+
50+
anyRes, count, err := g.Fetch()
51+
if err != nil {
52+
t.Fatalf("Fetch returned error: %v", err)
53+
}
54+
if count != 1 {
55+
t.Fatalf("expected count 1, got %d", count)
56+
}
57+
58+
res, ok := anyRes.(OIDCDiscoveryData)
59+
if !ok {
60+
t.Fatalf("unexpected result type: %T", anyRes)
61+
}
62+
63+
if res.OIDCConfig == nil {
64+
t.Fatalf("expected OIDCConfig, got nil")
65+
}
66+
if iss, _ := res.OIDCConfig["issuer"].(string); iss != "https://example" {
67+
t.Fatalf("unexpected issuer: %v", res.OIDCConfig["issuer"])
68+
}
69+
70+
if res.JWKS == nil {
71+
t.Fatalf("expected JWKS, got nil")
72+
}
73+
if _, ok := res.JWKS["keys"].([]any); !ok {
74+
t.Fatalf("expected keys to be a slice, got %#v", res.JWKS["keys"])
75+
}
76+
}
77+
78+
func TestFetch_Errors(t *testing.T) {
79+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
80+
switch r.URL.Path {
81+
case "/.well-known/openid-configuration":
82+
// return server error
83+
http.Error(w, "boom", http.StatusInternalServerError)
84+
case "/openid/v1/jwks":
85+
// return invalid JSON
86+
w.Header().Set("Content-Type", "application/json")
87+
_, _ = w.Write([]byte(`}{`))
88+
default:
89+
http.NotFound(w, r)
90+
}
91+
}))
92+
defer ts.Close()
93+
94+
rc := makeRESTClient(t, ts)
95+
g := &DataGathererOIDC{cl: rc}
96+
97+
anyRes, _, err := g.Fetch()
98+
if err != nil {
99+
t.Fatalf("Fetch returned error: %v", err)
100+
}
101+
102+
res, ok := anyRes.(OIDCDiscoveryData)
103+
if !ok {
104+
t.Fatalf("unexpected result type: %T", anyRes)
105+
}
106+
107+
if res.OIDCConfig != nil {
108+
t.Fatalf("expected nil OIDCConfig on error, got %#v", res.OIDCConfig)
109+
}
110+
if res.OIDCConfigError != "failed to get OIDC discovery document: an error on the server (\"boom\") has prevented the request from succeeding" {
111+
t.Fatalf("unexpected OIDCConfigError: %q", res.OIDCConfigError)
112+
}
113+
if res.JWKS != nil {
114+
t.Fatalf("expected nil JWKS on malformed JSON, got %#v", res.JWKS)
115+
}
116+
if res.JWKSError != "failed to unmarshal JWKS response: invalid character '}' looking for beginning of value" {
117+
t.Fatalf("unexpected JWKSError: %q", res.JWKSError)
118+
}
119+
}

0 commit comments

Comments
 (0)