-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add optional token hashing #25982
base: main-2.x
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A variety of smaller issues. I will need to review this again to fully understand the larger changes.
authorizer/task_test.go
Outdated
Bucket: "holder", | ||
RetentionPeriodSeconds: 1, | ||
}) | ||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
require.NoError
?
authorizer/task_test.go
Outdated
t.Fatal(err) | ||
}) | ||
if err != nil { | ||
t.Fatal(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
require.NoError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My usual add more context to error and log messages
and a few more substantive questions and suggestions
authorization/http_server_test.go
Outdated
@@ -185,21 +182,16 @@ func TestService_handlePostAuthorization(t *testing.T) { | |||
handler.handlePostAuthorization(w, r) | |||
|
|||
res := w.Result() | |||
content := res.Header.Get("Content-Type") | |||
contentType := res.Header.Get("Content-Type") | |||
body, _ := io.ReadAll(res.Body) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe an error check on ReadAll
? probably never fails, though....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handled now.
authorization/http_server_test.go
Outdated
} | ||
diff, err := jsonDiff(string(body), tt.wants.body) | ||
require.NoError(t, err) | ||
require.Empty(t, diff) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little context message in these two would help debugging: diff failed on <whatever this is doing>
and
expected no difference in authorization response
or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an error message.
authorization/http_server_test.go
Outdated
} | ||
diff, err := jsonDiff(string(body), tt.wants.body) | ||
require.NoError(t, err) | ||
require.Empty(t, diff) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an error message.
authorization/http_server_test.go
Outdated
t.Errorf("%q. handleDeleteAuthorization() = %v, want %v", tt.name, content, tt.wants.contentType) | ||
require.Equal(t, tt.wants.statusCode, res.StatusCode) | ||
if tt.wants.contentType != "" { | ||
require.Equal(t, tt.wants.contentType, contentType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mention handleDeleteAuthorization
in the log message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
} | ||
|
||
// deleteIndices removes indices for the given token and hashedToken. | ||
func (s *Store) deleteIndices(ctx context.Context, tx kv.Tx, token, hashedToken string) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if this should be more robust. Even if we can't get one bucket, remove the (hashed) token from the other, and if we can't remove one token, still try to remove the other. Would that leave us in a better state for recovery, or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If error will get returned up the call tree and cause the BoltDB transaction to be rolled back, so we won't leave BoltDB in an inconsistent state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All changes pertain to comments, but important one is whether to delete or include a commented out code section
Add optional token hashing with `--use-hashed-tokens` command line option.
Create duplicate tokens from being created. This is a bug introduced earlier in this PR. Also improve tests so they detect the bug and use testify throughout.
Fix a bug that only allowed hashed tokens to be looked up if they used the currently active hashing algorithm. Also added tests for configuration migration scenarios (enabling and disabling hashing, changing hashing scheme).
Address PR issues on comments, error handling, and add final token matching check in `Store.GetAuthorizationByToken`.
Changes in addition to minor cleanups: - `authentication.Store` can now log info and warnings - Improved logic in `UpdateAuthorization` when both Token and HashedToken are set. Added supporting test cases.
15f8e92
to
6aa2d1a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few comments mostly about extra safety checks and some error messaging. I've been able to get about half way through the PR. Going to step away for a minute to take a little brain break and then finish up the rest of the PR for my first pass.
// Create the hasher used for hashing new tokens before storage. | ||
hasher, err := influxdb2_algo.New(influxdb2_algo.WithVariant(options.hasherVariant)) | ||
if err != nil { | ||
return nil, fmt.Errorf("creating hasher for AuthorizationHasher: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
display hasherVariant for error?
fmt.Errorf("creating hasher with variant %s for AuthorizationHasher: %w", options.hasherVariant, err)
// AllHashes generates a list of PHC-encoded hashes of token for all deterministic (i.e. non-salted) supported hashes. | ||
func (h *AuthorizationHasher) AllHashes(token string) ([]string, error) { | ||
hashes := make([]string, len(h.allHashers)) | ||
for idx, hasher := range h.allHashers { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could there ever be a code path where h.allHashers
could be empty and we expect for it not to be? Would it be a good idea to check h.allHashers
len and warn on it if its == 0?
for idx, hasher := range h.allHashers { | ||
digest, err := hasher.Hash(token) | ||
if err != nil { | ||
variantName := "N/A" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding "N/A" here seems weird to me. Could this be an enum or a constant of some sort?
t.Fatal(err) | ||
} | ||
storage, err := NewStore(context.Background(), s, useHashedTokens) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add some context to this error so we know what failed
require.NoError(t, err, "creating new store")
t.Fatalf("failed to unmarshal authorization: %v", err) | ||
} | ||
req, err := newPostAuthorizationRequest(tt.args.authorization) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to above - add small context string.
return nil | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("error migrating hashed tokens: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding some more context to the error such as ID and maybe token description? Similar to the s.log.Warn above?
// Redact Token, if needed. This is done at the lowest level so it is impossible to serialize | ||
// raw tokens if hashing is enabled. | ||
if s.useHashedTokens { | ||
// Redact a copy, not the original. The raw Token value is still needed by the caller in some cases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice thank you for the comment, makes the code below much easier to reason about.
// error is returned. | ||
func (s *Store) transformToken(a *influxdb.Authorization) error { | ||
// Verify Token and HashedToken match if both are set. | ||
if a.Token != "" && a.HashedToken != "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since 'a' is a pointer should there be a nil check for safety before field access?
// Note that even if a.HashedToken is set, we will regenerate it here. This ensures | ||
// that a.HashedToken will be stored using the currently configured hashing algorithm. | ||
if hashedToken, err := s.hasher.Hash(a.Token); err != nil { | ||
return fmt.Errorf("error hashing token: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any other information such as description or ID?
if auth.HashedToken != "" { | ||
match, err := s.hasher.Match(auth.HashedToken, token) | ||
if err != nil { | ||
return false, fmt.Errorf("error matching hashed token for validation: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More information like ID and description?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a few more comments to storage_authorization.go
I noticed that a bunch of my comments for the require.NoError(..)
is similar to what david commented so my apologies for that :P
@@ -71,42 +142,12 @@ func (s *Store) CreateAuthorization(ctx context.Context, tx kv.Tx, a *influxdb.A | |||
a.ID = id | |||
} | |||
|
|||
// Token must be unique to create authorization. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If s.uniqueAuthToken
fails should the mutation of a.HashedToken = hashedToken
be reverted/rolled back at all? https://github.com/influxdata/EAR/issues/5819#issuecomment-2686492432
return string(got) == token | ||
} | ||
if len(allHashes) > 0 { | ||
if got, _, _, err := jsonparser.Get(value, "hashedToken"); err == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should you check if err
is not nil? It appears that this method will throw errors for a few different reasons. It may be good to add a safety check if err != nil.
allHashes, err := s.hasher.AllHashes(token) | ||
if err != nil { | ||
s.log.Error("error generating hashes in filterPredicateFn", zap.Error(err)) | ||
// On error, continue onward. allHashes is empty and we'll effectively ignore hashedToken, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding in a few more comments
Bucket: "holder", | ||
RetentionPeriodSeconds: 1, | ||
}) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context string for NoError
if err != nil { | ||
t.Fatal(err) | ||
} | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context string for NoError
t.Fatal(err) | ||
} | ||
err = svc.CreateOrganization(context.Background(), otherOrg) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context string for NoError
t.Fatal(err) | ||
} | ||
err := all.Up(context.Background(), zaptest.NewLogger(t), store) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context string for NoError
t.Fatal(err) | ||
} | ||
authStore, err := authorization.NewStore(context.Background(), store, useHashedTokens) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context string for NoError
ctx := context.Background() | ||
store := bolt.NewKVStore(cmd.logger.With(zap.String("system", "bolt-kvstore")), cmd.boltPath) | ||
if err := store.Open(ctx); err != nil { | ||
return err | ||
} | ||
defer store.Close() | ||
defer func() { | ||
rErr = errors.Join(store.Close(), rErr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For these defers can errors2.Capture
be used?
see
influxdb/tsdb/index/tsi1/partition.go
Line 301 in 1efb8da
defer errors2.Capture(&rErr, dir.Close)() |
if t.Token != "" { | ||
token = t.Token | ||
} else if t.HashedToken != "" { | ||
token = "REDACTED" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could "N/A" and "REDACTED" be Enums or consts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be added? I assume yes because its post fixed with testdata, but, just checking.
|
||
for _, want := range tc.want { | ||
actual, err := v2.authSvc.FindAuthorizationByToken(ctx, want.Token) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add string context to NoError
} | ||
if diff := cmp.Diff(want.Status, actual.Status); diff != "" { | ||
t.Fatal(diff) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String context to NoError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to pick back up my review next week for a second pass. Consider this first pass finished up.
t.Run(tc.name, func(t *testing.T) { // better do not run in parallel | ||
ctx := context.Background() | ||
log := zaptest.NewLogger(t) | ||
for _, useHashedTokens := range []bool{false, true} { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add strings with context to NoError
's in this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also did not mean to approve. Dismissing my approval.
Add optional token hashing with --use-hashed-tokens command line option.