Skip to content

Commit 5e30719

Browse files
validate capi-auth-token on dqlite/remove (#56)
1 parent 5083ae8 commit 5e30719

File tree

9 files changed

+137
-7
lines changed

9 files changed

+137
-7
lines changed

pkg/api/v2/consts.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package v2
2+
3+
const (
4+
// CAPIAuthTokenHeader is the header used to pass the CAPI auth token.
5+
CAPIAuthTokenHeader = "capi-auth-token"
6+
)

pkg/api/v2/register.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ func (a *API) RegisterServer(server *http.ServeMux, middleware func(f http.Handl
6767
return
6868
}
6969

70-
if rc, err := a.RemoveFromDqlite(r.Context(), req); err != nil {
70+
token := r.Header.Get(CAPIAuthTokenHeader)
71+
72+
if rc, err := a.RemoveFromDqlite(r.Context(), req, token); err != nil {
7173
httputil.Error(w, rc, fmt.Errorf("failed to remove from dqlite: %w", err))
7274
return
7375
}

pkg/api/v2/remove.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,20 @@ import (
1111
// RemoveFromDqliteRequest represents a request to remove a node from the dqlite cluster.
1212
type RemoveFromDqliteRequest struct {
1313
// RemoveEndpoint is the endpoint of the node to remove from the dqlite cluster.
14-
RemoveEndpoint string `json:"removeEndpoint"`
14+
RemoveEndpoint string `json:"remove_endpoint"`
1515
}
1616

1717
// RemoveFromDqlite implements the "POST /v2/dqlite/remove" endpoint and removes a node from the dqlite cluster.
18-
func (a *API) RemoveFromDqlite(ctx context.Context, req RemoveFromDqliteRequest) (int, error) {
18+
func (a *API) RemoveFromDqlite(ctx context.Context, req RemoveFromDqliteRequest, token string) (int, error) {
19+
isValid, err := a.Snap.IsCAPIAuthTokenValid(token)
20+
if err != nil {
21+
return http.StatusInternalServerError, fmt.Errorf("failed to validate CAPI auth token: %w", err)
22+
}
23+
24+
if !isValid {
25+
return http.StatusUnauthorized, fmt.Errorf("invalid CAPI auth token %q", token)
26+
}
27+
1928
if err := snaputil.RemoveNodeFromDqlite(ctx, a.Snap, req.RemoveEndpoint); err != nil {
2029
return http.StatusInternalServerError, fmt.Errorf("failed to remove node from dqlite: %w", err)
2130
}

pkg/api/v2/remove_test.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,55 @@ func TestRemove(t *testing.T) {
1717
cmdErr := errors.New("failed to run command")
1818
apiv2 := &v2.API{
1919
Snap: &mock.Snap{
20-
RunCommandErr: cmdErr,
20+
RunCommandErr: cmdErr,
21+
CAPIAuthTokenValid: true,
2122
},
2223
}
2324

24-
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"})
25+
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")
2526

2627
g := NewWithT(t)
2728
g.Expect(err).To(MatchError(cmdErr))
2829
g.Expect(rc).To(Equal(http.StatusInternalServerError))
2930
})
3031

32+
t.Run("InvalidToken", func(t *testing.T) {
33+
apiv2 := &v2.API{
34+
Snap: &mock.Snap{
35+
CAPIAuthTokenValid: false, // explicitly set to false
36+
},
37+
}
38+
39+
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")
40+
41+
g := NewWithT(t)
42+
g.Expect(err).To(HaveOccurred())
43+
g.Expect(rc).To(Equal(http.StatusUnauthorized))
44+
})
45+
46+
t.Run("TokenFileNotFound", func(t *testing.T) {
47+
tokenErr := errors.New("token file not found")
48+
apiv2 := &v2.API{
49+
Snap: &mock.Snap{
50+
CAPIAuthTokenError: tokenErr,
51+
},
52+
}
53+
54+
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")
55+
56+
g := NewWithT(t)
57+
g.Expect(err).To(MatchError(tokenErr))
58+
g.Expect(rc).To(Equal(http.StatusInternalServerError))
59+
})
60+
3161
t.Run("RemovesSuccessfully", func(t *testing.T) {
3262
apiv2 := &v2.API{
33-
Snap: &mock.Snap{},
63+
Snap: &mock.Snap{
64+
CAPIAuthTokenValid: true,
65+
},
3466
}
3567

36-
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"})
68+
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")
3769

3870
g := NewWithT(t)
3971
g.Expect(err).ToNot(HaveOccurred())

pkg/snap/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ type Snap interface {
1313
GetSnapDataPath(parts ...string) string
1414
// GetSnapCommonPath returns the path to a file or directory in the snap's common directory.
1515
GetSnapCommonPath(parts ...string) string
16+
// GetCAPIPath returns the path to a file or directory in the CAPI directory.
17+
GetCAPIPath(parts ...string) string
1618

1719
// RunCommand runs a shell command.
1820
RunCommand(ctx context.Context, commands ...string) error
@@ -98,6 +100,9 @@ type Snap interface {
98100
// GetKnownToken returns the token for a known user from the known_users.csv file.
99101
GetKnownToken(username string) (string, error)
100102

103+
// IsCAPIAuthTokenValid returns true if token is a valid CAPI auth token.
104+
IsCAPIAuthTokenValid(token string) (bool, error)
105+
101106
// SignCertificate signs the certificate signing request, and returns the certificate in PEM format.
102107
SignCertificate(ctx context.Context, csrPEM []byte) ([]byte, error)
103108

pkg/snap/mock/mock.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Snap struct {
3434
SnapDir string
3535
SnapDataDir string
3636
SnapCommonDir string
37+
CAPIDir string
3738

3839
RunCommandCalledWith []RunCommandCall
3940
RunCommandErr error
@@ -85,6 +86,9 @@ type Snap struct {
8586
KubeletTokens map[string]string // map hostname to token
8687
KnownTokens map[string]string // map username to token
8788

89+
CAPIAuthTokenValid bool
90+
CAPIAuthTokenError error
91+
8892
SignCertificateCalledWith []string // string(csrPEM)
8993
SignedCertificate string
9094

@@ -116,6 +120,11 @@ func (s *Snap) GetSnapCommonPath(parts ...string) string {
116120
return filepath.Join(append([]string{s.SnapCommonDir}, parts...)...)
117121
}
118122

123+
// GetCAPIPath is a mock implementation for the snap.Snap interface.
124+
func (s *Snap) GetCAPIPath(parts ...string) string {
125+
return filepath.Join(append([]string{s.CAPIDir}, parts...)...)
126+
}
127+
119128
// RunCommand is a mock implementation for the snap.Snap interface.
120129
func (s *Snap) RunCommand(_ context.Context, commands ...string) error {
121130
s.RunCommandCalledWith = append(s.RunCommandCalledWith, RunCommandCall{Commands: commands})
@@ -320,6 +329,11 @@ func (s *Snap) GetKnownToken(username string) (string, error) {
320329
return "", fmt.Errorf("no known token for user %s", username)
321330
}
322331

332+
// IsCAPIAuthTokenValid is a mock implementation for the snap.Snap interface.
333+
func (s *Snap) IsCAPIAuthTokenValid(token string) (bool, error) {
334+
return s.CAPIAuthTokenValid, s.CAPIAuthTokenError
335+
}
336+
323337
// RunUpgrade is a mock implementation for the snap.Snap interface.
324338
func (s *Snap) RunUpgrade(ctx context.Context, upgrade string, phase string) error {
325339
s.RunUpgradeCalledWith = append(s.RunUpgradeCalledWith, fmt.Sprintf("%s %s", upgrade, phase))

pkg/snap/options.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,10 @@ func WithCommandRunner(f func(context.Context, ...string) error) func(s *snap) {
2222
s.runCommand = f
2323
}
2424
}
25+
26+
// WithCAPIPath configures the path to the CAPI directory.
27+
func WithCAPIPath(path string) func(s *snap) {
28+
return func(s *snap) {
29+
s.capiPath = path
30+
}
31+
}

pkg/snap/snap.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type snap struct {
2323
snapDir string
2424
snapDataDir string
2525
snapCommonDir string
26+
capiPath string
2627
runCommand func(context.Context, ...string) error
2728

2829
clusterTokensMu sync.Mutex
@@ -34,13 +35,18 @@ type snap struct {
3435
applyCNIBackoff time.Duration
3536
}
3637

38+
const (
39+
defaultCAPIPath = "/capi"
40+
)
41+
3742
// NewSnap creates a new interface with the MicroK8s snap.
3843
// NewSnap accepts the $SNAP, $SNAP_DATA and $SNAP_COMMON, directories, and a number of options.
3944
func NewSnap(snapDir, snapDataDir, snapCommonDir string, options ...func(s *snap)) Snap {
4045
s := &snap{
4146
snapDir: snapDir,
4247
snapDataDir: snapDataDir,
4348
snapCommonDir: snapCommonDir,
49+
capiPath: defaultCAPIPath,
4450
runCommand: util.RunCommand,
4551
}
4652

@@ -65,6 +71,9 @@ func (s *snap) GetSnapDataPath(parts ...string) string {
6571
func (s *snap) GetSnapCommonPath(parts ...string) string {
6672
return filepath.Join(append([]string{s.snapCommonDir}, parts...)...)
6773
}
74+
func (s *snap) GetCAPIPath(parts ...string) string {
75+
return filepath.Join(append([]string{s.capiPath}, parts...)...)
76+
}
6877

6978
func (s *snap) GetGroupName() string {
7079
if s.isStrict() {
@@ -331,6 +340,15 @@ func (s *snap) GetKnownToken(username string) (string, error) {
331340
return "", fmt.Errorf("no known token found for user %s", username)
332341
}
333342

343+
// IsCAPIAuthTokenValid checks if the given CAPI auth token is valid.
344+
func (s *snap) IsCAPIAuthTokenValid(token string) (bool, error) {
345+
contents, err := util.ReadFile(s.GetCAPIPath("etc", "token"))
346+
if err != nil {
347+
return false, fmt.Errorf("failed to read token file: %w", err)
348+
}
349+
return strings.TrimSpace(contents) == token, nil
350+
}
351+
334352
func (s *snap) SignCertificate(ctx context.Context, csrPEM []byte) ([]byte, error) {
335353
// TODO: consider using crypto/x509 for this instead of relying on openssl commands.
336354
// NOTE(neoaggelos): x509.CreateCertificate() has some hardcoded fields that are incompatible with MicroK8s.

pkg/snap/snap_capi_token_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package snap_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
. "github.com/onsi/gomega"
9+
10+
"github.com/canonical/microk8s-cluster-agent/pkg/snap"
11+
)
12+
13+
func TestCAPIAuthToken(t *testing.T) {
14+
capiTestPath := "./capi-test"
15+
os.RemoveAll(capiTestPath)
16+
s := snap.NewSnap("", "", "", snap.WithCAPIPath(capiTestPath))
17+
token := "token123"
18+
19+
g := NewWithT(t)
20+
21+
isValid, err := s.IsCAPIAuthTokenValid(token)
22+
g.Expect(err).To(MatchError(os.ErrNotExist))
23+
g.Expect(isValid).To(BeFalse())
24+
25+
capiEtc := filepath.Join(capiTestPath, "etc")
26+
defer os.RemoveAll(capiTestPath)
27+
g.Expect(os.MkdirAll(capiEtc, 0755)).To(Succeed())
28+
g.Expect(os.WriteFile("./capi-test/etc/token", []byte(token), 0600)).To(Succeed())
29+
30+
isValid, err = s.IsCAPIAuthTokenValid("random-token")
31+
g.Expect(err).ToNot(HaveOccurred())
32+
g.Expect(isValid).To(BeFalse())
33+
34+
isValid, err = s.IsCAPIAuthTokenValid(token)
35+
g.Expect(err).ToNot(HaveOccurred())
36+
g.Expect(isValid).To(BeTrue())
37+
}

0 commit comments

Comments
 (0)