Skip to content

Commit

Permalink
Implemented API endpoint to sign arbitrary data
Browse files Browse the repository at this point in the history
  • Loading branch information
erickskrauch committed Mar 5, 2024
1 parent f5bc474 commit 436ff7c
Show file tree
Hide file tree
Showing 20 changed files with 747 additions and 254 deletions.
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d h1:CbB/Ef3TyBvSSJx2HDSUiw49ONTpaX6BGiI0jJEX6b8=
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d/go.mod h1:cfn0Ycx1ASzCkl8+04zI4hrclf9YQ1QfncxzFiNtQLo=
github.com/brunomvsouza/singleflight v0.4.0 h1:9dNcTeYoXSus3xbZEM0EEZ11EcCRjUZOvVW8rnDMG5Y=
github.com/brunomvsouza/singleflight v0.4.0/go.mod h1:8RYo9j5WQRupmsnUz5DlUWZxDLNi+t9Zhj3EZFmns7I=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
Expand Down
34 changes: 34 additions & 0 deletions internal/client/signer/local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package signer

import (
"context"
"io"
"strings"
)

type Signer interface {
Sign(data io.Reader) ([]byte, error)
GetPublicKey(format string) ([]byte, error)
}

type LocalSigner struct {
Signer
}

func (s *LocalSigner) Sign(ctx context.Context, data string) (string, error) {
signed, err := s.Signer.Sign(strings.NewReader(data))
if err != nil {
return "", err
}

return string(signed), nil
}

func (s *LocalSigner) GetPublicKey(ctx context.Context, format string) (string, error) {
publicKey, err := s.Signer.GetPublicKey(format)
if err != nil {
return "", err
}

return string(publicKey), nil
}
104 changes: 104 additions & 0 deletions internal/client/signer/local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package signer

import (
"context"
"errors"
"io"
"testing"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)

type SignerMock struct {
mock.Mock
}

func (m *SignerMock) Sign(data io.Reader) ([]byte, error) {
args := m.Called(data)
var result []byte
if casted, ok := args.Get(0).([]byte); ok {
result = casted
}

return result, args.Error(1)
}

func (m *SignerMock) GetPublicKey(format string) ([]byte, error) {
args := m.Called(format)
var result []byte
if casted, ok := args.Get(0).([]byte); ok {
result = casted
}

return result, args.Error(1)
}

type LocalSignerServiceTestSuite struct {
suite.Suite

Service *LocalSigner

Signer *SignerMock
}

func (t *LocalSignerServiceTestSuite) SetupSubTest() {
t.Signer = &SignerMock{}

t.Service = &LocalSigner{
Signer: t.Signer,
}
}

func (t *LocalSignerServiceTestSuite) TearDownSubTest() {
t.Signer.AssertExpectations(t.T())
}

func (t *LocalSignerServiceTestSuite) TestSign() {
t.Run("successfully sign", func() {
signature := []byte("mock signature")
t.Signer.On("Sign", mock.Anything).Return(signature, nil).Run(func(args mock.Arguments) {
r, _ := io.ReadAll(args.Get(0).(io.Reader))
t.Equal([]byte("mock body to sign"), r)
})

result, err := t.Service.Sign(context.Background(), "mock body to sign")
t.NoError(err)
t.Equal(string(signature), result)
})

t.Run("handle error during sign", func() {
expectedErr := errors.New("mock error")
t.Signer.On("Sign", mock.Anything).Return(nil, expectedErr)

result, err := t.Service.Sign(context.Background(), "mock body to sign")
t.Error(err)
t.Same(expectedErr, err)
t.Empty(result)
})
}

func (t *LocalSignerServiceTestSuite) TestGetPublicKey() {
t.Run("successfully get", func() {
publicKey := []byte("mock public key")
t.Signer.On("GetPublicKey", "pem").Return(publicKey, nil)

result, err := t.Service.GetPublicKey(context.Background(), "pem")
t.NoError(err)
t.Equal(string(publicKey), result)
})

t.Run("handle error", func() {
expectedErr := errors.New("mock error")
t.Signer.On("GetPublicKey", "pem").Return(nil, expectedErr)

result, err := t.Service.GetPublicKey(context.Background(), "pem")
t.Error(err)
t.Same(expectedErr, err)
t.Empty(result)
})
}

func TestLocalSignerService(t *testing.T) {
suite.Run(t, new(LocalSignerServiceTestSuite))
}
4 changes: 3 additions & 1 deletion internal/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package cmd

import (
"github.com/spf13/cobra"

"ely.by/chrly/internal/di"
)

var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts HTTP handler for the skins system",
RunE: func(cmd *cobra.Command, args []string) error {
return startServer("skinsystem", "api")
return startServer(di.ModuleSkinsystem, di.ModuleProfiles, di.ModuleSigner)
},
}

Expand Down
13 changes: 10 additions & 3 deletions internal/cmd/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
)

var tokenCmd = &cobra.Command{
Use: "token",
Short: "Creates a new token, which allows to interact with Chrly API",
Use: "token scope1 ...",
Example: "token profiles sign",
Short: "Creates a new token, which allows to interact with Chrly API",
ValidArgs: []string{string(security.ProfilesScope), string(security.SignScope)},
RunE: func(cmd *cobra.Command, args []string) error {
container := shouldGetContainer()
var auth *security.Jwt
Expand All @@ -19,7 +21,12 @@ var tokenCmd = &cobra.Command{
return err
}

token, err := auth.NewToken(security.ProfileScope)
scopes := make([]security.Scope, len(args))
for i := range args {
scopes[i] = security.Scope(args[i])
}

token, err := auth.NewToken(scopes...)
if err != nil {
return fmt.Errorf("Unable to create a new token. The error is %v\n", err)
}
Expand Down
6 changes: 1 addition & 5 deletions internal/di/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,5 @@ import (
)

var configDiOptions = di.Options(
di.Provide(newConfig),
di.Provide(viper.GetViper),
)

func newConfig() *viper.Viper {
return viper.GetViper()
}
58 changes: 45 additions & 13 deletions internal/di/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import (
"github.com/spf13/viper"

. "ely.by/chrly/internal/http"
"ely.by/chrly/internal/security"
)

const ModuleSkinsystem = "skinsystem"
const ModuleProfiles = "profiles"
const ModuleSigner = "signer"

var handlersDiOptions = di.Options(
di.Provide(newHandlerFactory, di.As(new(http.Handler))),
di.Provide(newSkinsystemHandler, di.WithName("skinsystem")),
di.Provide(newApiHandler, di.WithName("api")),
di.Provide(newSkinsystemHandler, di.WithName(ModuleSkinsystem)),
di.Provide(newProfilesApiHandler, di.WithName(ModuleProfiles)),
di.Provide(newSignerApiHandler, di.WithName(ModuleSigner)),
)

func newHandlerFactory(
Expand All @@ -30,8 +36,8 @@ func newHandlerFactory(
// if you set an empty prefix. Since the main application should be mounted at the root prefix,
// we use it as the base router
var router *mux.Router
if slices.Contains(enabledModules, "skinsystem") {
if err := container.Resolve(&router, di.Name("skinsystem")); err != nil {
if slices.Contains(enabledModules, ModuleSkinsystem) {
if err := container.Resolve(&router, di.Name(ModuleSkinsystem)); err != nil {
return nil, err
}
} else {
Expand All @@ -41,9 +47,9 @@ func newHandlerFactory(
router.StrictSlash(true)
router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)

if slices.Contains(enabledModules, "api") {
var apiRouter *mux.Router
if err := container.Resolve(&apiRouter, di.Name("api")); err != nil {
if slices.Contains(enabledModules, ModuleProfiles) {
var profilesApiRouter *mux.Router
if err := container.Resolve(&profilesApiRouter, di.Name(ModuleProfiles)); err != nil {
return nil, err
}

Expand All @@ -52,9 +58,29 @@ func newHandlerFactory(
return nil, err
}

apiRouter.Use(CreateAuthenticationMiddleware(authenticator))
profilesApiRouter.Use(NewAuthenticationMiddleware(authenticator, security.ProfilesScope))

mount(router, "/api", apiRouter)
mount(router, "/api/profiles", profilesApiRouter)
}

if slices.Contains(enabledModules, ModuleSigner) {
var signerApiRouter *mux.Router
if err := container.Resolve(&signerApiRouter, di.Name(ModuleSigner)); err != nil {
return nil, err
}

var authenticator Authenticator
if err := container.Resolve(&authenticator); err != nil {
return nil, err
}

authMiddleware := NewAuthenticationMiddleware(authenticator, security.SignScope)
conditionalAuth := NewConditionalMiddleware(func(req *http.Request) bool {
return req.Method != "GET"
}, authMiddleware)
signerApiRouter.Use(conditionalAuth)

mount(router, "/api/signer", signerApiRouter)
}

// Resolve health checkers last, because all the services required by the application
Expand All @@ -79,25 +105,31 @@ func newHandlerFactory(
func newSkinsystemHandler(
config *viper.Viper,
profilesProvider ProfilesProvider,
texturesSigner TexturesSigner,
texturesSigner SignerService,
) *mux.Router {
config.SetDefault("textures.extra_param_name", "chrly")
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")

return (&Skinsystem{
ProfilesProvider: profilesProvider,
TexturesSigner: texturesSigner,
SignerService: texturesSigner,
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
}).Handler()
}

func newApiHandler(profilesManager ProfilesManager) *mux.Router {
return (&Api{
func newProfilesApiHandler(profilesManager ProfilesManager) *mux.Router {
return (&ProfilesApi{
ProfilesManager: profilesManager,
}).Handler()
}

func newSignerApiHandler(signer Signer) *mux.Router {
return (&SignerApi{
Signer: signer,
}).Handler()
}

func mount(router *mux.Router, path string, handler http.Handler) {
router.PathPrefix(path).Handler(
http.StripPrefix(
Expand Down
15 changes: 12 additions & 3 deletions internal/di/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/pem"
"strings"

signerClient "ely.by/chrly/internal/client/signer"
"ely.by/chrly/internal/http"
"ely.by/chrly/internal/security"

Expand All @@ -16,12 +17,14 @@ import (
)

var securityDiOptions = di.Options(
di.Provide(newTexturesSigner,
di.As(new(http.TexturesSigner)),
di.Provide(newSigner,
di.As(new(http.Signer)),
di.As(new(signerClient.Signer)),
),
di.Provide(newSignerService),
)

func newTexturesSigner(config *viper.Viper) (*security.Signer, error) {
func newSigner(config *viper.Viper) (*security.Signer, error) {
keyStr := config.GetString("chrly.signing.key")
if keyStr == "" {
// TODO: log a message about the generated signing key and the way to specify it permanently
Expand Down Expand Up @@ -54,3 +57,9 @@ func newTexturesSigner(config *viper.Viper) (*security.Signer, error) {

return security.NewSigner(privateKey), nil
}

func newSignerService(signer signerClient.Signer) http.SignerService {
return &signerClient.LocalSigner{
Signer: signer,
}
}
Loading

0 comments on commit 436ff7c

Please sign in to comment.