Skip to content

Commit 009300b

Browse files
authored
Merge pull request #41 from Everything-is-Code/fix/visualize-auth-inference
fix(visualize): infer auth type from YAML and legacy proxy fields
2 parents bdae2a0 + 3ab1963 commit 009300b

5 files changed

Lines changed: 389 additions & 7 deletions

File tree

internal/visualize/auth.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package visualize
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"strings"
7+
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
func resolveProductAuthType(product *Product, yamlPath string) {
12+
if product == nil {
13+
return
14+
}
15+
16+
if fromYAML, err := readAuthTypeFromYAML(yamlPath); err == nil && fromYAML != "" {
17+
product.AuthType = fromYAML
18+
return
19+
}
20+
21+
authType := normalizeAuthType(product.AuthType)
22+
if authType == "" && product.OIDC != nil && strings.TrimSpace(product.OIDC.IssuerEndpoint) != "" {
23+
authType = "oidc"
24+
}
25+
product.AuthType = authType
26+
}
27+
28+
func readAuthTypeFromYAML(path string) (string, error) {
29+
data, err := os.ReadFile(path)
30+
if err != nil {
31+
return "", err
32+
}
33+
spec, ok := productSpecFromYAML(data)
34+
if !ok {
35+
return "", nil
36+
}
37+
return authTypeFromProductSpec(spec), nil
38+
}
39+
40+
func productSpecFromYAML(data []byte) (map[string]any, bool) {
41+
var root map[string]any
42+
if err := yaml.Unmarshal(data, &root); err != nil {
43+
return nil, false
44+
}
45+
46+
kind, _ := root["kind"].(string)
47+
switch kind {
48+
case "Product":
49+
spec, _ := root["spec"].(map[string]any)
50+
return spec, spec != nil
51+
case "List":
52+
items, _ := root["items"].([]any)
53+
for _, item := range items {
54+
entry, _ := item.(map[string]any)
55+
if entry == nil {
56+
continue
57+
}
58+
if entryKind, _ := entry["kind"].(string); entryKind != "Product" {
59+
continue
60+
}
61+
spec, _ := entry["spec"].(map[string]any)
62+
if spec != nil {
63+
return spec, true
64+
}
65+
}
66+
}
67+
return nil, false
68+
}
69+
70+
func authTypeFromProductSpec(spec map[string]any) string {
71+
deployment, _ := spec["deployment"].(map[string]any)
72+
if deployment == nil {
73+
return ""
74+
}
75+
apicast, _ := deployment["apicastHosted"].(map[string]any)
76+
if apicast == nil {
77+
return ""
78+
}
79+
authentication, _ := apicast["authentication"].(map[string]any)
80+
if authentication == nil {
81+
return ""
82+
}
83+
if _, ok := authentication["oidc"]; ok {
84+
return "oidc"
85+
}
86+
if _, ok := authentication["userkey"]; ok {
87+
return "api_key"
88+
}
89+
if _, ok := authentication["userKey"]; ok {
90+
return "api_key"
91+
}
92+
if _, ok := authentication["appKeyAppID"]; ok {
93+
return "app_id_and_app_key"
94+
}
95+
return ""
96+
}
97+
98+
func inferAuthTypeFromProxy(authType, userKey, appID, appKey, oidcIssuerType, oidcIssuerEndpoint string, policiesConfig json.RawMessage) string {
99+
if t := normalizeAuthType(authType); t != "" {
100+
return t
101+
}
102+
if strings.TrimSpace(oidcIssuerEndpoint) != "" {
103+
return "oidc"
104+
}
105+
if fromPolicy := authTypeFromPoliciesConfig(policiesConfig); fromPolicy != "" {
106+
return fromPolicy
107+
}
108+
109+
userEnabled := proxyAuthModeEnabled(userKey)
110+
appIDEnabled := proxyAuthModeEnabled(appID)
111+
appKeyEnabled := proxyAuthModeEnabled(appKey)
112+
113+
if strings.EqualFold(strings.TrimSpace(userKey), "true") && strings.EqualFold(strings.TrimSpace(appID), "false") {
114+
return "api_key"
115+
}
116+
if strings.EqualFold(strings.TrimSpace(appID), "true") && strings.EqualFold(strings.TrimSpace(userKey), "false") {
117+
return "app_id_and_app_key"
118+
}
119+
if userEnabled && !appIDEnabled && !appKeyEnabled {
120+
return "api_key"
121+
}
122+
if appIDEnabled && appKeyEnabled {
123+
return "app_id_and_app_key"
124+
}
125+
if userEnabled {
126+
return "api_key"
127+
}
128+
return ""
129+
}
130+
131+
func authTypeFromPoliciesConfig(raw json.RawMessage) string {
132+
if len(raw) == 0 {
133+
return ""
134+
}
135+
var policies []struct {
136+
Name string `json:"name"`
137+
Enabled bool `json:"enabled"`
138+
Configuration struct {
139+
AuthType string `json:"auth_type"`
140+
} `json:"configuration"`
141+
}
142+
if err := json.Unmarshal(raw, &policies); err != nil {
143+
return ""
144+
}
145+
for _, policy := range policies {
146+
if policy.Name != "default_credentials" {
147+
continue
148+
}
149+
if t := normalizeAuthType(policy.Configuration.AuthType); t != "" {
150+
if policy.Enabled {
151+
return t
152+
}
153+
}
154+
}
155+
for _, policy := range policies {
156+
if policy.Name != "default_credentials" {
157+
continue
158+
}
159+
if t := normalizeAuthType(policy.Configuration.AuthType); t != "" {
160+
return t
161+
}
162+
}
163+
return ""
164+
}
165+
166+
func proxyAuthModeEnabled(value string) bool {
167+
switch strings.ToLower(strings.TrimSpace(value)) {
168+
case "", "false":
169+
return false
170+
default:
171+
return true
172+
}
173+
}
174+
175+
func normalizeAuthType(raw string) string {
176+
switch strings.ToLower(strings.TrimSpace(raw)) {
177+
case "":
178+
return ""
179+
case "api_key", "user_key", "apikey", "userkey":
180+
return "api_key"
181+
case "app_key", "app_id", "app_id_and_app_key", "app_key_and_app_id":
182+
return "app_id_and_app_key"
183+
case "oidc", "openid_connect":
184+
return "oidc"
185+
default:
186+
return strings.ToLower(strings.TrimSpace(raw))
187+
}
188+
}

internal/visualize/auth_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package visualize
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestInferAuthTypeFromProxyLegacyFields(t *testing.T) {
11+
cases := []struct {
12+
name string
13+
userKey string
14+
appID string
15+
appKey string
16+
oidcURL string
17+
want string
18+
}{
19+
{name: "api key booleans", userKey: "true", appID: "false", want: "api_key"},
20+
{name: "app id booleans", userKey: "false", appID: "true", appKey: "app_key", want: "app_id_and_app_key"},
21+
{name: "copec param names", userKey: "user_key", appID: "app_id", appKey: "app_key", want: "app_id_and_app_key"},
22+
{name: "oidc issuer", oidcURL: "https://sso.example.com/realm/demo", want: "oidc"},
23+
{name: "explicit auth type", userKey: "true", appID: "false", want: "api_key"},
24+
}
25+
for _, tc := range cases {
26+
t.Run(tc.name, func(t *testing.T) {
27+
got := inferAuthTypeFromProxy("", tc.userKey, tc.appID, tc.appKey, "keycloak", tc.oidcURL, nil)
28+
if got != tc.want {
29+
t.Fatalf("inferAuthTypeFromProxy() = %q, want %q", got, tc.want)
30+
}
31+
})
32+
}
33+
}
34+
35+
func TestInferAuthTypeFromProxyDefaultCredentialsPolicy(t *testing.T) {
36+
raw := []byte(`[
37+
{"name":"apicast","enabled":true,"configuration":{}},
38+
{"name":"default_credentials","enabled":true,"configuration":{"auth_type":"user_key"}}
39+
]`)
40+
got := inferAuthTypeFromProxy("", "app_id", "app_key", "user_key", "", "", raw)
41+
if got != "api_key" {
42+
t.Fatalf("got %q, want api_key", got)
43+
}
44+
}
45+
46+
func TestReadAuthTypeFromYAMLListProduct(t *testing.T) {
47+
dir := t.TempDir()
48+
path := filepath.Join(dir, "B2B-IS.yaml")
49+
content := `apiVersion: v1
50+
kind: List
51+
items:
52+
- apiVersion: capabilities.3scale.net/v1beta1
53+
kind: Product
54+
spec:
55+
deployment:
56+
apicastHosted:
57+
authentication:
58+
appKeyAppID:
59+
appID: app_id
60+
appKey: app_key
61+
`
62+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
63+
t.Fatal(err)
64+
}
65+
got, err := readAuthTypeFromYAML(path)
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
if got != "app_id_and_app_key" {
70+
t.Fatalf("got %q, want app_id_and_app_key", got)
71+
}
72+
}
73+
74+
func TestReadAuthTypeFromYAMLUserKey(t *testing.T) {
75+
dir := t.TempDir()
76+
path := filepath.Join(dir, "Comisiones.yaml")
77+
content := `apiVersion: v1
78+
kind: List
79+
items:
80+
- kind: Product
81+
spec:
82+
deployment:
83+
apicastHosted:
84+
authentication:
85+
userkey:
86+
authUserKey: user_key
87+
`
88+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
89+
t.Fatal(err)
90+
}
91+
got, err := readAuthTypeFromYAML(path)
92+
if err != nil {
93+
t.Fatal(err)
94+
}
95+
if got != "api_key" {
96+
t.Fatalf("got %q, want api_key", got)
97+
}
98+
}
99+
100+
func TestLoadExportCopecStyleProxy(t *testing.T) {
101+
dir := t.TempDir()
102+
writeMinimalManifest(t, dir, false)
103+
writeJSON(t, filepath.Join(dir, "backends", "billing.json"), map[string]any{
104+
"id": 1, "system_name": "billing", "name": "billing",
105+
})
106+
productDir := filepath.Join(dir, "products", "demo")
107+
if err := os.MkdirAll(productDir, 0o755); err != nil {
108+
t.Fatal(err)
109+
}
110+
writeJSON(t, filepath.Join(productDir, "proxy.json"), map[string]any{
111+
"proxy": map[string]any{
112+
"service_id": 10,
113+
"auth_app_id": "app_id",
114+
"auth_app_key": "app_key",
115+
"auth_user_key": "user_key",
116+
"endpoint": "https://demo.example.com",
117+
},
118+
})
119+
yaml := `apiVersion: v1
120+
kind: List
121+
items:
122+
- kind: Product
123+
spec:
124+
name: Demo Product
125+
systemName: demo
126+
deployment:
127+
apicastHosted:
128+
authentication:
129+
userkey:
130+
authUserKey: user_key
131+
`
132+
if err := os.WriteFile(filepath.Join(dir, "products", "demo.yaml"), []byte(yaml), 0o644); err != nil {
133+
t.Fatal(err)
134+
}
135+
136+
tenant, err := LoadExport(dir)
137+
if err != nil {
138+
t.Fatal(err)
139+
}
140+
product := findProduct(t, tenant, "demo")
141+
if product.AuthType != "api_key" {
142+
t.Fatalf("AuthType = %q, want api_key", product.AuthType)
143+
}
144+
}
145+
146+
func writeMinimalManifest(t *testing.T, dir string, includeApps bool) {
147+
t.Helper()
148+
if err := os.MkdirAll(filepath.Join(dir, "backends"), 0o755); err != nil {
149+
t.Fatal(err)
150+
}
151+
if err := os.MkdirAll(filepath.Join(dir, "products"), 0o755); err != nil {
152+
t.Fatal(err)
153+
}
154+
manifest := map[string]any{
155+
"schema_version": "1.0",
156+
"exported_at": "2026-06-05T12:00:00Z",
157+
"admin_url": "https://tenant-admin.example.com",
158+
"product_count": 1,
159+
"backend_count": 1,
160+
"include_applications": includeApps,
161+
"incomplete": false,
162+
}
163+
writeJSON(t, filepath.Join(dir, "manifest.json"), manifest)
164+
}
165+
166+
func writeJSON(t *testing.T, path string, payload any) {
167+
t.Helper()
168+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
169+
t.Fatal(err)
170+
}
171+
data, err := json.Marshal(payload)
172+
if err != nil {
173+
t.Fatal(err)
174+
}
175+
if err := os.WriteFile(path, data, 0o644); err != nil {
176+
t.Fatal(err)
177+
}
178+
}

0 commit comments

Comments
 (0)