mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-20 22:44:25 +00:00
# Which Problems Are Solved Since #10666 (v4.1+, backported to v4.x), metadata values set through actions v1 (`api.metadata.push` and `api.v1.user.appendMetadata`) are always JSON-encoded via `json.Marshal`. This made the write path consistent with the JSON-based read path, but removed the ability to store raw (unencoded) metadata values: - A scalar string is now always stored quoted (`"de"` instead of `de`). - The previous byte-array convention (mapping a string to an integer array in the script, handled by `mapBytesToByteArray` introduced in #5526) now stores the literal integer-array text (e.g. `[100,101]`) instead of the raw bytes. Customers migrating from v3.x whose downstream systems base64-decode metadata values from tokens and expect raw bytes have no way to produce them anymore — changing the consuming system is not always possible. Reverting the default is not an option either, as clients that adopted actions v1 on v4.x now rely on the JSON encoding. # How the Problems Are Solved - Adds a new, opt-in function `api.v1.user.appendMetadataRaw(key, value)` to the actions v1 login flows (external / internal authentication post authentication and pre creation), next to the existing `appendMetadata`. - The value is stored as raw bytes without JSON encoding: - a string is stored as its plain UTF-8 bytes (`de`, not `"de"`) - byte arrays (`Uint8Array` or a plain array of integers 0-255, the old convention) are stored as-is, so existing v3 scripts using a string-to-byte-array helper only need to switch the function name - other types (and empty values) throw an error - The existing `appendMetadata` and `api.metadata.push` behavior remains byte-for-byte unchanged. # Additional Changes - Documented `appendMetadataRaw` (and the JSON encoding behavior of `appendMetadata`) in the external and internal authentication actions docs. - Added unit tests for the new function (through a real goja runtime) and a test locking in the existing `appendMetadata` JSON-encoding behavior. # Additional Context - Regression introduced as a side effect of #10666 (which fixed #10470); the raw byte handling was originally introduced in #5526. - Reported by a customer upgrading from v3.4.x to v4.16.x, whose PostAuthentication action maps token payload claims into user metadata. - Requires backport to v4.x. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <[email protected]>
249 lines
5.4 KiB
Go
249 lines
5.4 KiB
Go
package object
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
)
|
|
|
|
func TestMetadataListToDomain(t *testing.T) {
|
|
type args struct {
|
|
metadataList *MetadataList
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want []*domain.Metadata
|
|
}{
|
|
{
|
|
name: "nil",
|
|
args: args{metadataList: nil},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "empty",
|
|
args: args{metadataList: &MetadataList{}},
|
|
want: []*domain.Metadata{},
|
|
},
|
|
{
|
|
name: "from mapped value",
|
|
args: args{metadataList: &MetadataList{
|
|
metadata: []*Metadata{
|
|
{
|
|
Key: "key1",
|
|
value: []byte("value1"),
|
|
},
|
|
},
|
|
}},
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: []byte("value1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "from goja value string",
|
|
args: args{metadataList: &MetadataList{
|
|
metadata: []*Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: (&goja.Runtime{}).ToValue("value1"),
|
|
},
|
|
},
|
|
}},
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: []byte(`"value1"`),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "from goja value int",
|
|
args: args{metadataList: &MetadataList{
|
|
metadata: []*Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: (&goja.Runtime{}).ToValue(1),
|
|
},
|
|
},
|
|
}},
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: []byte("1"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "from goja value float",
|
|
args: args{metadataList: &MetadataList{
|
|
metadata: []*Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: (&goja.Runtime{}).ToValue(1.2),
|
|
},
|
|
},
|
|
}},
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: []byte("1.2"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "from goja value bool",
|
|
args: args{metadataList: &MetadataList{
|
|
metadata: []*Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: (&goja.Runtime{}).ToValue(true),
|
|
},
|
|
},
|
|
}},
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: []byte("true"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, MetadataListToDomain(tt.args.metadataList))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppendMetadataRawFunc(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
script string
|
|
want []*domain.Metadata
|
|
}{
|
|
{
|
|
name: "string is stored as raw bytes without quotes",
|
|
script: `appendMetadataRaw("locale", "de")`,
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "locale",
|
|
Value: []byte("de"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "json string is stored verbatim",
|
|
script: `appendMetadataRaw("tokenPayload", '{"acr":"phrh"}')`,
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "tokenPayload",
|
|
Value: []byte(`{"acr":"phrh"}`),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "uint8 array is stored as raw bytes",
|
|
script: `appendMetadataRaw("locale", new Uint8Array([100, 101]))`,
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "locale",
|
|
Value: []byte("de"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "integer array is stored as raw bytes",
|
|
script: `appendMetadataRaw("locale", [100, 101])`,
|
|
want: []*domain.Metadata{
|
|
{
|
|
Key: "locale",
|
|
Value: []byte("de"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
runtime := goja.New()
|
|
metadataList := &MetadataList{}
|
|
require.NoError(t, runtime.Set("appendMetadataRaw", metadataList.AppendMetadataRawFunc))
|
|
_, err := runtime.RunString(tt.script)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, MetadataListToDomain(metadataList))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppendMetadataRawFuncPanics(t *testing.T) {
|
|
runtime := goja.New()
|
|
tests := []struct {
|
|
name string
|
|
args []goja.Value
|
|
}{
|
|
{
|
|
name: "wrong argument count",
|
|
args: []goja.Value{runtime.ToValue("locale")},
|
|
},
|
|
{
|
|
name: "empty string value",
|
|
args: []goja.Value{runtime.ToValue("locale"), runtime.ToValue("")},
|
|
},
|
|
{
|
|
name: "empty array value",
|
|
args: []goja.Value{runtime.ToValue("locale"), runtime.ToValue([]interface{}{})},
|
|
},
|
|
{
|
|
name: "number value",
|
|
args: []goja.Value{runtime.ToValue("locale"), runtime.ToValue(1)},
|
|
},
|
|
{
|
|
name: "object value",
|
|
args: []goja.Value{runtime.ToValue("locale"), runtime.ToValue(map[string]interface{}{"locale": "de"})},
|
|
},
|
|
{
|
|
name: "array value out of byte range",
|
|
args: []goja.Value{runtime.ToValue("locale"), runtime.ToValue([]interface{}{int64(300)})},
|
|
},
|
|
{
|
|
name: "array value with non integer",
|
|
args: []goja.Value{runtime.ToValue("locale"), runtime.ToValue([]interface{}{1.5})},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
metadataList := &MetadataList{}
|
|
assert.Panics(t, func() {
|
|
metadataList.AppendMetadataRawFunc(goja.FunctionCall{Arguments: tt.args})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAppendMetadataFuncJSONEncodes locks in that the existing appendMetadata
|
|
// still JSON-encodes its value (a string is stored quoted), in contrast to appendMetadataRaw.
|
|
func TestAppendMetadataFuncJSONEncodes(t *testing.T) {
|
|
runtime := goja.New()
|
|
metadataList := &MetadataList{}
|
|
require.NoError(t, runtime.Set("appendMetadata", metadataList.AppendMetadataFunc))
|
|
require.NoError(t, runtime.Set("appendMetadataRaw", metadataList.AppendMetadataRawFunc))
|
|
_, err := runtime.RunString(`appendMetadata("locale", "de"); appendMetadataRaw("locale", "de")`)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []*domain.Metadata{
|
|
{
|
|
Key: "locale",
|
|
Value: []byte(`"de"`),
|
|
},
|
|
{
|
|
Key: "locale",
|
|
Value: []byte(`de`),
|
|
},
|
|
}, MetadataListToDomain(metadataList))
|
|
}
|