Skip to content
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

Token creation counters #9052

Merged
merged 8 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions helper/metricsutil/bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package metricsutil

import (
"sort"
"time"
)

var bucketBoundaries = []struct {
Value time.Duration
Label string
}{
{1 * time.Minute, "1m"},
{10 * time.Minute, "10m"},
{20 * time.Minute, "20m"},
{1 * time.Hour, "1h"},
{2 * time.Hour, "2h"},
{24 * time.Hour, "1d"},
{2 * 24 * time.Hour, "2d"},
{7 * 24 * time.Hour, "7d"},
{30 * 24 * time.Hour, "30d"},
}

const overflowBucket = "+Inf"

// TTLBucket computes the label to apply for a token TTL.
func TTLBucket(ttl time.Duration) string {
upperBound := sort.Search(
len(bucketBoundaries),
func(i int) bool {
return ttl <= bucketBoundaries[i].Value
},
)
if upperBound >= len(bucketBoundaries) {
return overflowBucket
} else {
return bucketBoundaries[upperBound].Label
}

}
28 changes: 28 additions & 0 deletions helper/metricsutil/bucket_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package metricsutil

import (
"testing"
"time"
)

func TestTTLBucket_Lookup(t *testing.T) {
testCases := []struct {
Input time.Duration
Expected string
}{
{30 * time.Second, "1m"},
{0 * time.Second, "1m"},
{2 * time.Hour, "2h"},
{2*time.Hour - time.Second, "2h"},
{2*time.Hour + time.Second, "1d"},
{30 * 24 * time.Hour, "30d"},
{31 * 24 * time.Hour, "+Inf"},
}

for _, tc := range testCases {
bucket := TTLBucket(tc.Input)
if bucket != tc.Expected {
t.Errorf("Expected %q, got %q for duration %v.", tc.Expected, bucket, tc.Input)
}
}
}
14 changes: 14 additions & 0 deletions vault/request_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
multierror "github.com/hashicorp/go-multierror"
sockaddr "github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/sdk/framework"
Expand Down Expand Up @@ -1168,6 +1169,19 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
// Attach the display name, might be used by audit backends
req.DisplayName = auth.DisplayName

// Count the successful token creation
ttl_label := metricsutil.TTLBucket(tokenTTL)
c.metricSink.IncrCounterWithLabels(
[]string{"token", "creation"},
1,
[]metrics.Label{
{"namespace", ns.ID},
{"auth_method", req.MountType},
{"mount_point", req.MountPoint},
{"creation_ttl", ttl_label},
{"token_type", auth.TokenType.String()},
},
)
}

return resp, auth, routeErr
Expand Down
126 changes: 126 additions & 0 deletions vault/request_handling_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package vault

import (
"strings"
"testing"
"time"

"github.com/armon/go-metrics"
uuid "github.com/hashicorp/go-uuid"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/logical"
)
Expand Down Expand Up @@ -144,3 +147,126 @@ func TestRequestHandling_LoginWrapping(t *testing.T) {
t.Fatalf("bad: %#v", resp)
}
}

func TestRequestHandling_LoginMetric(t *testing.T) {
core, _, root := TestCoreUnsealed(t)

if err := core.loadMounts(namespace.RootContext(nil)); err != nil {
t.Fatalf("err: %v", err)
}

core.credentialBackends["userpass"] = credUserpass.Factory

inmemSink := metrics.NewInmemSink(
1000000*time.Hour,
2000000*time.Hour)
core.metricSink = &metricsutil.ClusterMetricSink{
ClusterName: "test-cluster",
Sink: inmemSink,
}

// Setup mount
req := &logical.Request{
Path: "sys/auth/userpass",
ClientToken: root,
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"type": "userpass",
},
Connection: &logical.Connection{},
}
resp, err := core.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %#v", resp)
}

// Create user
req.Path = "auth/userpass/users/test"
req.Data = map[string]interface{}{
"password": "foo",
"policies": "default",
}
resp, err = core.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %#v", resp)
}

// Login
req = &logical.Request{
Path: "auth/userpass/login/test",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"password": "foo",
},
Connection: &logical.Connection{},
}
resp, err = core.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp == nil {
t.Fatalf("bad: %v", resp)
}
if resp.WrapInfo != nil {
t.Fatalf("bad: %#v", resp)
}

intervals := inmemSink.Data()
if len(intervals) > 1 {
t.Skip("Detected interval crossing.")
}

// TODO: can this sort of check go into a test utility for
// use in multiple test cases? (It's a copy of code over
// in token_store_test.go)
keyPrefix := "token.creation"
var counter *metrics.SampledValue = nil

for _, c := range intervals[0].Counters {
if strings.HasPrefix(c.Name, keyPrefix) {
counter = &c
break
}
}
if counter == nil {
t.Fatal("No token.creation counter found.")
}

if counter.Count != 1 {
t.Errorf("Counter number of samples %v is not 1.", counter.Count)
}

if counter.Sum != 1.0 {
t.Errorf("Counter sum %v is not 1.", counter.Sum)
}

labels := make(map[string]string)
for _, l := range counter.Labels {
labels[l.Name] = l.Value
}
// FIXME: keep the final / in metrics, or not?
expected := map[string]string{
"cluster": "test-cluster",
"namespace": "root",
"auth_method": "userpass",
"mount_point": "auth/userpass/",
"creation_ttl": "+Inf",
"token_type": "service",
}
for expected_label, expected_val := range expected {
if v, ok := labels[expected_label]; ok {
if v != expected_val {
t.Errorf("Label %q incorrect, expected %q, got %q", expected_label, expected_val, v)
}
} else {
t.Errorf("Label %q missing", expected_label)
}
}

}
15 changes: 15 additions & 0 deletions vault/token_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/base62"
Expand Down Expand Up @@ -2716,6 +2717,20 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

// Count the successful token creation.
ttl_label := metricsutil.TTLBucket(te.TTL)
ts.core.metricSink.IncrCounterWithLabels(
[]string{"token", "creation"},
1,
[]metrics.Label{
{"namespace", ns.ID},
{"auth_method", "token"},
{"mount_point", req.MountPoint}, // path, not accessor
{"creation_ttl", ttl_label},
{"token_type", tokenType.String()},
},
)

// Generate the response
resp.Auth = &logical.Auth{
NumUses: te.NumUses,
Expand Down
75 changes: 75 additions & 0 deletions vault/token_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import (
"testing"
"time"

"github.com/armon/go-metrics"
"github.com/go-test/deep"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/locksutil"
"github.com/hashicorp/vault/sdk/helper/parseutil"
Expand Down Expand Up @@ -2119,6 +2121,79 @@ func TestTokenStore_HandleRequest_CreateToken_TTL(t *testing.T) {
}
}

func TestTokenStore_HandleRequest_CreateToken_Metric(t *testing.T) {
c, _, root := TestCoreUnsealed(t)
ts := c.tokenStore

inmemSink := metrics.NewInmemSink(
1000000*time.Hour,
2000000*time.Hour)
c.metricSink = &metricsutil.ClusterMetricSink{
ClusterName: "test-cluster",
Sink: inmemSink,
}

req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
req.Data["ttl"] = "3h"
req.Data["policies"] = []string{"foo"}
req.MountPoint = "test/mount"

resp := testMakeTokenViaRequest(t, ts, req)
if resp.Auth.ClientToken == "" {
t.Fatalf("bad: %#v", resp)
}

intervals := inmemSink.Data()
// Test crossed an interval boundary, don't try to deal with it.
if len(intervals) > 1 {
t.Skip("Detected interval crossing.")
}

keyPrefix := "token.creation"
var counter *metrics.SampledValue = nil

for _, c := range intervals[0].Counters {
if strings.HasPrefix(c.Name, keyPrefix) {
counter = &c
break
}
}
if counter == nil {
t.Fatal("No token.creation counter found.")
}

if counter.Count != 1 {
t.Errorf("Counter number of samples %v is not 1.", counter.Count)
}

if counter.Sum != 1.0 {
t.Errorf("Counter sum %v is not 1.", counter.Sum)
}

labels := make(map[string]string)
for _, l := range counter.Labels {
labels[l.Name] = l.Value
}
expected := map[string]string{
"cluster": "test-cluster",
"namespace": "root",
"auth_method": "token",
"mount_point": req.MountPoint,
"creation_ttl": "1d",
"token_type": "service",
}
for expected_label, expected_val := range expected {
if v, ok := labels[expected_label]; ok {
if v != expected_val {
t.Errorf("Label %q incorrect, expected %q, got %q", expected_label, expected_val, v)
}
} else {
t.Errorf("Label %q missing", expected_label)
}
}
}

func TestTokenStore_HandleRequest_Revoke(t *testing.T) {
exp := mockExpiration(t)
ts := exp.tokenStore
Expand Down