Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sql/auth): base authentication storage implementation #1095

Merged
merged 14 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions cmd/flipt/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,27 +75,23 @@ func runImport(ctx context.Context, logger *zap.Logger, args []string) error {

defer in.Close()

// drop tables if specified
if dropBeforeImport {
logger.Debug("dropping tables before import")

tables := []string{"schema_migrations", "distributions", "rules", "constraints", "variants", "segments", "flags"}

for _, table := range tables {
if _, err := db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", table)); err != nil {
return fmt.Errorf("dropping tables: %w", err)
}
}
}

GeorgeMac marked this conversation as resolved.
Show resolved Hide resolved
migrator, err := sql.NewMigrator(*cfg, logger)
if err != nil {
return err
}

defer migrator.Close()

if err := migrator.Run(forceMigrate); err != nil {
// drop tables if specified
if dropBeforeImport {
logger.Debug("dropping tables before import")

if err := migrator.Down(); err != nil {
return fmt.Errorf("attempting to drop during import: %w", err)
}
}

if err := migrator.Up(forceMigrate); err != nil {
return err
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/flipt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func main() {

defer migrator.Close()

if err := migrator.Run(true); err != nil {
if err := migrator.Up(true); err != nil {
logger().Fatal("running migrator", zap.Error(err))
}
},
Expand Down Expand Up @@ -396,7 +396,7 @@ func run(ctx context.Context, logger *zap.Logger) error {

defer migrator.Close()

if err := migrator.Run(forceMigrate); err != nil {
if err := migrator.Up(forceMigrate); err != nil {
return err
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX hashed_client_token_authentications_index;
DROP TABLE IF EXISTS authentications;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS authentications (
id VARCHAR(255) PRIMARY KEY UNIQUE NOT NULL,
hashed_client_token VARCHAR(255) UNIQUE NOT NULL,
method INTEGER DEFAULT 0 NOT NULL,
metadata TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE UNIQUE INDEX hashed_client_token_authentications_index ON authentications (hashed_client_token);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX hashed_client_token_authentications_index ON authentications;
DROP TABLE IF EXISTS authentications;
12 changes: 12 additions & 0 deletions config/migrations/mysql/2_create_table_authentications.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS authentications (
id VARCHAR(255) UNIQUE NOT NULL,
hashed_client_token VARCHAR(255) UNIQUE NOT NULL,
method INTEGER DEFAULT 0 NOT NULL,
metadata TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
);

CREATE UNIQUE INDEX hashed_client_token_authentications_index ON authentications (hashed_client_token);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX hashed_client_token_authentications_index;
DROP TABLE IF EXISTS authentications;
11 changes: 11 additions & 0 deletions config/migrations/postgres/4_create_table_authentications.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS authentications (
id VARCHAR(255) PRIMARY KEY UNIQUE NOT NULL,
hashed_client_token VARCHAR(255) UNIQUE NOT NULL,
method INTEGER DEFAULT 0 NOT NULL,
metadata TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE UNIQUE INDEX hashed_client_token_authentications_index ON authentications (hashed_client_token);
9 changes: 4 additions & 5 deletions config/migrations/sqlite3/1_variants_unique_per_flag.down.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ CREATE TABLE variants_temp
(
id VARCHAR(255) PRIMARY KEY UNIQUE NOT NULL,
flag_key VARCHAR(255) NOT NULL REFERENCES flags ON DELETE CASCADE,
key VARCHAR(255) NOT NULL,
key VARCHAR(255) NOT NULL UNIQUE ON CONFLICT REPLACE,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
UNIQUE key ON CONFLICT REPLACE
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

INSERT INTO variants_temp (id, flag_key, key, name, description, created_at, updated_at)
SELECT id, flag_key, key, name, description, created_at, updated_at
INSERT INTO variants_temp (id, flag_key, `key`, name, description, created_at, updated_at)
SELECT id, flag_key, `key`, name, description, created_at, updated_at
FROM variants;

DROP TABLE variants;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX hashed_client_token_authentications_index;
DROP TABLE IF EXISTS authentications;
11 changes: 11 additions & 0 deletions config/migrations/sqlite3/4_create_table_authentications.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS authentications (
id VARCHAR(255) PRIMARY KEY UNIQUE NOT NULL,
hashed_client_token VARCHAR(255) UNIQUE NOT NULL,
method INTEGER DEFAULT 0 NOT NULL,
metadata TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE UNIQUE INDEX hashed_client_token_authentications_index ON authentications (hashed_client_token);
223 changes: 223 additions & 0 deletions internal/storage/sql/auth/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package auth

import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"

sq "github.com/Masterminds/squirrel"

"github.com/gofrs/uuid"
"go.flipt.io/flipt/internal/storage"
fliptsql "go.flipt.io/flipt/internal/storage/sql"
"go.flipt.io/flipt/rpc/flipt/auth"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/timestamppb"
)

// Store is the persistent storage layer for Authentications backed by SQL
// based relational database systems.
type Store struct {
logger *zap.Logger
driver fliptsql.Driver
builder sq.StatementBuilderType

now func() *timestamppb.Timestamp

generateID func() string
generateToken func() string
}

// Option is a type which configures a *Store
type Option func(*Store)

// NewStore constructs and configures a new instance of *Store.
// Queries are issued to the database via the provided statement builder.
func NewStore(driver fliptsql.Driver, builder sq.StatementBuilderType, logger *zap.Logger, opts ...Option) *Store {
store := &Store{
logger: logger,
driver: driver,
builder: builder,
now: timestamppb.Now,
generateID: func() string {
return uuid.Must(uuid.NewV4()).String()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we worried about (highly unlikely) potential for panic here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the only case uuid panics occur is when your system runs out of entropy (I could be mistaken).
I believe the advice in this situation is to panic for reals. So, I wouldn't be too worried about not capturing this.

},
generateToken: generateRandomToken,
}

for _, opt := range opts {
opt(store)
}

return store
}

// WithNowFunc overrides the stores now() function used to obtain
// a protobuf timestamp representative of the current time of evaluation.
func WithNowFunc(fn func() *timestamppb.Timestamp) Option {
return func(s *Store) {
s.now = fn
}
}

// WithTokenGeneratorFunc overrides the stores token generator function
// used to generate new random token strings as client tokens, when
// creating new instances of Authentication.
// The default is a pseudo-random string of bytes base64 encoded.
func WithTokenGeneratorFunc(fn func() string) Option {
return func(s *Store) {
s.generateToken = fn
}
}

// WithIDGeneratorFunc overrides the stores ID generator function
// used to generate new random ID strings, when creating new instances
// of Authentications.
// The default is a string containing a valid UUID (V4).
func WithIDGeneratorFunc(fn func() string) Option {
return func(s *Store) {
s.generateID = fn
}
}

// CreateAuthentication creates and persists an instance of an Authentication.
func (s *Store) CreateAuthentication(ctx context.Context, r *storage.CreateAuthenticationRequest) (string, *auth.Authentication, error) {
var (
now = s.now()
clientToken = s.generateToken()
authentication = auth.Authentication{
Id: s.generateID(),
Method: r.Method,
Metadata: r.Metadata,
ExpiresAt: r.ExpiresAt,
CreatedAt: now,
UpdatedAt: now,
}
)

hashedToken, err := hashClientToken(clientToken)
if err != nil {
return "", nil, fmt.Errorf("creating authentication: %w", err)
}

if _, err := s.builder.Insert("authentications").
Columns(
"id",
"hashed_client_token",
"method",
"metadata",
"expires_at",
"created_at",
"updated_at",
).
Values(
&authentication.Id,
&hashedToken,
&authentication.Method,
&fliptsql.JSONField[map[string]string]{T: authentication.Metadata},
&fliptsql.Timestamp{Timestamp: authentication.ExpiresAt},
&fliptsql.Timestamp{Timestamp: authentication.CreatedAt},
&fliptsql.Timestamp{Timestamp: authentication.UpdatedAt},
).
ExecContext(ctx); err != nil {
return "", nil, fmt.Errorf(
"inserting authentication %q: %w",
authentication.Id,
s.driver.AdaptError(err),
)
}

return clientToken, &authentication, nil
}

// GetAuthenticationByClientToken fetches the associated Authentication for the provided clientToken string.
//
// Given a row is present for the hash of the clientToken then materialize into an Authentication.
// Else, given it cannot be located, a storage.ErrNotFound error is wrapped and returned instead.
func (s *Store) GetAuthenticationByClientToken(ctx context.Context, clientToken string) (*auth.Authentication, error) {
hashedToken, err := hashClientToken(clientToken)
if err != nil {
return nil, fmt.Errorf("getting authentication by token: %w", err)
}

var (
authentication auth.Authentication
expiresAt fliptsql.Timestamp
createdAt fliptsql.Timestamp
updatedAt fliptsql.Timestamp
)

if err := s.builder.Select(
"id",
"method",
"metadata",
"expires_at",
"created_at",
"updated_at",
).
From("authentications").
Where(sq.Eq{"hashed_client_token": hashedToken}).
QueryRowContext(ctx).
Scan(
&authentication.Id,
&authentication.Method,
&fliptsql.JSONField[*map[string]string]{T: &authentication.Metadata},
&expiresAt,
&createdAt,
&updatedAt,
); err != nil {
return nil, fmt.Errorf(
"getting authentication by token: %w",
s.driver.AdaptError(err),
)
}

authentication.ExpiresAt = expiresAt.Timestamp
authentication.CreatedAt = createdAt.Timestamp
authentication.UpdatedAt = updatedAt.Timestamp

return &authentication, nil
}

const decodedTokenLen = 32

// generateRandomToken produces a URL safe base64 encoded string of random characters
// the data is sourced from a pseudo-random input stream
func generateRandomToken() string {
var token [decodedTokenLen]byte
if _, err := rand.Read(token[:]); err != nil {
panic(err)
}

return base64.URLEncoding.EncodeToString(token[:])
}

// hashClientToken performs a SHA256 sum on the input string
// it returns the result as a URL safe base64 encoded string
func hashClientToken(token string) (string, error) {
// produce SHA256 hash of token
hash := sha256.New()
if _, err := hash.Write([]byte(token)); err != nil {
return "", fmt.Errorf("hashing client token: %w", err)
}

// base64(sha256sum)
var (
data = make([]byte, 0, base64.URLEncoding.EncodedLen(hash.Size()))
buf = bytes.NewBuffer(data)
enc = base64.NewEncoder(base64.URLEncoding, buf)
)

if _, err := enc.Write(hash.Sum(nil)); err != nil {
return "", fmt.Errorf("hashing client token: %w", err)
}

if err := enc.Close(); err != nil {
return "", fmt.Errorf("hashing client token: %w", err)
}

return buf.String(), nil
}
Loading