Skip to content

Commit

Permalink
feat: improve the builder pattern and increase coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
1995parham committed Nov 30, 2023
1 parent 30bb76b commit ee732c8
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 75 deletions.
Binary file added cmd/soteria/soteria
Binary file not shown.
177 changes: 112 additions & 65 deletions internal/authenticator/builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package authenticator

import (
"errors"
"fmt"

"github.com/golang-jwt/jwt/v5"
Expand All @@ -11,105 +12,149 @@ import (
"go.uber.org/zap"
)

var (
ErrAdminAuthenticatorSystemKey = errors.New("admin authenticator supports only one key named system")
ErrNoAuthenticator = errors.New("at least one vendor should be enable to have soteria")
ErrNoDefaultCaseIssEntry = errors.New("default case for iss-entity map is required")
ErrNoDefaultCaseIssPeer = errors.New("default case for iss-peer map is required")
)

type Builder struct {
Vendors []config.Vendor
Logger zap.Logger
Logger *zap.Logger
ValidatorConfig config.Validator
}

// nolint: funlen
func (b Builder) Authenticators() map[string]Authenticator {
func (b Builder) Authenticators() (map[string]Authenticator, error) {
all := make(map[string]Authenticator)

for _, vendor := range b.Vendors {
b.ValidateMappers(vendor.IssEntityMap, vendor.IssPeerMap)

allowedAccessTypes := b.GetAllowedAccessTypes(vendor.AllowedAccessTypes)

hid, err := topics.NewHashIDManager(vendor.HashIDMap)
if err != nil {
b.Logger.Fatal("cannot create hash-id manager", zap.Error(err))
}

var auth Authenticator
var (
auth Authenticator
err error
)

switch {
case vendor.UseValidator:
client := validator.New(b.ValidatorConfig.URL, b.ValidatorConfig.Timeout)

auth = &AutoAuthenticator{
AllowedAccessTypes: allowedAccessTypes,
Company: vendor.Company,
TopicManager: topics.NewTopicManager(
vendor.Topics,
hid,
vendor.Company,
vendor.IssEntityMap,
vendor.IssPeerMap,
b.Logger.Named("topic-manager"),
),
JwtConfig: vendor.Jwt,
Validator: client,
Parser: jwt.NewParser(),
auth, err = b.autoAuthenticator(vendor)
if err != nil {
return nil, fmt.Errorf("cannot build auto authenticator %w", err)
}
case vendor.IsInternal:
if _, ok := vendor.Keys["system"]; !ok || len(vendor.Keys) != 1 {
b.Logger.Fatal("admin authenticator supports only one key named system")
}

keys := b.GenerateKeys(vendor.Jwt.SigningMethod, vendor.Keys)

auth = &AdminAuthenticator{
Key: keys["system"],
Company: vendor.Company,
JwtConfig: vendor.Jwt,
Parser: jwt.NewParser(),
auth, err = b.adminAuthenticator(vendor)
if err != nil {
return nil, fmt.Errorf("cannot build admin authenticator %w", err)
}
default:
keys := b.GenerateKeys(vendor.Jwt.SigningMethod, vendor.Keys)

auth = &ManualAuthenticator{
Keys: keys,
AllowedAccessTypes: allowedAccessTypes,
Company: vendor.Company,
TopicManager: topics.NewTopicManager(
vendor.Topics,
hid,
vendor.Company,
vendor.IssEntityMap,
vendor.IssPeerMap,
b.Logger.Named("topic-manager"),
),
JwtConfig: vendor.Jwt,
Parser: jwt.NewParser(jwt.WithValidMethods([]string{vendor.Jwt.SigningMethod})),
auth, err = b.manualAuthenticator(vendor)
if err != nil {
return nil, fmt.Errorf("cannot build manual authenticator %w", err)
}
}

all[vendor.Company] = auth
}

if len(all) == 0 {
b.Logger.Fatal("at least one vendor should be enable to have soteria")
return nil, ErrNoAuthenticator
}

return all, nil
}

func (b Builder) adminAuthenticator(vendor config.Vendor) (*AdminAuthenticator, error) {
if _, ok := vendor.Keys["system"]; !ok || len(vendor.Keys) != 1 {
return nil, ErrAdminAuthenticatorSystemKey
}

keys := b.GenerateKeys(vendor.Jwt.SigningMethod, vendor.Keys)

return &AdminAuthenticator{
Key: keys["system"],
Company: vendor.Company,
JwtConfig: vendor.Jwt,
Parser: jwt.NewParser(),
}, nil
}

func (b Builder) manualAuthenticator(vendor config.Vendor) (*ManualAuthenticator, error) {
if err := b.ValidateMappers(vendor.IssEntityMap, vendor.IssPeerMap); err != nil {
return nil, fmt.Errorf("failed to validate mappers %w", err)
}

allowedAccessTypes, err := b.GetAllowedAccessTypes(vendor.AllowedAccessTypes)
if err != nil {
return nil, fmt.Errorf("cannot parse allowed access types %w", err)
}

hid, err := topics.NewHashIDManager(vendor.HashIDMap)
if err != nil {
return nil, fmt.Errorf("cannot create hash-id manager %w", err)
}

return all
keys := b.GenerateKeys(vendor.Jwt.SigningMethod, vendor.Keys)

return &ManualAuthenticator{
Keys: keys,
AllowedAccessTypes: allowedAccessTypes,
Company: vendor.Company,
TopicManager: topics.NewTopicManager(
vendor.Topics,
hid,
vendor.Company,
vendor.IssEntityMap,
vendor.IssPeerMap,
b.Logger.Named("topic-manager"),
),
JwtConfig: vendor.Jwt,
Parser: jwt.NewParser(jwt.WithValidMethods([]string{vendor.Jwt.SigningMethod})),
}, nil
}

func (b Builder) autoAuthenticator(vendor config.Vendor) (*AutoAuthenticator, error) {
allowedAccessTypes, err := b.GetAllowedAccessTypes(vendor.AllowedAccessTypes)
if err != nil {
return nil, fmt.Errorf("cannot parse allowed access types %w", err)
}

hid, err := topics.NewHashIDManager(vendor.HashIDMap)
if err != nil {
return nil, fmt.Errorf("cannot create hash-id manager %w", err)
}

client := validator.New(b.ValidatorConfig.URL, b.ValidatorConfig.Timeout)

return &AutoAuthenticator{
AllowedAccessTypes: allowedAccessTypes,
Company: vendor.Company,
TopicManager: topics.NewTopicManager(
vendor.Topics,
hid,
vendor.Company,
vendor.IssEntityMap,
vendor.IssPeerMap,
b.Logger.Named("topic-manager"),
),
JwtConfig: vendor.Jwt,
Validator: client,
Parser: jwt.NewParser(),
}, nil
}

// GetAllowedAccessTypes will return all allowed access types in Soteria.
func (b Builder) GetAllowedAccessTypes(accessTypes []string) []acl.AccessType {
func (b Builder) GetAllowedAccessTypes(accessTypes []string) ([]acl.AccessType, error) {
allowedAccessTypes := make([]acl.AccessType, 0, len(accessTypes))

for _, a := range accessTypes {
at, err := toUserAccessType(a)
if err != nil {
err = fmt.Errorf("could not convert %s: %w", at, err)
b.Logger.Fatal("error while getting allowed access types", zap.Error(err))
return nil, fmt.Errorf("could not convert %s: %w", at, err)
}

allowedAccessTypes = append(allowedAccessTypes, at)
}

return allowedAccessTypes
return allowedAccessTypes, nil
}

// toUserAccessType will convert string access type to it's own type.
Expand All @@ -126,12 +171,14 @@ func toUserAccessType(access string) (acl.AccessType, error) {
return "", ErrInvalidAccessType
}

func (b Builder) ValidateMappers(issEntityMap, issPeerMap map[string]string) {
func (b Builder) ValidateMappers(issEntityMap, issPeerMap map[string]string) error {
if _, ok := issEntityMap[topics.Default]; !ok {
b.Logger.Fatal("default case for iss-entity map is required")
return ErrNoDefaultCaseIssEntry
}

if _, ok := issPeerMap[topics.Default]; !ok {
b.Logger.Fatal("default case for iss-peer map is required")
return ErrNoDefaultCaseIssPeer
}

return nil
}
49 changes: 49 additions & 0 deletions internal/authenticator/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package authenticator_test

import (
"testing"

"github.com/snapp-incubator/soteria/internal/authenticator"
"github.com/snapp-incubator/soteria/internal/config"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

func TestBuilderInternalAuthenticator(t *testing.T) {
t.Parallel()

require := require.New(t)

b := authenticator.Builder{
Vendors: []config.Vendor{
{
Company: "internal",
Jwt: config.Jwt{
IssName: "iss",
SubName: "sub",
SigningMethod: "HS512",
},
IsInternal: true,
UseValidator: false,
AllowedAccessTypes: nil,
Topics: nil,
HashIDMap: nil,
IssEntityMap: nil,
IssPeerMap: nil,
Keys: map[string]string{
"system": "c2VjcmV0",
},
},
},
Logger: zap.NewNop(),
ValidatorConfig: config.Validator{
URL: "",
Timeout: 0,
},
}

vendors, err := b.Authenticators()
require.NoError(err)
require.Len(vendors, 1)
require.Contains(vendors, "internal")
}
4 changes: 2 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ func Execute() {
Use: "soteria",
Short: "Soteria is the authentication service.",
Long: `Soteria is responsible for Authentication and Authorization of every request witch send to EMQ Server.`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, _ []string) {
cmd.Println("Run `soteria serve` to start serving requests")
},
}

serve.Serve{
Cfg: cfg,
Logger: *logger.Named("serve"),
Logger: logger.Named("serve"),
Tracer: tracer,
}.Register(root)

Expand Down
22 changes: 14 additions & 8 deletions internal/cmd/serve/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,25 @@ import (

type Serve struct {
Cfg config.Config
Logger zap.Logger
Logger *zap.Logger
Tracer trace.Tracer
}

func (s Serve) main() {
auth, err := authenticator.Builder{
Vendors: s.Cfg.Vendors,
Logger: s.Logger,
ValidatorConfig: s.Cfg.Validator,
}.Authenticators()
if err != nil {
s.Logger.Fatal("authenticator building failed", zap.Error(err))
}

api := api.API{
DefaultVendor: s.Cfg.DefaultVendor,
Authenticators: authenticator.Builder{
Vendors: s.Cfg.Vendors, Logger: s.Logger,
ValidatorConfig: s.Cfg.Validator,
}.Authenticators(),
Tracer: s.Tracer,
Logger: s.Logger.Named("api"),
DefaultVendor: s.Cfg.DefaultVendor,
Authenticators: auth,
Tracer: s.Tracer,
Logger: s.Logger.Named("api"),
}

if _, ok := api.Authenticators[s.Cfg.DefaultVendor]; !ok {
Expand Down

0 comments on commit ee732c8

Please sign in to comment.