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 <manderson@hashicorp.com>

* Add changelog

Signed-off-by: Mark Anderson <manderson@hashicorp.com>
This commit is contained in:
Mark Anderson 2021-02-17 08:16:57 -08:00 committed by GitHub
parent 15913a5e30
commit 1cf6a435dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 184 additions and 10 deletions

3
.changelog/9741.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
acl: extend the auth-methods list endpoint to include MaxTokenTTL and TokenLocality fields.
```

View File

@ -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

View File

@ -1090,11 +1090,14 @@ 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"`
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

View File

@ -272,6 +272,11 @@ type ACLAuthMethodListEntry struct {
Type string
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
@ -280,6 +285,46 @@ type ACLAuthMethodListEntry struct {
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) {

View File

@ -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)