internal/auth: add GrantsHash method to results

The new GrantsHash method can be used to track changes
to a users grants across requests.
pull/4202/head
Johan Brandhorst-Satzkorn 3 years ago
parent 7c996d0e02
commit 7b5a4be0ab

@ -5,9 +5,13 @@ package auth
import (
"context"
"encoding/binary"
stderrors "errors"
"fmt"
"hash"
"hash/fnv"
"net/http"
"slices"
"strings"
"time"
@ -96,6 +100,9 @@ type VerifyResults struct {
// Used for additional verification
v *verifier
// Used to generate a hash of all grants
grants []perms.GrantTuple
}
type verifier struct {
@ -271,10 +278,9 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
}
var authResults perms.ACLResults
var grantTuples []perms.GrantTuple
var userData template.Data
var err error
authResults, ret.UserData, ret.Scope, v.acl, grantTuples, err = v.performAuthCheck(ctx)
authResults, ret.UserData, ret.Scope, v.acl, ret.grants, err = v.performAuthCheck(ctx)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error performing authn/authz check"))
return
@ -319,8 +325,8 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
}
}
grants := make([]event.Grant, 0, len(grantTuples))
for _, g := range grantTuples {
grants := make([]event.Grant, 0, len(ret.grants))
for _, g := range ret.grants {
grants = append(grants, event.Grant{
Grant: g.Grant,
RoleId: g.RoleId,
@ -934,3 +940,52 @@ func (r *VerifyResults) ScopesAuthorizedForList(ctx context.Context, rootScopeId
return scopeResourceMap, nil
}
// GrantsHash returns a stable hash of all the grants in the verify results.
func (r *VerifyResults) GrantsHash(ctx context.Context) ([]byte, error) {
const op = "auth.GrantsHash"
var values []string
for _, grant := range r.grants {
values = append(values, grant.Grant, grant.RoleId, grant.ScopeId)
}
// Sort for deterministic output
slices.Sort(values)
hashVal, err := hashStrings(values...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return binary.LittleEndian.AppendUint64(make([]byte, 0, 4), hashVal), nil
}
func hashStrings(s ...string) (uint64, error) {
hasher := fnv.New64()
var h uint64
var err error
for _, current := range s {
hasher.Reset()
if _, err = hasher.Write([]byte(current)); err != nil {
return 0, err
}
if h, err = hashUpdateOrdered(hasher, h, hasher.Sum64()); err != nil {
return 0, err
}
}
return h, nil
}
// hashUpdateOrdered is taken directly from
// https://github.com/mitchellh/hashstructure
func hashUpdateOrdered(h hash.Hash64, a, b uint64) (uint64, error) {
// For ordered updates, use a real hash function
h.Reset()
e1 := binary.Write(h, binary.LittleEndian, a)
e2 := binary.Write(h, binary.LittleEndian, b)
if e1 != nil {
return 0, e1
}
if e2 != nil {
return 0, e2
}
return h.Sum64(), nil
}

@ -4,6 +4,7 @@
package auth
import (
"bytes"
"context"
"fmt"
"net/http"
@ -263,3 +264,82 @@ func TestVerify_AuditEvent(t *testing.T) {
})
}
}
func TestGrantsHash(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
tokenRepo, err := authtoken.NewRepository(ctx, rw, rw, kms)
require.NoError(t, err)
iamRepo := iam.TestRepo(t, conn, wrapper)
tokenRepoFn := func() (*authtoken.Repository, error) {
return tokenRepo, nil
}
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
serversRepoFn := func() (*server.Repository, error) {
return server.NewRepository(ctx, rw, rw, kms)
}
o, _ := iam.TestScopes(t, iamRepo)
req := httptest.NewRequest("GET", "http://127.0.0.1/v1/scopes/"+o.GetPublicId(), nil)
at := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
encToken, err := authtoken.EncryptToken(context.Background(), kms, o.GetPublicId(), at.GetPublicId(), at.GetToken())
require.NoError(t, err)
tokValue := at.GetPublicId() + "_" + encToken
// Add values for authn/authz checking
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tokValue))
requestInfo := authpb.RequestInfo{
Path: req.URL.Path,
Method: req.Method,
}
requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(context.TODO(), kms, req)
verifierCtx := NewVerifierContext(ctx, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo)
// Create auth result from token
res := Verify(verifierCtx, WithScopeId(o.GetPublicId()))
hash1, err := res.GrantsHash(ctx)
require.NoError(t, err)
// Look up the user and create a second token
user, _, err := iamRepo.LookupUser(ctx, at.IamUserId)
require.NoError(t, err)
at, err = tokenRepo.CreateAuthToken(ctx, user, at.AuthAccountId)
require.NoError(t, err)
encToken, err = authtoken.EncryptToken(context.Background(), kms, o.GetPublicId(), at.GetPublicId(), at.GetToken())
require.NoError(t, err)
tokValue = at.GetPublicId() + "_" + encToken
// Add values for authn/authz checking
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tokValue))
requestInfo = authpb.RequestInfo{
Path: req.URL.Path,
Method: req.Method,
}
requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(context.TODO(), kms, req)
verifierCtx = NewVerifierContext(ctx, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo)
// Create auth result from token
res = Verify(verifierCtx, WithScopeId(o.GetPublicId()))
hash2, err := res.GrantsHash(ctx)
require.NoError(t, err)
assert.True(t, bytes.Equal(hash1, hash2))
// Change grants of the user
newRole := iam.TestRole(t, conn, o.GetPublicId())
_ = iam.TestRoleGrant(t, conn, newRole.GetPublicId(), "id=*;type=*;actions=list-keys")
_ = iam.TestUserRole(t, conn, newRole.GetPublicId(), user.GetPublicId())
// Recreate auth result with new grants, should have a new hash
res = Verify(verifierCtx, WithScopeId(o.GetPublicId()))
hash3, err := res.GrantsHash(ctx)
require.NoError(t, err)
assert.False(t, bytes.Equal(hash1, hash3))
assert.False(t, bytes.Equal(hash1, hash3))
}

Loading…
Cancel
Save