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

Implement admin authenticator #5

Merged
merged 11 commits into from
Nov 13, 2023
2 changes: 1 addition & 1 deletion internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,6 @@

return c.Status(http.StatusOK).JSON(authResponse{
Result: "allow",
IsSuperuser: false,
IsSuperuser: authenticator.IsSuperuser(),

Check warning on line 147 in internal/api/auth.go

View check run for this annotation

Codecov / codecov/patch

internal/api/auth.go#L147

Added line #L147 was not covered by tests
})
}
64 changes: 64 additions & 0 deletions internal/authenticator/admin_authenticator.go
Original file line number Diff line number Diff line change
@@ -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
}

Check warning on line 29 in internal/authenticator/admin_authenticator.go

View check run for this annotation

Codecov / codecov/patch

internal/authenticator/admin_authenticator.go#L28-L29

Added lines #L28 - L29 were not covered by tests
if claims[a.JwtConfig.IssName] == nil {
return nil, ErrIssNotFound
}

Check warning on line 32 in internal/authenticator/admin_authenticator.go

View check run for this annotation

Codecov / codecov/patch

internal/authenticator/admin_authenticator.go#L31-L32

Added lines #L31 - L32 were not covered by tests

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(
Copy link
Member

Choose a reason for hiding this comment

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

@1995parham In long term view we can think about restricting services on their topics.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right Ahmad jan, EMQ has a concept which creates tenants for you by prefixing topics. We can use this feature for supporting different vendors but right now we need shared topics. For example Snapp Box want to read the Snapp events this means we need to validate Snapp Box tokens for Snapp topics.

_ acl.AccessType,
_ string,
_ string,
) (bool, error) {
return true, nil

Check warning on line 51 in internal/authenticator/admin_authenticator.go

View check run for this annotation

Codecov / codecov/patch

internal/authenticator/admin_authenticator.go#L50-L51

Added lines #L50 - L51 were not covered by tests
}

func (a AdminAuthenticator) ValidateAccessType(_ acl.AccessType) bool {
return true

Check warning on line 55 in internal/authenticator/admin_authenticator.go

View check run for this annotation

Codecov / codecov/patch

internal/authenticator/admin_authenticator.go#L54-L55

Added lines #L54 - L55 were not covered by tests
}

func (a AdminAuthenticator) GetCompany() string {
return a.Company

Check warning on line 59 in internal/authenticator/admin_authenticator.go

View check run for this annotation

Codecov / codecov/patch

internal/authenticator/admin_authenticator.go#L58-L59

Added lines #L58 - L59 were not covered by tests
}

func (a AdminAuthenticator) IsSuperuser() bool {
return true

Check warning on line 63 in internal/authenticator/admin_authenticator.go

View check run for this annotation

Codecov / codecov/patch

internal/authenticator/admin_authenticator.go#L62-L63

Added lines #L62 - L63 were not covered by tests
}
65 changes: 65 additions & 0 deletions internal/authenticator/admin_authenticator_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
7 changes: 6 additions & 1 deletion internal/authenticator/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
89 changes: 89 additions & 0 deletions internal/authenticator/authenticator_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions internal/authenticator/auto_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@
func (a AutoAuthenticator) GetCompany() string {
return a.Company
}

func (a AutoAuthenticator) IsSuperuser() bool {
return false

Check warning on line 104 in internal/authenticator/auto_authenticator.go

View check run for this annotation

Codecov / codecov/patch

internal/authenticator/auto_authenticator.go#L103-L104

Added lines #L103 - L104 were not covered by tests
}
Loading
Loading