Twice the identity provider's admin kept an older password than the one the mesh minted (an adopted, then a moved database), and the provisioner failed every consumer until it was repaired by hand (hq issue 179). The module now checks the admin's login and repairs a refusal itself through the server's bootstrap command, verifies, brakes a failed repair and announces it, and stops asking the server while refused. Ported to Go to change it.
170 lines
4.3 KiB
Go
170 lines
4.3 KiB
Go
package main
|
|
|
|
// A fake Keycloak: the token endpoint, accepting one password that a test may change, and the admin
|
|
// routes this module touches on one realm's clients, answering with the status codes and shapes
|
|
// Keycloak gives.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
var ctx = context.Background()
|
|
|
|
type fakeKeycloak struct {
|
|
mu sync.Mutex
|
|
srv *httptest.Server
|
|
realm string
|
|
password string // what the admin's login accepts
|
|
down bool // answer 503, as a starting server does
|
|
logins int
|
|
clients map[string]Rep
|
|
calls []string
|
|
}
|
|
|
|
func newFakeKeycloak(t *testing.T, realm, password string) *fakeKeycloak {
|
|
f := &fakeKeycloak{realm: realm, password: password, clients: map[string]Rep{}}
|
|
f.srv = httptest.NewServer(http.HandlerFunc(f.serve))
|
|
t.Cleanup(f.srv.Close)
|
|
return f
|
|
}
|
|
|
|
func uuid() string {
|
|
b := make([]byte, 16)
|
|
_, _ = rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func send(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if v != nil {
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
}
|
|
|
|
func (f *fakeKeycloak) set(fn func()) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
fn()
|
|
}
|
|
|
|
func (f *fakeKeycloak) serve(w http.ResponseWriter, r *http.Request) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, r.Method+" "+r.URL.Path)
|
|
if f.down {
|
|
send(w, 503, nil)
|
|
return
|
|
}
|
|
if r.URL.Path == "/realms/master/protocol/openid-connect/token" {
|
|
_ = r.ParseForm()
|
|
f.logins++
|
|
if r.PostForm.Get("password") != f.password {
|
|
send(w, 401, map[string]string{"error": "invalid_grant", "error_description": "Invalid user credentials"})
|
|
return
|
|
}
|
|
send(w, 200, map[string]any{"access_token": "t", "expires_in": 300})
|
|
return
|
|
}
|
|
base := "/admin/realms/" + f.realm + "/clients"
|
|
if !strings.HasPrefix(r.URL.Path, base) {
|
|
send(w, 404, map[string]string{"error": "Realm not found."})
|
|
return
|
|
}
|
|
var rest []string
|
|
for _, p := range strings.Split(strings.TrimPrefix(r.URL.Path, base), "/") {
|
|
if p != "" {
|
|
rest = append(rest, p)
|
|
}
|
|
}
|
|
body := func() Rep {
|
|
raw, _ := io.ReadAll(r.Body)
|
|
var v Rep
|
|
_ = json.Unmarshal(raw, &v)
|
|
return v
|
|
}
|
|
if len(rest) == 0 && r.Method == "GET" {
|
|
want := r.URL.Query().Get("clientId")
|
|
out := []Rep{}
|
|
for _, c := range f.clients {
|
|
if want == "" || c["clientId"] == want {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
send(w, 200, out)
|
|
return
|
|
}
|
|
if len(rest) == 0 && r.Method == "POST" {
|
|
rep := body()
|
|
for _, c := range f.clients {
|
|
if c["clientId"] == rep["clientId"] {
|
|
send(w, 409, map[string]string{"errorMessage": "exists"})
|
|
return
|
|
}
|
|
}
|
|
id := uuid()
|
|
mappers := []any{}
|
|
if list, ok := rep["protocolMappers"].([]any); ok {
|
|
for _, m := range list {
|
|
mm := m.(map[string]any)
|
|
mm["id"] = uuid()
|
|
mappers = append(mappers, mm)
|
|
}
|
|
}
|
|
rep["id"], rep["protocolMappers"] = id, mappers
|
|
f.clients[id] = rep
|
|
send(w, 201, nil)
|
|
return
|
|
}
|
|
c := f.clients[rest[0]]
|
|
if c == nil {
|
|
send(w, 404, map[string]string{"error": "Could not find client"})
|
|
return
|
|
}
|
|
switch {
|
|
case len(rest) == 1 && r.Method == "PUT":
|
|
// Keycloak ignores protocolMappers on a client update: they have their own endpoints.
|
|
rep := body()
|
|
rep["id"], rep["protocolMappers"] = c["id"], c["protocolMappers"]
|
|
f.clients[rest[0]] = rep
|
|
send(w, 204, nil)
|
|
case len(rest) == 1 && r.Method == "DELETE":
|
|
delete(f.clients, rest[0])
|
|
send(w, 204, nil)
|
|
case rest[1] == "client-secret" && r.Method == "GET":
|
|
send(w, 200, map[string]any{"type": "secret", "value": c["secret"]})
|
|
case rest[1] == "protocol-mappers" && r.Method == "GET":
|
|
send(w, 200, c["protocolMappers"])
|
|
case rest[1] == "protocol-mappers" && r.Method == "POST":
|
|
m := body()
|
|
m["id"] = uuid()
|
|
list, _ := c["protocolMappers"].([]any)
|
|
c["protocolMappers"] = append(list, m)
|
|
send(w, 201, nil)
|
|
case rest[1] == "protocol-mappers" && r.Method == "PUT":
|
|
m := body()
|
|
list, _ := c["protocolMappers"].([]any)
|
|
for i, x := range list {
|
|
if x.(map[string]any)["id"] == rest[4] {
|
|
list[i] = m
|
|
}
|
|
}
|
|
send(w, 204, nil)
|
|
default:
|
|
send(w, 405, nil)
|
|
}
|
|
}
|
|
|
|
func (f *fakeKeycloak) client(password string) *Client {
|
|
return NewClient(f.srv.URL, "admin", func() (string, error) { return password, nil }, "master")
|
|
}
|