From 1cf6a435dcd3dbd5b386c253ffe19a7afe6c4c09 Mon Sep 17 00:00:00 2001 From: Mark Anderson Date: Wed, 17 Feb 2021 08:16:57 -0800 Subject: [PATCH] Add fields to the /acl/auth-methods endpoint. (#9741) * A GET of the /acl/auth-method/:name endpoint returns the fields MaxTokenTTL and TokenLocality, while a LIST (/acl/auth-methods) does not. The list command returns a filtered subset of the full set. This is somewhat deliberate, so that secrets aren't shown, but the TTL and Locality fields aren't (IMO) security critical, and it is useful for the front end to be able to show them. For consistency these changes mirror the 'omit empty' and string representation choices made for the GET call. This includes changes to the gRPC and API code in the client. The new output looks similar to this curl 'http://localhost:8500/v1/acl/auth-methods' | jq '.' { "MaxTokenTTL": "8m20s", "Name": "minikube-ttl-local2", "Type": "kubernetes", "Description": "minikube auth method", "TokenLocality": "local", "CreateIndex": 530, "ModifyIndex": 530, "Namespace": "default" } ] Signed-off-by: Mark Anderson * Add changelog Signed-off-by: Mark Anderson --- .changelog/9741.txt | 3 ++ agent/acl_endpoint_test.go | 5 ++ agent/structs/acl.go | 37 ++++++++++++--- api/acl.go | 53 +++++++++++++++++++-- api/acl_test.go | 96 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 .changelog/9741.txt 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)