Skip to content

Commit

Permalink
PMM-12686 Basic/Token auth between server and client. (#2852)
Browse files Browse the repository at this point in the history
* PMM-12686 Authorization between server and client.

* PMM-12686 Tests.

* PMM-12686 Lint.

* PMM-12686 Comment fix.

* PMM-12686 New required permissions for Connect endpoint.

* PMM-12686 Apply suggestion.

* PMM-12686 Add unit test for authenticate method.

* PMM-12686 Format.

* PMM-12686 Part changes after review.

* PMM-12686 Unit tests for auth server.

* PMM-12686 Revert unnecessary changes anymore.

* PMM-12686 Dynamic names in auth test.

* PMM-12686 Skip check for pmm-server agent.

* PMM-12686 Better local auth check.

* PMM-12686 Local auth check.

* PMM-12686 Refactor.

* PMM-12686 Refactor.

* PMM-12686 Refactor.

* PMM-12686 Fix.

* Update managed/services/grafana/auth_server.go

Co-authored-by: Alex Demidoff <[email protected]>

* PMM-12686 Revert of some changes.

* PMM-12686 Another reverted changes.

* PMM-12686 Years in licence.

* PMM-12686 Missed parallel in one test case.

---------

Co-authored-by: Alex Demidoff <[email protected]>
  • Loading branch information
JiriCtvrtka and ademidoff authored Apr 1, 2024
1 parent 01a14f0 commit 3ac6d7f
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 14 deletions.
46 changes: 34 additions & 12 deletions managed/services/grafana/auth_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ import (
"github.com/percona/pmm/managed/models"
)

const (
connectionEndpoint = "/agent.Agent/Connect"
)

// rules maps original URL prefix to minimal required role.
var rules = map[string]role{
// TODO https://jira.percona.com/browse/PMM-4420
"/agent.Agent/Connect": none,
connectionEndpoint: admin,

"/inventory.": admin,
"/management.": admin,
Expand Down Expand Up @@ -459,6 +462,17 @@ func nextPrefix(path string) string {
return path[:i+1]
}

func isLocalAgentConnection(req *http.Request) bool {
ip := strings.Split(req.RemoteAddr, ":")[0]
pmmAgent := req.Header.Get("Pmm-Agent-Id")
path := req.Header.Get("X-Original-Uri")
if ip == "127.0.0.1" && pmmAgent == "pmm-server" && path == connectionEndpoint {
return true
}

return false
}

// authenticate checks if user has access to a specific path.
// It returns user information retrieved during authentication.
// Paths which require no Grafana role return zero value for
Expand Down Expand Up @@ -498,22 +512,30 @@ func (s *AuthServer) authenticate(ctx context.Context, req *http.Request, l *log
return nil, nil
}

// Get authenticated user from Grafana
authUser, authErr := s.getAuthUser(ctx, req, l)
if authErr != nil {
return nil, authErr
var user *authUser
if isLocalAgentConnection(req) {
user = &authUser{
role: rules[connectionEndpoint],
userID: 0,
}
} else {
var authErr *authError
// Get authenticated user from Grafana
user, authErr = s.getAuthUser(ctx, req, l)
if authErr != nil {
return nil, authErr
}
}
l = l.WithField("role", user.role.String())

l = l.WithField("role", authUser.role.String())

if authUser.role == grafanaAdmin {
if user.role == grafanaAdmin {
l.Debugf("Grafana admin, allowing access.")
return authUser, nil
return user, nil
}

if minRole <= authUser.role {
if minRole <= user.role {
l.Debugf("Minimal required role is %q, granting access.", minRole)
return authUser, nil
return user, nil
}

l.Warnf("Minimal required role is %q.", minRole)
Expand Down
73 changes: 71 additions & 2 deletions managed/services/grafana/auth_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"gopkg.in/reform.v1"
"gopkg.in/reform.v1/dialects/postgresql"

Expand Down Expand Up @@ -161,7 +162,6 @@ func TestAuthServerMustSetup(t *testing.T) {

func TestAuthServerAuthenticate(t *testing.T) {
t.Parallel()
// logrus.SetLevel(logrus.TraceLevel)

checker := &mockAwsInstanceChecker{}
checker.Test(t)
Expand Down Expand Up @@ -198,7 +198,7 @@ func TestAuthServerAuthenticate(t *testing.T) {
})

for uri, minRole := range map[string]role{
"/agent.Agent/Connect": none,
"/agent.Agent/Connect": admin,

"/inventory.Nodes/ListNodes": admin,
"/management.Actions/StartMySQLShowTableStatusAction": viewer,
Expand Down Expand Up @@ -270,6 +270,75 @@ func TestAuthServerAuthenticate(t *testing.T) {
}
}

func TestServerClientConnection(t *testing.T) {
t.Parallel()

checker := &mockAwsInstanceChecker{}
checker.Test(t)
t.Cleanup(func() { checker.AssertExpectations(t) })

ctx := context.Background()
c := NewClient("127.0.0.1:3000")
s := NewAuthServer(c, checker, nil)

t.Run("Basic auth - success", func(t *testing.T) {
t.Parallel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil)
require.NoError(t, err)
req.SetBasicAuth("admin", "admin")

_, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name()))
assert.Nil(t, authError)
})

t.Run("Basic auth - fail", func(t *testing.T) {
t.Parallel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil)
require.NoError(t, err)
req.SetBasicAuth("admin", "wrong")

_, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name()))
assert.Equal(t, codes.Unauthenticated, authError.code)
})

t.Run("Token auth - success", func(t *testing.T) {
t.Parallel()

nodeName := fmt.Sprintf("N1-%d", time.Now().UnixNano())
headersMD := metadata.New(map[string]string{
"Authorization": "Basic YWRtaW46YWRtaW4=",
})
ctx := metadata.NewIncomingContext(context.Background(), headersMD)
_, serviceToken, err := c.CreateServiceAccount(ctx, nodeName, true)
require.NoError(t, err)
defer func() {
warning, err := c.DeleteServiceAccount(ctx, nodeName, true)
require.NoError(t, err)
require.Empty(t, warning)
}()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil)
require.NoError(t, err)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", serviceToken))

_, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name()))
assert.Nil(t, authError)
})

t.Run("Token auth - fail", func(t *testing.T) {
t.Parallel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectionEndpoint, nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer wrong")

_, authError := s.authenticate(ctx, req, logrus.WithField("test", t.Name()))
assert.Equal(t, codes.Unauthenticated, authError.code)
})
}

func TestAuthServerAddVMGatewayToken(t *testing.T) {
ctx := logger.Set(context.Background(), t.Name())
uuid.SetRand(&tests.IDReader{})
Expand Down

0 comments on commit 3ac6d7f

Please sign in to comment.