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

validate capi-auth-token on dqlite/remove #56

6 changes: 6 additions & 0 deletions cmd/cluster-agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ var clusterAgentCmd = &cobra.Command{
Long: `The MicroK8s cluster agent is an API server that orchestrates the
lifecycle of a MicroK8s cluster.`,
Run: func(cmd *cobra.Command, args []string) {
capiPath := os.Getenv("CAPI_PATH")
if capiPath == "" {
capiPath = capiDefaultPath
}

s := snap.NewSnap(
os.Getenv("SNAP"),
os.Getenv("SNAP_DATA"),
os.Getenv("SNAP_COMMON"),
capiPath,
HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved
snap.WithRetryApplyCNI(20, 3*time.Second),
)

Expand Down
5 changes: 5 additions & 0 deletions cmd/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cmd

const (
capiDefaultPath = "/capi"
)
6 changes: 6 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ var (
Short: "Apply MicroK8s configurations",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
capiPath := os.Getenv("CAPI_PATH")
if capiPath == "" {
capiPath = capiDefaultPath
}

s := snap.NewSnap(
os.Getenv("SNAP"),
os.Getenv("SNAP_DATA"),
os.Getenv("SNAP_COMMON"),
capiPath,
)
l := k8sinit.NewLauncher(s, initPreInit)

Expand Down
6 changes: 6 additions & 0 deletions pkg/api/v2/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package v2

const (
// CAPIAuthTokenHeader is the header used to pass the CAPI auth token.
CAPIAuthTokenHeader = "capi-auth-token"
)
4 changes: 3 additions & 1 deletion pkg/api/v2/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ func (a *API) RegisterServer(server *http.ServeMux, middleware func(f http.Handl
return
}

if rc, err := a.RemoveFromDqlite(r.Context(), req); err != nil {
token := r.Header.Get(CAPIAuthTokenHeader)

if rc, err := a.RemoveFromDqlite(r.Context(), req, token); err != nil {
httputil.Error(w, rc, fmt.Errorf("failed to remove from dqlite: %w", err))
return
}
Expand Down
10 changes: 8 additions & 2 deletions pkg/api/v2/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ import (
// RemoveFromDqliteRequest represents a request to remove a node from the dqlite cluster.
type RemoveFromDqliteRequest struct {
// RemoveEndpoint is the endpoint of the node to remove from the dqlite cluster.
RemoveEndpoint string `json:"removeEndpoint"`
RemoveEndpoint string `json:"remove_endpoint"`
}

// RemoveFromDqlite implements the "POST /v2/dqlite/remove" endpoint and removes a node from the dqlite cluster.
func (a *API) RemoveFromDqlite(ctx context.Context, req RemoveFromDqliteRequest) (int, error) {
func (a *API) RemoveFromDqlite(ctx context.Context, req RemoveFromDqliteRequest, token string) (int, error) {
if isValid, err := a.Snap.IsCAPIAuthTokenValid(token); err != nil {
return http.StatusUnauthorized, fmt.Errorf("failed to validate CAPI auth token: %w", err)
HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved
} else if !isValid {
return http.StatusUnauthorized, fmt.Errorf("invalid CAPI auth token %q", token)
}
HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved

if err := snaputil.RemoveNodeFromDqlite(ctx, a.Snap, req.RemoveEndpoint); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to remove node from dqlite: %w", err)
}
Expand Down
40 changes: 36 additions & 4 deletions pkg/api/v2/remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,55 @@ func TestRemove(t *testing.T) {
cmdErr := errors.New("failed to run command")
apiv2 := &v2.API{
Snap: &mock.Snap{
RunCommandErr: cmdErr,
RunCommandErr: cmdErr,
CAPIAuthTokenValid: true,
},
}

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

g := NewWithT(t)
g.Expect(err).To(MatchError(cmdErr))
g.Expect(rc).To(Equal(http.StatusInternalServerError))
})

t.Run("InvalidToken", func(t *testing.T) {
apiv2 := &v2.API{
Snap: &mock.Snap{
CAPIAuthTokenValid: false, // explicitly set to false
},
}

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

g := NewWithT(t)
g.Expect(err).To(HaveOccurred())
g.Expect(rc).To(Equal(http.StatusUnauthorized))
})

t.Run("TokenFileNotFound", func(t *testing.T) {
tokenErr := errors.New("token file not found")
apiv2 := &v2.API{
Snap: &mock.Snap{
CAPIAuthTokenError: tokenErr,
},
}

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

g := NewWithT(t)
g.Expect(err).To(MatchError(tokenErr))
g.Expect(rc).To(Equal(http.StatusUnauthorized))
})

t.Run("RemovesSuccessfully", func(t *testing.T) {
apiv2 := &v2.API{
Snap: &mock.Snap{},
Snap: &mock.Snap{
CAPIAuthTokenValid: true,
},
}

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

g := NewWithT(t)
g.Expect(err).ToNot(HaveOccurred())
Expand Down
5 changes: 5 additions & 0 deletions pkg/snap/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type Snap interface {
GetSnapDataPath(parts ...string) string
// GetSnapCommonPath returns the path to a file or directory in the snap's common directory.
GetSnapCommonPath(parts ...string) string
// GetCAPIPath returns the path to a file or directory in the CAPI directory.
GetCAPIPath(parts ...string) string

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

// IsCAPIAuthTokenValid returns true if token is a valid CAPI auth token.
IsCAPIAuthTokenValid(token string) (bool, error)

// SignCertificate signs the certificate signing request, and returns the certificate in PEM format.
SignCertificate(ctx context.Context, csrPEM []byte) ([]byte, error)

Expand Down
14 changes: 14 additions & 0 deletions pkg/snap/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Snap struct {
SnapDir string
SnapDataDir string
SnapCommonDir string
CAPIDir string

RunCommandCalledWith []RunCommandCall
RunCommandErr error
Expand Down Expand Up @@ -85,6 +86,9 @@ type Snap struct {
KubeletTokens map[string]string // map hostname to token
KnownTokens map[string]string // map username to token

CAPIAuthTokenValid bool
CAPIAuthTokenError error

SignCertificateCalledWith []string // string(csrPEM)
SignedCertificate string

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

// GetCAPIPath is a mock implementation for the snap.Snap interface.
func (s *Snap) GetCAPIPath(parts ...string) string {
return filepath.Join(append([]string{s.CAPIDir}, parts...)...)
}

// RunCommand is a mock implementation for the snap.Snap interface.
func (s *Snap) RunCommand(_ context.Context, commands ...string) error {
s.RunCommandCalledWith = append(s.RunCommandCalledWith, RunCommandCall{Commands: commands})
Expand Down Expand Up @@ -320,6 +329,11 @@ func (s *Snap) GetKnownToken(username string) (string, error) {
return "", fmt.Errorf("no known token for user %s", username)
}

// IsCAPIAuthTokenValid is a mock implementation for the snap.Snap interface.
func (s *Snap) IsCAPIAuthTokenValid(token string) (bool, error) {
return s.CAPIAuthTokenValid, s.CAPIAuthTokenError
}

// RunUpgrade is a mock implementation for the snap.Snap interface.
func (s *Snap) RunUpgrade(ctx context.Context, upgrade string, phase string) error {
s.RunUpgradeCalledWith = append(s.RunUpgradeCalledWith, fmt.Sprintf("%s %s", upgrade, phase))
Expand Down
16 changes: 15 additions & 1 deletion pkg/snap/snap.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type snap struct {
snapDir string
snapDataDir string
snapCommonDir string
capiPath string
runCommand func(context.Context, ...string) error

clusterTokensMu sync.Mutex
Expand All @@ -36,11 +37,12 @@ type snap struct {

// NewSnap creates a new interface with the MicroK8s snap.
// NewSnap accepts the $SNAP, $SNAP_DATA and $SNAP_COMMON, directories, and a number of options.
func NewSnap(snapDir, snapDataDir, snapCommonDir string, options ...func(s *snap)) Snap {
func NewSnap(snapDir, snapDataDir, snapCommonDir, capiPath string, options ...func(s *snap)) Snap {
s := &snap{
snapDir: snapDir,
snapDataDir: snapDataDir,
snapCommonDir: snapCommonDir,
capiPath: capiPath,
runCommand: util.RunCommand,
}

Expand All @@ -65,6 +67,9 @@ func (s *snap) GetSnapDataPath(parts ...string) string {
func (s *snap) GetSnapCommonPath(parts ...string) string {
return filepath.Join(append([]string{s.snapCommonDir}, parts...)...)
}
func (s *snap) GetCAPIPath(parts ...string) string {
return filepath.Join(append([]string{s.capiPath}, parts...)...)
}

func (s *snap) GetGroupName() string {
if s.isStrict() {
Expand Down Expand Up @@ -331,6 +336,15 @@ func (s *snap) GetKnownToken(username string) (string, error) {
return "", fmt.Errorf("no known token found for user %s", username)
}

// IsCAPIAuthTokenValid checks if the given CAPI auth token is valid.
func (s *snap) IsCAPIAuthTokenValid(token string) (bool, error) {
contents, err := util.ReadFile(s.GetCAPIPath("etc", "token"))
if err != nil {
return false, fmt.Errorf("failed to read token file: %w", err)
}
return strings.TrimSpace(contents) == token, nil
}

func (s *snap) SignCertificate(ctx context.Context, csrPEM []byte) ([]byte, error) {
// TODO: consider using crypto/x509 for this instead of relying on openssl commands.
// NOTE(neoaggelos): x509.CreateCertificate() has some hardcoded fields that are incompatible with MicroK8s.
Expand Down
4 changes: 2 additions & 2 deletions pkg/snap/snap_addons_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func TestAddons(t *testing.T) {
t.Run("EnableDisable", func(t *testing.T) {
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

s.EnableAddon(context.Background(), "dns")
s.EnableAddon(context.Background(), "dns", "10.0.0.2")
Expand All @@ -32,7 +32,7 @@ func TestAddons(t *testing.T) {

t.Run("AddRepository", func(t *testing.T) {
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

s.AddAddonsRepository(context.Background(), "core", "/snap/microk8s/current/addons/core", "", false)
s.AddAddonsRepository(context.Background(), "core", "/snap/microk8s/current/addons/core", "", true)
Expand Down
37 changes: 37 additions & 0 deletions pkg/snap/snap_capi_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package snap_test

import (
"os"
"path/filepath"
"testing"

. "github.com/onsi/gomega"

"github.com/canonical/microk8s-cluster-agent/pkg/snap"
)

func TestCAPIAuthToken(t *testing.T) {
capiTestPath := "./capi-test"
os.RemoveAll(capiTestPath)
s := snap.NewSnap("", "", "", capiTestPath)
token := "token123"

g := NewWithT(t)

isValid, err := s.IsCAPIAuthTokenValid(token)
g.Expect(err).To(MatchError(os.ErrNotExist))
g.Expect(isValid).To(BeFalse())

capiEtc := filepath.Join(capiTestPath, "etc")
defer os.RemoveAll(capiTestPath)
g.Expect(os.MkdirAll(capiEtc, 0755)).To(Succeed())
g.Expect(os.WriteFile("./capi-test/etc/token", []byte(token), 0600)).To(Succeed())

isValid, err = s.IsCAPIAuthTokenValid("random-token")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(isValid).To(BeFalse())

isValid, err = s.IsCAPIAuthTokenValid(token)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(isValid).To(BeTrue())
}
2 changes: 1 addition & 1 deletion pkg/snap/snap_containerd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestUpdateContainerdRegistryConfigs(t *testing.T) {
}
defer os.RemoveAll("testdata/args")

s := snap.NewSnap("testdata", "testdata", "testdata")
s := snap.NewSnap("testdata", "testdata", "testdata", "")

t.Run("Mirror", func(t *testing.T) {
g := NewWithT(t)
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestFiles(t *testing.T) {
defer os.RemoveAll(filepath.Dir(file))
}

s := snap.NewSnap("testdata", "testdata", "testdata")
s := snap.NewSnap("testdata", "testdata", "testdata", "")

for _, tc := range []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestImportImage(t *testing.T) {
os.Remove("testdata/arguments")
}()
mockRunner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata/common", snap.WithCommandRunner(mockRunner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata/common", "", snap.WithCommandRunner(mockRunner.Run))

g := NewWithT(t)
err := s.ImportImage(context.Background(), bytes.NewBufferString("IMAGEDATA"))
Expand Down
6 changes: 3 additions & 3 deletions pkg/snap/snap_join_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestJoinCluster(t *testing.T) {
t.Run("PropagateError", func(t *testing.T) {
g := NewWithT(t)
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))
runner.Err = fmt.Errorf("some error")

err := s.JoinCluster(context.Background(), "some-url", false)
Expand All @@ -26,7 +26,7 @@ func TestJoinCluster(t *testing.T) {
t.Run("ControlPlane", func(t *testing.T) {
g := NewWithT(t)
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

err := s.JoinCluster(context.Background(), "10.10.10.10:25000/token/hash", false)
g.Expect(err).To(BeNil())
Expand All @@ -36,7 +36,7 @@ func TestJoinCluster(t *testing.T) {
t.Run("Worker", func(t *testing.T) {
g := NewWithT(t)
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

err := s.JoinCluster(context.Background(), "10.10.10.10:25000/token/hash", true)
g.Expect(err).To(BeNil())
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func TestLock(t *testing.T) {
s := snap.NewSnap("testdata", "testdata", "testdata")
s := snap.NewSnap("testdata", "testdata", "testdata", "")
if err := os.MkdirAll("testdata/var/lock", 0755); err != nil {
t.Fatalf("Failed to create directory: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func TestServiceRestart(t *testing.T) {
mockRunner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(mockRunner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(mockRunner.Run))

t.Run("NoKubelite", func(t *testing.T) {
for _, tc := range []struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestSignCertificate(t *testing.T) {
os.Remove("testdata/arguments")
}()
mockRunner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(mockRunner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(mockRunner.Run))

g := NewWithT(t)
b, err := s.SignCertificate(context.Background(), []byte("MOCK CSR"))
Expand Down
Loading
Loading