Creating Auth Token Repo (#125)
This commit is contained in:
1 parent
bb4a185412
commit
f69d6b9952
17 files changed
+1905
No files matched your search
@@ -72,6 +72,7 @@ protobuild:
|
||||
@protoc-go-inject-tag -input=./internal/iam/store/group.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/db/db_test/db_test.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/host/static/store/static.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/authtoken/store/authtoken.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/iam/store/auth_account.pb.go
|
||||
@rm -R ${TMP_DIR}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package authtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-kms-wrapping/structwrapping"
|
||||
"github.com/hashicorp/vault/sdk/helper/base62"
|
||||
"github.com/hashicorp/watchtower/internal/authtoken/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// A AuthToken contains auth tokens. It is owned by a scope.
|
||||
type AuthToken struct {
|
||||
*store.AuthToken
|
||||
tableName string `gorm:"-"`
|
||||
}
|
||||
|
||||
func (s *AuthToken) clone() *AuthToken {
|
||||
cp := proto.Clone(s.AuthToken)
|
||||
return &AuthToken{
|
||||
AuthToken: cp.(*store.AuthToken),
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt the entry's data using the provided cipher (wrapping.Wrapper)
|
||||
func (s *AuthToken) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
// structwrapping doesn't support embedding, so we'll pass in the store.Entry directly
|
||||
if err := structwrapping.WrapStruct(ctx, cipher, s.AuthToken, nil); err != nil {
|
||||
return fmt.Errorf("error encrypting auth token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decrypt will decrypt the auth token's value using the provided cipher (wrapping.Wrapper)
|
||||
func (s *AuthToken) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
// structwrapping doesn't support embedding, so we'll pass in the store.Entry directly
|
||||
if err := structwrapping.UnwrapStruct(ctx, cipher, s.AuthToken, nil); err != nil {
|
||||
return fmt.Errorf("error decrypting auth token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
AuthTokenPrefix = "t"
|
||||
// The version prefix is used to differentiate token versions just for future proofing.
|
||||
TokenValueVersionPrefix = "0"
|
||||
tokenLength = 24
|
||||
)
|
||||
|
||||
func newAuthTokenId() (string, error) {
|
||||
id, err := db.NewPublicId(AuthTokenPrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new auth token id: %w", err)
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
// newAuthToken generates a token with a version prefix.
|
||||
func newAuthToken() (string, error) {
|
||||
token, err := base62.Random(tokenLength)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to generate auth token: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("%s%s", TokenValueVersionPrefix, token), nil
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package authtoken
|
||||
|
||||
// This file contains tests for methods defined in authtoken.go as well as tests which exercise the db
|
||||
// functionality directly without going through the respository. Repository centric tests should be
|
||||
// placed in repository_test.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/hashicorp/watchtower/internal/authtoken/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/db/timestamp"
|
||||
"github.com/hashicorp/watchtower/internal/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestAuthToken_DbUpdate(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
org, _ := iam.TestScopes(t, conn)
|
||||
u := iam.TestUser(t, conn, org.GetPublicId())
|
||||
amId := setupAuthMethod(t, conn, org.GetPublicId())
|
||||
acct := setupAuthAccount(t, conn, org.GetPublicId(), amId, u.GetPublicId())
|
||||
|
||||
newAuthTokId, err := newAuthTokenId()
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
fieldMask []string
|
||||
nullMask []string
|
||||
authTok *store.AuthToken
|
||||
}
|
||||
future, err := ptypes.TimestampProto(time.Now().Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
args args
|
||||
want *AuthToken
|
||||
cnt int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "immutable-authacctid",
|
||||
args: args{
|
||||
fieldMask: []string{"AuthAccountId"},
|
||||
authTok: &store.AuthToken{AuthAccountId: acct.GetPublicId()},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "immutable-publicid",
|
||||
args: args{
|
||||
fieldMask: []string{"PublicId"},
|
||||
authTok: &store.AuthToken{PublicId: newAuthTokId},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "update-last-access-time",
|
||||
args: args{
|
||||
fieldMask: []string{"ApproximateLastAccessTime"},
|
||||
authTok: &store.AuthToken{ApproximateLastAccessTime: ×tamp.Timestamp{Timestamp: future}},
|
||||
},
|
||||
cnt: 1,
|
||||
},
|
||||
{
|
||||
name: "nullify-last-access-time",
|
||||
args: args{
|
||||
nullMask: []string{"ApproximateLastAccessTime"},
|
||||
authTok: &store.AuthToken{},
|
||||
},
|
||||
cnt: 1,
|
||||
},
|
||||
{
|
||||
name: "update-expiration",
|
||||
args: args{
|
||||
fieldMask: []string{"ExpirationTime"},
|
||||
authTok: &store.AuthToken{ExpirationTime: ×tamp.Timestamp{Timestamp: future}},
|
||||
},
|
||||
cnt: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
w := db.New(conn)
|
||||
|
||||
authTok := testAuthToken(t, conn, wrapper)
|
||||
proto.Merge(authTok.AuthToken, tt.args.authTok)
|
||||
|
||||
err := authTok.encrypt(context.Background(), wrapper)
|
||||
require.NoError(t, err)
|
||||
cnt, err := w.Update(context.Background(), authTok, tt.args.fieldMask, tt.args.nullMask)
|
||||
if tt.wantErr {
|
||||
t.Logf("Got error :%v", err)
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
assert.Equal(tt.cnt, cnt)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthToken_DbCreate(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
org, _ := iam.TestScopes(t, conn)
|
||||
u := iam.TestUser(t, conn, org.GetPublicId())
|
||||
amId := setupAuthMethod(t, conn, org.GetPublicId())
|
||||
acct := setupAuthAccount(t, conn, org.GetPublicId(), amId, u.GetPublicId())
|
||||
createdAuthToken := testAuthToken(t, conn, wrapper)
|
||||
|
||||
testAuthTokenId := func() string {
|
||||
id, err := newAuthTokenId()
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
in *store.AuthToken
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
in: &store.AuthToken{
|
||||
PublicId: testAuthTokenId(),
|
||||
Token: "anything",
|
||||
AuthAccountId: acct.GetPublicId(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate-id",
|
||||
in: &store.AuthToken{
|
||||
PublicId: createdAuthToken.GetPublicId(),
|
||||
Token: "duplicateid_test",
|
||||
AuthAccountId: acct.GetPublicId(),
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
at := &AuthToken{AuthToken: tt.in}
|
||||
err := at.encrypt(context.Background(), wrapper)
|
||||
require.NoError(t, err)
|
||||
err = db.New(conn).Create(context.Background(), at)
|
||||
if tt.wantError {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthToken_DbDelete(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
testAuthTokenId := func() string {
|
||||
id, err := newAuthTokenId()
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
wrapper := db.TestWrapper(t)
|
||||
existingAuthTok := testAuthToken(t, conn, wrapper)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
at *AuthToken
|
||||
wantError bool
|
||||
wantCnt int
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
at: &AuthToken{AuthToken: &store.AuthToken{PublicId: existingAuthTok.GetPublicId()}},
|
||||
wantCnt: 1,
|
||||
},
|
||||
{
|
||||
name: "delete-nothing",
|
||||
at: &AuthToken{AuthToken: &store.AuthToken{PublicId: testAuthTokenId()}},
|
||||
wantCnt: 0,
|
||||
},
|
||||
{
|
||||
name: "delete-nil",
|
||||
at: nil,
|
||||
wantCnt: 0,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "delete-no-public-id",
|
||||
at: &AuthToken{AuthToken: &store.AuthToken{}},
|
||||
wantCnt: 0,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cnt, err := db.New(conn).Delete(context.Background(), tt.at)
|
||||
assert.Equal(tt.wantCnt, cnt)
|
||||
if tt.wantError {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Package authtoken provides an authtoken with an encrypted value and
|
||||
// an associated expiration time. It also provides a repository which
|
||||
// manages the lifetime of the token.
|
||||
//
|
||||
// The auth token value is a base62 bit value with a version prefix. This
|
||||
// value is encrypted at rest and is used to authenticate incoming requests
|
||||
// to the controller. It is associated with a public id which allows admins
|
||||
// to operate on it without knowing the token itself. It also has an
|
||||
// expiration time and a last accessed time which are used to determine if the
|
||||
// token can still be used.
|
||||
//
|
||||
// Repository
|
||||
//
|
||||
// A repository provides methods for creating, validating a provided token value,
|
||||
// and deleting the auth token. At validation time if the token is determined
|
||||
// to be expired or stale it will be removed from the backing storage by the repo.
|
||||
package authtoken
|
||||
@@ -0,0 +1,16 @@
|
||||
package authtoken
|
||||
|
||||
// TableName returns the table name for the auth token.
|
||||
func (s *AuthToken) TableName() string {
|
||||
if s.tableName != "" {
|
||||
return s.tableName
|
||||
}
|
||||
return "auth_token"
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (s *AuthToken) SetTableName(n string) {
|
||||
if n != "" {
|
||||
s.tableName = n
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package authtoken
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments.
|
||||
type Option func(*options)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withTokenValue bool
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{}
|
||||
}
|
||||
|
||||
// withTokenValue allows the auth token value to be included in the lookup response.
|
||||
// This is purposefully not exported as it should only be used internally by the auth token repository itself.
|
||||
func withTokenValue() Option {
|
||||
return func(o *options) {
|
||||
o.withTokenValue = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package authtoken
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test_GetOpts provides unit tests for GetOpts and all the options
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("withTokenValue", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(withTokenValue())
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withTokenValue = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package authtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/watchtower/internal/db/timestamp"
|
||||
"github.com/hashicorp/watchtower/internal/iam"
|
||||
iamStore "github.com/hashicorp/watchtower/internal/iam/store"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/authtoken/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/oplog"
|
||||
)
|
||||
|
||||
// TODO (ICU-406): Make these fields configurable.
|
||||
var (
|
||||
lastAccessedUpdateDuration = 10 * time.Minute
|
||||
maxStaleness = 24 * time.Hour
|
||||
maxTokenDuration = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// A Repository stores and retrieves the persistent types in the authtoken
|
||||
// package. It is not safe to use a repository concurrently.
|
||||
type Repository struct {
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
wrapper wrapping.Wrapper
|
||||
}
|
||||
|
||||
// NewRepository creates a new Repository. The returned repository is not safe for concurrent go
|
||||
// routines to access it.
|
||||
func NewRepository(r db.Reader, w db.Writer, wrapper wrapping.Wrapper) (*Repository, error) {
|
||||
switch {
|
||||
case r == nil:
|
||||
return nil, fmt.Errorf("db.Reader: auth token: %w", db.ErrNilParameter)
|
||||
case w == nil:
|
||||
return nil, fmt.Errorf("db.Writer: auth token: %w", db.ErrNilParameter)
|
||||
case wrapper == nil:
|
||||
return nil, fmt.Errorf("wrapping.Wrapper: auth token: %w", db.ErrNilParameter)
|
||||
}
|
||||
|
||||
return &Repository{
|
||||
reader: r,
|
||||
writer: w,
|
||||
wrapper: wrapper,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateAuthToken inserts an Auth Token into the repository and returns a new Auth Token. The returned auth token
|
||||
// contains the auth token value. The provided IAM User ID must be associated to the provided auth account id
|
||||
// or an error will be returned. All options are ignored.
|
||||
func (r *Repository) CreateAuthToken(ctx context.Context, withIamUserId, withAuthAccountId string, opt ...Option) (*AuthToken, error) {
|
||||
if withIamUserId == "" {
|
||||
return nil, fmt.Errorf("create: auth token: no user id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if withAuthAccountId == "" {
|
||||
return nil, fmt.Errorf("create: auth token: no auth account id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
at := allocAuthToken()
|
||||
at.AuthAccountId = withAuthAccountId
|
||||
|
||||
id, err := newAuthTokenId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create: auth token id: %w", err)
|
||||
}
|
||||
at.PublicId = id
|
||||
|
||||
token, err := newAuthToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create: auth token value: %w", err)
|
||||
}
|
||||
at.Token = token
|
||||
|
||||
// TODO: Allow the caller to specify something different than the default duration.
|
||||
expiration, err := ptypes.TimestampProto(time.Now().Add(maxTokenDuration))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
at.ExpirationTime = ×tamp.Timestamp{Timestamp: expiration}
|
||||
|
||||
var newAuthToken *AuthToken
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(read db.Reader, w db.Writer) error {
|
||||
// TODO: Remove this and either rely on either Alloc or a method exposed by the auth repo.
|
||||
acct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{PublicId: withAuthAccountId}}
|
||||
if err := read.LookupByPublicId(ctx, acct); err != nil {
|
||||
return fmt.Errorf("create: auth token: auth account lookup: %w", err)
|
||||
}
|
||||
if acct.GetIamUserId() != withIamUserId {
|
||||
return fmt.Errorf("create: auth token: auth account %q mismatch with iam user %q", withAuthAccountId, withIamUserId)
|
||||
}
|
||||
at.ScopeId = acct.GetScopeId()
|
||||
at.AuthMethodId = acct.GetAuthMethodId()
|
||||
at.IamUserId = acct.GetIamUserId()
|
||||
|
||||
metadata := newAuthTokenMetadata(at, oplog.OpType_OP_TYPE_CREATE)
|
||||
|
||||
newAuthToken = at.clone()
|
||||
if err := newAuthToken.encrypt(ctx, r.wrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Create(ctx, newAuthToken, db.WithOplog(r.wrapper, metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
newAuthToken.CtToken = nil
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create: auth token: %v: %w", at, err)
|
||||
}
|
||||
return newAuthToken, nil
|
||||
}
|
||||
|
||||
// LookupAuthToken returns the AuthToken for the provided id. Returns nil, nil if no AuthToken is found for id.
|
||||
// For security reasons, the actual token is not included in the returned AuthToken.
|
||||
// All exported options are ignored.
|
||||
func (r *Repository) LookupAuthToken(ctx context.Context, id string, opt ...Option) (*AuthToken, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("lookup: auth token: missing public id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
|
||||
at := allocAuthToken()
|
||||
at.PublicId = id
|
||||
// Aggregate the fields across auth token and auth accounts by using this view instead of issuing 2 db lookups.
|
||||
at.SetTableName("auth_token_account")
|
||||
if err := r.reader.LookupByPublicId(ctx, at); err != nil {
|
||||
if errors.Is(err, db.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("auth token: lookup: %w", err)
|
||||
}
|
||||
if opts.withTokenValue {
|
||||
if err := at.decrypt(ctx, r.wrapper); err != nil {
|
||||
return nil, fmt.Errorf("lookup: auth token: cannot decrypt auth token value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
at.CtToken = nil
|
||||
return at, nil
|
||||
}
|
||||
|
||||
// ValidateToken returns a token from storage if the auth token with the provided id and token exists. The
|
||||
// approximate last accessed time may be updated depending on how long it has been since the last time the token
|
||||
// was validated. If a token is returned it is guaranteed to be valid. For security reasons, the actual token
|
||||
// value is not included in the returned AuthToken. If no valid auth token is found nil, nil is returned.
|
||||
// All options are ignored.
|
||||
//
|
||||
// NOTE: Do not log or add the token string to any errors to avoid leaking it as it is a secret.
|
||||
func (r *Repository) ValidateToken(ctx context.Context, id, token string, opt ...Option) (*AuthToken, error) {
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("validate token: auth token: missing token: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("validate token: auth token: missing public id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
retAT, err := r.LookupAuthToken(ctx, id, withTokenValue())
|
||||
if err != nil {
|
||||
retAT = nil
|
||||
if errors.Is(err, db.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("validate token: %w", err)
|
||||
}
|
||||
if retAT == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If the token is too old or stale invalidate it and return nothing.
|
||||
exp, err := ptypes.Timestamp(retAT.GetExpirationTime().GetTimestamp())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validate token: expiration time : %w", err)
|
||||
}
|
||||
lastAccessed, err := ptypes.Timestamp(retAT.GetApproximateLastAccessTime().GetTimestamp())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validate token: last accessed time : %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
sinceLastAccessed := now.Sub(lastAccessed)
|
||||
if now.After(exp) || sinceLastAccessed > maxStaleness {
|
||||
// If the token has expired or has become too stale, delete it from the DB.
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
metadata := newAuthTokenMetadata(retAT, oplog.OpType_OP_TYPE_DELETE)
|
||||
delAt := retAT.clone()
|
||||
if _, err := w.Delete(ctx, delAt, db.WithOplog(r.wrapper, metadata)); err != nil {
|
||||
return fmt.Errorf("validate token: delete auth token: %w", err)
|
||||
}
|
||||
retAT = nil
|
||||
return nil
|
||||
})
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if retAT.GetToken() != token {
|
||||
return nil, fmt.Errorf("validate token: auth token mismatch: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
// retAT.Token set to empty string so the value is not returned as described in the methods' doc.
|
||||
retAT.Token = ""
|
||||
|
||||
if sinceLastAccessed >= lastAccessedUpdateDuration {
|
||||
// To save the db from being updated too frequently, we only update the
|
||||
// LastAccessTime if it hasn't been updated within lastAccessedUpdateDuration.
|
||||
// TODO: Make this duration configurable.
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
// Setting the ApproximateLastAccessTime to null through using the null mask allows a defined db's
|
||||
// trigger to set ApproximateLastAccessTime to the commit timestamp.
|
||||
at := retAT.clone()
|
||||
metadata := newAuthTokenMetadata(retAT, oplog.OpType_OP_TYPE_UPDATE)
|
||||
rowsUpdated, err := w.Update(
|
||||
ctx,
|
||||
at,
|
||||
nil,
|
||||
[]string{"ApproximateLastAccessTime"},
|
||||
db.WithOplog(r.wrapper, metadata),
|
||||
)
|
||||
if err == nil && rowsUpdated > 1 {
|
||||
return db.ErrMultipleRecords
|
||||
}
|
||||
return err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validate token: auth token: %s: %w", id, err)
|
||||
}
|
||||
return retAT, nil
|
||||
}
|
||||
|
||||
// TODO(ICU-344): Add ListAuthTokens
|
||||
|
||||
// DeleteAuthToken deletes the token with the provided id from the repository returning a count of the
|
||||
// number of records deleted. All options are ignored.
|
||||
func (r *Repository) DeleteAuthToken(ctx context.Context, id string, opt ...Option) (int, error) {
|
||||
if id == "" {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete: auth token: missing public id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
at, err := r.LookupAuthToken(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrRecordNotFound) {
|
||||
return db.NoRowsAffected, nil
|
||||
}
|
||||
return db.NoRowsAffected, fmt.Errorf("delete: auth token: lookup %w", err)
|
||||
}
|
||||
if at == nil {
|
||||
return db.NoRowsAffected, nil
|
||||
}
|
||||
|
||||
var rowsDeleted int
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
metadata := newAuthTokenMetadata(at, oplog.OpType_OP_TYPE_DELETE)
|
||||
|
||||
deleteAT := at.clone()
|
||||
rowsDeleted, err = w.Delete(ctx, deleteAT, db.WithOplog(r.wrapper, metadata))
|
||||
if err == nil && rowsDeleted > 1 {
|
||||
return db.ErrMultipleRecords
|
||||
}
|
||||
return err
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, fmt.Errorf("delete: auth token: %s: %w", id, err)
|
||||
}
|
||||
|
||||
return rowsDeleted, nil
|
||||
}
|
||||
|
||||
func allocAuthToken() *AuthToken {
|
||||
fresh := &AuthToken{
|
||||
AuthToken: &store.AuthToken{},
|
||||
}
|
||||
return fresh
|
||||
}
|
||||
|
||||
func newAuthTokenMetadata(at *AuthToken, op oplog.OpType) oplog.Metadata {
|
||||
metadata := oplog.Metadata{
|
||||
"scope-id": []string{at.GetScopeId()},
|
||||
"resource-public-id": []string{at.GetPublicId()},
|
||||
"resource-type": []string{"auth token"},
|
||||
"op-type": []string{op.String()},
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
package authtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
iamStore "github.com/hashicorp/watchtower/internal/iam/store"
|
||||
"github.com/hashicorp/watchtower/internal/oplog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/authtoken/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/iam"
|
||||
)
|
||||
|
||||
func TestRepository_New(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
type args struct {
|
||||
r db.Reader
|
||||
w db.Writer
|
||||
wrapper wrapping.Wrapper
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
args args
|
||||
want *Repository
|
||||
wantIsErr error
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
wrapper: wrapper,
|
||||
},
|
||||
want: &Repository{
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
wrapper: wrapper,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil-reader",
|
||||
args: args{
|
||||
r: nil,
|
||||
w: rw,
|
||||
wrapper: wrapper,
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: db.ErrNilParameter,
|
||||
},
|
||||
{
|
||||
name: "nil-writer",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: nil,
|
||||
wrapper: wrapper,
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: db.ErrNilParameter,
|
||||
},
|
||||
{
|
||||
name: "nil-wrapper",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
wrapper: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: db.ErrNilParameter,
|
||||
},
|
||||
{
|
||||
name: "all-nils",
|
||||
args: args{
|
||||
r: nil,
|
||||
w: nil,
|
||||
wrapper: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantIsErr: db.ErrNilParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
got, err := NewRepository(tt.args.r, tt.args.w, tt.args.wrapper)
|
||||
if tt.wantIsErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
|
||||
assert.Nil(got)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.NotNil(got)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_CreateAuthToken(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
org1, _ := iam.TestScopes(t, conn)
|
||||
u1 := iam.TestUser(t, conn, org1.GetPublicId())
|
||||
amId1 := setupAuthMethod(t, conn, org1.GetPublicId())
|
||||
aAcct := setupAuthAccount(t, conn, org1.GetPublicId(), amId1, u1.GetPublicId())
|
||||
|
||||
org2, _ := iam.TestScopes(t, conn)
|
||||
u2 := iam.TestUser(t, conn, org2.GetPublicId())
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
iamUserId string
|
||||
authAcctId string
|
||||
want *AuthToken
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
iamUserId: u1.GetPublicId(),
|
||||
authAcctId: aAcct.GetPublicId(),
|
||||
want: &AuthToken{
|
||||
AuthToken: &store.AuthToken{
|
||||
AuthAccountId: aAcct.GetPublicId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unconnected-authaccount-user",
|
||||
iamUserId: u2.GetPublicId(),
|
||||
authAcctId: aAcct.GetPublicId(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no-authacctid",
|
||||
iamUserId: u1.GetPublicId(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no-userid",
|
||||
authAcctId: aAcct.GetPublicId(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-authacctid",
|
||||
iamUserId: u1.GetPublicId(),
|
||||
authAcctId: "this_is_invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-userid",
|
||||
iamUserId: "this_is_invalid",
|
||||
authAcctId: aAcct.GetPublicId(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
require.NoError(err)
|
||||
require.NotNil(repo)
|
||||
got, err := repo.CreateAuthToken(context.Background(), tt.iamUserId, tt.authAcctId)
|
||||
if tt.wantErr {
|
||||
assert.Error(err)
|
||||
assert.Nil(got)
|
||||
return
|
||||
}
|
||||
require.NoError(err, "Got error for CreateAuthToken(ctx, %v, %v)", tt.iamUserId, tt.authAcctId)
|
||||
assert.NotNil(got)
|
||||
db.AssertPublicId(t, AuthTokenPrefix, got.PublicId)
|
||||
assert.Equal(tt.authAcctId, got.GetAuthAccountId())
|
||||
assert.Equal(got.CreateTime, got.UpdateTime)
|
||||
assert.Equal(got.CreateTime, got.ApproximateLastAccessTime)
|
||||
assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_LookupAuthToken(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
at := testAuthToken(t, conn, wrapper)
|
||||
at.Token = ""
|
||||
at.CtToken = nil
|
||||
|
||||
badId, err := newAuthTokenId()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, badId)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
id string
|
||||
want *AuthToken
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "found",
|
||||
id: at.GetPublicId(),
|
||||
want: at,
|
||||
},
|
||||
{
|
||||
name: "not-found",
|
||||
id: badId,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "bad-public-id",
|
||||
id: "",
|
||||
want: nil,
|
||||
wantErr: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
require.NoError(err)
|
||||
require.NotNil(repo)
|
||||
|
||||
got, err := repo.LookupAuthToken(context.Background(), tt.id)
|
||||
if tt.wantErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantErr), "want err: %q got: %q", tt.wantErr, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
if got == nil {
|
||||
assert.Nil(tt.want)
|
||||
return
|
||||
}
|
||||
require.NotNil(tt.want, "got %v, wanted nil", got)
|
||||
assert.Empty(cmp.Diff(tt.want.AuthToken, got.AuthToken, protocmp.Transform()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_ValidateToken(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
lastAccessedUpdateDuration = 0
|
||||
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
at := testAuthToken(t, conn, wrapper)
|
||||
atToken := at.GetToken()
|
||||
at.Token = ""
|
||||
at.CtToken = nil
|
||||
atTime, err := ptypes.Timestamp(at.GetApproximateLastAccessTime().GetTimestamp())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, atTime)
|
||||
|
||||
badId, err := newAuthTokenId()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, badId)
|
||||
|
||||
badToken, err := newAuthToken()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, badToken)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
id string
|
||||
token string
|
||||
want *AuthToken
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "exists",
|
||||
id: at.GetPublicId(),
|
||||
token: atToken,
|
||||
want: at,
|
||||
},
|
||||
{
|
||||
name: "doesnt-exist",
|
||||
id: badId,
|
||||
token: badToken,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty-token",
|
||||
id: at.GetPublicId(),
|
||||
token: "",
|
||||
want: nil,
|
||||
wantErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "mismatched-token",
|
||||
id: at.GetPublicId(),
|
||||
token: badToken,
|
||||
want: nil,
|
||||
wantErr: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := repo.ValidateToken(context.Background(), tt.id, tt.token)
|
||||
if tt.wantErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantErr), "want err: %q got: %q", tt.wantErr, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
if got == nil {
|
||||
assert.Nil(tt.want)
|
||||
// No need to compare updated time if we didn't get an initial auth token to compare against.
|
||||
return
|
||||
}
|
||||
require.NotNil(tt.want, "Got %v but wanted nil", got)
|
||||
assert.Empty(cmp.Diff(tt.want.AuthToken, got.AuthToken, protocmp.Transform()))
|
||||
// preTime1 should be the value prior to the ValidateToken was called so it should equal creation time
|
||||
preTime1, err := ptypes.Timestamp(got.GetApproximateLastAccessTime().GetTimestamp())
|
||||
require.NoError(err)
|
||||
assert.True(preTime1.Equal(atTime), "Create time %q doesn't match the time from the first call to MaybeUpdateLastAccesssed: %q.", atTime, preTime1)
|
||||
|
||||
// Enable the duration which limits how frequently a token's approximate last accessed time can be updated
|
||||
// so the next call doesn't cause the last accessed time to be updated.
|
||||
lastAccessedUpdateDuration = 1 * time.Hour
|
||||
|
||||
got2, err := repo.ValidateToken(context.Background(), tt.id, tt.token)
|
||||
assert.NoError(err)
|
||||
preTime2, err := ptypes.Timestamp(got2.GetApproximateLastAccessTime().GetTimestamp())
|
||||
require.NoError(err)
|
||||
assert.True(preTime2.After(preTime1), "First updated time %q was not after the creation time %q", preTime2, preTime1)
|
||||
|
||||
assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE)))
|
||||
|
||||
got3, err := repo.ValidateToken(context.Background(), tt.id, tt.token)
|
||||
preTime3, err := ptypes.Timestamp(got3.GetApproximateLastAccessTime().GetTimestamp())
|
||||
require.NoError(err)
|
||||
assert.True(preTime3.Equal(preTime2), "The 3rd timestamp %q was not equal to the second time %q", preTime3, preTime2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_ValidateToken_expired(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
baseAT := testAuthToken(t, conn, wrapper)
|
||||
baseAT.GetAuthAccountId()
|
||||
aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{PublicId: baseAT.GetAuthAccountId()}}
|
||||
require.NoError(t, rw.LookupByPublicId(context.Background(), aAcct))
|
||||
iamUserId := aAcct.GetIamUserId()
|
||||
|
||||
defaultStaleTime := maxStaleness
|
||||
defaultExpireDuration := maxTokenDuration
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
staleDuration time.Duration
|
||||
expirationDuration time.Duration
|
||||
wantReturned bool
|
||||
}{
|
||||
{
|
||||
name: "not-stale-or-expired",
|
||||
staleDuration: maxStaleness,
|
||||
expirationDuration: maxTokenDuration,
|
||||
wantReturned: true,
|
||||
},
|
||||
{
|
||||
name: "stale",
|
||||
staleDuration: 0,
|
||||
expirationDuration: maxTokenDuration,
|
||||
wantReturned: false,
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
staleDuration: maxStaleness,
|
||||
expirationDuration: 0,
|
||||
wantReturned: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
maxStaleness = tt.staleDuration
|
||||
maxTokenDuration = tt.expirationDuration
|
||||
|
||||
ctx := context.Background()
|
||||
at, err := repo.CreateAuthToken(ctx, iamUserId, baseAT.GetAuthAccountId())
|
||||
require.NoError(err)
|
||||
|
||||
got, err := repo.ValidateToken(ctx, at.GetPublicId(), at.GetToken())
|
||||
require.NoError(err)
|
||||
|
||||
if tt.wantReturned {
|
||||
assert.NotNil(got)
|
||||
} else {
|
||||
assert.NoError(db.TestVerifyOplog(t, rw, at.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE)))
|
||||
assert.Nil(got)
|
||||
}
|
||||
|
||||
// reset the system default params
|
||||
maxStaleness = defaultStaleTime
|
||||
maxTokenDuration = defaultExpireDuration
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_DeleteAuthToken(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
|
||||
at := testAuthToken(t, conn, wrapper)
|
||||
badId, err := newAuthTokenId()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, badId)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
id string
|
||||
want int
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "found",
|
||||
id: at.GetPublicId(),
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "not-found",
|
||||
id: badId,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "empty-public-id",
|
||||
id: "",
|
||||
want: 0,
|
||||
wantErr: db.ErrInvalidParameter,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
require.NoError(err)
|
||||
require.NotNil(repo)
|
||||
|
||||
got, err := repo.DeleteAuthToken(context.Background(), tt.id)
|
||||
if tt.wantErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantErr), "want err: %q got: %q", tt.wantErr, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tt.want, got, "row count")
|
||||
if tt.want != 0 {
|
||||
assert.NoError(db.TestVerifyOplog(t, rw, tt.id, db.WithOperation(oplog.OpType_OP_TYPE_DELETE)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.24.0
|
||||
// protoc v3.12.3
|
||||
// source: controller/storage/authtoken/store/v1/authtoken.proto
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
timestamp "github.com/hashicorp/watchtower/internal/db/timestamp"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This is a compile-time assertion that a sufficiently up-to-date version
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
type AuthToken struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// public_id is used to access the auth token via an API
|
||||
// @inject_tag: gorm:"primary_key"
|
||||
PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"`
|
||||
// create_time from the RDBMS
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// update_time from the RDBMS
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// last_access_time indicates the last time the auth token was used on the watchtower API.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
ApproximateLastAccessTime *timestamp.Timestamp `protobuf:"bytes,4,opt,name=approximate_last_access_time,json=approximateLastAccessTime,proto3" json:"approximate_last_access_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// expiration_time indicates when this session will expire.
|
||||
// If null a default duration and create_time is used to calculate expiration.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
ExpirationTime *timestamp.Timestamp `protobuf:"bytes,5,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty" gorm:"default:null"`
|
||||
// ciphertext token value stored in the database
|
||||
// @inject_tag: gorm:"column:token;not_null" wrapping:"ct,authtoken_token"
|
||||
CtToken []byte `protobuf:"bytes,6,opt,name=ct_token,json=ctToken,proto3" json:"ct_token,omitempty" gorm:"column:token;not_null" wrapping:"ct,authtoken_token"`
|
||||
// plain text version of the decrypted authtoken value
|
||||
// we are NOT storing this plain-text entry data in the db
|
||||
// token is the field stored and used by the client
|
||||
// @inject_tag: gorm:"-" wrapping:"pt,authtoken_token"
|
||||
Token string `protobuf:"bytes,7,opt,name=token,proto3" json:"token,omitempty" gorm:"-" wrapping:"pt,authtoken_token"`
|
||||
// auth_account_id is the public id for the auth account this auth token
|
||||
// was generated for.
|
||||
// @inject_tag: `gorm:"default:not_null"`
|
||||
AuthAccountId string `protobuf:"bytes,10,opt,name=auth_account_id,json=authAccountId,proto3" json:"auth_account_id,omitempty" gorm:"default:not_null"`
|
||||
// scope_id is not stored in the backing DB but it derived from the linked to auth account.
|
||||
// @inject_tag: gorm:"-"
|
||||
ScopeId string `protobuf:"bytes,11,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"-"`
|
||||
// auth_method_id is not stored in the backing DB but it derived from the linked to auth account.
|
||||
// @inject_tag: gorm:"-"
|
||||
AuthMethodId string `protobuf:"bytes,12,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty" gorm:"-"`
|
||||
// iam_user_id is not stored in the backing DB but it derived from the linked to auth account.
|
||||
// @inject_tag: gorm:"-"
|
||||
IamUserId string `protobuf:"bytes,13,opt,name=iam_user_id,json=iamUserId,proto3" json:"iam_user_id,omitempty" gorm:"-"`
|
||||
}
|
||||
|
||||
func (x *AuthToken) Reset() {
|
||||
*x = AuthToken{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_controller_storage_authtoken_store_v1_authtoken_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AuthToken) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthToken) ProtoMessage() {}
|
||||
|
||||
func (x *AuthToken) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_controller_storage_authtoken_store_v1_authtoken_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthToken.ProtoReflect.Descriptor instead.
|
||||
func (*AuthToken) Descriptor() ([]byte, []int) {
|
||||
return file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetPublicId() string {
|
||||
if x != nil {
|
||||
return x.PublicId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetCreateTime() *timestamp.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetUpdateTime() *timestamp.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetApproximateLastAccessTime() *timestamp.Timestamp {
|
||||
if x != nil {
|
||||
return x.ApproximateLastAccessTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetExpirationTime() *timestamp.Timestamp {
|
||||
if x != nil {
|
||||
return x.ExpirationTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetCtToken() []byte {
|
||||
if x != nil {
|
||||
return x.CtToken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetToken() string {
|
||||
if x != nil {
|
||||
return x.Token
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetAuthAccountId() string {
|
||||
if x != nil {
|
||||
return x.AuthAccountId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetScopeId() string {
|
||||
if x != nil {
|
||||
return x.ScopeId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetAuthMethodId() string {
|
||||
if x != nil {
|
||||
return x.AuthMethodId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthToken) GetIamUserId() string {
|
||||
if x != nil {
|
||||
return x.IamUserId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_controller_storage_authtoken_store_v1_authtoken_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_controller_storage_authtoken_store_v1_authtoken_proto_rawDesc = []byte{
|
||||
0x0a, 0x35, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x61, 0x67, 0x65, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2f, 0x73,
|
||||
0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x25, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
|
||||
0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x61, 0x75, 0x74, 0x68,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f,
|
||||
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61,
|
||||
0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
||||
0xbe, 0x04, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b, 0x0a,
|
||||
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72,
|
||||
0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65,
|
||||
0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74,
|
||||
0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63,
|
||||
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
|
||||
0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54,
|
||||
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65,
|
||||
0x54, 0x69, 0x6d, 0x65, 0x12, 0x6b, 0x0a, 0x1c, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x6d,
|
||||
0x61, 0x74, 0x65, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e,
|
||||
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x19, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x6d,
|
||||
0x61, 0x74, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x12, 0x53, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e,
|
||||
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x74, 0x5f, 0x74, 0x6f, 0x6b,
|
||||
0x65, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x74, 0x54, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x26, 0x0a, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x5f,
|
||||
0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12,
|
||||
0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75,
|
||||
0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64,
|
||||
0x12, 0x1e, 0x0a, 0x0b, 0x69, 0x61, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
|
||||
0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x61, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
|
||||
0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68,
|
||||
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f,
|
||||
0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74,
|
||||
0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescOnce sync.Once
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescData = file_controller_storage_authtoken_store_v1_authtoken_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescGZIP() []byte {
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescOnce.Do(func() {
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescData)
|
||||
})
|
||||
return file_controller_storage_authtoken_store_v1_authtoken_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_controller_storage_authtoken_store_v1_authtoken_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_controller_storage_authtoken_store_v1_authtoken_proto_goTypes = []interface{}{
|
||||
(*AuthToken)(nil), // 0: controller.storage.authtoken.store.v1.AuthToken
|
||||
(*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp
|
||||
}
|
||||
var file_controller_storage_authtoken_store_v1_authtoken_proto_depIdxs = []int32{
|
||||
1, // 0: controller.storage.authtoken.store.v1.AuthToken.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
1, // 1: controller.storage.authtoken.store.v1.AuthToken.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
1, // 2: controller.storage.authtoken.store.v1.AuthToken.approximate_last_access_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
1, // 3: controller.storage.authtoken.store.v1.AuthToken.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_controller_storage_authtoken_store_v1_authtoken_proto_init() }
|
||||
func file_controller_storage_authtoken_store_v1_authtoken_proto_init() {
|
||||
if File_controller_storage_authtoken_store_v1_authtoken_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AuthToken); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_controller_storage_authtoken_store_v1_authtoken_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_controller_storage_authtoken_store_v1_authtoken_proto_goTypes,
|
||||
DependencyIndexes: file_controller_storage_authtoken_store_v1_authtoken_proto_depIdxs,
|
||||
MessageInfos: file_controller_storage_authtoken_store_v1_authtoken_proto_msgTypes,
|
||||
}.Build()
|
||||
File_controller_storage_authtoken_store_v1_authtoken_proto = out.File
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_rawDesc = nil
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_goTypes = nil
|
||||
file_controller_storage_authtoken_store_v1_authtoken_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package authtoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/iam"
|
||||
iamStore "github.com/hashicorp/watchtower/internal/iam/store"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testAuthToken(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper) *AuthToken {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
org, _ := iam.TestScopes(t, conn)
|
||||
u := iam.TestUser(t, conn, org.GetPublicId())
|
||||
amId := setupAuthMethod(t, conn, org.GetPublicId())
|
||||
|
||||
// auth account is only used to join auth method to user.
|
||||
// We don't do anything else with the auth account in the test setup.
|
||||
acct := setupAuthAccount(t, conn, org.GetPublicId(), amId, u.GetPublicId())
|
||||
|
||||
rw := db.New(conn)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
require.NoError(err)
|
||||
|
||||
ctx := context.Background()
|
||||
at, err := repo.CreateAuthToken(ctx, u.GetPublicId(), acct.GetPublicId())
|
||||
require.NoError(err)
|
||||
return at
|
||||
}
|
||||
|
||||
// Returns auth method id
|
||||
// TODO: Remove this when the auth method repos are created with the relevant test methods.
|
||||
func setupAuthMethod(t *testing.T, conn *gorm.DB, scope string) string {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
insert := `insert into auth_method
|
||||
(public_id, scope_id)
|
||||
values
|
||||
($1, $2);`
|
||||
amId, err := db.NewPublicId("am")
|
||||
require.NoError(err)
|
||||
_, err = conn.DB().Exec(insert, amId, scope)
|
||||
require.NoError(err)
|
||||
return amId
|
||||
}
|
||||
|
||||
// TODO: Remove this when the auth method repos are created with the relevant test methods.
|
||||
func setupAuthAccount(t *testing.T, conn *gorm.DB, scopeId, authMethodId, userId string) *iam.AuthAccount {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
require.NotEmpty(scopeId)
|
||||
require.NotEmpty(authMethodId)
|
||||
require.NotEmpty(userId)
|
||||
|
||||
authAcctId, err := db.NewPublicId("aa")
|
||||
require.NoError(err)
|
||||
|
||||
acct := &iam.AuthAccount{
|
||||
AuthAccount: &iamStore.AuthAccount{
|
||||
PublicId: authAcctId,
|
||||
ScopeId: scopeId,
|
||||
AuthMethodId: authMethodId,
|
||||
IamUserId: userId,
|
||||
},
|
||||
}
|
||||
rw := db.New(conn)
|
||||
err = rw.Create(context.Background(), acct)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(acct.PublicId)
|
||||
return acct
|
||||
}
|
||||
@@ -237,6 +237,7 @@ create index if not exists idx_oplog_metatadata_value on oplog_metadata(value);
|
||||
|
||||
insert into oplog_ticket (name, version)
|
||||
values
|
||||
('auth_token', 1),
|
||||
('default', 1),
|
||||
('iam_scope', 1),
|
||||
('iam_user', 1),
|
||||
@@ -893,6 +894,156 @@ begin;
|
||||
|
||||
commit;
|
||||
|
||||
`),
|
||||
},
|
||||
"migrations/11_auth_token.down.sql": {
|
||||
name: "11_auth_token.down.sql",
|
||||
bytes: []byte(`
|
||||
begin;
|
||||
|
||||
drop view auth_token_account cascade;
|
||||
drop table auth_token cascade;
|
||||
|
||||
drop function update_last_access_time cascade;
|
||||
drop function immutable_auth_token_columns cascade;
|
||||
drop function expire_time_not_older_than_token cascade;
|
||||
|
||||
commit;
|
||||
|
||||
`),
|
||||
},
|
||||
"migrations/11_auth_token.up.sql": {
|
||||
name: "11_auth_token.up.sql",
|
||||
bytes: []byte(`
|
||||
begin;
|
||||
|
||||
-- an auth token belongs to 1 and only 1 auth account
|
||||
-- an auth account can have 0 to many auth tokens
|
||||
create table auth_token (
|
||||
public_id wt_public_id primary key,
|
||||
token bytea not null unique,
|
||||
auth_account_id wt_public_id not null
|
||||
references auth_account(public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp,
|
||||
-- This column is not updated every time this auth token is accessed.
|
||||
-- It is updated after X minutes from the last time it was updated on
|
||||
-- a per row basis.
|
||||
approximate_last_access_time wt_timestamp
|
||||
check(
|
||||
approximate_last_access_time <= expiration_time
|
||||
),
|
||||
expiration_time wt_timestamp
|
||||
check(
|
||||
create_time <= expiration_time
|
||||
)
|
||||
);
|
||||
|
||||
create view auth_token_account as
|
||||
select at.public_id,
|
||||
at.token,
|
||||
at.auth_account_id,
|
||||
at.create_time,
|
||||
at.update_time,
|
||||
at.approximate_last_access_time,
|
||||
at.expiration_time,
|
||||
aa.scope_id,
|
||||
aa.iam_user_id,
|
||||
aa.auth_method_id
|
||||
from auth_token as at
|
||||
inner join auth_account as aa
|
||||
on at.auth_account_id = aa.public_id;
|
||||
|
||||
create or replace function
|
||||
update_last_access_time()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.approximate_last_access_time is distinct from old.approximate_last_access_time then
|
||||
new.approximate_last_access_time = now();
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
comment on function
|
||||
update_last_access_time()
|
||||
is
|
||||
'function used in before update triggers to properly set last_access_time columns';
|
||||
|
||||
create or replace function
|
||||
immutable_auth_token_columns()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.auth_account_id is distinct from old.auth_account_id then
|
||||
raise exception 'auth_account_id is read-only';
|
||||
end if;
|
||||
if new.token is distinct from old.token then
|
||||
raise exception 'token is read-only';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
comment on function
|
||||
immutable_auth_token_columns()
|
||||
is
|
||||
'function used in before update triggers to make specific columns immutable';
|
||||
|
||||
-- This allows the expiration to be calculated on the server side and still hold the constraint that
|
||||
-- the expiration time cant be before the creation time of the auth token.
|
||||
create or replace function
|
||||
expire_time_not_older_than_token()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.expiration_time < new.create_time then
|
||||
new.expiration_time = new.create_time;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
comment on function
|
||||
expire_time_not_older_than_token()
|
||||
is
|
||||
'function used in before insert triggers to ensure expiration time is not older than create time';
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before insert on auth_token
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create trigger
|
||||
expire_time_not_older_than_token
|
||||
before insert on auth_token
|
||||
for each row execute procedure expire_time_not_older_than_token();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before update on auth_token
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
update_last_access_time
|
||||
before update on auth_token
|
||||
for each row execute procedure update_last_access_time();
|
||||
|
||||
create trigger
|
||||
immutable_create_time
|
||||
before update on auth_token
|
||||
for each row execute procedure immutable_create_time_func();
|
||||
|
||||
create trigger
|
||||
immutable_auth_token_columns
|
||||
before update on auth_token
|
||||
for each row execute procedure immutable_auth_token_columns();
|
||||
|
||||
commit;
|
||||
|
||||
`),
|
||||
},
|
||||
}
|
||||
@@ -86,6 +86,7 @@ create index if not exists idx_oplog_metatadata_value on oplog_metadata(value);
|
||||
|
||||
insert into oplog_ticket (name, version)
|
||||
values
|
||||
('auth_token', 1),
|
||||
('default', 1),
|
||||
('iam_scope', 1),
|
||||
('iam_user', 1),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
begin;
|
||||
|
||||
drop view auth_token_account cascade;
|
||||
drop table auth_token cascade;
|
||||
|
||||
drop function update_last_access_time cascade;
|
||||
drop function immutable_auth_token_columns cascade;
|
||||
drop function expire_time_not_older_than_token cascade;
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,128 @@
|
||||
begin;
|
||||
|
||||
-- an auth token belongs to 1 and only 1 auth account
|
||||
-- an auth account can have 0 to many auth tokens
|
||||
create table auth_token (
|
||||
public_id wt_public_id primary key,
|
||||
token bytea not null unique,
|
||||
auth_account_id wt_public_id not null
|
||||
references auth_account(public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp,
|
||||
-- This column is not updated every time this auth token is accessed.
|
||||
-- It is updated after X minutes from the last time it was updated on
|
||||
-- a per row basis.
|
||||
approximate_last_access_time wt_timestamp
|
||||
check(
|
||||
approximate_last_access_time <= expiration_time
|
||||
),
|
||||
expiration_time wt_timestamp
|
||||
check(
|
||||
create_time <= expiration_time
|
||||
)
|
||||
);
|
||||
|
||||
create view auth_token_account as
|
||||
select at.public_id,
|
||||
at.token,
|
||||
at.auth_account_id,
|
||||
at.create_time,
|
||||
at.update_time,
|
||||
at.approximate_last_access_time,
|
||||
at.expiration_time,
|
||||
aa.scope_id,
|
||||
aa.iam_user_id,
|
||||
aa.auth_method_id
|
||||
from auth_token as at
|
||||
inner join auth_account as aa
|
||||
on at.auth_account_id = aa.public_id;
|
||||
|
||||
create or replace function
|
||||
update_last_access_time()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.approximate_last_access_time is distinct from old.approximate_last_access_time then
|
||||
new.approximate_last_access_time = now();
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
comment on function
|
||||
update_last_access_time()
|
||||
is
|
||||
'function used in before update triggers to properly set last_access_time columns';
|
||||
|
||||
create or replace function
|
||||
immutable_auth_token_columns()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.auth_account_id is distinct from old.auth_account_id then
|
||||
raise exception 'auth_account_id is read-only';
|
||||
end if;
|
||||
if new.token is distinct from old.token then
|
||||
raise exception 'token is read-only';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
comment on function
|
||||
immutable_auth_token_columns()
|
||||
is
|
||||
'function used in before update triggers to make specific columns immutable';
|
||||
|
||||
-- This allows the expiration to be calculated on the server side and still hold the constraint that
|
||||
-- the expiration time cant be before the creation time of the auth token.
|
||||
create or replace function
|
||||
expire_time_not_older_than_token()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
if new.expiration_time < new.create_time then
|
||||
new.expiration_time = new.create_time;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
comment on function
|
||||
expire_time_not_older_than_token()
|
||||
is
|
||||
'function used in before insert triggers to ensure expiration time is not older than create time';
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before insert on auth_token
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create trigger
|
||||
expire_time_not_older_than_token
|
||||
before insert on auth_token
|
||||
for each row execute procedure expire_time_not_older_than_token();
|
||||
|
||||
create trigger
|
||||
update_time_column
|
||||
before update on auth_token
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger
|
||||
update_last_access_time
|
||||
before update on auth_token
|
||||
for each row execute procedure update_last_access_time();
|
||||
|
||||
create trigger
|
||||
immutable_create_time
|
||||
before update on auth_token
|
||||
for each row execute procedure immutable_create_time_func();
|
||||
|
||||
create trigger
|
||||
immutable_auth_token_columns
|
||||
before update on auth_token
|
||||
for each row execute procedure immutable_auth_token_columns();
|
||||
|
||||
commit;
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -52,6 +53,16 @@ func TestWrapper(t *testing.T) wrapping.Wrapper {
|
||||
return root
|
||||
}
|
||||
|
||||
// AssertPublicId is a test helper that asserts that the provided id is in
|
||||
// the format of a public id.
|
||||
func AssertPublicId(t *testing.T, prefix, actual string) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, actual)
|
||||
parts := strings.Split(actual, "_")
|
||||
assert.Equalf(t, 2, len(parts), "want one '_' in PublicId, got multiple in %q", actual)
|
||||
assert.Equalf(t, prefix, parts[0], "PublicId want prefix: %q, got: %q in %q", prefix, parts[0], actual)
|
||||
}
|
||||
|
||||
// TestVerifyOplog will verify that there is an oplog entry. An error is
|
||||
// returned if the entry or it's metadata is not found. Returning an error
|
||||
// allows clients to test if an entry was not written, which is a valid use case.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package controller.storage.authtoken.store.v1;
|
||||
option go_package = "github.com/hashicorp/watchtower/internal/authtoken/store;store";
|
||||
|
||||
import "controller/storage/timestamp/v1/timestamp.proto";
|
||||
|
||||
message AuthToken {
|
||||
// public_id is used to access the auth token via an API
|
||||
// @inject_tag: gorm:"primary_key"
|
||||
string public_id = 1;
|
||||
|
||||
// create_time from the RDBMS
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
timestamp.v1.Timestamp create_time = 2;
|
||||
|
||||
// update_time from the RDBMS
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
timestamp.v1.Timestamp update_time = 3;
|
||||
|
||||
// last_access_time indicates the last time the auth token was used on the watchtower API.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
timestamp.v1.Timestamp approximate_last_access_time = 4;
|
||||
|
||||
// expiration_time indicates when this session will expire.
|
||||
// If null a default duration and create_time is used to calculate expiration.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
timestamp.v1.Timestamp expiration_time = 5;
|
||||
|
||||
// ciphertext token value stored in the database
|
||||
// @inject_tag: gorm:"column:token;not_null" wrapping:"ct,authtoken_token"
|
||||
bytes ct_token = 6;
|
||||
|
||||
// plain text version of the decrypted authtoken value
|
||||
// we are NOT storing this plain-text entry data in the db
|
||||
// token is the field stored and used by the client
|
||||
// @inject_tag: gorm:"-" wrapping:"pt,authtoken_token"
|
||||
string token = 7;
|
||||
|
||||
// auth_account_id is the public id for the auth account this auth token
|
||||
// was generated for.
|
||||
// @inject_tag: `gorm:"default:not_null"`
|
||||
string auth_account_id = 10;
|
||||
|
||||
// scope_id is not stored in the backing DB but it derived from the linked to auth account.
|
||||
// @inject_tag: gorm:"-"
|
||||
string scope_id = 11;
|
||||
|
||||
// auth_method_id is not stored in the backing DB but it derived from the linked to auth account.
|
||||
// @inject_tag: gorm:"-"
|
||||
string auth_method_id = 12;
|
||||
|
||||
// iam_user_id is not stored in the backing DB but it derived from the linked to auth account.
|
||||
// @inject_tag: gorm:"-"
|
||||
string iam_user_id = 13;
|
||||
}
|
||||
Reference in new issue
Block a user