Skip to content

Commit 436ff7c

Browse files
committed
Implemented API endpoint to sign arbitrary data
1 parent f5bc474 commit 436ff7c

20 files changed

+747
-254
lines changed

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d h1:CbB/Ef3TyBvSSJx2HDSUiw49ONTpaX6BGiI0jJEX6b8=
2+
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d/go.mod h1:cfn0Ycx1ASzCkl8+04zI4hrclf9YQ1QfncxzFiNtQLo=
13
github.com/brunomvsouza/singleflight v0.4.0 h1:9dNcTeYoXSus3xbZEM0EEZ11EcCRjUZOvVW8rnDMG5Y=
24
github.com/brunomvsouza/singleflight v0.4.0/go.mod h1:8RYo9j5WQRupmsnUz5DlUWZxDLNi+t9Zhj3EZFmns7I=
35
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=

internal/client/signer/local.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package signer
2+
3+
import (
4+
"context"
5+
"io"
6+
"strings"
7+
)
8+
9+
type Signer interface {
10+
Sign(data io.Reader) ([]byte, error)
11+
GetPublicKey(format string) ([]byte, error)
12+
}
13+
14+
type LocalSigner struct {
15+
Signer
16+
}
17+
18+
func (s *LocalSigner) Sign(ctx context.Context, data string) (string, error) {
19+
signed, err := s.Signer.Sign(strings.NewReader(data))
20+
if err != nil {
21+
return "", err
22+
}
23+
24+
return string(signed), nil
25+
}
26+
27+
func (s *LocalSigner) GetPublicKey(ctx context.Context, format string) (string, error) {
28+
publicKey, err := s.Signer.GetPublicKey(format)
29+
if err != nil {
30+
return "", err
31+
}
32+
33+
return string(publicKey), nil
34+
}

internal/client/signer/local_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package signer
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io"
7+
"testing"
8+
9+
"github.com/stretchr/testify/mock"
10+
"github.com/stretchr/testify/suite"
11+
)
12+
13+
type SignerMock struct {
14+
mock.Mock
15+
}
16+
17+
func (m *SignerMock) Sign(data io.Reader) ([]byte, error) {
18+
args := m.Called(data)
19+
var result []byte
20+
if casted, ok := args.Get(0).([]byte); ok {
21+
result = casted
22+
}
23+
24+
return result, args.Error(1)
25+
}
26+
27+
func (m *SignerMock) GetPublicKey(format string) ([]byte, error) {
28+
args := m.Called(format)
29+
var result []byte
30+
if casted, ok := args.Get(0).([]byte); ok {
31+
result = casted
32+
}
33+
34+
return result, args.Error(1)
35+
}
36+
37+
type LocalSignerServiceTestSuite struct {
38+
suite.Suite
39+
40+
Service *LocalSigner
41+
42+
Signer *SignerMock
43+
}
44+
45+
func (t *LocalSignerServiceTestSuite) SetupSubTest() {
46+
t.Signer = &SignerMock{}
47+
48+
t.Service = &LocalSigner{
49+
Signer: t.Signer,
50+
}
51+
}
52+
53+
func (t *LocalSignerServiceTestSuite) TearDownSubTest() {
54+
t.Signer.AssertExpectations(t.T())
55+
}
56+
57+
func (t *LocalSignerServiceTestSuite) TestSign() {
58+
t.Run("successfully sign", func() {
59+
signature := []byte("mock signature")
60+
t.Signer.On("Sign", mock.Anything).Return(signature, nil).Run(func(args mock.Arguments) {
61+
r, _ := io.ReadAll(args.Get(0).(io.Reader))
62+
t.Equal([]byte("mock body to sign"), r)
63+
})
64+
65+
result, err := t.Service.Sign(context.Background(), "mock body to sign")
66+
t.NoError(err)
67+
t.Equal(string(signature), result)
68+
})
69+
70+
t.Run("handle error during sign", func() {
71+
expectedErr := errors.New("mock error")
72+
t.Signer.On("Sign", mock.Anything).Return(nil, expectedErr)
73+
74+
result, err := t.Service.Sign(context.Background(), "mock body to sign")
75+
t.Error(err)
76+
t.Same(expectedErr, err)
77+
t.Empty(result)
78+
})
79+
}
80+
81+
func (t *LocalSignerServiceTestSuite) TestGetPublicKey() {
82+
t.Run("successfully get", func() {
83+
publicKey := []byte("mock public key")
84+
t.Signer.On("GetPublicKey", "pem").Return(publicKey, nil)
85+
86+
result, err := t.Service.GetPublicKey(context.Background(), "pem")
87+
t.NoError(err)
88+
t.Equal(string(publicKey), result)
89+
})
90+
91+
t.Run("handle error", func() {
92+
expectedErr := errors.New("mock error")
93+
t.Signer.On("GetPublicKey", "pem").Return(nil, expectedErr)
94+
95+
result, err := t.Service.GetPublicKey(context.Background(), "pem")
96+
t.Error(err)
97+
t.Same(expectedErr, err)
98+
t.Empty(result)
99+
})
100+
}
101+
102+
func TestLocalSignerService(t *testing.T) {
103+
suite.Run(t, new(LocalSignerServiceTestSuite))
104+
}

internal/cmd/serve.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package cmd
22

33
import (
44
"github.com/spf13/cobra"
5+
6+
"ely.by/chrly/internal/di"
57
)
68

79
var serveCmd = &cobra.Command{
810
Use: "serve",
911
Short: "Starts HTTP handler for the skins system",
1012
RunE: func(cmd *cobra.Command, args []string) error {
11-
return startServer("skinsystem", "api")
13+
return startServer(di.ModuleSkinsystem, di.ModuleProfiles, di.ModuleSigner)
1214
},
1315
}
1416

internal/cmd/token.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
)
1010

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

22-
token, err := auth.NewToken(security.ProfileScope)
24+
scopes := make([]security.Scope, len(args))
25+
for i := range args {
26+
scopes[i] = security.Scope(args[i])
27+
}
28+
29+
token, err := auth.NewToken(scopes...)
2330
if err != nil {
2431
return fmt.Errorf("Unable to create a new token. The error is %v\n", err)
2532
}

internal/di/config.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,5 @@ import (
66
)
77

88
var configDiOptions = di.Options(
9-
di.Provide(newConfig),
9+
di.Provide(viper.GetViper),
1010
)
11-
12-
func newConfig() *viper.Viper {
13-
return viper.GetViper()
14-
}

internal/di/handlers.go

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import (
1111
"github.com/spf13/viper"
1212

1313
. "ely.by/chrly/internal/http"
14+
"ely.by/chrly/internal/security"
1415
)
1516

17+
const ModuleSkinsystem = "skinsystem"
18+
const ModuleProfiles = "profiles"
19+
const ModuleSigner = "signer"
20+
1621
var handlersDiOptions = di.Options(
1722
di.Provide(newHandlerFactory, di.As(new(http.Handler))),
18-
di.Provide(newSkinsystemHandler, di.WithName("skinsystem")),
19-
di.Provide(newApiHandler, di.WithName("api")),
23+
di.Provide(newSkinsystemHandler, di.WithName(ModuleSkinsystem)),
24+
di.Provide(newProfilesApiHandler, di.WithName(ModuleProfiles)),
25+
di.Provide(newSignerApiHandler, di.WithName(ModuleSigner)),
2026
)
2127

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

44-
if slices.Contains(enabledModules, "api") {
45-
var apiRouter *mux.Router
46-
if err := container.Resolve(&apiRouter, di.Name("api")); err != nil {
50+
if slices.Contains(enabledModules, ModuleProfiles) {
51+
var profilesApiRouter *mux.Router
52+
if err := container.Resolve(&profilesApiRouter, di.Name(ModuleProfiles)); err != nil {
4753
return nil, err
4854
}
4955

@@ -52,9 +58,29 @@ func newHandlerFactory(
5258
return nil, err
5359
}
5460

55-
apiRouter.Use(CreateAuthenticationMiddleware(authenticator))
61+
profilesApiRouter.Use(NewAuthenticationMiddleware(authenticator, security.ProfilesScope))
5662

57-
mount(router, "/api", apiRouter)
63+
mount(router, "/api/profiles", profilesApiRouter)
64+
}
65+
66+
if slices.Contains(enabledModules, ModuleSigner) {
67+
var signerApiRouter *mux.Router
68+
if err := container.Resolve(&signerApiRouter, di.Name(ModuleSigner)); err != nil {
69+
return nil, err
70+
}
71+
72+
var authenticator Authenticator
73+
if err := container.Resolve(&authenticator); err != nil {
74+
return nil, err
75+
}
76+
77+
authMiddleware := NewAuthenticationMiddleware(authenticator, security.SignScope)
78+
conditionalAuth := NewConditionalMiddleware(func(req *http.Request) bool {
79+
return req.Method != "GET"
80+
}, authMiddleware)
81+
signerApiRouter.Use(conditionalAuth)
82+
83+
mount(router, "/api/signer", signerApiRouter)
5884
}
5985

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

87113
return (&Skinsystem{
88114
ProfilesProvider: profilesProvider,
89-
TexturesSigner: texturesSigner,
115+
SignerService: texturesSigner,
90116
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
91117
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
92118
}).Handler()
93119
}
94120

95-
func newApiHandler(profilesManager ProfilesManager) *mux.Router {
96-
return (&Api{
121+
func newProfilesApiHandler(profilesManager ProfilesManager) *mux.Router {
122+
return (&ProfilesApi{
97123
ProfilesManager: profilesManager,
98124
}).Handler()
99125
}
100126

127+
func newSignerApiHandler(signer Signer) *mux.Router {
128+
return (&SignerApi{
129+
Signer: signer,
130+
}).Handler()
131+
}
132+
101133
func mount(router *mux.Router, path string, handler http.Handler) {
102134
router.PathPrefix(path).Handler(
103135
http.StripPrefix(

internal/di/security.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/pem"
99
"strings"
1010

11+
signerClient "ely.by/chrly/internal/client/signer"
1112
"ely.by/chrly/internal/http"
1213
"ely.by/chrly/internal/security"
1314

@@ -16,12 +17,14 @@ import (
1617
)
1718

1819
var securityDiOptions = di.Options(
19-
di.Provide(newTexturesSigner,
20-
di.As(new(http.TexturesSigner)),
20+
di.Provide(newSigner,
21+
di.As(new(http.Signer)),
22+
di.As(new(signerClient.Signer)),
2123
),
24+
di.Provide(newSignerService),
2225
)
2326

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

5558
return security.NewSigner(privateKey), nil
5659
}
60+
61+
func newSignerService(signer signerClient.Signer) http.SignerService {
62+
return &signerClient.LocalSigner{
63+
Signer: signer,
64+
}
65+
}

0 commit comments

Comments
 (0)