From 7b5a4be0ab6658655f0c2128b3362829eae0176e Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Mon, 23 Oct 2023 16:57:35 -0700 Subject: [PATCH] internal/auth: add GrantsHash method to results The new GrantsHash method can be used to track changes to a users grants across requests. --- internal/daemon/controller/auth/auth.go | 63 ++++++++++++++- internal/daemon/controller/auth/auth_test.go | 80 ++++++++++++++++++++ 2 files changed, 139 insertions(+), 4 deletions(-) diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index af2f642a84..9baeb1a341 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -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 +} diff --git a/internal/daemon/controller/auth/auth_test.go b/internal/daemon/controller/auth/auth_test.go index 664466a46d..54be6033c3 100644 --- a/internal/daemon/controller/auth/auth_test.go +++ b/internal/daemon/controller/auth/auth_test.go @@ -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)) +}