Skip to content

Commit

Permalink
feat(token): add support for ttl and max_ttl (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
thevilledev committed Jul 5, 2023
1 parent 78cca8f commit 8090629
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 32 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ lint:
golangci-lint run

test:
go test -v -race -parallel=4 ./...
go test -v -race -parallel=8 ./...

test-acc:
ACC_TEST=yes go test -race -parallel=4 ./...
Expand Down
28 changes: 14 additions & 14 deletions pkg/client/fixtures/auth_token.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interactions:
host: api.vercel.com
remote_addr: ""
request_uri: ""
body: '{"name":"vault-plugin-secrets-vercel-fixtures-token-1688557985495261000"}'
body: '{"name":"vault-plugin-secrets-vercel-fixtures-token-1688573641201647000"}'
form: {}
headers:
Content-Type:
Expand All @@ -27,7 +27,7 @@ interactions:
trailer: {}
content_length: 351
uncompressed: false
body: '{"token":{"id":"e69a2ddb9d93b314f67bd8caf205380d6a9d1be66548cc3e841687c502be083d","name":"vault-plugin-secrets-vercel-fixtures-token-1688557985495261000","type":"token","origin":"manual"},"bearerToken":"[REDACTED]"}'
body: '{"bearerToken":"REDACTED","token":{"activeAt":1688573641976,"createdAt":1688573641976,"id":"d92bf44b70e0a6ff96ac73764f7b973072e181f1365377a63f155ab75bbbeec2","name":"vault-plugin-secrets-vercel-fixtures-token-1688573641201647000","origin":"manual","scopes":[{"createdAt":1688573641976,"origin":"manual","type":"user"}],"type":"token"}}'
headers:
Access-Control-Allow-Headers:
- Authorization, Accept, Content-Type
Expand All @@ -44,24 +44,24 @@ interactions:
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 05 Jul 2023 11:53:06 GMT
- Wed, 05 Jul 2023 16:14:01 GMT
Server:
- Vercel
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
X-Ratelimit-Limit:
- "32"
X-Ratelimit-Remaining:
- "26"
- "19"
X-Ratelimit-Reset:
- "1688559854"
- "1688574204"
X-Vercel-Cache:
- MISS
X-Vercel-Id:
- sfo1::h4mlx-1688557986279-f70b30120caf
- sfo1::8ddlz-1688573641955-31483ee57ec3
status: 200 OK
code: 200
duration: 902.193791ms
duration: 836.402917ms
- id: 1
request:
proto: HTTP/1.1
Expand All @@ -78,7 +78,7 @@ interactions:
headers:
Content-Type:
- application/json
url: https://api.vercel.com/v3/user/tokens/e69a2ddb9d93b314f67bd8caf205380d6a9d1be66548cc3e841687c502be083d
url: https://api.vercel.com/v3/user/tokens/d92bf44b70e0a6ff96ac73764f7b973072e181f1365377a63f155ab75bbbeec2
method: DELETE
response:
proto: HTTP/1.1
Expand All @@ -88,7 +88,7 @@ interactions:
trailer: {}
content_length: 78
uncompressed: false
body: '{"tokenId":"e69a2ddb9d93b314f67bd8caf205380d6a9d1be66548cc3e841687c502be083d"}'
body: '{"tokenId":"d92bf44b70e0a6ff96ac73764f7b973072e181f1365377a63f155ab75bbbeec2"}'
headers:
Access-Control-Allow-Headers:
- Authorization, Accept, Content-Type
Expand All @@ -105,21 +105,21 @@ interactions:
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 05 Jul 2023 11:53:07 GMT
- Wed, 05 Jul 2023 16:14:03 GMT
Server:
- Vercel
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
X-Ratelimit-Limit:
- "50"
X-Ratelimit-Remaining:
- "45"
- "46"
X-Ratelimit-Reset:
- "1688558217"
- "1688573802"
X-Vercel-Cache:
- MISS
X-Vercel-Id:
- sfo1::h4mlx-1688557987539-e432e73a1484
- sfo1::8ddlz-1688573643105-597f8d4d7eaf
status: 200 OK
code: 200
duration: 376.691125ms
duration: 280.953167ms
2 changes: 1 addition & 1 deletion pkg/client/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

type CreateAuthTokenRequest struct {
Name string `json:"name"`
ExpiresAt int `json:"expiresAt,omitempty"`
ExpiresAt int64 `json:"expiresAt,omitempty"`
}

type CreateAuthTokenResponse struct {
Expand Down
14 changes: 8 additions & 6 deletions pkg/client/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,22 @@ func recordHelper(t *testing.T, fixture string, f func(context.Context, *testing
delete(i.Request.Headers, "Authorization")

if strings.Contains(i.Request.URL, "/user/tokens") && i.Request.Method == http.MethodPost {
c := &CreateAuthTokenResponse{}
if e := json.Unmarshal([]byte(i.Response.Body), c); e != nil {
var c map[string]any
if e := json.Unmarshal([]byte(i.Response.Body), &c); e != nil {
return e
}

c.BearerToken = "[REDACTED]"

newBody, e := json.Marshal(c)
_, ok := c["bearerToken"]
if ok {
c["bearerToken"] = "REDACTED"
}

res, e := json.Marshal(c)
if e != nil {
return e
}

i.Response.Body = string(newBody)
i.Response.Body = string(res)
}

return nil
Expand Down
30 changes: 28 additions & 2 deletions pkg/plugin/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package plugin
import (
"context"
"errors"
"time"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
Expand All @@ -13,6 +14,8 @@ const (
pathPatternConfig = "config"
pathConfigAPIKey = "api_key"
pathConfigBaseURL = "base_url"
pathConfigMaxTTL = "max_ttl"
defaultMaxTTL = 600
)

var (
Expand All @@ -21,8 +24,9 @@ var (
)

type backendConfig struct {
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
MaxTTL time.Duration `json:"max_ttl"`
}

func (b *backend) pathConfig() []*framework.Path {
Expand All @@ -40,6 +44,10 @@ func (b *backend) pathConfig() []*framework.Path {
Type: framework.TypeString,
Description: "Optional API base URL used by this backend.",
},
pathConfigMaxTTL: {
Type: framework.TypeDurationSecond,
Default: defaultMaxTTL,
},
},

Operations: map[logical.Operation]framework.OperationHandler{
Expand Down Expand Up @@ -93,6 +101,18 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request,
}
}

if vr, ok := data.GetOk(pathConfigMaxTTL); ok {
v, ta := vr.(int)
if !ta {
b.Logger().Trace("type assertion failed: %+v", v)
return nil, errTypeAssertionFailed
}

ttl := time.Duration(v) * time.Second

config.MaxTTL = time.Duration(ttl.Seconds())
}

if config.APIKey == "" {
return nil, errMissingAPIKey
}
Expand All @@ -101,6 +121,10 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request,
config.BaseURL = client.DefaultBaseURL
}

if config.MaxTTL == 0 {
config.MaxTTL = defaultMaxTTL
}

e, err := logical.StorageEntryJSON(pathPatternConfig, config)
if err != nil {
return nil, err
Expand All @@ -110,5 +134,7 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request,
return nil, err
}

b.Logger().Info("config initialised")

return &logical.Response{}, nil
}
35 changes: 30 additions & 5 deletions pkg/plugin/path_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const (
pathPatternToken = "token"
pathTokenID = "token_id"
pathTokenBearerToken = "bearer_token"
ttl = 10 * time.Second // TODO: add token specific TTL
pathTokenTTL = "ttl"
)

func (b *backend) pathToken() []*framework.Path {
Expand All @@ -31,6 +31,10 @@ func (b *backend) pathToken() []*framework.Path {
Type: framework.TypeString,
Description: "Generated API key.",
},
pathTokenTTL: {
Type: framework.TypeDurationSecond,
Description: "TTL for the generated API key. Less than or equal to the maximum TTL set in configuration.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Expand All @@ -48,7 +52,7 @@ func (b *backend) pathToken() []*framework.Path {
}

func (b *backend) pathTokenWrite(ctx context.Context, req *logical.Request,
_ *framework.FieldData) (*logical.Response, error) {
data *framework.FieldData) (*logical.Response, error) {
cfg, err := b.getConfig(ctx, req.Storage)
if err != nil {
return nil, err
Expand All @@ -58,11 +62,33 @@ func (b *backend) pathTokenWrite(ctx context.Context, req *logical.Request,
return nil, errMissingAPIKey
}

ttl := int64(0)

if vr, ok := data.GetOk(pathTokenTTL); ok {
v, ta := vr.(int)
if !ta {
b.Logger().Trace("type assertion failed: %+v", v)
return nil, errTypeAssertionFailed
}

ttl = int64(v)
}

if ttl == 0 {
ttl = int64(cfg.MaxTTL)
}

if ttl > int64(cfg.MaxTTL) {
return nil, fmt.Errorf("TTL %d exceeds maximum of %d", ttl, int64(cfg.MaxTTL))
}

svc := service.NewWithBaseURL(cfg.APIKey, cfg.BaseURL)
ts := time.Now().UnixNano()
name := fmt.Sprintf("%s-%d", keyPrefix, ts)

tokenID, bearerToken, err := svc.CreateAuthToken(ctx, name)
b.Logger().Info(fmt.Sprintf("creating token with %s and with TTL of %d", name, ttl))

tokenID, bearerToken, err := svc.CreateAuthToken(ctx, name, ttl)
if err != nil {
return nil, err
}
Expand All @@ -78,8 +104,7 @@ func (b *backend) pathTokenWrite(ctx context.Context, req *logical.Request,
pathTokenID: tokenID,
},
LeaseOptions: logical.LeaseOptions{
// TODO: add user-configurable TTL
TTL: time.Until(time.Now().Add(ttl)),
TTL: time.Duration(ttl) * time.Second,
},
},
}, nil
Expand Down
47 changes: 47 additions & 0 deletions pkg/plugin/path_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -73,6 +74,7 @@ func TestToken_Create(t *testing.T) {
require.NotNil(t, r)
require.Equal(t, r.Data["token_id"], "foo")
require.Equal(t, r.Data["bearer_token"], "zyzz")
require.Equal(t, r.Secret.LeaseOptions.TTL, defaultMaxTTL*time.Second)
require.Equal(t, r.Secret.InternalData["token_id"], "foo")
})

Expand Down Expand Up @@ -112,4 +114,49 @@ func TestToken_Create(t *testing.T) {
require.Error(t, err)
require.Nil(t, r)
})

t.Run("CreateTokenWithConflictingTTLs", func(t *testing.T) {
t.Parallel()

b, storage := newTestBackend(t)

ts := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
t.Helper()

body, _ := json.Marshal(&client.CreateAuthTokenResponse{
Token: client.Token{
ID: "foo",
Name: "bar",
},
BearerToken: "zyzz",
})
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}),
)
defer ts.Close()

_, err := b.HandleRequest(context.Background(), &logical.Request{
Storage: storage,
Operation: logical.CreateOperation,
Path: pathPatternConfig,
Data: map[string]interface{}{
"api_key": "foo",
"base_url": ts.URL,
"max_ttl": 10,
},
})
require.NoError(t, err)

_, err = b.HandleRequest(context.Background(), &logical.Request{
Storage: storage,
Operation: logical.CreateOperation,
Path: pathPatternToken,
Data: map[string]any{
"ttl": 11,
},
})
require.Error(t, err)
})
}
13 changes: 11 additions & 2 deletions pkg/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package service

import (
"context"
"fmt"
"net/http"
"time"

"github.com/thevilledev/vault-plugin-secrets-vercel/pkg/client"
)
Expand Down Expand Up @@ -32,10 +34,17 @@ func NewWithBaseURL(apiKey string, baseURL string) *Service {
}
}

func (s *Service) CreateAuthToken(ctx context.Context, name string) (string, string, error) {
func (s *Service) CreateAuthToken(ctx context.Context, name string, ttl int64) (string, string, error) {
if ttl <= 0 {
return "", "", fmt.Errorf("cannot create token with a ttl of 0")
}

expiresAt := time.Now().Add(time.Duration(ttl) * time.Second).UTC().UnixMilli()
r, err := s.apiClient.CreateAuthToken(ctx, &client.CreateAuthTokenRequest{
Name: name,
Name: name,
ExpiresAt: expiresAt,
})

if err != nil {
return "", "", err
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ func TestIntegration_Token(t *testing.T) {
a := New(token)
ctx := context.Background()

tokenID, bearerToken, err := a.CreateAuthToken(ctx, "foobar")
ttl := int64(10)
tokenID, bearerToken, err := a.CreateAuthToken(ctx, "foobar", ttl)
require.NoError(t, err)
require.NotEmpty(t, tokenID)
require.NotEmpty(t, bearerToken)
Expand Down

0 comments on commit 8090629

Please sign in to comment.