Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions internal/visualize/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package visualize

import (
"encoding/json"
"os"
"strings"

"gopkg.in/yaml.v3"
)

func resolveProductAuthType(product *Product, yamlPath string) {
if product == nil {
return
}

if fromYAML, err := readAuthTypeFromYAML(yamlPath); err == nil && fromYAML != "" {
product.AuthType = fromYAML
return
}

authType := normalizeAuthType(product.AuthType)
if authType == "" && product.OIDC != nil && strings.TrimSpace(product.OIDC.IssuerEndpoint) != "" {
authType = "oidc"
}
product.AuthType = authType
}

func readAuthTypeFromYAML(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
spec, ok := productSpecFromYAML(data)
if !ok {
return "", nil
}
return authTypeFromProductSpec(spec), nil
}

func productSpecFromYAML(data []byte) (map[string]any, bool) {
var root map[string]any
if err := yaml.Unmarshal(data, &root); err != nil {
return nil, false
}

kind, _ := root["kind"].(string)
switch kind {
case "Product":
spec, _ := root["spec"].(map[string]any)
return spec, spec != nil
case "List":
items, _ := root["items"].([]any)
for _, item := range items {
entry, _ := item.(map[string]any)
if entry == nil {
continue
}
if entryKind, _ := entry["kind"].(string); entryKind != "Product" {
continue
}
spec, _ := entry["spec"].(map[string]any)
if spec != nil {
return spec, true
}
}
}
return nil, false
}

func authTypeFromProductSpec(spec map[string]any) string {
deployment, _ := spec["deployment"].(map[string]any)
if deployment == nil {
return ""
}
apicast, _ := deployment["apicastHosted"].(map[string]any)
if apicast == nil {
return ""
}
authentication, _ := apicast["authentication"].(map[string]any)
if authentication == nil {
return ""
}
if _, ok := authentication["oidc"]; ok {
return "oidc"
}
if _, ok := authentication["userkey"]; ok {
return "api_key"
}
if _, ok := authentication["userKey"]; ok {
return "api_key"
}
if _, ok := authentication["appKeyAppID"]; ok {
return "app_id_and_app_key"
}
return ""
}

func inferAuthTypeFromProxy(authType, userKey, appID, appKey, oidcIssuerType, oidcIssuerEndpoint string, policiesConfig json.RawMessage) string {
if t := normalizeAuthType(authType); t != "" {
return t
}
if strings.TrimSpace(oidcIssuerEndpoint) != "" {
return "oidc"
}
if fromPolicy := authTypeFromPoliciesConfig(policiesConfig); fromPolicy != "" {
return fromPolicy
}

userEnabled := proxyAuthModeEnabled(userKey)
appIDEnabled := proxyAuthModeEnabled(appID)
appKeyEnabled := proxyAuthModeEnabled(appKey)

if strings.EqualFold(strings.TrimSpace(userKey), "true") && strings.EqualFold(strings.TrimSpace(appID), "false") {
return "api_key"
}
if strings.EqualFold(strings.TrimSpace(appID), "true") && strings.EqualFold(strings.TrimSpace(userKey), "false") {
return "app_id_and_app_key"
}
if userEnabled && !appIDEnabled && !appKeyEnabled {
return "api_key"
}
if appIDEnabled && appKeyEnabled {
return "app_id_and_app_key"
}
if userEnabled {
return "api_key"
}
return ""
}

func authTypeFromPoliciesConfig(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var policies []struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Configuration struct {
AuthType string `json:"auth_type"`
} `json:"configuration"`
}
if err := json.Unmarshal(raw, &policies); err != nil {
return ""
}
for _, policy := range policies {
if policy.Name != "default_credentials" {
continue
}
if t := normalizeAuthType(policy.Configuration.AuthType); t != "" {
if policy.Enabled {
return t
}
}
}
for _, policy := range policies {
if policy.Name != "default_credentials" {
continue
}
if t := normalizeAuthType(policy.Configuration.AuthType); t != "" {
return t
}
}
return ""
}

func proxyAuthModeEnabled(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "false":
return false
default:
return true
}
}

func normalizeAuthType(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "":
return ""
case "api_key", "user_key", "apikey", "userkey":
return "api_key"
case "app_key", "app_id", "app_id_and_app_key", "app_key_and_app_id":
return "app_id_and_app_key"
case "oidc", "openid_connect":
return "oidc"
default:
return strings.ToLower(strings.TrimSpace(raw))
}
}
178 changes: 178 additions & 0 deletions internal/visualize/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package visualize

import (
"encoding/json"
"os"
"path/filepath"
"testing"
)

func TestInferAuthTypeFromProxyLegacyFields(t *testing.T) {
cases := []struct {
name string
userKey string
appID string
appKey string
oidcURL string
want string
}{
{name: "api key booleans", userKey: "true", appID: "false", want: "api_key"},
{name: "app id booleans", userKey: "false", appID: "true", appKey: "app_key", want: "app_id_and_app_key"},
{name: "copec param names", userKey: "user_key", appID: "app_id", appKey: "app_key", want: "app_id_and_app_key"},
{name: "oidc issuer", oidcURL: "https://sso.example.com/realm/demo", want: "oidc"},
{name: "explicit auth type", userKey: "true", appID: "false", want: "api_key"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := inferAuthTypeFromProxy("", tc.userKey, tc.appID, tc.appKey, "keycloak", tc.oidcURL, nil)
if got != tc.want {
t.Fatalf("inferAuthTypeFromProxy() = %q, want %q", got, tc.want)
}
})
}
}

func TestInferAuthTypeFromProxyDefaultCredentialsPolicy(t *testing.T) {
raw := []byte(`[
{"name":"apicast","enabled":true,"configuration":{}},
{"name":"default_credentials","enabled":true,"configuration":{"auth_type":"user_key"}}
]`)
got := inferAuthTypeFromProxy("", "app_id", "app_key", "user_key", "", "", raw)
if got != "api_key" {
t.Fatalf("got %q, want api_key", got)
}
}

func TestReadAuthTypeFromYAMLListProduct(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "B2B-IS.yaml")
content := `apiVersion: v1
kind: List
items:
- apiVersion: capabilities.3scale.net/v1beta1
kind: Product
spec:
deployment:
apicastHosted:
authentication:
appKeyAppID:
appID: app_id
appKey: app_key
`
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
got, err := readAuthTypeFromYAML(path)
if err != nil {
t.Fatal(err)
}
if got != "app_id_and_app_key" {
t.Fatalf("got %q, want app_id_and_app_key", got)
}
}

func TestReadAuthTypeFromYAMLUserKey(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "Comisiones.yaml")
content := `apiVersion: v1
kind: List
items:
- kind: Product
spec:
deployment:
apicastHosted:
authentication:
userkey:
authUserKey: user_key
`
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
got, err := readAuthTypeFromYAML(path)
if err != nil {
t.Fatal(err)
}
if got != "api_key" {
t.Fatalf("got %q, want api_key", got)
}
}

func TestLoadExportCopecStyleProxy(t *testing.T) {
dir := t.TempDir()
writeMinimalManifest(t, dir, false)
writeJSON(t, filepath.Join(dir, "backends", "billing.json"), map[string]any{
"id": 1, "system_name": "billing", "name": "billing",
})
productDir := filepath.Join(dir, "products", "demo")
if err := os.MkdirAll(productDir, 0o755); err != nil {
t.Fatal(err)
}
writeJSON(t, filepath.Join(productDir, "proxy.json"), map[string]any{
"proxy": map[string]any{
"service_id": 10,
"auth_app_id": "app_id",
"auth_app_key": "app_key",
"auth_user_key": "user_key",
"endpoint": "https://demo.example.com",
},
})
yaml := `apiVersion: v1
kind: List
items:
- kind: Product
spec:
name: Demo Product
systemName: demo
deployment:
apicastHosted:
authentication:
userkey:
authUserKey: user_key
`
if err := os.WriteFile(filepath.Join(dir, "products", "demo.yaml"), []byte(yaml), 0o644); err != nil {
t.Fatal(err)
}

tenant, err := LoadExport(dir)
if err != nil {
t.Fatal(err)
}
product := findProduct(t, tenant, "demo")
if product.AuthType != "api_key" {
t.Fatalf("AuthType = %q, want api_key", product.AuthType)
}
}

func writeMinimalManifest(t *testing.T, dir string, includeApps bool) {
t.Helper()
if err := os.MkdirAll(filepath.Join(dir, "backends"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(dir, "products"), 0o755); err != nil {
t.Fatal(err)
}
manifest := map[string]any{
"schema_version": "1.0",
"exported_at": "2026-06-05T12:00:00Z",
"admin_url": "https://tenant-admin.example.com",
"product_count": 1,
"backend_count": 1,
"include_applications": includeApps,
"incomplete": false,
}
writeJSON(t, filepath.Join(dir, "manifest.json"), manifest)
}

func writeJSON(t *testing.T, path string, payload any) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
data, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatal(err)
}
}
Loading
Loading