Creating Auth Token Repo (#125)

This commit is contained in:
Todd Knight authored and GitHub committed 2020-07-07 07:44:49 -07:00
1 parent bb4a185412
commit f69d6b9952
17 files changed
+1905

No files matched your search

+1
View File
@@ -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}
+68
View File
@@ -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
}
+225
View File
@@ -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: &timestamp.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: &timestamp.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)
}
})
}
}
+17
View File
@@ -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
+16
View File
@@ -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
}
}
+30
View File
@@ -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
}
}
+19
View File
@@ -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)
})
}
+311
View File
@@ -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 = &timestamp.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
}
+482
View File
@@ -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)))
}
})
}
}
+303
View File
@@ -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
}
+76
View File
@@ -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
}
+151
View File
@@ -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;
+11
View File
@@ -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;
}