mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-08-31 00:47:15 +02:00
fix(auth): repair the inert auth configuration schema
The schema string was the inside of a "properties" block: jwts, oidc, ldap and
required sat at the root with no enclosing "type"/"properties". jsonschema
compiled it as a document with only unknown keywords, so every auth section
validated and no option was ever checked.
Wrapped it correctly and dropped the root-level required: ["jwts"]. Init logs
and continues when the jwts section is absent ("Missing JWT configuration: No
JWT token support!"), so enforcing it would abort OIDC- or LDAP-only
deployments that work today. The per-subsection required lists are unchanged.
Validation errors are now printed with err.Error() instead of %#v, which
rendered a jsonschema.ValidationError unreadably.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-4
@@ -5,8 +5,13 @@
|
|||||||
|
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
|
// configSchema describes the "auth" section of config.json. Every subsection is
|
||||||
|
// optional: Init() logs and continues when "jwts", "oidc" or "ldap" is absent,
|
||||||
|
// so only the fields within a configured subsection are required.
|
||||||
var configSchema = `
|
var configSchema = `
|
||||||
{
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
"jwts": {
|
"jwts": {
|
||||||
"description": "For JWT token authentication.",
|
"description": "For JWT token authentication.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -55,6 +60,7 @@ var configSchema = `
|
|||||||
"required": ["max-age"]
|
"required": ["max-age"]
|
||||||
},
|
},
|
||||||
"oidc": {
|
"oidc": {
|
||||||
|
"description": "For OpenID Connect authentication.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"provider": {
|
"provider": {
|
||||||
@@ -148,6 +154,6 @@ var configSchema = `
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["url", "user-base", "search-dn", "user-bind", "user-filter"]
|
"required": ["url", "user-base", "search-dn", "user-bind", "user-filter"]
|
||||||
},
|
}
|
||||||
"required": ["jwts"]
|
}
|
||||||
}`
|
}`
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
||||||
|
// All rights reserved. This file is part of cc-backend.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// compileConfigSchema guards against the schema silently degrading into a
|
||||||
|
// document with no constraints (which is what happens when the "type"/
|
||||||
|
// "properties" wrapper is missing: every instance validates).
|
||||||
|
func compileConfigSchema(t *testing.T) *jsonschema.Schema {
|
||||||
|
t.Helper()
|
||||||
|
sch, err := jsonschema.CompileString("schema.json", configSchema)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compiling auth config schema: %v", err)
|
||||||
|
}
|
||||||
|
return sch
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAuthConfig(t *testing.T, sch *jsonschema.Schema, raw string) error {
|
||||||
|
t.Helper()
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
||||||
|
t.Fatalf("invalid test json: %v", err)
|
||||||
|
}
|
||||||
|
return sch.Validate(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSchemaAccepts(t *testing.T) {
|
||||||
|
sch := compileConfigSchema(t)
|
||||||
|
|
||||||
|
tests := map[string]string{
|
||||||
|
"jwts only": `{"jwts":{"max-age":"2000h"}}`,
|
||||||
|
"oidc only": `{"oidc":{"provider":"http://localhost:8080/realms/cc"}}`,
|
||||||
|
"empty": `{}`,
|
||||||
|
"ldap": `{"ldap":{"url":"ldap://127.0.0.1:389","user-base":"ou=users,dc=example,dc=com","search-dn":"cn=admin,dc=example,dc=com","user-bind":"uid={username},ou=users,dc=example,dc=com","user-filter":"(objectclass=posixAccount)","role-filters":{"admin":"(memberOf=cn=cc-admin,ou=groups,dc=example,dc=com)"}}}`,
|
||||||
|
"role-mapping": `{"oidc":{"provider":"http://p","role-mapping":{"cc-admin":"admin"}}}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, raw := range tests {
|
||||||
|
if err := validateAuthConfig(t, sch, raw); err != nil {
|
||||||
|
t.Errorf("%s: unexpected validation error: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSchemaRejects(t *testing.T) {
|
||||||
|
sch := compileConfigSchema(t)
|
||||||
|
|
||||||
|
tests := map[string]string{
|
||||||
|
"jwts without max-age": `{"jwts":{"public-key":"abc"}}`,
|
||||||
|
"jwts max-age not a string": `{"jwts":{"max-age":2000}}`,
|
||||||
|
"oidc without provider": `{"oidc":{"client-id":"cc-backend"}}`,
|
||||||
|
"ldap without user-bind": `{"ldap":{"url":"ldap://127.0.0.1:389","user-base":"ou=users","search-dn":"cn=admin","user-filter":"(objectclass=posixAccount)"}}`,
|
||||||
|
"role-filters not strings": `{"ldap":{"url":"u","user-base":"b","search-dn":"d","user-bind":"ub","user-filter":"f","role-filters":{"admin":true}}}`,
|
||||||
|
"jwts not an object": `{"jwts":"2000h"}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, raw := range tests {
|
||||||
|
if err := validateAuthConfig(t, sch, raw); err == nil {
|
||||||
|
t.Errorf("%s: expected validation error, got none", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigSchemaExampleConfigs validates the auth sections of the shipped
|
||||||
|
// example configurations.
|
||||||
|
func TestConfigSchemaExampleConfigs(t *testing.T) {
|
||||||
|
sch := compileConfigSchema(t)
|
||||||
|
|
||||||
|
for _, path := range []string{
|
||||||
|
"../../configs/config.json",
|
||||||
|
"../../configs/config-demo.json",
|
||||||
|
"../../configs/config-large.json",
|
||||||
|
} {
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var sections map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, §ions); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
authCfg, ok := sections["auth"]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(authCfg, &v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := sch.Validate(v); err != nil {
|
||||||
|
t.Errorf("%s: auth section does not validate: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,6 @@ func Validate(schema string, instance json.RawMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = sch.Validate(v); err != nil {
|
if err = sch.Validate(v); err != nil {
|
||||||
cclog.Fatalf("%#v", err)
|
cclog.Fatalf("config validation failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user