diff --git a/.changelog/9741.txt b/.changelog/9741.txt new file mode 100644 index 000000000..6871600bf --- /dev/null +++ b/.changelog/9741.txt @@ -0,0 +1,3 @@ +```release-note:improvement +acl: extend the auth-methods list endpoint to include MaxTokenTTL and TokenLocality fields. +``` diff --git a/agent/acl_endpoint_test.go b/agent/acl_endpoint_test.go index 1642e3332..3c67757a9 100644 --- a/agent/acl_endpoint_test.go +++ b/agent/acl_endpoint_test.go @@ -1201,6 +1201,8 @@ func TestACL_LoginProcedure_HTTP(t *testing.T) { Config: map[string]interface{}{ "SessionID": testSessionID, }, + TokenLocality: "global", + MaxTokenTTL: 500_000_000_000, } req, _ := http.NewRequest("PUT", "/v1/acl/auth-method?token=root", jsonBody(methodInput)) @@ -1284,6 +1286,7 @@ func TestACL_LoginProcedure_HTTP(t *testing.T) { resp := httptest.NewRecorder() raw, err := a.srv.ACLAuthMethodList(resp, req) require.NoError(t, err) + methods, ok := raw.(structs.ACLAuthMethodListStubs) require.True(t, ok) @@ -1297,6 +1300,8 @@ func TestACL_LoginProcedure_HTTP(t *testing.T) { require.Equal(t, expected.Name, actual.Name) require.Equal(t, expected.Type, actual.Type) require.Equal(t, expected.Description, actual.Description) + require.Equal(t, expected.MaxTokenTTL, actual.MaxTokenTTL) + require.Equal(t, expected.TokenLocality, actual.TokenLocality) require.Equal(t, expected.CreateIndex, actual.CreateIndex) require.Equal(t, expected.ModifyIndex, actual.ModifyIndex) found = true diff --git a/agent/structs/acl.go b/agent/structs/acl.go index d509abee6..64dc42821 100644 --- a/agent/structs/acl.go +++ b/agent/structs/acl.go @@ -1090,13 +1090,16 @@ func (rules ACLBindingRules) Sort() { }) } +// Note: this is a subset of ACLAuthMethod's fields type ACLAuthMethodListStub struct { - Name string - Type string - DisplayName string `json:",omitempty"` - Description string `json:",omitempty"` - CreateIndex uint64 - ModifyIndex uint64 + Name string + Type string + DisplayName string `json:",omitempty"` + Description string `json:",omitempty"` + MaxTokenTTL time.Duration `json:",omitempty"` + TokenLocality string `json:",omitempty"` + CreateIndex uint64 + ModifyIndex uint64 EnterpriseMeta } @@ -1106,12 +1109,34 @@ func (p *ACLAuthMethod) Stub() *ACLAuthMethodListStub { Type: p.Type, DisplayName: p.DisplayName, Description: p.Description, + MaxTokenTTL: p.MaxTokenTTL, + TokenLocality: p.TokenLocality, CreateIndex: p.CreateIndex, ModifyIndex: p.ModifyIndex, EnterpriseMeta: p.EnterpriseMeta, } } +// This is nearly identical to the ACLAuthMethod MarshalJSON +// Unmarshaling is not implemented because the API is read only +func (m *ACLAuthMethodListStub) MarshalJSON() ([]byte, error) { + type Alias ACLAuthMethodListStub + exported := &struct { + MaxTokenTTL string `json:",omitempty"` + *Alias + }{ + MaxTokenTTL: m.MaxTokenTTL.String(), + Alias: (*Alias)(m), + } + if m.MaxTokenTTL == 0 { + exported.MaxTokenTTL = "" + } + + data, err := json.Marshal(exported) + + return data, err +} + type ACLAuthMethods []*ACLAuthMethod type ACLAuthMethodListStubs []*ACLAuthMethodListStub diff --git a/api/acl.go b/api/acl.go index 7453feb8a..d8e0e04d9 100644 --- a/api/acl.go +++ b/api/acl.go @@ -270,16 +270,61 @@ type ACLAuthMethodNamespaceRule struct { type ACLAuthMethodListEntry struct { Name string Type string - DisplayName string `json:",omitempty"` - Description string `json:",omitempty"` - CreateIndex uint64 - ModifyIndex uint64 + DisplayName string `json:",omitempty"` + Description string `json:",omitempty"` + MaxTokenTTL time.Duration `json:",omitempty"` + + // TokenLocality defines the kind of token that this auth method produces. + // This can be either 'local' or 'global'. If empty 'local' is assumed. + TokenLocality string `json:",omitempty"` + CreateIndex uint64 + ModifyIndex uint64 // Namespace is the namespace the ACLAuthMethodListEntry is associated with. // Namespacing is a Consul Enterprise feature. Namespace string `json:",omitempty"` } +// This is nearly identical to the ACLAuthMethod MarshalJSON +func (m *ACLAuthMethodListEntry) MarshalJSON() ([]byte, error) { + type Alias ACLAuthMethodListEntry + exported := &struct { + MaxTokenTTL string `json:",omitempty"` + *Alias + }{ + MaxTokenTTL: m.MaxTokenTTL.String(), + Alias: (*Alias)(m), + } + if m.MaxTokenTTL == 0 { + exported.MaxTokenTTL = "" + } + + return json.Marshal(exported) +} + +// This is nearly identical to the ACLAuthMethod UnmarshalJSON +func (m *ACLAuthMethodListEntry) UnmarshalJSON(data []byte) error { + type Alias ACLAuthMethodListEntry + aux := &struct { + MaxTokenTTL string + *Alias + }{ + Alias: (*Alias)(m), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + var err error + if aux.MaxTokenTTL != "" { + if m.MaxTokenTTL, err = time.ParseDuration(aux.MaxTokenTTL); err != nil { + return err + } + } + + return nil +} + // ParseKubernetesAuthMethodConfig takes a raw config map and returns a parsed // KubernetesAuthMethodConfig. func ParseKubernetesAuthMethodConfig(raw map[string]interface{}) (*KubernetesAuthMethodConfig, error) { diff --git a/api/acl_test.go b/api/acl_test.go index 082608c87..956d60417 100644 --- a/api/acl_test.go +++ b/api/acl_test.go @@ -3,6 +3,7 @@ package api import ( "strings" "testing" + "time" "github.com/hashicorp/consul/sdk/testutil/retry" @@ -657,6 +658,101 @@ func TestAPI_ACLToken_Clone(t *testing.T) { require.Equal(t, cloned, read) } +// +func TestAPI_AuthMethod_List(t *testing.T) { + t.Parallel() + c, s := makeACLClient(t) + defer s.Stop() + + acl := c.ACL() + s.WaitForSerfCheck(t) + + method1 := ACLAuthMethod{ + Name: "test_1", + Type: "kubernetes", + Description: "test 1", + MaxTokenTTL: 260 * time.Second, + TokenLocality: "global", + Config: AuthMethodCreateKubernetesConfigHelper(), + } + + created1, wm, err := acl.AuthMethodCreate(&method1, nil) + + require.NoError(t, err) + require.NotNil(t, created1) + require.NotEqual(t, "", created1.Name) + require.NotEqual(t, 0, wm.RequestTime) + + method2 := ACLAuthMethod{ + Name: "test_2", + Type: "kubernetes", + Description: "test 2", + MaxTokenTTL: 0, + TokenLocality: "local", + Config: AuthMethodCreateKubernetesConfigHelper(), + } + + _, _, err = acl.AuthMethodCreate(&method2, nil) + require.NoError(t, err) + + entries, _, err := acl.AuthMethodList(nil) + require.NoError(t, err) + require.NotNil(t, entries) + require.Equal(t, 2, len(entries)) + + { + entry := entries[0] + require.Equal(t, "test_1", entry.Name) + require.Equal(t, 260*time.Second, entry.MaxTokenTTL) + require.Equal(t, "global", entry.TokenLocality) + } + { + entry := entries[1] + require.Equal(t, "test_2", entry.Name) + require.Equal(t, time.Duration(0), entry.MaxTokenTTL) + require.Equal(t, "local", entry.TokenLocality) + } +} + +func AuthMethodCreateKubernetesConfigHelper() (result map[string]interface{}) { + var pemData = ` +-----BEGIN CERTIFICATE----- +MIIE1DCCArwCCQC2kx7TchbxAzANBgkqhkiG9w0BAQsFADAsMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCV0ExEDAOBgNVBAcMB1NlYXR0bGUwHhcNMjEwMTI3MDIzNDA1 +WhcNMjIwMTI3MDIzNDA1WjAsMQswCQYDVQQGEwJVUzELMAkGA1UECAwCV0ExEDAO +BgNVBAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCt +j3zRFLg2A2DcZFwoc1HvIsGzqcfvxjee/OQjKyIuXbdpbJGIahB2piNYtd49zU/5 +ofRAuqIQOco3V9LfL52I7NchNBvPQOrXjbpcM3qF2qQvunVlnnaPCIf8S5hsFMaq +w2/+jnLjaUdXGJ9bold5E/bms87uRahvhUpY7MhkSDNsAen+YThpwucc9JFRmrz3 +EXGtTzcpyEn9b0s6ut9mum2UVqghAQyLeW8cNx1zeg6Bi5USjOKF6CQgF7o4kZ9X +D0Nk5vB9eePs/q5N9LHkDFKVCmzAYgzcQeGZFEzNcgK7N5y+aB2xXKpH3tydpwRd +uS+g05Jvk8M8P34wteUb8tq3jZuY7UYzlINMSrPuZdFhcGjmxPjC5hl1SZy4vF1s +GAD9RsleTZ8yeC6Cfo4mba214C9CqYkC2NBw2HO53pzO/tYI844QPhjmVBJ7bb35 +S052HD7m+AzbfY6w9CDH4D4mzIM4u1yRB6OlXdXTH58BhgxHdEnugLYr13QlVWRW +4nZgMFKiTY7cBscpPcVRsne/VR9VwSatp3adj+G8+WUtwQLJC2OcCFYvmHfdSOs0 +B15LH/tGeJcfKViKC9ifPq5abVZByr66jTQMAdBWet03OBnmLqJs9TI4wci0MkK/ +HlHYdy734rReD81LY9fCRCRFV4ZtMx2rfj7cqgKLlwIDAQABMA0GCSqGSIb3DQEB +CwUAA4ICAQB6ji6wA9ROFx8ZhLPlEnDiielSUN8LR2K8cmAjxxffJo3GxRH/zZYl +CM+DzU5VVzW6RGWuTNzcFNsxlaRx20sj5RyXLH90wFYLO2Rrs1XKWmqpfdN0Iiue +W7rYdNPV7YPjIVQVoijEt8kwx24jE9mU5ILXe4+WKPWavG+dHA1r8lQdg7wmE/8R +E/nSVtusuX0JRVdL96iy2HB37DYj+rJEE0C7fKAk51o0C4F6fOzUsWCaP/23pZNI +rA6hCq2CJeT4ObVukCIrnylrckZs8ElcZ7PvJ9bCNvma+dAxbL0uEkv0q0feLeVh +OTttNIVTUjYjr3KE6rtE1Rr35R/6HCK+zZDOkKf+TVEQsFuI4DRVEuntzjo9bgZf +fAL6G+UXpzW440BJzmzADnSthawMZFdqVrrBzpzb+B2d9VLDEoyCCFzaJyj/Gyff +kqxRFTHZJRKC/3iIRXOX64bIr1YmXHFHCBkcq7eyh1oeaTrGZ43HimaveWwcsPv/ +SxTJANJHqf4BiFtVjN7LZXi3HUIRAsceEbd0TfW5be9SQ0tbDyyGYt/bXtBLGTIh +9kerr9eWDHlpHMTyP01+Ua3EacbfgrmvD9sa3s6gC4SnwlvLdubmyLwoorCs77eF +15bSOU7NsVZfwLw+M+DyNWPxI1BR/XOP+YoyTgIEChIC9eYnmlWU2Q== +-----END CERTIFICATE-----` + + result = map[string]interface{}{ + "Host": "https://192.0.2.42:8443", + "CACert": pemData, + "ServiceAccountJWT": `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImQxYTZiYzE5LWZiODItNDI5ZC05NmUxLTg1YTFjYjEyNGQ3MCIsImlhdCI6MTYxMTcxNTQ5NiwiZXhwIjoxNjExNzE5MDk2fQ.rrVS5h1Yw20eI41RsTl2YAqzKKikKNg3qMkDmspTPQs`, + } + return +} + func TestAPI_RulesTranslate_FromToken(t *testing.T) { t.Parallel() c, s := makeACLClient(t)