making ACLToken.ExpirationTime a *time.Time value instead of time.Time (#5663)

This is mainly to avoid having the API return "0001-01-01T00:00:00Z" as
a value for the ExpirationTime field when it is not set. Unfortunately
time.Time doesn't respect the json marshalling "omitempty" directive.
This commit is contained in:
R.B. Boyer 2019-04-15 13:35:55 -05:00 committed by R.B. Boyer
parent db43fc3a20
commit 7928305279
7 changed files with 59 additions and 34 deletions

View File

@ -358,16 +358,16 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
if token.ExpirationTTL < 0 { if token.ExpirationTTL < 0 {
return fmt.Errorf("Token Expiration TTL '%s' should be > 0", token.ExpirationTTL) return fmt.Errorf("Token Expiration TTL '%s' should be > 0", token.ExpirationTTL)
} }
if !token.ExpirationTime.IsZero() { if token.HasExpirationTime() {
return fmt.Errorf("Token Expiration TTL and Expiration Time cannot both be set") return fmt.Errorf("Token Expiration TTL and Expiration Time cannot both be set")
} }
token.ExpirationTime = token.CreateTime.Add(token.ExpirationTTL) token.ExpirationTime = timePointer(token.CreateTime.Add(token.ExpirationTTL))
token.ExpirationTTL = 0 token.ExpirationTTL = 0
} }
if !token.ExpirationTime.IsZero() { if token.HasExpirationTime() {
if token.CreateTime.After(token.ExpirationTime) { if token.CreateTime.After(*token.ExpirationTime) {
return fmt.Errorf("ExpirationTime cannot be before CreateTime") return fmt.Errorf("ExpirationTime cannot be before CreateTime")
} }
@ -417,7 +417,15 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
return fmt.Errorf("cannot toggle local mode of %s", token.AccessorID) return fmt.Errorf("cannot toggle local mode of %s", token.AccessorID)
} }
if token.ExpirationTTL != 0 || !token.ExpirationTime.Equal(existing.ExpirationTime) { if token.ExpirationTTL != 0 {
return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
}
if !token.HasExpirationTime() {
token.ExpirationTime = existing.ExpirationTime
} else if !existing.HasExpirationTime() {
return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
} else if !token.ExpirationTime.Equal(*existing.ExpirationTime) {
return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID) return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
} }
@ -1041,3 +1049,7 @@ func (a *ACL) ReplicationStatus(args *structs.DCSpecificRequest,
a.srv.aclReplicationStatusLock.RUnlock() a.srv.aclReplicationStatusLock.RUnlock()
return nil return nil
} }
func timePointer(t time.Time) *time.Time {
return &t
}

View File

@ -1054,7 +1054,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
Description: "foobar", Description: "foobar",
Policies: nil, Policies: nil,
Local: false, Local: false,
ExpirationTime: time.Now().Add(test.offset), ExpirationTime: timePointer(time.Now().Add(test.offset)),
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -1099,7 +1099,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
Description: "foobar", Description: "foobar",
Policies: nil, Policies: nil,
Local: false, Local: false,
ExpirationTime: time.Now().Add(4 * time.Second), ExpirationTime: timePointer(time.Now().Add(4 * time.Second)),
ExpirationTTL: 4 * time.Second, ExpirationTTL: 4 * time.Second,
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
@ -1138,7 +1138,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
require.NotNil(t, token.AccessorID) require.NotNil(t, token.AccessorID)
require.Equal(t, token.Description, "foobar") require.Equal(t, token.Description, "foobar")
require.Equal(t, token.AccessorID, resp.AccessorID) require.Equal(t, token.AccessorID, resp.AccessorID)
requireTimeEquals(t, expectExpTime, resp.ExpirationTime) requireTimeEquals(t, &expectExpTime, resp.ExpirationTime)
tokenID = token.AccessorID tokenID = token.AccessorID
}) })
@ -1152,7 +1152,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
Description: "foobar", Description: "foobar",
Policies: nil, Policies: nil,
Local: false, Local: false,
ExpirationTime: expTime, ExpirationTime: &expTime,
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -1170,7 +1170,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
require.NotNil(t, token.AccessorID) require.NotNil(t, token.AccessorID)
require.Equal(t, token.Description, "foobar") require.Equal(t, token.Description, "foobar")
require.Equal(t, token.AccessorID, resp.AccessorID) require.Equal(t, token.AccessorID, resp.AccessorID)
requireTimeEquals(t, expTime, resp.ExpirationTime) requireTimeEquals(t, &expTime, resp.ExpirationTime)
tokenID = token.AccessorID tokenID = token.AccessorID
}) })
@ -1183,7 +1183,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
ACLToken: structs.ACLToken{ ACLToken: structs.ACLToken{
Description: "new-description", Description: "new-description",
AccessorID: tokenID, AccessorID: tokenID,
ExpirationTime: expTime.Add(-1 * time.Second), ExpirationTime: timePointer(expTime.Add(-1 * time.Second)),
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -1202,7 +1202,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
ACLToken: structs.ACLToken{ ACLToken: structs.ACLToken{
Description: "new-description", Description: "new-description",
AccessorID: tokenID, AccessorID: tokenID,
ExpirationTime: expTime, ExpirationTime: &expTime,
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -1220,7 +1220,7 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
require.NotNil(t, token.AccessorID) require.NotNil(t, token.AccessorID)
require.Equal(t, token.Description, "new-description") require.Equal(t, token.Description, "new-description")
require.Equal(t, token.AccessorID, resp.AccessorID) require.Equal(t, token.AccessorID, resp.AccessorID)
requireTimeEquals(t, expTime, resp.ExpirationTime) requireTimeEquals(t, &expTime, resp.ExpirationTime)
}) })
t.Run("cannot update a token that is past its expiration time", func(t *testing.T) { t.Run("cannot update a token that is past its expiration time", func(t *testing.T) {
@ -2217,10 +2217,16 @@ func retrieveTestPolicy(codec rpc.ClientCodec, masterToken string, datacenter st
return &out, nil return &out, nil
} }
func requireTimeEquals(t *testing.T, expect, got time.Time) { func requireTimeEquals(t *testing.T, expect, got *time.Time) {
t.Helper() t.Helper()
if !expect.Equal(got) { if expect == nil && got == nil {
t.Fatalf("expected=%q != got=%q", expect, got) return
} else if expect == nil && got != nil {
t.Fatalf("expected=NIL != got=%q", *got)
} else if expect != nil && got == nil {
t.Fatalf("expected=%q != got=NIL", *expect)
} else if !expect.Equal(*got) {
t.Fatalf("expected=%q != got=%q", *expect, *got)
} }
} }

View File

@ -79,14 +79,14 @@ func (s *TokenExpirationIndex) FromObject(obj interface{}) (bool, []byte, error)
if s.LocalFilter != token.Local { if s.LocalFilter != token.Local {
return false, nil, nil return false, nil, nil
} }
if token.ExpirationTime.IsZero() { if !token.HasExpirationTime() {
return false, nil, nil return false, nil, nil
} }
if token.ExpirationTime.Unix() < 0 { if token.ExpirationTime.Unix() < 0 {
return false, nil, fmt.Errorf("token expiration time cannot be before the unix epoch: %s", token.ExpirationTime) return false, nil, fmt.Errorf("token expiration time cannot be before the unix epoch: %s", token.ExpirationTime)
} }
buf := s.encodeTime(token.ExpirationTime) buf := s.encodeTime(*token.ExpirationTime)
return true, buf, nil return true, buf, nil
} }
@ -669,10 +669,10 @@ func (s *Store) ACLTokenMinExpirationTime(local bool) (time.Time, error) {
token := item.(*structs.ACLToken) token := item.(*structs.ACLToken)
return token.ExpirationTime, nil return *token.ExpirationTime, nil
} }
// ACLTokenListExpires lists tokens that are expires as of the provided time. // ACLTokenListExpires lists tokens that are expired as of the provided time.
// The returned set will be no larger than the max value provided. // The returned set will be no larger than the max value provided.
func (s *Store) ACLTokenListExpired(local bool, asOf time.Time, max int) (structs.ACLTokens, <-chan struct{}, error) { func (s *Store) ACLTokenListExpired(local bool, asOf time.Time, max int) (structs.ACLTokens, <-chan struct{}, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
@ -689,8 +689,7 @@ func (s *Store) ACLTokenListExpired(local bool, asOf time.Time, max int) (struct
) )
for raw := iter.Next(); raw != nil; raw = iter.Next() { for raw := iter.Next(); raw != nil; raw = iter.Next() {
token := raw.(*structs.ACLToken) token := raw.(*structs.ACLToken)
if token.ExpirationTime != nil && !token.ExpirationTime.Before(asOf) {
if !token.ExpirationTime.Before(asOf) {
return tokens, nil, nil return tokens, nil, nil
} }

View File

@ -1741,7 +1741,7 @@ func TestTokenPoliciesIndex(t *testing.T) {
SecretID: newUUID(), SecretID: newUUID(),
Description: desc, Description: desc,
Local: local, Local: local,
ExpirationTime: expTime, ExpirationTime: &expTime,
CreateTime: baseTime, CreateTime: baseTime,
RaftIndex: structs.RaftIndex{ RaftIndex: structs.RaftIndex{
CreateIndex: 9, CreateIndex: 9,

View File

@ -218,7 +218,11 @@ type ACLToken struct {
// ExpirationTime represents the point after which a token should be // ExpirationTime represents the point after which a token should be
// considered revoked and is eligible for destruction. The zero value // considered revoked and is eligible for destruction. The zero value
// represents NO expiration. // represents NO expiration.
ExpirationTime time.Time `json:",omitempty"` //
// This is a pointer value so that the zero value is omitted properly
// during json serialization. time.Time does not respect json omitempty
// directives unfortunately.
ExpirationTime *time.Time `json:",omitempty"`
// ExpirationTTL is a convenience field for helping set ExpirationTime to a // ExpirationTTL is a convenience field for helping set ExpirationTime to a
// value of CreateTime+ExpirationTTL. This can only be set during // value of CreateTime+ExpirationTTL. This can only be set during
@ -293,17 +297,21 @@ func (t *ACLToken) ServiceIdentityList() []*ACLServiceIdentity {
} }
func (t *ACLToken) IsExpired(asOf time.Time) bool { func (t *ACLToken) IsExpired(asOf time.Time) bool {
if asOf.IsZero() || t.ExpirationTime.IsZero() { if asOf.IsZero() || !t.HasExpirationTime() {
return false return false
} }
return t.ExpirationTime.Before(asOf) return t.ExpirationTime.Before(asOf)
} }
func (t *ACLToken) HasExpirationTime() bool {
return t.ExpirationTime != nil && !t.ExpirationTime.IsZero()
}
func (t *ACLToken) UsesNonLegacyFields() bool { func (t *ACLToken) UsesNonLegacyFields() bool {
return len(t.Policies) > 0 || return len(t.Policies) > 0 ||
len(t.ServiceIdentities) > 0 || len(t.ServiceIdentities) > 0 ||
t.Type == "" || t.Type == "" ||
!t.ExpirationTime.IsZero() || t.HasExpirationTime() ||
t.ExpirationTTL != 0 t.ExpirationTTL != 0
} }
@ -405,7 +413,7 @@ type ACLTokenListStub struct {
Policies []ACLTokenPolicyLink `json:",omitempty"` Policies []ACLTokenPolicyLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
Local bool Local bool
ExpirationTime time.Time `json:",omitempty"` ExpirationTime *time.Time `json:",omitempty"`
CreateTime time.Time `json:",omitempty"` CreateTime time.Time `json:",omitempty"`
Hash []byte Hash []byte
CreateIndex uint64 CreateIndex uint64

View File

@ -31,7 +31,7 @@ type ACLToken struct {
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
Local bool Local bool
ExpirationTTL time.Duration `json:",omitempty"` ExpirationTTL time.Duration `json:",omitempty"`
ExpirationTime time.Time `json:",omitempty"` ExpirationTime *time.Time `json:",omitempty"`
CreateTime time.Time `json:",omitempty"` CreateTime time.Time `json:",omitempty"`
Hash []byte `json:",omitempty"` Hash []byte `json:",omitempty"`
@ -48,7 +48,7 @@ type ACLTokenListEntry struct {
Policies []*ACLTokenPolicyLink `json:",omitempty"` Policies []*ACLTokenPolicyLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
Local bool Local bool
ExpirationTime time.Time `json:",omitempty"` ExpirationTime *time.Time `json:",omitempty"`
CreateTime time.Time CreateTime time.Time
Hash []byte Hash []byte
Legacy bool Legacy bool

View File

@ -15,8 +15,8 @@ func PrintToken(token *api.ACLToken, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("Description: %s", token.Description)) ui.Info(fmt.Sprintf("Description: %s", token.Description))
ui.Info(fmt.Sprintf("Local: %t", token.Local)) ui.Info(fmt.Sprintf("Local: %t", token.Local))
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime)) ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
if !token.ExpirationTime.IsZero() { if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() {
ui.Info(fmt.Sprintf("Expiration Time: %v", token.ExpirationTime)) ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime))
} }
if showMeta { if showMeta {
ui.Info(fmt.Sprintf("Hash: %x", token.Hash)) ui.Info(fmt.Sprintf("Hash: %x", token.Hash))
@ -46,8 +46,8 @@ func PrintTokenListEntry(token *api.ACLTokenListEntry, ui cli.Ui, showMeta bool)
ui.Info(fmt.Sprintf("Description: %s", token.Description)) ui.Info(fmt.Sprintf("Description: %s", token.Description))
ui.Info(fmt.Sprintf("Local: %t", token.Local)) ui.Info(fmt.Sprintf("Local: %t", token.Local))
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime)) ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
if !token.ExpirationTime.IsZero() { if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() {
ui.Info(fmt.Sprintf("Expiration Time: %v", token.ExpirationTime)) ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime))
} }
ui.Info(fmt.Sprintf("Legacy: %t", token.Legacy)) ui.Info(fmt.Sprintf("Legacy: %t", token.Legacy))
if showMeta { if showMeta {