diff --git a/internal/api/auth.go b/internal/api/auth.go index bee6da0..35331be 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -144,6 +144,6 @@ func (a API) Authv2(c *fiber.Ctx) error { return c.Status(http.StatusOK).JSON(authResponse{ Result: "allow", - IsSuperuser: false, + IsSuperuser: authenticator.IsSuperuser(), }) } diff --git a/internal/authenticator/admin_authenticator.go b/internal/authenticator/admin_authenticator.go new file mode 100644 index 0000000..6fbbece --- /dev/null +++ b/internal/authenticator/admin_authenticator.go @@ -0,0 +1,64 @@ +package authenticator + +import ( + "fmt" + + "github.com/golang-jwt/jwt/v5" + "github.com/snapp-incubator/soteria/internal/config" + "github.com/snapp-incubator/soteria/pkg/acl" +) + +// AdminAuthenticator is responsible for Acl/Auth/Token of the internal system users, +// these users have admin access. +type AdminAuthenticator struct { + Key any + Company string + JwtConfig config.Jwt + Parser *jwt.Parser +} + +// Auth check user authentication by checking the user's token +// isSuperuser is a flag that authenticator set it true when credentials is related to a superuser. +func (a AdminAuthenticator) Auth(tokenString string) error { + _, err := a.Parser.Parse(tokenString, func( + token *jwt.Token, + ) (interface{}, error) { + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, ErrInvalidClaims + } + if claims[a.JwtConfig.IssName] == nil { + return nil, ErrIssNotFound + } + + return a.Key, nil + }) + if err != nil { + return fmt.Errorf("token is invalid: %w", err) + } + + return nil +} + +// ACL check a system user access to a topic. +// because we returns is-admin: true, this endpoint shouldn't +// be called. +func (a AdminAuthenticator) ACL( + _ acl.AccessType, + _ string, + _ string, +) (bool, error) { + return true, nil +} + +func (a AdminAuthenticator) ValidateAccessType(_ acl.AccessType) bool { + return true +} + +func (a AdminAuthenticator) GetCompany() string { + return a.Company +} + +func (a AdminAuthenticator) IsSuperuser() bool { + return true +} diff --git a/internal/authenticator/admin_authenticator_test.go b/internal/authenticator/admin_authenticator_test.go new file mode 100644 index 0000000..6386338 --- /dev/null +++ b/internal/authenticator/admin_authenticator_test.go @@ -0,0 +1,65 @@ +package authenticator_test + +import ( + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/snapp-incubator/soteria/internal/authenticator" + "github.com/snapp-incubator/soteria/internal/config" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type AdminAuthenticatorTestSuite struct { + suite.Suite + + AdminToken string + + Authenticator authenticator.Authenticator +} + +func TestAdminAuthenticator_suite(t *testing.T) { + t.Parallel() + + st := new(AdminAuthenticatorTestSuite) + + pkey0, err := getPublicKey("admin") + require.NoError(t, err) + + st.Authenticator = authenticator.AdminAuthenticator{ + Key: pkey0, + Company: "snapp-admin", + Parser: jwt.NewParser(), + JwtConfig: config.Jwt{ + IssName: "iss", + SubName: "sub", + SigningMethod: "rsa256", + }, + } + + suite.Run(t, st) +} + +func (suite *AdminAuthenticatorTestSuite) SetupSuite() { + require := suite.Require() + + key, err := getPrivateKey("admin") + require.NoError(err) + + adminToken, err := getSampleToken("admin", key) + require.NoError(err) + + suite.AdminToken = adminToken +} + +func (suite *AdminAuthenticatorTestSuite) TestAuth() { + require := suite.Require() + + suite.Run("testing admin token auth", func() { + require.NoError(suite.Authenticator.Auth(suite.AdminToken)) + }) + + suite.Run("testing invalid token auth", func() { + require.Error(suite.Authenticator.Auth(invalidToken)) + }) +} diff --git a/internal/authenticator/authenticator.go b/internal/authenticator/authenticator.go index 27b990b..2180522 100644 --- a/internal/authenticator/authenticator.go +++ b/internal/authenticator/authenticator.go @@ -5,7 +5,8 @@ import ( ) type Authenticator interface { - // Auth check user authentication by checking the user's token + // Auth check user authentication by checking the user's token. + // it retruns error in case of any issue with the user token. Auth(tokenString string) error // ACL check a user access to a topic. @@ -20,4 +21,8 @@ type Authenticator interface { // GetCompany Return the Company Field of The Inherited Objects GetCompany() string + + // IsSuperuser changes the Auth response in case of successful authentication + // and shows user as superuser which disables the ACL. + IsSuperuser() bool } diff --git a/internal/authenticator/authenticator_test.go b/internal/authenticator/authenticator_test.go index 7d777c7..91faa21 100644 --- a/internal/authenticator/authenticator_test.go +++ b/internal/authenticator/authenticator_test.go @@ -1,5 +1,15 @@ package authenticator_test +import ( + "crypto/rsa" + "errors" + "fmt" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + const ( // nolint: gosec, lll invalidToken = "ey1JhbGciOiJSUzI1NiIsInR5cCI56kpXVCJ9.eyJzdWIiOiJCRzdScDFkcnpWRE5RcjYiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaXNzIjowLCJpYXQiOjE1MTYyMzkwMjJ9.1cYXFEhcewOYFjGJYhB8dsaFO9uKEXwlM8954rkt4Tsu0lWMITbRf_hHh1l9QD4MFqD-0LwRPUYaiaemy0OClMu00G2sujLCWaquYDEP37iIt8RoOQAh8Jb5vT8LX5C3PEKvbW_i98u8HHJoFUR9CXJmzrKi48sAcOYvXVYamN0S9KoY38H-Ze37Mdu3o6B58i73krk7QHecsc2_PkCJisvUVAzb0tiInIalBc8-zI3QZSxwNLr_hjlBg1sUxTUvH5SCcRR7hxI8TxJzkOHqAHWDRO84NC_DSAoO2p04vrHpqglN9XPJ8RC2YWpfefvD2ttH554RJWu_0RlR2kAYvQ" @@ -40,3 +50,82 @@ const ( invalidDriverCallOutgoingTopic = "snapp/driver/0596923be632d673560af9adadd2f78a/call/receive" invalidPassengerCallOutgoingTopic = "snapp/passenger/0596923be632d673560af9adadd2f78a/call/receive" ) + +var ( + ErrPrivateKeyNotFound = errors.New("invalid user, private key not found") + ErrPublicKeyNotFound = errors.New("invalid user, public key not found") +) + +func getPublicKey(u string) (*rsa.PublicKey, error) { + var fileName string + + switch u { + case "1": + fileName = "../../test/snapp-1.pem" + case "0": + fileName = "../../test/snapp-0.pem" + case "admin": + fileName = "../../test/snapp-admin.pem" + default: + return nil, ErrPublicKeyNotFound + } + + pem, err := os.ReadFile(fileName) + if err != nil { + return nil, fmt.Errorf("reading public key failed %w", err) + } + + publicKey, err := jwt.ParseRSAPublicKeyFromPEM(pem) + if err != nil { + return nil, fmt.Errorf("paring public key failed %w", err) + } + + return publicKey, nil +} + +func getPrivateKey(u string) (*rsa.PrivateKey, error) { + var fileName string + + switch u { + case "0": + fileName = "../../test/snapp-0.private.pem" + case "1": + fileName = "../../test/snapp-1.private.pem" + case "admin": + fileName = "../../test/snapp-admin.private.pem" + default: + return nil, ErrPrivateKeyNotFound + } + + pem, err := os.ReadFile(fileName) + if err != nil { + return nil, fmt.Errorf("reading private key failed %w", err) + } + + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(pem) + if err != nil { + return nil, fmt.Errorf("paring private key failed %w", err) + } + + return privateKey, nil +} + +func getSampleToken(issuer string, key *rsa.PrivateKey) (string, error) { + exp := time.Now().Add(time.Hour * 24 * 365 * 10) + sub := "DXKgaNQa7N5Y7bo" + + // nolint: exhaustruct + claims := jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(exp), + Issuer: issuer, + Subject: sub, + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + tokenString, err := token.SignedString(key) + if err != nil { + return "", fmt.Errorf("cannot generate a signed string %w", err) + } + + return tokenString, nil +} diff --git a/internal/authenticator/auto_authenticator.go b/internal/authenticator/auto_authenticator.go index 8fa2d63..64febb4 100644 --- a/internal/authenticator/auto_authenticator.go +++ b/internal/authenticator/auto_authenticator.go @@ -99,3 +99,7 @@ func (a AutoAuthenticator) ValidateAccessType(accessType acl.AccessType) bool { func (a AutoAuthenticator) GetCompany() string { return a.Company } + +func (a AutoAuthenticator) IsSuperuser() bool { + return false +} diff --git a/internal/authenticator/auto_authenticator_test.go b/internal/authenticator/auto_authenticator_test.go index b3269f7..15ff3bf 100644 --- a/internal/authenticator/auto_authenticator_test.go +++ b/internal/authenticator/auto_authenticator_test.go @@ -2,8 +2,10 @@ package authenticator_test import ( "crypto/rsa" - "errors" - "os" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" "testing" "time" @@ -12,6 +14,7 @@ import ( "github.com/snapp-incubator/soteria/internal/config" "github.com/snapp-incubator/soteria/internal/topics" "github.com/snapp-incubator/soteria/pkg/acl" + "github.com/snapp-incubator/soteria/pkg/validator" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.uber.org/zap" @@ -20,64 +23,83 @@ import ( type AutoAuthenticatorTestSuite struct { suite.Suite - Tokens struct { - Passenger string - Driver string - } + Token string + PublicKey *rsa.PublicKey - PublicKeys struct { - Passenger *rsa.PublicKey - Driver *rsa.PublicKey - } + Server *httptest.Server Authenticator authenticator.Authenticator } -func (suite *AutoAuthenticatorTestSuite) SetupSuite() { - require := suite.Require() - - driverToken := suite.getSampleToken(topics.DriverIss) +func TestAutoAuthenticator_suite(t *testing.T) { + t.Parallel() - suite.Tokens.Driver = driverToken + suite.Run(t, new(AutoAuthenticatorTestSuite)) +} - passengerToken := suite.getSampleToken(topics.PassengerIss) +func (suite *AutoAuthenticatorTestSuite) SetupSuite() { + cfg := config.SnappVendor() - suite.Tokens.Passenger = passengerToken + require := suite.Require() - pkey0, err := suite.getPublicKey(topics.DriverIss) + pkey0, err := getPublicKey("0") require.NoError(err) - suite.PublicKeys.Driver = pkey0 + suite.PublicKey = pkey0 - pkey1, err := suite.getPublicKey(topics.PassengerIss) + key0, err := getPrivateKey("0") require.NoError(err) - suite.PublicKeys.Passenger = pkey1 + token, err := getSampleToken("0", key0) + require.NoError(err) - cfg := config.SnappVendor() - cfg.UseValidator = true + suite.Token = token hid, err := topics.NewHashIDManager(cfg.HashIDMap) require.NoError(err) - // nolint: exhaustruct + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + authHeader := req.Header.Get("Authorization") + tokenString := strings.TrimPrefix(authHeader, "bearer ") + + _, err := jwt.Parse(tokenString, func( + token *jwt.Token, + ) (interface{}, error) { + return pkey0, nil + }) + if err != nil { + res.WriteHeader(http.StatusUnauthorized) + + return + } + + userData, err := json.Marshal(map[string]any{}) + require.NoError(err) + res.Header().Add("X-User-Data", string(userData)) + + res.WriteHeader(http.StatusOK) + })) + suite.Server = testServer + suite.Authenticator = authenticator.AutoAuthenticator{ + Validator: validator.New(testServer.URL, time.Second), AllowedAccessTypes: []acl.AccessType{acl.Pub, acl.Sub, acl.PubSub}, Company: "snapp", + Parser: jwt.NewParser(), TopicManager: topics.NewTopicManager(cfg.Topics, hid, "snapp", cfg.IssEntityMap, cfg.IssPeerMap, zap.NewNop()), - // Validator: validatorClient, + JwtConfig: config.Jwt{ + IssName: "iss", + SubName: "sub", + SigningMethod: "rsa256", + }, } } func (suite *AutoAuthenticatorTestSuite) TestAuth() { require := suite.Require() - suite.Run("testing driver token auth", func() { - require.NoError(suite.Authenticator.Auth(suite.Tokens.Driver)) - }) - - suite.Run("testing passenger token auth", func() { - require.NoError(suite.Authenticator.Auth(suite.Tokens.Passenger)) + suite.Run("testing valid token auth", func() { + require.NoError(suite.Authenticator.Auth(suite.Token)) }) suite.Run("testing invalid token auth", func() { @@ -85,210 +107,14 @@ func (suite *AutoAuthenticatorTestSuite) TestAuth() { }) } -// nolint: dupl -func (suite *AutoAuthenticatorTestSuite) TestACL_Basics() { - require := suite.Require() - - suite.Run("testing acl with invalid access type", func() { - ok, err := suite.Authenticator.ACL("invalid-access", suite.Tokens.Passenger, "test") - require.Error(err) - require.False(ok) - require.ErrorIs(err, authenticator.ErrInvalidAccessType) - }) - - suite.Run("testing acl with invalid token", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, invalidToken, validDriverCabEventTopic) - require.False(ok) - require.Error(err) - require.Equal("token is invalid illegal base64 data at input byte 36", err.Error()) - }) - - suite.Run("testing acl with valid inputs", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, suite.Tokens.Passenger, validPassengerCabEventTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing acl with invalid topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, suite.Tokens.Passenger, invalidPassengerCabEventTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing acl with invalid access type", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, suite.Tokens.Passenger, validPassengerCabEventTopic) - require.Error(err) - require.False(ok) - }) -} - -// nolint: funlen -func (suite *AutoAuthenticatorTestSuite) TestACL_Passenger() { - require := suite.Require() - token := suite.Tokens.Passenger - - suite.Run("testing passenger subscribe on valid superapp event topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validPassengerSuperappEventTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing passenger subscribe on invalid superapp event topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidPassengerSuperappEventTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing passenger subscribe on valid shared location topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validPassengerSharedTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing passenger subscribe on invalid shared location topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidPassengerSharedTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing passenger subscribe on valid chat topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validPassengerChatTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing passenger subscribe on invalid chat topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidPassengerChatTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing passenger subscribe on valid entry call topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, validPassengerCallEntryTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing passenger subscribe on invalid call entry topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, invalidPassengerCallEntryTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing passenger subscribe on valid outgoing call topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validPassengerCallOutgoingTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing passenger subscribe on valid outgoing call node topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, validPassengerNodeCallEntryTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing passenger subscribe on invalid call outgoing topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidPassengerCallOutgoingTopic) - require.Error(err) - require.False(ok) - }) -} - -// nolint: funlen -func (suite *AutoAuthenticatorTestSuite) TestACL_Driver() { - require := suite.Require() - token := suite.Tokens.Driver - - suite.Run("testing driver publish on its location topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, validDriverLocationTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing driver publish on invalid location topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, invalidDriverLocationTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing driver subscribe on invalid cab event topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidDriverCabEventTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing driver subscribe on valid superapp event topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validDriverSuperappEventTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing driver subscribe on invalid superapp event topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidDriverSuperappEventTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing driver subscribe on valid shared location topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validDriverSharedTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing driver subscribe on invalid shared location topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidDriverSharedTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing driver subscribe on valid chat topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validDriverChatTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing driver subscribe on invalid chat topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidDriverChatTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing driver subscribe on valid call entry topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, validDriverCallEntryTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing driver subscribe on invalid call entry topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, invalidDriverCallEntryTopic) - require.Error(err) - require.False(ok) - }) - - suite.Run("testing driver subscribe on valid call outgoing topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, validDriverCallOutgoingTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing driver subscribe on valid call outgoing node topic", func() { - ok, err := suite.Authenticator.ACL(acl.Pub, token, validDriverNodeCallEntryTopic) - require.NoError(err) - require.True(ok) - }) - - suite.Run("testing driver subscribe on invalid call outgoing topic", func() { - ok, err := suite.Authenticator.ACL(acl.Sub, token, invalidDriverCallOutgoingTopic) - require.Error(err) - require.False(ok) - }) +func (suite *AutoAuthenticatorTestSuite) TearDownSuite() { + suite.Server.Close() } func TestAutoAuthenticator_ValidateTopicBySender(t *testing.T) { t.Parallel() cfg := config.SnappVendor() - cfg.UseValidator = true hid, err := topics.NewHashIDManager(cfg.HashIDMap) require.NoError(t, err) @@ -302,171 +128,7 @@ func TestAutoAuthenticator_ValidateTopicBySender(t *testing.T) { t.Run("testing valid driver cab event", func(t *testing.T) { t.Parallel() - topicTemplate := authenticator.TopicManager.ParseTopic(validDriverCabEventTopic, topics.DriverIss, "DXKgaNQa7N5Y7bo") require.NotNil(t, topicTemplate) }) } - -// nolint: funlen, dupl -func TestAutoAuthenticator_validateAccessType(t *testing.T) { - t.Parallel() - - type fields struct { - AllowedAccessTypes []acl.AccessType - } - - type args struct { - accessType acl.AccessType - } - - tests := []struct { - name string - fields fields - args args - want bool - }{ - { - name: "#1 testing with no allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{}}, - args: args{accessType: acl.Sub}, - want: false, - }, - { - name: "#2 testing with no allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{}}, - args: args{accessType: acl.Pub}, - want: false, - }, - { - name: "#3 testing with no allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{}}, - args: args{accessType: acl.PubSub}, - want: false, - }, - { - name: "#4 testing with one allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{acl.Pub}}, - args: args{accessType: acl.Pub}, - want: true, - }, - { - name: "#5 testing with one allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{acl.Pub}}, - args: args{accessType: acl.Sub}, - want: false, - }, - { - name: "#6 testing with two allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{acl.Pub, acl.Sub}}, - args: args{accessType: acl.Sub}, - want: true, - }, - { - name: "#7 testing with two allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{acl.Pub, acl.Sub}}, - args: args{accessType: acl.Pub}, - want: true, - }, - { - name: "#8 testing with two allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{acl.Pub, acl.Sub}}, - args: args{accessType: acl.PubSub}, - want: false, - }, - { - name: "#9 testing with three allowed access type", - fields: fields{AllowedAccessTypes: []acl.AccessType{acl.Pub, acl.Sub, acl.PubSub}}, - args: args{accessType: acl.PubSub}, - want: true, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - // nolint: exhaustruct - a := authenticator.AutoAuthenticator{ - AllowedAccessTypes: tt.fields.AllowedAccessTypes, - } - if got := a.ValidateAccessType(tt.args.accessType); got != tt.want { - t.Errorf("validateAccessType() = %v, want %v", got, tt.want) - } - }) - } -} - -// nolint: goerr113, wrapcheck -func (suite *AutoAuthenticatorTestSuite) getPublicKey(u string) (*rsa.PublicKey, error) { - var fileName string - - switch u { - case topics.PassengerIss: - fileName = "../../test/1.pem" - case topics.DriverIss: - fileName = "../../test/0.pem" - default: - return nil, errors.New("invalid user, public key not found") - } - - pem, err := os.ReadFile(fileName) - if err != nil { - return nil, err - } - - publicKey, err := jwt.ParseRSAPublicKeyFromPEM(pem) - if err != nil { - return nil, err - } - - return publicKey, nil -} - -// nolint: goerr113, wrapcheck -func (suite *AutoAuthenticatorTestSuite) getPrivateKey(u string) (*rsa.PrivateKey, error) { - var fileName string - - switch u { - case topics.DriverIss: - fileName = "../../test/0.private.pem" - case topics.PassengerIss: - fileName = "../../test/1.private.pem" - default: - return nil, errors.New("invalid user, private key not found") - } - - pem, err := os.ReadFile(fileName) - if err != nil { - return nil, err - } - - privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(pem) - if err != nil { - return nil, err - } - - return privateKey, nil -} - -func (suite *AutoAuthenticatorTestSuite) getSampleToken(issuer string) string { - key, err := suite.getPrivateKey(issuer) - suite.Require().NoError(err) - - exp := time.Now().Add(time.Hour * 24 * 365 * 10) - sub := "DXKgaNQa7N5Y7bo" - - // nolint: exhaustruct - claims := jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(exp), - Issuer: issuer, - Subject: sub, - } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - - tokenString, err := token.SignedString(key) - suite.Require().NoError(err) - - return tokenString -} diff --git a/internal/authenticator/builder.go b/internal/authenticator/builder.go index ed2edf6..4735c5f 100644 --- a/internal/authenticator/builder.go +++ b/internal/authenticator/builder.go @@ -17,6 +17,7 @@ type Builder struct { ValidatorConfig config.Validator } +// nolint: funlen func (b Builder) Authenticators() map[string]Authenticator { all := make(map[string]Authenticator) @@ -30,11 +31,12 @@ func (b Builder) Authenticators() map[string]Authenticator { b.Logger.Fatal("cannot create hash-id manager", zap.Error(err)) } - client := validator.New(b.ValidatorConfig.URL, b.ValidatorConfig.Timeout) - var auth Authenticator - if vendor.UseValidator { + switch { + case vendor.UseValidator: + client := validator.New(b.ValidatorConfig.URL, b.ValidatorConfig.Timeout) + auth = &AutoAuthenticator{ AllowedAccessTypes: allowedAccessTypes, Company: vendor.Company, @@ -50,7 +52,20 @@ func (b Builder) Authenticators() map[string]Authenticator { Validator: client, Parser: jwt.NewParser(), } - } else { + 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(), + } + default: keys := b.GenerateKeys(vendor.Jwt.SigningMethod, vendor.Keys) auth = &ManualAuthenticator{ diff --git a/internal/authenticator/manual_authenticator.go b/internal/authenticator/manual_authenticator.go index 7c71211..abfbb1f 100644 --- a/internal/authenticator/manual_authenticator.go +++ b/internal/authenticator/manual_authenticator.go @@ -9,7 +9,8 @@ import ( "github.com/snapp-incubator/soteria/pkg/acl" ) -// ManualAuthenticator is responsible for Acl/Auth/Token of users. +// ManualAuthenticator is responsible for Acl/Auth/Token of users without calling +// any http client, etc. type ManualAuthenticator struct { Keys map[string]any AllowedAccessTypes []acl.AccessType @@ -19,8 +20,7 @@ type ManualAuthenticator struct { Parser *jwt.Parser } -// Auth check user authentication by checking the user's token -// isSuperuser is a flag that authenticator set it true when credentials is related to a superuser. +// Auth check user authentication by checking the user's token. func (a ManualAuthenticator) Auth(tokenString string) error { _, err := a.Parser.Parse(tokenString, func( token *jwt.Token, @@ -129,3 +129,7 @@ func (a ManualAuthenticator) ValidateAccessType(accessType acl.AccessType) bool func (a ManualAuthenticator) GetCompany() string { return a.Company } + +func (a ManualAuthenticator) IsSuperuser() bool { + return false +} diff --git a/internal/authenticator/manual_authenticator_test.go b/internal/authenticator/manual_authenticator_test.go index 69ec1e5..75ba768 100644 --- a/internal/authenticator/manual_authenticator_test.go +++ b/internal/authenticator/manual_authenticator_test.go @@ -2,10 +2,7 @@ package authenticator_test import ( "crypto/rsa" - "errors" - "os" "testing" - "time" "github.com/golang-jwt/jwt/v5" "github.com/snapp-incubator/soteria/internal/authenticator" @@ -33,33 +30,48 @@ type ManualAuthenticatorTestSuite struct { Authenticator authenticator.Authenticator } +func TestManualAuthenticator_suite(t *testing.T) { + t.Parallel() + + suite.Run(t, new(ManualAuthenticatorTestSuite)) +} + func (suite *ManualAuthenticatorTestSuite) SetupSuite() { + cfg := config.SnappVendor() + require := suite.Require() - driverToken := suite.getSampleToken(topics.DriverIss) + pkey0, err := getPublicKey("0") + require.NoError(err) - suite.Tokens.Driver = driverToken + suite.PublicKeys.Driver = pkey0 - passengerToken := suite.getSampleToken(topics.PassengerIss) + pkey1, err := getPublicKey("1") + require.NoError(err) - suite.Tokens.Passenger = passengerToken + suite.PublicKeys.Passenger = pkey1 - pkey0, err := suite.getPublicKey(topics.DriverIss) + key0, err := getPrivateKey("0") require.NoError(err) suite.PublicKeys.Driver = pkey0 - pkey1, err := suite.getPublicKey(topics.PassengerIss) + key1, err := getPrivateKey("1") require.NoError(err) - suite.PublicKeys.Passenger = pkey1 + driverToken, err := getSampleToken("0", key0) + require.NoError(err) - cfg := config.SnappVendor() + suite.Tokens.Driver = driverToken + + passengerToken, err := getSampleToken("1", key1) + require.NoError(err) + + suite.Tokens.Passenger = passengerToken hid, err := topics.NewHashIDManager(cfg.HashIDMap) require.NoError(err) - // nolint: exhaustruct suite.Authenticator = authenticator.ManualAuthenticator{ Keys: map[string]any{ topics.DriverIss: pkey0, @@ -67,7 +79,13 @@ func (suite *ManualAuthenticatorTestSuite) SetupSuite() { }, AllowedAccessTypes: []acl.AccessType{acl.Pub, acl.Sub, acl.PubSub}, Company: "snapp", + Parser: jwt.NewParser(), TopicManager: topics.NewTopicManager(cfg.Topics, hid, "snapp", cfg.IssEntityMap, cfg.IssPeerMap, zap.NewNop()), + JwtConfig: config.Jwt{ + IssName: "iss", + SubName: "sub", + SigningMethod: "rsa256", + }, } } @@ -87,7 +105,6 @@ func (suite *ManualAuthenticatorTestSuite) TestAuth() { }) } -// nolint: dupl func (suite *ManualAuthenticatorTestSuite) TestACL_Basics() { require := suite.Require() @@ -102,7 +119,7 @@ func (suite *ManualAuthenticatorTestSuite) TestACL_Basics() { ok, err := suite.Authenticator.ACL(acl.Pub, invalidToken, validDriverCabEventTopic) require.False(ok) require.Error(err) - require.Equal("token is invalid illegal base64 data at input byte 36", err.Error()) + require.ErrorIs(err, jwt.ErrTokenMalformed) }) suite.Run("testing acl with valid inputs", func() { @@ -308,7 +325,7 @@ func TestManualAuthenticator_ValidateTopicBySender(t *testing.T) { }) } -// nolint: funlen, dupl +// nolint: funlen func TestManualAuthenticator_validateAccessType(t *testing.T) { t.Parallel() @@ -397,76 +414,3 @@ func TestManualAuthenticator_validateAccessType(t *testing.T) { }) } } - -// nolint: goerr113, wrapcheck -func (suite *ManualAuthenticatorTestSuite) getPublicKey(u string) (*rsa.PublicKey, error) { - var fileName string - - switch u { - case topics.PassengerIss: - fileName = "../../test/1.pem" - case topics.DriverIss: - fileName = "../../test/0.pem" - default: - return nil, errors.New("invalid user, public key not found") - } - - pem, err := os.ReadFile(fileName) - if err != nil { - return nil, err - } - - publicKey, err := jwt.ParseRSAPublicKeyFromPEM(pem) - if err != nil { - return nil, err - } - - return publicKey, nil -} - -// nolint: goerr113, wrapcheck -func (suite *ManualAuthenticatorTestSuite) getPrivateKey(u string) (*rsa.PrivateKey, error) { - var fileName string - - switch u { - case topics.DriverIss: - fileName = "../../test/0.private.pem" - case topics.PassengerIss: - fileName = "../../test/1.private.pem" - default: - return nil, errors.New("invalid user, private key not found") - } - - pem, err := os.ReadFile(fileName) - if err != nil { - return nil, err - } - - privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(pem) - if err != nil { - return nil, err - } - - return privateKey, nil -} - -func (suite *ManualAuthenticatorTestSuite) getSampleToken(issuer string) string { - key, err := suite.getPrivateKey(issuer) - suite.Require().NoError(err) - - exp := time.Now().Add(time.Hour * 24 * 365 * 10) - sub := "DXKgaNQa7N5Y7bo" - - // nolint: exhaustruct - claims := jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(exp), - Issuer: issuer, - Subject: sub, - } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - - tokenString, err := token.SignedString(key) - suite.Require().NoError(err) - - return tokenString -} diff --git a/internal/config/config.go b/internal/config/config.go index 101cf9f..33121c9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -41,10 +41,13 @@ type ( IssEntityMap map[string]string `json:"iss_entity_map,omitempty" koanf:"iss_entity_map"` IssPeerMap map[string]string `json:"iss_peer_map,omitempty" koanf:"iss_peer_map"` Jwt Jwt `json:"jwt,omitempty" koanf:"jwt"` - // by setting do validate to false we don't validate the jwt token and deligate - // it into a function. - UseValidator bool `json:"use_validator,omitempty" koanf:"use_validator"` - HashIDMap map[string]topics.HashData `json:"hash_id_map,omitempty" koanf:"hashid_map"` + // by setting use validator to true we don't validate the jwt token and deligate + // it into an http client. + UseValidator bool `json:"use_validator,omitempty" koanf:"use_validator"` + // by setting is internal to true we use internal authenticator which provides admin access + // on the authentication method. + IsInternal bool `json:"is_internal,omitempty" koanf:"is_internal"` + HashIDMap map[string]topics.HashData `json:"hash_id_map,omitempty" koanf:"hashid_map"` } Jwt struct { diff --git a/internal/config/default.go b/internal/config/default.go index 516daeb..b8020e4 100644 --- a/internal/config/default.go +++ b/internal/config/default.go @@ -43,6 +43,7 @@ func Default() Config { func SnappVendor() Vendor { return Vendor{ UseValidator: false, + IsInternal: false, AllowedAccessTypes: []string{ "pub", "sub", diff --git a/test/snapp-admin.pem b/test/snapp-admin.pem new file mode 100644 index 0000000..c73d608 --- /dev/null +++ b/test/snapp-admin.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1lNRwyNsDieWs6LvHOJ+ +GyehhRC4Pn5yL5edKP3565F3LtRDMrkzwDRsQbqnUtTea9HCdTdBv+lI8vE17qRi +RQn10IMaIH6e4Aa3OWNClFhuqNOag7VmffsjTOgxHgHpfGAKVF/4BwqOHrdHFbAD +VOiWB1hv9Uc0C5laffGAub7fj+EAI02zlrsNDxYW8vyF2H47N7VWcvgd3RhZpxlG +8bq9phl7Ja55YmQiT2Ic3/K5tsazg5z9lz6OTrx+JvWbefHFlJpjCLz5yefEaRmX +9L/zyDMi4jgFTZEWNXC2vIrxwZMFwFhBXEp0PcCbuHJgJIucbRrbwukQC16uHJwP +zQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/test/snapp-admin.private.pem b/test/snapp-admin.private.pem new file mode 100644 index 0000000..bffadbc --- /dev/null +++ b/test/snapp-admin.private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDWU1HDI2wOJ5az +ou8c4n4bJ6GFELg+fnIvl50o/fnrkXcu1EMyuTPANGxBuqdS1N5r0cJ1N0G/6Ujy +8TXupGJFCfXQgxogfp7gBrc5Y0KUWG6o05qDtWZ9+yNM6DEeAel8YApUX/gHCo4e +t0cVsANU6JYHWG/1RzQLmVp98YC5vt+P4QAjTbOWuw0PFhby/IXYfjs3tVZy+B3d +GFmnGUbxur2mGXslrnliZCJPYhzf8rm2xrODnP2XPo5OvH4m9Zt58cWUmmMIvPnJ +58RpGZf0v/PIMyLiOAVNkRY1cLa8ivHBkwXAWEFcSnQ9wJu4cmAki5xtGtvC6RAL +Xq4cnA/NAgMBAAECggEBAKC93JR9/IyhJGWVzb/cHrg/AOTLpWM5cmo/S59y2/3R +G5IDoGJqhGWi645gbx2wiSBUMYO37ZgBXrTWM5zHrOwajEHWEcJNNNrQLprb1xNr +P5NfIIOniTbzI4aKnkvjIzokzZp6X4roX66pxqZ9XAJSbjMUIOPUgzQvz66lceXb +4aKXSEPdBIRQRmwEUhOV3W8aXNDULR+LwPRt1PYxRm1+W0OPtOwnjfSxBdTbFf8+ +Lqsg92/kI/qfPnFq0LsbIdCF9yS/0GhmmJZNErwGl320SF2oXJXKiK2PXKj0mbPV +LLXOMvEkbwlD+6Nbgz3vIrTdcJXc0TfawZOwvin3AAECgYEA+s5Wl+qS+AEP2gPS +w0J/KqXA22jYN/bBXwZInBBFM7lugmX1A1OGh3Sbu7S1aGQI81OQ7aDSzhkeK2sI +irKnnaLQKzXsalQZvPSG9Q6De2Tj9sN5N0eI7VdB72b+sz/NpI2IsRE6+pBAvEzJ +YXFwH4mrj4SbHbcaUA6bhdkAP4UCgYEA2sOT2b8zsGk6JLrfq5CdbywdpT/xITdA +PivmsRF2bQ9F2j2V1y0B+cgfGJLWJgN5iSwg7SGlalUcFs188JcTgfJB5XsPXH8E +kNmD7fkfAoohn5d480fQRKuEboNa//F+zHq7yQVTcTnFsWctsXppv8A1ME7DNJrp +c2mAEHVU7akCgYEAqmEL8G3ZY4MNKrTYM+9zhhxOFH94CySlHpGdN+/Rox7AVPNA +a/8M7+4mcXCEoCL89Zf6Z4OOUZY8qZAvoFFXjr3xHrmmHmF9jqCrIcS3S1cxigwm +x4fgHCPf1euo8UpRwAyqJGepIlhmscSUNY8jdTlIA9o4qgoeZO5XdqkBAyUCgYEA +m+Ykx1hrDZzvwp0qKKm2iDN4LPuUa4dkUOoYTLeVHcN0lEKvNdjtP4ROJMT/t7di +NU8tZ9BCgbSFf/qQvyPq0wBB1bgNCm26Yz+ftUeDwduOep0HpNfYpBdXGSqi/yKq +qi4NBQS2okn5iKNu/LuwAOaJARQgKKz9ETJuAUycaYECgYA43841te0rJ81lNKE5 +wnCL/tx3OURDhSUJVf6tjwB+vo2FOIUfUJTBowzdWn5cTXCwGTp04JZBrZlY5RDH +24Tzo+/PTIbH9jiSYtKJCwJ+umPxek3OuBQo+4zAejN55jwcPOJcadp6u4hwq6yy +wpqfsglQldbuiZ0TilCRNhv00g== +-----END PRIVATE KEY----- \ No newline at end of file