Skip to content

Commit

Permalink
feat(auth): bootstrapping process for first token (#1114)
Browse files Browse the repository at this point in the history
fix: ensure authentication store is always instantiated

refactor(storage): rename query params Validate or Normalize

chore(rpc/auth): prefix method enum values with METHOD_

chore(sql/testing): use wait.ForSQL during mysql testcontainer boot

chore(storage): increase list limit from 10 to 25

feat(openapi): add Authorization header security requirements

feat(openapi): add security requirements
  • Loading branch information
GeorgeMac committed Nov 8, 2022
1 parent 1897f17 commit 1b626c4
Show file tree
Hide file tree
Showing 15 changed files with 361 additions and 97 deletions.
5 changes: 1 addition & 4 deletions cmd/flipt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,13 +488,10 @@ func run(ctx context.Context, logger *zap.Logger) error {
otelgrpc.UnaryServerInterceptor(),
}

// authentication interceptor
var authenticationStore storage.AuthenticationStore
authenticationStore := authsql.NewStore(driver, sql.BuilderFor(db, driver), logger)

// only enable enforcement middleware if authentication required
if cfg.Authentication.Required {
authenticationStore = authsql.NewStore(driver, sql.BuilderFor(db, driver), logger)

interceptors = append(interceptors, auth.UnaryInterceptor(logger, authenticationStore))

logger.Info("authentication middleware enabled")
Expand Down
2 changes: 1 addition & 1 deletion internal/server/auth/method/token/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func NewServer(logger *zap.Logger, store storage.AuthenticationStore) *Server {
// Along with the created Authentication, which includes it's identifier and associated timestamps.
func (s *Server) CreateToken(ctx context.Context, req *auth.CreateTokenRequest) (*auth.CreateTokenResponse, error) {
clientToken, authentication, err := s.store.CreateAuthentication(ctx, &storage.CreateAuthenticationRequest{
Method: auth.Method_TOKEN,
Method: auth.Method_METHOD_TOKEN,
ExpiresAt: req.ExpiresAt,
Metadata: map[string]string{
storageMetadataNameKey: req.GetName(),
Expand Down
2 changes: 1 addition & 1 deletion internal/server/auth/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestUnaryInterceptor(t *testing.T) {
authenticator := memory.NewStore()
clientToken, storedAuth, err := authenticator.CreateAuthentication(
context.TODO(),
&storage.CreateAuthenticationRequest{Method: authrpc.Method_TOKEN},
&storage.CreateAuthenticationRequest{Method: authrpc.Method_METHOD_TOKEN},
)
require.NoError(t, err)

Expand Down
4 changes: 2 additions & 2 deletions internal/storage/auth/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
// Bootstrap creates an initial static authentication of type token
// if one does not already exist.
func Bootstrap(ctx context.Context, store storage.AuthenticationStore) (string, error) {
req := storage.NewListRequest(ListWithMethod(rpcauth.Method_TOKEN))
req := storage.NewListRequest(ListWithMethod(rpcauth.Method_METHOD_TOKEN))
set, err := store.ListAuthentications(ctx, req)
if err != nil {
return "", fmt.Errorf("bootstrapping authentication store: %w", err)
Expand All @@ -23,7 +23,7 @@ func Bootstrap(ctx context.Context, store storage.AuthenticationStore) (string,
}

clientToken, _, err := store.CreateAuthentication(ctx, &storage.CreateAuthenticationRequest{
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "initial_bootstrap_token",
"io.flipt.auth.token.description": "Initial token created when bootstrapping authentication",
Expand Down
6 changes: 3 additions & 3 deletions internal/storage/auth/memory/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ func (s *Store) GetAuthenticationByClientToken(ctx context.Context, clientToken

func (s *Store) ListAuthentications(ctx context.Context, req *storage.ListRequest[storage.ListAuthenticationsPredicate]) (storage.ResultSet[*rpcauth.Authentication], error) {
var set storage.ResultSet[*rpcauth.Authentication]
if err := req.QueryParams.Validate(); err != nil {
return set, fmt.Errorf("listing authentications: %w", err)
}

// adjust the query parameters within normal bounds
req.QueryParams.Normalize()

// copy all auths into slice
s.mu.Lock()
Expand Down
5 changes: 2 additions & 3 deletions internal/storage/auth/sql/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,8 @@ func (s *Store) ListAuthentications(ctx context.Context, req *storage.ListReques
}
}()

if err = req.QueryParams.Validate(); err != nil {
return
}
// adjust the query parameters within normal bounds
req.QueryParams.Normalize()

query := s.builder.
Select(
Expand Down
46 changes: 23 additions & 23 deletions internal/storage/auth/sql/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestAuthenticationStoreHarness(t *testing.T) {

func TestAuthentication_CreateAuthentication(t *testing.T) {
// established a store factory with a single seeded auth entry
storeFn := newTestStore(t, createAuth("create_auth_id", "create_auth_token", rpcauth.Method_TOKEN))
storeFn := newTestStore(t, createAuth("create_auth_id", "create_auth_token", rpcauth.Method_METHOD_TOKEN))

ctx := context.TODO()
for _, test := range []struct {
Expand All @@ -58,7 +58,7 @@ func TestAuthentication_CreateAuthentication(t *testing.T) {
name: "successfully creates authentication",
opts: commonOpts,
req: &storage.CreateAuthenticationRequest{
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
ExpiresAt: timestamppb.New(time.Unix(2, 0)),
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_all_areas",
Expand All @@ -68,7 +68,7 @@ func TestAuthentication_CreateAuthentication(t *testing.T) {
expectedToken: "token:TestAuthentication_CreateAuthentication/successfully_creates_authentication",
expectedAuthentication: &rpcauth.Authentication{
Id: "id:TestAuthentication_CreateAuthentication/successfully_creates_authentication",
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_all_areas",
"io.flipt.auth.token.description": "The keys to the castle",
Expand All @@ -82,7 +82,7 @@ func TestAuthentication_CreateAuthentication(t *testing.T) {
name: "successfully creates authentication (no expiration)",
opts: commonOpts,
req: &storage.CreateAuthenticationRequest{
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_all_areas",
"io.flipt.auth.token.description": "The keys to the castle",
Expand All @@ -91,7 +91,7 @@ func TestAuthentication_CreateAuthentication(t *testing.T) {
expectedToken: "token:TestAuthentication_CreateAuthentication/successfully_creates_authentication_(no_expiration)",
expectedAuthentication: &rpcauth.Authentication{
Id: "id:TestAuthentication_CreateAuthentication/successfully_creates_authentication_(no_expiration)",
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_all_areas",
"io.flipt.auth.token.description": "The keys to the castle",
Expand All @@ -111,7 +111,7 @@ func TestAuthentication_CreateAuthentication(t *testing.T) {
}
},
req: &storage.CreateAuthenticationRequest{
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
ExpiresAt: timestamppb.New(time.Unix(2, 0)),
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_all_areas",
Expand All @@ -131,7 +131,7 @@ func TestAuthentication_CreateAuthentication(t *testing.T) {
}
},
req: &storage.CreateAuthenticationRequest{
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
ExpiresAt: timestamppb.New(time.Unix(2, 0)),
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_all_areas",
Expand Down Expand Up @@ -163,7 +163,7 @@ func TestAuthentication_GetAuthenticationByClientToken(t *testing.T) {
ctx := context.TODO()

// established a store factory with a single seeded auth entry
storeFn := newTestStore(t, createAuth("get_auth_id", "get_auth_token", rpcauth.Method_TOKEN))
storeFn := newTestStore(t, createAuth("get_auth_id", "get_auth_token", rpcauth.Method_METHOD_TOKEN))

// run table tests
for _, test := range []struct {
Expand All @@ -182,7 +182,7 @@ func TestAuthentication_GetAuthenticationByClientToken(t *testing.T) {
clientToken: "get_auth_token",
expectedAuthentication: &rpcauth.Authentication{
Id: "get_auth_id",
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_some_areas",
"io.flipt.auth.token.description": "The keys to some of the castle",
Expand Down Expand Up @@ -225,25 +225,25 @@ func TestAuthentication_ListAuthentications_ByMethod(t *testing.T) {
}

storeFn := newTestStore(t,
createAuth("none_id_one", "none_client_token_one", rpcauth.Method_NONE, withOpts(seedOpts)),
createAuth("none_id_two", "none_client_token_two", rpcauth.Method_NONE, withOpts(seedOpts)),
createAuth("none_id_three", "none_client_token_three", rpcauth.Method_NONE, withOpts(seedOpts)),
createAuth("token_id_one", "token_client_token_one", rpcauth.Method_TOKEN, withOpts(seedOpts)),
createAuth("token_id_two", "token_client_token_two", rpcauth.Method_TOKEN, withOpts(seedOpts)),
createAuth("token_id_three", "token_client_token_three", rpcauth.Method_TOKEN, withOpts(seedOpts)),
createAuth("none_id_one", "none_client_token_one", rpcauth.Method_METHOD_NONE, withOpts(seedOpts)),
createAuth("none_id_two", "none_client_token_two", rpcauth.Method_METHOD_NONE, withOpts(seedOpts)),
createAuth("none_id_three", "none_client_token_three", rpcauth.Method_METHOD_NONE, withOpts(seedOpts)),
createAuth("token_id_one", "token_client_token_one", rpcauth.Method_METHOD_TOKEN, withOpts(seedOpts)),
createAuth("token_id_two", "token_client_token_two", rpcauth.Method_METHOD_TOKEN, withOpts(seedOpts)),
createAuth("token_id_three", "token_client_token_three", rpcauth.Method_METHOD_TOKEN, withOpts(seedOpts)),
)

t.Run("method == none", func(t *testing.T) {
// list predicated with none auth method
req := storage.NewListRequest(storageauth.ListWithMethod(rpcauth.Method_NONE))
req := storage.NewListRequest(storageauth.ListWithMethod(rpcauth.Method_METHOD_NONE))
noneMethod, err := storeFn().ListAuthentications(ctx, req)

require.NoError(t, err)
assert.Equal(t, storage.ResultSet[*rpcauth.Authentication]{
Results: []*rpcauth.Authentication{
{
Id: "none_id_one",
Method: rpcauth.Method_NONE,
Method: rpcauth.Method_METHOD_NONE,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_some_areas",
"io.flipt.auth.token.description": "The keys to some of the castle",
Expand All @@ -254,7 +254,7 @@ func TestAuthentication_ListAuthentications_ByMethod(t *testing.T) {
},
{
Id: "none_id_two",
Method: rpcauth.Method_NONE,
Method: rpcauth.Method_METHOD_NONE,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_some_areas",
"io.flipt.auth.token.description": "The keys to some of the castle",
Expand All @@ -265,7 +265,7 @@ func TestAuthentication_ListAuthentications_ByMethod(t *testing.T) {
},
{
Id: "none_id_three",
Method: rpcauth.Method_NONE,
Method: rpcauth.Method_METHOD_NONE,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_some_areas",
"io.flipt.auth.token.description": "The keys to some of the castle",
Expand All @@ -280,14 +280,14 @@ func TestAuthentication_ListAuthentications_ByMethod(t *testing.T) {

t.Run("method == token", func(t *testing.T) {
// list predicated with token auth method
req := storage.NewListRequest(storageauth.ListWithMethod(rpcauth.Method_TOKEN))
req := storage.NewListRequest(storageauth.ListWithMethod(rpcauth.Method_METHOD_TOKEN))
tokenMethod, err := storeFn().ListAuthentications(ctx, req)
require.NoError(t, err)
assert.Equal(t, storage.ResultSet[*rpcauth.Authentication]{
Results: []*rpcauth.Authentication{
{
Id: "token_id_one",
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_some_areas",
"io.flipt.auth.token.description": "The keys to some of the castle",
Expand All @@ -298,7 +298,7 @@ func TestAuthentication_ListAuthentications_ByMethod(t *testing.T) {
},
{
Id: "token_id_two",
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_some_areas",
"io.flipt.auth.token.description": "The keys to some of the castle",
Expand All @@ -309,7 +309,7 @@ func TestAuthentication_ListAuthentications_ByMethod(t *testing.T) {
},
{
Id: "token_id_three",
Method: rpcauth.Method_TOKEN,
Method: rpcauth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"io.flipt.auth.token.name": "access_some_areas",
"io.flipt.auth.token.description": "The keys to some of the castle",
Expand Down
11 changes: 6 additions & 5 deletions internal/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

const (
// DefaultListLimit is the default limit applied to any list operation page size when one is not provided.
DefaultListLimit uint64 = 10
DefaultListLimit uint64 = 25

// MaxListLimit is the upper limit applied to any list operation page size.
MaxListLimit uint64 = 100
Expand Down Expand Up @@ -54,16 +54,17 @@ type QueryParams struct {
Order Order // not exposed to the user yet
}

func (q *QueryParams) Validate() error {
// Normalize adjusts query parameters within the enforced boundaries.
// For example, limit is adjusted to be in the range (0, max].
// Given the limit is not supplied (0) it is set to the default limit.
func (q *QueryParams) Normalize() {
if q.Limit == 0 {
q.Limit = DefaultListLimit
}

if q.Limit > MaxListLimit {
return fmt.Errorf("maximum page size limit (%d) exceeded: %d", MaxListLimit, q.Limit)
q.Limit = MaxListLimit
}

return nil
}

type QueryOption func(p *QueryParams)
Expand Down
4 changes: 2 additions & 2 deletions internal/storage/testing/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ func TestAuthenticationStoreHarness(t *testing.T, fn func(t *testing.T) storage.
)

t.Run(fmt.Sprintf("Create %d authentications", authCount), func(t *testing.T) {
for i := 0; i < 100; i++ {
for i := 0; i < authCount; i++ {
token, auth, err := store.CreateAuthentication(ctx, &storage.CreateAuthenticationRequest{
Method: auth.Method_TOKEN,
Method: auth.Method_METHOD_TOKEN,
Metadata: map[string]string{
"name": fmt.Sprintf("foo_%d", i),
"description": "bar",
Expand Down
Loading

0 comments on commit 1b626c4

Please sign in to comment.