Skip to content

Commit 7964281

Browse files
committed
Merge branch 'v5' into otel
# Conflicts: # go.sum # internal/cmd/serve.go # internal/http/http.go
2 parents f037fb1 + 528b131 commit 7964281

19 files changed

+758
-275
lines changed

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/spf13/cobra"
88
"github.com/spf13/viper"
99

10+
"ely.by/chrly/internal/di"
1011
"ely.by/chrly/internal/http"
1112
"ely.by/chrly/internal/otel"
1213
)
@@ -15,7 +16,7 @@ var serveCmd = &cobra.Command{
1516
Use: "serve",
1617
Short: "Starts HTTP handler for the skins system",
1718
RunE: func(cmd *cobra.Command, args []string) error {
18-
return startServer("skinsystem", "api")
19+
return startServer(di.ModuleSkinsystem, di.ModuleProfiles, di.ModuleSigner)
1920
},
2021
}
2122

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
@@ -12,12 +12,18 @@ import (
1212
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
1313

1414
. "ely.by/chrly/internal/http"
15+
"ely.by/chrly/internal/security"
1516
)
1617

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

2329
func newHandlerFactory(
@@ -31,8 +37,8 @@ func newHandlerFactory(
3137
// if you set an empty prefix. Since the main application should be mounted at the root prefix,
3238
// we use it as the base router
3339
var router *mux.Router
34-
if slices.Contains(enabledModules, "skinsystem") {
35-
if err := container.Resolve(&router, di.Name("skinsystem")); err != nil {
40+
if slices.Contains(enabledModules, ModuleSkinsystem) {
41+
if err := container.Resolve(&router, di.Name(ModuleSkinsystem)); err != nil {
3642
return nil, err
3743
}
3844
} else {
@@ -43,9 +49,9 @@ func newHandlerFactory(
4349
router.Use(otelmux.Middleware("chrly"))
4450
router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
4551

46-
if slices.Contains(enabledModules, "api") {
47-
var apiRouter *mux.Router
48-
if err := container.Resolve(&apiRouter, di.Name("api")); err != nil {
52+
if slices.Contains(enabledModules, ModuleProfiles) {
53+
var profilesApiRouter *mux.Router
54+
if err := container.Resolve(&profilesApiRouter, di.Name(ModuleProfiles)); err != nil {
4955
return nil, err
5056
}
5157

@@ -54,9 +60,29 @@ func newHandlerFactory(
5460
return nil, err
5561
}
5662

57-
apiRouter.Use(CreateAuthenticationMiddleware(authenticator))
63+
profilesApiRouter.Use(NewAuthenticationMiddleware(authenticator, security.ProfilesScope))
5864

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

6288
// Resolve health checkers last, because all the services required by the application
@@ -81,25 +107,31 @@ func newHandlerFactory(
81107
func newSkinsystemHandler(
82108
config *viper.Viper,
83109
profilesProvider ProfilesProvider,
84-
texturesSigner TexturesSigner,
110+
texturesSigner SignerService,
85111
) *mux.Router {
86112
config.SetDefault("textures.extra_param_name", "chrly")
87113
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
88114

89115
return (&Skinsystem{
90116
ProfilesProvider: profilesProvider,
91-
TexturesSigner: texturesSigner,
117+
SignerService: texturesSigner,
92118
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
93119
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
94120
}).Handler()
95121
}
96122

97-
func newApiHandler(profilesManager ProfilesManager) *mux.Router {
98-
return (&Api{
123+
func newProfilesApiHandler(profilesManager ProfilesManager) *mux.Router {
124+
return (&ProfilesApi{
99125
ProfilesManager: profilesManager,
100126
}).Handler()
101127
}
102128

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

internal/di/security.go

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"crypto/rand"
55
"crypto/rsa"
66
"crypto/x509"
7-
"encoding/base64"
87
"encoding/pem"
9-
"strings"
8+
"errors"
9+
"log/slog"
1010

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

@@ -16,41 +17,43 @@ 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(signer.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) {
28+
var privateKey *rsa.PrivateKey
29+
var err error
30+
2531
keyStr := config.GetString("chrly.signing.key")
2632
if keyStr == "" {
27-
// TODO: log a message about the generated signing key and the way to specify it permanently
28-
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
33+
privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
2934
if err != nil {
3035
return nil, err
3136
}
3237

33-
return security.NewSigner(privateKey), nil
34-
}
38+
slog.Warn("A private signing key has been generated. To make it permanent, specify the valid RSA private key in the config parameter chrly.signing.key")
39+
} else {
40+
keyBytes := []byte(keyStr)
41+
rawPem, _ := pem.Decode(keyBytes)
42+
if rawPem == nil {
43+
return nil, errors.New("unable to decode pem key")
44+
}
3545

36-
var keyBytes []byte
37-
if strings.HasPrefix(keyStr, "base64:") {
38-
base64Value := keyStr[7:]
39-
decodedKey, err := base64.URLEncoding.DecodeString(base64Value)
46+
privateKey, err = x509.ParsePKCS1PrivateKey(rawPem.Bytes)
4047
if err != nil {
4148
return nil, err
4249
}
43-
44-
keyBytes = decodedKey
45-
} else {
46-
keyBytes = []byte(keyStr)
47-
}
48-
49-
rawPem, _ := pem.Decode(keyBytes)
50-
privateKey, err := x509.ParsePKCS1PrivateKey(rawPem.Bytes)
51-
if err != nil {
52-
return nil, err
5350
}
5451

5552
return security.NewSigner(privateKey), nil
5653
}
54+
55+
func newSignerService(s signer.Signer) http.SignerService {
56+
return &signer.LocalSigner{
57+
Signer: s,
58+
}
59+
}

0 commit comments

Comments
 (0)