Skip to content

Commit

Permalink
streamlined the Credentials interface; ensure support for remote cred…
Browse files Browse the repository at this point in the history
…entials
  • Loading branch information
johnabass committed Sep 19, 2024
1 parent 661f2e1 commit 98e72d5
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 406 deletions.
25 changes: 25 additions & 0 deletions basculehash/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehash

import "context"

// Credentials is a source of principals and their associated digests. A
// credentials instance may be in-memory or a remote system.
type Credentials interface {
// Get returns the Digest associated with the given Principal.
// This method returns false if the principal did not exist.
Get(ctx context.Context, principal string) (d Digest, exists bool)

// Set associates a principal with a Digest. If the principal already
// exists, its digest is replaced.
Set(ctx context.Context, principal string, d Digest)

// Delete removes one or more principals from this set.
Delete(ctx context.Context, principals ...string)

// Update performs a bulk update of these credentials. Any existing
// principals are replaced.
Update(ctx context.Context, p Principals)
}
110 changes: 110 additions & 0 deletions basculehash/credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehash

import (
"context"

"golang.org/x/crypto/bcrypt"
)

// CredentialsTestSuite runs a standard battery of tests against
// a Credentials implementation.
//
// Tests of UnmarshalJSON need to be done in tests of concrete types
// due to the way unmarshalling works in golang.
type CredentialsTestSuite[C Credentials] struct {
TestSuite

// Implementations should supply SetupTest and SetupSubTest
// methods that populate this member. Don't forget to call
// TestSuite.SetupTest and TestSuite.SetupSubTest!
credentials C

testCtx context.Context
hasher Hasher
}

// SetupSuite initializes a hasher and comparer to use when verifying
// and creating digests.
func (suite *CredentialsTestSuite[C]) SetupSuite() {
suite.testCtx = context.Background()
suite.hasher = Bcrypt{Cost: bcrypt.MinCost}
}

// exists asserts that a given principal exists with the given Digest.
func (suite *CredentialsTestSuite[C]) exists(principal string, expected Digest) {
d, ok := suite.credentials.Get(suite.testCtx, principal)
suite.Require().True(ok)
suite.Require().Equal(expected, d)
}

// notExists asserts that the given principal did not exist.
func (suite *CredentialsTestSuite[C]) notExists(principal string) {
d, ok := suite.credentials.Get(suite.testCtx, principal)
suite.Require().False(ok)
suite.Require().Empty(d)
}

// defaultHash creates a distinct hash of the suite plaintext for testing.
func (suite *CredentialsTestSuite[C]) defaultHash() Digest {
return suite.goodHash(
suite.hasher.Hash(
suite.plaintext,
),
)
}

func (suite *CredentialsTestSuite[C]) TestGetSetDelete() {
suite.T().Log("delete from empty")
suite.credentials.Delete(suite.testCtx, "joe")

suite.T().Log("add")
joeDigest := suite.defaultHash()
suite.credentials.Set(suite.testCtx, "joe", joeDigest)
suite.exists("joe", joeDigest)

suite.T().Log("add another")
fredDigest := suite.defaultHash()
suite.credentials.Set(suite.testCtx, "fred", fredDigest)
suite.exists("joe", joeDigest)
suite.exists("fred", fredDigest)

suite.T().Log("replace")
newJoeDigest := suite.defaultHash()
suite.Require().NotEqual(newJoeDigest, joeDigest) // hashes should always generate salt to make them distinct
suite.credentials.Set(suite.testCtx, "joe", newJoeDigest)
suite.exists("joe", newJoeDigest)
suite.exists("fred", fredDigest)

suite.T().Log("delete a principal")
suite.credentials.Delete(suite.testCtx, "fred")
suite.notExists("fred")
suite.exists("joe", newJoeDigest)
}

func (suite *CredentialsTestSuite[C]) TestUpdate() {
suite.credentials.Update(suite.testCtx, nil)

joeDigest := suite.defaultHash()
fredDigest := suite.defaultHash()
suite.credentials.Update(suite.testCtx, Principals{
"joe": joeDigest,
"fred": fredDigest,
})

suite.exists("joe", joeDigest)
suite.exists("fred", fredDigest)

joeDigest = suite.defaultHash()
moeDigest := suite.defaultHash()
suite.credentials.Update(suite.testCtx, Principals{
"joe": joeDigest,
"moe": moeDigest,
})

suite.exists("joe", joeDigest)
suite.exists("fred", fredDigest)
suite.exists("moe", moeDigest)
}
47 changes: 0 additions & 47 deletions basculehash/hasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,50 +45,3 @@ var defaultHash HasherComparer = Bcrypt{}
func Default() HasherComparer {
return defaultHash
}

// Matches tests if the given Comparer strategy indicates a match
// between the plaintext and the digest. If cmp is nil, the
// Default() is used.
func Matches(cmp Comparer, plaintext []byte, d Digest) error {
if cmp == nil {
cmp = Default()
}

return cmp.Matches(plaintext, d)
}

// Matcher is anything that can test if a principal's digest matches a digest.
type Matcher interface {
// Matches checks the associated digest with the given plaintext.
Matches(cmp Comparer, principal string, plaintext []byte) error
}

// Credentials is a set of principals and associated digests. Each principal may
// have exactly zero (0) or one (1) associated Digest.
//
// All implementations of this interface in this package can be marshaled and
// unmarshaled from JSON.
type Credentials interface {
Matcher

// Len returns the count of principals in this set.
Len() int

// Get returns the Digest associated with the given Principal.
// This method returns false if the principal did not exist.
Get(principal string) (d Digest, exists bool)

// Set associates a principal with a Digest. If the principal already
// exists, its digest is replaced.
//
// This method does not retain d. An copy is made and stored internally.
Set(principal string, d Digest)

// Delete removes the principal from this set. If the principal did
// not exist, it returns false.
Delete(principal string) (d Digest, existed bool)

// Update performs a bulk update of these credentials. Any existing
// principals are replaced.
Update(p Principals)
}
Loading

0 comments on commit 98e72d5

Please sign in to comment.