From d353fc53be8f2286a50a101034173d0676a356a3 Mon Sep 17 00:00:00 2001 From: James Rasell Date: Wed, 13 Aug 2025 15:19:50 +0200 Subject: [PATCH] acl/identity: Ensure client intro token create can decode TTL. (#26512) --- nomad/structs/acl.go | 47 +++++++++++++++++++++++++++++ nomad/structs/acl_test.go | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/nomad/structs/acl.go b/nomad/structs/acl.go index f62e7e116..f332ea9a7 100644 --- a/nomad/structs/acl.go +++ b/nomad/structs/acl.go @@ -2045,6 +2045,53 @@ func (a *ACLCreateClientIntroductionTokenRequest) IdentityTTL( return a.TTL } +// MarshalJSON implements the json.Marshaler interface and allows +// ACLCreateClientIntroductionTokenRequest.TTL to be marshaled correctly. +func (a *ACLCreateClientIntroductionTokenRequest) MarshalJSON() ([]byte, error) { + type Alias ACLCreateClientIntroductionTokenRequest + exported := &struct { + TTL string + *Alias + }{ + TTL: a.TTL.String(), + Alias: (*Alias)(a), + } + if a.TTL == 0 { + exported.TTL = "" + } + return json.Marshal(exported) +} + +// UnmarshalJSON implements the json.Unmarshaler interface and allows +// ACLCreateClientIntroductionTokenRequest.TTL to be unmarshalled correctly. +func (a *ACLCreateClientIntroductionTokenRequest) UnmarshalJSON(data []byte) (err error) { + type Alias ACLCreateClientIntroductionTokenRequest + aux := &struct { + TTL interface{} + *Alias + }{ + Alias: (*Alias)(a), + } + if err = json.Unmarshal(data, &aux); err != nil { + return err + } + if aux.TTL != nil { + switch v := aux.TTL.(type) { + case string: + if v != "" { + if a.TTL, err = time.ParseDuration(v); err != nil { + return err + } + } + case float64: + a.TTL = time.Duration(v) + default: + return fmt.Errorf("unexpected TTL type: %v", v) + } + } + return nil +} + // ACLCreateClientIntroductionTokenResponse is the response object used within the ACL // client introduction RPC handler. type ACLCreateClientIntroductionTokenResponse struct { diff --git a/nomad/structs/acl_test.go b/nomad/structs/acl_test.go index b8ec5ca21..4a424ebdf 100644 --- a/nomad/structs/acl_test.go +++ b/nomad/structs/acl_test.go @@ -4,6 +4,7 @@ package structs import ( + "encoding/json" "errors" "fmt" "testing" @@ -2241,3 +2242,65 @@ func TestACLClientIntroductionTokenRequest_IdentityTTL(t *testing.T) { }) } } + +func TestACLCreateClientIntroductionTokenRequest_MarshalJSON(t *testing.T) { + ci.Parallel(t) + + inputACLCreateClientIntroductionTokenRequest := ACLCreateClientIntroductionTokenRequest{ + NodeName: "test-node", + NodePool: "test-node-pool", + TTL: 10 * time.Minute, + } + + data, err := json.Marshal(&inputACLCreateClientIntroductionTokenRequest) + must.NoError(t, err) + must.SliceNotEmpty(t, data) +} + +func TestACLCreateClientIntroductionTokenRequest_UnmarshalJSON(t *testing.T) { + ci.Parallel(t) + + t.Run("valid ttl", func(t *testing.T) { + input := []byte(`{"NodeName":"test-node","NodePool":"test-node-pool","TTL":"10m"}`) + + var output ACLCreateClientIntroductionTokenRequest + + must.NoError(t, json.Unmarshal(input, &output)) + + must.Eq( + t, + ACLCreateClientIntroductionTokenRequest{ + NodeName: "test-node", + NodePool: "test-node-pool", + TTL: 10 * time.Minute, + }, + output, + ) + }) + + t.Run("invalid ttl", func(t *testing.T) { + input := []byte(`{"NodeName":"test-node","NodePool":"test-node-pool","TTL":["10m"]}`) + + var output ACLCreateClientIntroductionTokenRequest + + must.ErrorContains(t, json.Unmarshal(input, &output), "unexpected TTL type") + }) + + t.Run("empty ttl", func(t *testing.T) { + input := []byte(`{"NodeName":"test-node","NodePool":"test-node-pool","TTL":""}`) + + var output ACLCreateClientIntroductionTokenRequest + + must.NoError(t, json.Unmarshal(input, &output)) + + must.Eq( + t, + ACLCreateClientIntroductionTokenRequest{ + NodeName: "test-node", + NodePool: "test-node-pool", + TTL: 0, + }, + output, + ) + }) +}