Skip to content

Commit

Permalink
acl: tokens can be created with an optional expiration time
Browse files Browse the repository at this point in the history
  • Loading branch information
rboyer committed Mar 14, 2019
1 parent 33d0922 commit 473db0d
Show file tree
Hide file tree
Showing 24 changed files with 1,564 additions and 204 deletions.
30 changes: 25 additions & 5 deletions agent/acl_endpoint.go
Expand Up @@ -268,15 +268,35 @@ func (s *HTTPServer) ACLPolicyCreate(resp http.ResponseWriter, req *http.Request
return s.ACLPolicyWrite(resp, req, "")
}

// fixCreateTimeAndHash is used to help in decoding the CreateTime and Hash
// fixTimeAndHashFields is used to help in decoding the ExpirationTTL, ExpirationTime, CreateTime, and Hash
// attributes from the ACL Token/Policy create/update requests. It is needed
// to help mapstructure decode things properly when decodeBody is used.
func fixCreateTimeAndHash(raw interface{}) error {
func fixTimeAndHashFields(raw interface{}) error {
rawMap, ok := raw.(map[string]interface{})
if !ok {
return nil
}

if val, ok := rawMap["ExpirationTTL"]; ok {
if sval, ok := val.(string); ok {
d, err := time.ParseDuration(sval)
if err != nil {
return err
}
rawMap["ExpirationTTL"] = d
}
}

if val, ok := rawMap["ExpirationTime"]; ok {
if sval, ok := val.(string); ok {
t, err := time.Parse(time.RFC3339, sval)
if err != nil {
return err
}
rawMap["ExpirationTime"] = t
}
}

if val, ok := rawMap["CreateTime"]; ok {
if sval, ok := val.(string); ok {
t, err := time.Parse(time.RFC3339, sval)
Expand All @@ -301,7 +321,7 @@ func (s *HTTPServer) ACLPolicyWrite(resp http.ResponseWriter, req *http.Request,
}
s.parseToken(req, &args.Token)

if err := decodeBody(req, &args.Policy, fixCreateTimeAndHash); err != nil {
if err := decodeBody(req, &args.Policy, fixTimeAndHashFields); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("Policy decoding failed: %v", err)}
}

Expand Down Expand Up @@ -472,7 +492,7 @@ func (s *HTTPServer) ACLTokenSet(resp http.ResponseWriter, req *http.Request, to
}
s.parseToken(req, &args.Token)

if err := decodeBody(req, &args.ACLToken, fixCreateTimeAndHash); err != nil {
if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)}
}

Expand Down Expand Up @@ -513,7 +533,7 @@ func (s *HTTPServer) ACLTokenClone(resp http.ResponseWriter, req *http.Request,
Datacenter: s.agent.config.Datacenter,
}

if err := decodeBody(req, &args.ACLToken, fixCreateTimeAndHash); err != nil && err.Error() != "EOF" {
if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil && err.Error() != "EOF" {
return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)}
}
s.parseToken(req, &args.Token)
Expand Down
11 changes: 10 additions & 1 deletion agent/consul/acl.go
Expand Up @@ -31,9 +31,16 @@ const (
// with all tokens in it.
aclUpgradeBatchSize = 128

// aclUpgradeRateLimit is the number of batch upgrade requests per second.
// aclUpgradeRateLimit is the number of batch upgrade requests per second allowed.
aclUpgradeRateLimit rate.Limit = 1.0

// aclTokenReapingRateLimit is the number of batch token reaping requests per second allowed.
aclTokenReapingRateLimit rate.Limit = 1.0

// aclTokenReapingBurst is the number of batch token reaping requests per second
// that can burst after a period of idleness.
aclTokenReapingBurst = 5

// aclBatchDeleteSize is the number of deletions to send in a single batch operation. 4096 should produce a batch that is <150KB
// in size but should be sufficiently large to handle 1 replication round in a single batch
aclBatchDeleteSize = 4096
Expand Down Expand Up @@ -608,6 +615,8 @@ func (r *ACLResolver) resolveTokenToIdentityAndPolicies(token string) (structs.A
return nil, nil, err
} else if identity == nil {
return nil, nil, acl.ErrNotFound
} else if identity.IsExpired(time.Now()) {
return nil, nil, acl.ErrNotFound
}

lastIdentity = identity
Expand Down
57 changes: 52 additions & 5 deletions agent/consul/acl_endpoint.go
Expand Up @@ -221,6 +221,10 @@ func (a *ACL) TokenRead(args *structs.ACLTokenGetRequest, reply *structs.ACLToke
index, token, err = state.ACLTokenGetBySecret(ws, args.TokenID)
}

if token != nil && token.IsExpired(time.Now()) {
token = nil
}

if err != nil {
return err
}
Expand Down Expand Up @@ -256,7 +260,7 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
_, token, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, args.ACLToken.AccessorID)
if err != nil {
return err
} else if token == nil {
} else if token == nil || token.IsExpired(time.Now()) {
return acl.ErrNotFound
} else if !a.srv.InACLDatacenter() && !token.Local {
// global token writes must be forwarded to the primary DC
Expand All @@ -271,9 +275,10 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
cloneReq := structs.ACLTokenSetRequest{
Datacenter: args.Datacenter,
ACLToken: structs.ACLToken{
Policies: token.Policies,
Local: token.Local,
Description: token.Description,
Policies: token.Policies,
Local: token.Local,
Description: token.Description,
ExpirationTime: token.ExpirationTime,
},
WriteRequest: args.WriteRequest,
}
Expand Down Expand Up @@ -342,6 +347,34 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
}

token.CreateTime = time.Now()

// Ensure an ExpirationTTL is valid if provided.
if token.ExpirationTTL != 0 {
if token.ExpirationTTL < 0 {
return fmt.Errorf("Token Expiration TTL '%s' should be > 0", token.ExpirationTTL)
}
if !token.ExpirationTime.IsZero() {
return fmt.Errorf("Token Expiration TTL and Expiration Time cannot both be set")
}

token.ExpirationTime = token.CreateTime.Add(token.ExpirationTTL)
token.ExpirationTTL = 0
}

if !token.ExpirationTime.IsZero() {
if token.CreateTime.After(token.ExpirationTime) {
return fmt.Errorf("ExpirationTime cannot be before CreateTime")
}

expiresIn := token.ExpirationTime.Sub(token.CreateTime)
if expiresIn > a.srv.config.ACLTokenMaxExpirationTTL {
return fmt.Errorf("ExpirationTime cannot be more than %s in the future (was %s)",
a.srv.config.ACLTokenMaxExpirationTTL, expiresIn)
} else if expiresIn < a.srv.config.ACLTokenMinExpirationTTL {
return fmt.Errorf("ExpirationTime cannot be less than %s in the future (was %s)",
a.srv.config.ACLTokenMinExpirationTTL, expiresIn)
}
}
} else {
// Token Update
if _, err := uuid.ParseUUID(token.AccessorID); err != nil {
Expand All @@ -365,7 +398,7 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
if err != nil {
return fmt.Errorf("Failed to lookup the acl token %q: %v", token.AccessorID, err)
}
if existing == nil {
if existing == nil || existing.IsExpired(time.Now()) {
return fmt.Errorf("Cannot find token %q", token.AccessorID)
}
if token.SecretID == "" {
Expand All @@ -379,6 +412,10 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
return fmt.Errorf("cannot toggle local mode of %s", token.AccessorID)
}

if token.ExpirationTTL != 0 || !token.ExpirationTime.Equal(existing.ExpirationTime) {
return fmt.Errorf("Cannot change expiration time of %s", token.AccessorID)
}

if upgrade {
token.CreateTime = time.Now()
} else {
Expand Down Expand Up @@ -440,6 +477,7 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
return respErr
}

// Don't check expiration times here as it doesn't really matter.
if _, updatedToken, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, token.AccessorID); err == nil && token != nil {
*reply = *updatedToken
} else {
Expand Down Expand Up @@ -490,6 +528,8 @@ func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) er
return fmt.Errorf("Deletion of the request's authorization token is not permitted")
}

// No need to check expiration time because it's being deleted.

if !a.srv.InACLDatacenter() && !token.Local {
args.Datacenter = a.srv.config.ACLDatacenter
return a.srv.forwardDC("ACL.TokenDelete", a.srv.config.ACLDatacenter, args, reply)
Expand Down Expand Up @@ -553,8 +593,13 @@ func (a *ACL) TokenList(args *structs.ACLTokenListRequest, reply *structs.ACLTok
return err
}

now := time.Now()

stubs := make([]*structs.ACLTokenListStub, 0, len(tokens))
for _, token := range tokens {
if token.IsExpired(now) {
continue
}
stubs = append(stubs, token.Stub())
}
reply.Index, reply.Tokens = index, stubs
Expand Down Expand Up @@ -589,6 +634,8 @@ func (a *ACL) TokenBatchRead(args *structs.ACLTokenBatchGetRequest, reply *struc
return err
}

// This RPC is used for replication, so don't filter out expired tokens here.

a.srv.filterACLWithAuthorizer(rule, &tokens)

reply.Index, reply.Tokens = index, tokens
Expand Down
15 changes: 13 additions & 2 deletions agent/consul/acl_endpoint_legacy.go
Expand Up @@ -93,8 +93,9 @@ func aclApplyInternal(srv *Server, args *structs.ACLRequest, reply *string) erro
return fmt.Errorf("Invalid ACL Type")
}

// No need to check expiration times as those did not exist in legacy tokens.
_, existing, _ := srv.fsm.State().ACLTokenGetBySecret(nil, args.ACL.ID)
if existing != nil && len(existing.Policies) > 0 {
if existing != nil && existing.UsesNonLegacyFields() {
return fmt.Errorf("Cannot use legacy endpoint to modify a non-legacy token")
}

Expand Down Expand Up @@ -210,8 +211,13 @@ func (a *ACL) Get(args *structs.ACLSpecificRequest,
return err
}

// converting an ACLToken to an ACL will return nil and an error
// Converting an ACLToken to an ACL will return nil and an error
// (which we ignore) when it is unconvertible.
//
// This also means we won't have to check expiration times since
// any legacy tokens never had expiration times and no non-legacy
// tokens can be converted.

var acl *structs.ACL
if token != nil {
acl, _ = token.Convert()
Expand Down Expand Up @@ -254,8 +260,13 @@ func (a *ACL) List(args *structs.DCSpecificRequest,
return err
}

now := time.Now()

var acls structs.ACLs
for _, token := range tokens {
if token.IsExpired(now) {
continue
}
if acl, err := token.Convert(); err == nil && acl != nil {
acls = append(acls, acl)
}
Expand Down

0 comments on commit 473db0d

Please sign in to comment.