Skip to content

Commit 0b3f84e

Browse files
committed
add payload tests for external oidc feature
Signed-off-by: Bryce Palmer <[email protected]>
1 parent aecf7c8 commit 0b3f84e

File tree

9 files changed

+1423
-0
lines changed

9 files changed

+1423
-0
lines changed

pkg/testsuites/standard_suites.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,16 @@ var staticSuites = []ginkgo.TestSuite{
430430
},
431431
TestTimeout: 60 * time.Minute,
432432
},
433+
{
434+
Name: "openshift/auth/external-oidc",
435+
Description: templates.LongDesc(`
436+
This test suite runs tests to validate cluster behavior when cluster authentication is configured to use an external OIDC provider.
437+
`),
438+
Qualifiers: []string{
439+
`name.contains("[Suite:openshift/auth/external-oidc") && !name.contains("[Skipped]")`,
440+
},
441+
TestTimeout: 120 * time.Minute,
442+
},
433443
}
434444

435445
func withExcludedTestsFilter(baseExpr string) string {
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package authentication
2+
3+
import (
4+
"bytes"
5+
"crypto/tls"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"net/url"
12+
13+
"k8s.io/apimachinery/pkg/runtime"
14+
)
15+
16+
type keycloakClient struct {
17+
realm string
18+
client *http.Client
19+
adminURL *url.URL
20+
21+
accessToken string
22+
idToken string
23+
}
24+
25+
func keycloakClientFor(keycloakURL string) (*keycloakClient, error) {
26+
baseURL, err := url.Parse(keycloakURL)
27+
if err != nil {
28+
return nil, fmt.Errorf("parsing url: %w", err)
29+
}
30+
31+
transport := &http.Transport{
32+
TLSClientConfig: &tls.Config{
33+
InsecureSkipVerify: true,
34+
},
35+
}
36+
37+
return &keycloakClient{
38+
realm: "master",
39+
client: &http.Client{
40+
Transport: transport,
41+
},
42+
adminURL: baseURL.JoinPath("admin", "realms", "master"),
43+
}, nil
44+
}
45+
46+
func (kc *keycloakClient) CreateGroup(name string) error {
47+
groupURL := kc.adminURL.JoinPath("groups")
48+
49+
group := map[string]interface{}{
50+
"name": name,
51+
}
52+
53+
groupBytes, err := json.Marshal(group)
54+
if err != nil {
55+
return fmt.Errorf("marshalling group configuration %v", group)
56+
}
57+
58+
resp, err := kc.DoRequest(http.MethodPost, groupURL.String(), runtime.ContentTypeJSON, true, bytes.NewBuffer(groupBytes))
59+
if err != nil {
60+
return fmt.Errorf("sending POST request to %q to create group %s", groupURL.String(), name)
61+
}
62+
defer resp.Body.Close()
63+
64+
if resp.StatusCode != http.StatusCreated {
65+
respBytes, _ := io.ReadAll(resp.Body)
66+
return fmt.Errorf("failed creating group %q: %s - %s", name, resp.Status, respBytes)
67+
}
68+
69+
return nil
70+
}
71+
72+
func (kc *keycloakClient) CreateUser(username, password string, groups ...string) error {
73+
userURL := kc.adminURL.JoinPath("users")
74+
75+
user := map[string]interface{}{
76+
"username": username,
77+
"email": fmt.Sprintf("%[email protected]", username),
78+
"enabled": true,
79+
"emailVerified": true,
80+
"groups": groups,
81+
"credentials": []map[string]interface{}{
82+
{
83+
"temporary": false,
84+
"type": "password",
85+
"value": password,
86+
},
87+
},
88+
}
89+
90+
userBytes, err := json.Marshal(user)
91+
if err != nil {
92+
return fmt.Errorf("marshalling user configuration %v", user)
93+
}
94+
95+
resp, err := kc.DoRequest(http.MethodPost, userURL.String(), runtime.ContentTypeJSON, true, bytes.NewBuffer(userBytes))
96+
if err != nil {
97+
return fmt.Errorf("sending POST request to %q to create user %v", userURL.String(), user)
98+
}
99+
defer resp.Body.Close()
100+
101+
if resp.StatusCode != http.StatusCreated {
102+
respBytes, _ := io.ReadAll(resp.Body)
103+
return fmt.Errorf("failed creating user %v: %s - %s", user, resp.Status, respBytes)
104+
}
105+
106+
return nil
107+
}
108+
109+
func (kc *keycloakClient) Authenticate(clientID, username, password string) error {
110+
data := url.Values{
111+
"username": []string{username},
112+
"password": []string{password},
113+
"grant_type": []string{"password"},
114+
"client_id": []string{clientID},
115+
"scope": []string{"openid"},
116+
}
117+
118+
tokenURL := *kc.adminURL
119+
tokenURL.Path = fmt.Sprintf("/realms/%s/protocol/openid-connect/token", kc.realm)
120+
121+
resp, err := kc.DoRequest(http.MethodPost, tokenURL.String(), "application/x-www-form-urlencoded", false, bytes.NewBuffer([]byte(data.Encode())))
122+
if err != nil {
123+
return fmt.Errorf("authenticating as user %q: %w", username, err)
124+
}
125+
defer resp.Body.Close()
126+
127+
respBody := map[string]interface{}{}
128+
respBodyData, err := io.ReadAll(resp.Body)
129+
if err != nil {
130+
return fmt.Errorf("reading response data: %w", err)
131+
}
132+
133+
err = json.Unmarshal(respBodyData, &respBody)
134+
if err != nil {
135+
return fmt.Errorf("unmarshalling response body %s: %w", respBodyData, err)
136+
}
137+
138+
accessTokenData, ok := respBody["access_token"]
139+
if !ok {
140+
return errors.New("unable to extract access token from the response body: access_token field is missing")
141+
}
142+
143+
accessToken, ok := accessTokenData.(string)
144+
if !ok {
145+
return fmt.Errorf("expected accessToken to be of type string but was %T", accessTokenData)
146+
}
147+
kc.accessToken = accessToken
148+
149+
idTokenData, ok := respBody["id_token"]
150+
if !ok {
151+
return errors.New("unable to extract id token from the response body: id_token field is missing")
152+
}
153+
154+
idToken, ok := idTokenData.(string)
155+
if !ok {
156+
return fmt.Errorf("expected idToken to be of type string but was %T", idTokenData)
157+
}
158+
kc.idToken = idToken
159+
160+
return nil
161+
}
162+
163+
func (kc *keycloakClient) DoRequest(method, url, contentType string, authenticated bool, body io.Reader) (*http.Response, error) {
164+
if len(kc.accessToken) == 0 && authenticated {
165+
panic("must authenticate before calling keycloakClient.DoRequest")
166+
}
167+
168+
req, err := http.NewRequest(method, url, body)
169+
if err != nil {
170+
return nil, fmt.Errorf("building request: %w", err)
171+
}
172+
173+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", kc.accessToken))
174+
req.Header.Set("Content-Type", contentType)
175+
req.Header.Set("Accept", runtime.ContentTypeJSON)
176+
177+
return kc.client.Do(req)
178+
}
179+
180+
func (kc *keycloakClient) AccessToken() string {
181+
return kc.accessToken
182+
}
183+
184+
func (kc *keycloakClient) IdToken() string {
185+
return kc.idToken
186+
}
187+
188+
func (kc *keycloakClient) ConfigureClient(clientId string) error {
189+
client, err := kc.GetClientByClientID(clientId)
190+
if err != nil {
191+
return fmt.Errorf("getting client %q: %w", clientId, err)
192+
}
193+
194+
id, ok := client["id"]
195+
if !ok {
196+
return fmt.Errorf("client %q doesn't have 'id'", clientId)
197+
}
198+
199+
idStr, ok := id.(string)
200+
if !ok {
201+
return fmt.Errorf("client %q 'id' is not of type string: %T", clientId, id)
202+
}
203+
204+
if err := kc.CreateClientGroupMapper(idStr, "test-groups-mapper", "groups"); err != nil {
205+
return fmt.Errorf("creating group mapper for client %q: %w", clientId, err)
206+
}
207+
208+
if err := kc.CreateClientAudienceMapper(idStr, "test-aud-mapper"); err != nil {
209+
return fmt.Errorf("creating audience mapper for client %q: %w", clientId, err)
210+
}
211+
212+
return nil
213+
}
214+
215+
func (kc *keycloakClient) CreateClientGroupMapper(clientId, name, claim string) error {
216+
mappersURL := *kc.adminURL
217+
mappersURL.Path += fmt.Sprintf("/clients/%s/protocol-mappers/models", clientId)
218+
219+
mapper := map[string]interface{}{
220+
"name": name,
221+
"protocol": "openid-connect",
222+
"protocolMapper": "oidc-group-membership-mapper", // protocol-mapper type provided by Keycloak
223+
"config": map[string]string{
224+
"full.path": "false",
225+
"id.token.claim": "true",
226+
"access.token.claim": "true",
227+
"userinfo.token.claim": "true",
228+
"claim.name": claim,
229+
},
230+
}
231+
232+
mapperBytes, err := json.Marshal(mapper)
233+
if err != nil {
234+
return err
235+
}
236+
237+
// Keycloak does not return the object on successful create so there's no need to attempt to retrieve it from the response
238+
resp, err := kc.DoRequest(http.MethodPost, mappersURL.String(), runtime.ContentTypeJSON, true, bytes.NewBuffer(mapperBytes))
239+
if err != nil {
240+
return err
241+
}
242+
defer resp.Body.Close()
243+
244+
if resp.StatusCode != http.StatusCreated {
245+
respBytes, _ := io.ReadAll(resp.Body)
246+
return fmt.Errorf("failed creating mapper %q: %s %s", name, resp.Status, respBytes)
247+
}
248+
249+
return nil
250+
}
251+
252+
func (kc *keycloakClient) CreateClientAudienceMapper(clientId, name string) error {
253+
mappersURL := *kc.adminURL
254+
mappersURL.Path += fmt.Sprintf("/clients/%s/protocol-mappers/models", clientId)
255+
256+
mapper := map[string]interface{}{
257+
"name": name,
258+
"protocol": "openid-connect",
259+
"protocolMapper": "oidc-audience-mapper", // protocol-mapper type provided by Keycloak
260+
"config": map[string]string{
261+
"id.token.claim": "false",
262+
"access.token.claim": "true",
263+
"introspection.token.claim": "true",
264+
"included.client.audience": "admin-cli",
265+
"included.custom.audience": "",
266+
"lightweight.claim": "false",
267+
},
268+
}
269+
270+
mapperBytes, err := json.Marshal(mapper)
271+
if err != nil {
272+
return err
273+
}
274+
275+
// Keycloak does not return the object on successful create so there's no need to attempt to retrieve it from the response
276+
resp, err := kc.DoRequest(http.MethodPost, mappersURL.String(), runtime.ContentTypeJSON, true, bytes.NewBuffer(mapperBytes))
277+
if err != nil {
278+
return err
279+
}
280+
defer resp.Body.Close()
281+
282+
if resp.StatusCode != http.StatusCreated {
283+
respBytes, _ := io.ReadAll(resp.Body)
284+
return fmt.Errorf("failed creating mapper %q: %s %s", name, resp.Status, respBytes)
285+
}
286+
287+
return nil
288+
}
289+
290+
// ListClients retrieves all clients
291+
func (kc *keycloakClient) ListClients() ([]map[string]interface{}, error) {
292+
clientsURL := *kc.adminURL
293+
clientsURL.Path += "/clients"
294+
295+
resp, err := kc.DoRequest(http.MethodGet, clientsURL.String(), runtime.ContentTypeJSON, true, nil)
296+
if err != nil {
297+
return nil, err
298+
}
299+
defer resp.Body.Close()
300+
301+
respBytes, err := io.ReadAll(resp.Body)
302+
if err != nil {
303+
return nil, err
304+
}
305+
if resp.StatusCode != http.StatusOK {
306+
return nil, fmt.Errorf("listing clients failed: %s: %s", resp.Status, respBytes)
307+
}
308+
309+
clients := []map[string]interface{}{}
310+
err = json.Unmarshal(respBytes, &clients)
311+
312+
return clients, err
313+
}
314+
315+
func (kc *keycloakClient) GetClientByClientID(clientID string) (map[string]interface{}, error) {
316+
clients, err := kc.ListClients()
317+
if err != nil {
318+
return nil, err
319+
}
320+
321+
for _, c := range clients {
322+
if c["clientId"].(string) == clientID {
323+
return c, nil
324+
}
325+
}
326+
327+
return nil, fmt.Errorf("client with clientID %q not found", clientID)
328+
}

0 commit comments

Comments
 (0)