Skip to content

Commit

Permalink
Implement users filter (#1067)
Browse files Browse the repository at this point in the history
* proto: change ListUsers filter

* chore: compile protos

* backend: implement ListUsers filter
  • Loading branch information
weeco authored Feb 2, 2024
1 parent 27529ea commit 8a8bd0a
Show file tree
Hide file tree
Showing 8 changed files with 623 additions and 239 deletions.
23 changes: 18 additions & 5 deletions backend/pkg/api/connect/integration/api_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"time"

"github.com/cloudhut/common/rest"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/adminapi"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
Expand All @@ -36,11 +37,12 @@ import (
type APISuite struct {
suite.Suite

redpandaContainer *redpanda.Container
kConnectContainer testcontainers.Container
network *testcontainers.DockerNetwork
kafkaClient *kgo.Client
kafkaAdminClient *kadm.Client
redpandaContainer *redpanda.Container
kConnectContainer testcontainers.Container
network *testcontainers.DockerNetwork
kafkaClient *kgo.Client
kafkaAdminClient *kadm.Client
redpandaAdminClient *adminapi.AdminAPI

cfg *config.Config
api *api.API
Expand Down Expand Up @@ -80,6 +82,8 @@ func (s *APISuite) SetupSuite() {
require.NoError(err)
schemaRegistryAddress, err := container.SchemaRegistryAddress(ctx)
require.NoError(err)
adminApiAddr, err := container.AdminAPIAddress(ctx)
require.NoError(err)

require.NoError(err)

Expand All @@ -102,6 +106,11 @@ func (s *APISuite) SetupSuite() {
kConnectClusterURL, err := kConnectContainer.PortEndpoint(ctx, "8083/tcp", "http")
require.NoError(err)

// 5. Create Redpanda client
adminApiClient, err := adminapi.NewAdminAPI([]string{adminApiAddr}, &adminapi.NopAuth{}, nil)
require.NoError(err)
s.redpandaAdminClient = adminApiClient

// 5. Configure & start Redpanda Console
httpListenPort := rand.Intn(50000) + 10000
s.cfg = &config.Config{}
Expand All @@ -117,6 +126,10 @@ func (s *APISuite) SetupSuite() {
s.cfg.Kafka.Schema.Enabled = true
s.cfg.Kafka.Schema.URLs = []string{schemaRegistryAddress}

s.cfg.Redpanda.AdminAPI.Enabled = true
s.cfg.Redpanda.AdminAPI.URLs = []string{adminApiAddr}
s.cfg.Redpanda.AdminAPI.TLS.Enabled = false

s.cfg.Connect = config.Connect{
Enabled: true,
Clusters: []config.ConnectCluster{
Expand Down
213 changes: 213 additions & 0 deletions backend/pkg/api/connect/integration/user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright 2024 Redpanda Data, Inc.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.md
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0

//go:build integration

package integration

import (
"context"
"net/http"
"testing"
"time"

"connectrpc.com/connect"
"github.com/carlmjohnson/requests"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/adminapi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1alpha1"
v1alpha1connect "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1alpha1/dataplanev1alpha1connect"
)

func (s *APISuite) TestListUsers() {
t := s.T()
require := require.New(t)
assert := assert.New(t)

t.Run("list users with valid request (connect-go)", func(t *testing.T) {
// 1. Create some users in Redpanda
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()

// Helper function to create a user
createUser := func(username string) {
childCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

err := s.redpandaAdminClient.CreateUser(childCtx, username, "random", adminapi.ScramSha256)
require.NoError(err)
}
deleteUser := func(username string) {
childCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

err := s.redpandaAdminClient.DeleteUser(childCtx, username)
assert.NoError(err)
}

username1 := "console-integration-test-list-users-1"
username2 := "console-integration-test-list-users-2"
createUser(username1)
createUser(username2)
defer deleteUser(username1)
defer deleteUser(username1)

// 2. List users
client := v1alpha1connect.NewUserServiceClient(http.DefaultClient, s.httpAddress())
res, err := client.ListUsers(ctx, connect.NewRequest(&v1alpha1.ListUsersRequest{}))
require.NoError(err)
assert.GreaterOrEqual(2, len(res.Msg.Users))

foundUser1 := false
foundUser2 := false
for _, user := range res.Msg.Users {
if user.Name == username1 {
foundUser1 = true
}
if user.Name == username2 {
foundUser2 = true
}
}
assert.Truef(foundUser1, "expected to find previously created user1 in list users response")
assert.Truef(foundUser2, "expected to find previously created user2 in list users response")
})

t.Run("list users with valid request (http)", func(t *testing.T) {
// 1. Create some users in Redpanda
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()

// Helper function to create a user
createUser := func(username string) {
childCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

err := s.redpandaAdminClient.CreateUser(childCtx, username, "random", adminapi.ScramSha256)
require.NoError(err)
}
deleteUser := func(username string) {
childCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

err := s.redpandaAdminClient.DeleteUser(childCtx, username)
assert.NoError(err)
}

username1 := "console-integration-test-list-users-1"
username2 := "console-integration-test-list-users-2"
createUser(username1)
createUser(username2)
defer deleteUser(username1)
defer deleteUser(username1)

// 2. List users
type listUsersRes struct {
Users []struct {
Name string `json:"name"`
} `json:"users"`
}
var httpRes listUsersRes
var errResponse string
err := requests.
URL(s.httpAddress() + "/v1alpha1/users").
ToJSON(&httpRes).
AddValidator(requests.ValidatorHandler(
requests.CheckStatus(http.StatusOK), // Allows 2xx otherwise
requests.ToString(&errResponse),
)).
Fetch(ctx)
assert.Empty(errResponse)
require.NoError(err)

foundUser1 := false
foundUser2 := false
for _, user := range httpRes.Users {
if user.Name == username1 {
foundUser1 = true
}
if user.Name == username2 {
foundUser2 = true
}
}
assert.Truef(foundUser1, "expected to find previously created user1 in list users response")
assert.Truef(foundUser2, "expected to find previously created user2 in list users response")
})

t.Run("list users with valid filter (connect-go)", func(t *testing.T) {
// 1. Create some users in Redpanda
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()

// Helper function to create a user
createUser := func(username string) {
childCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

err := s.redpandaAdminClient.CreateUser(childCtx, username, "random", adminapi.ScramSha256)
require.NoError(err)
}
deleteUser := func(username string) {
childCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

err := s.redpandaAdminClient.DeleteUser(childCtx, username)
assert.NoError(err)
}

users := []string{
"console-integration-test-list-users-1",
"console-integration-test-list-users-2",
"console-integration-test-list-users-3",
"console-integration-test-list-users-4",
"console-integration-test-different-name-1",
}
for _, user := range users {
createUser(user)
}
defer func() {
for _, user := range users {
deleteUser(user)
}
}()

// 2. List users with name contains that should yield exactly one user
client := v1alpha1connect.NewUserServiceClient(http.DefaultClient, s.httpAddress())
res, err := client.ListUsers(ctx, connect.NewRequest(&v1alpha1.ListUsersRequest{
Filter: &v1alpha1.ListUsersRequest_Filter{
NameContains: "different-name",
},
}))
require.NoError(err)
require.Equal(1, len(res.Msg.Users))

foundUser := res.Msg.Users[0]
assert.Equal("console-integration-test-different-name-1", foundUser.Name)

// 3. List users with name contains that should yield exactly 4 users
res, err = client.ListUsers(ctx, connect.NewRequest(&v1alpha1.ListUsersRequest{
Filter: &v1alpha1.ListUsersRequest_Filter{
NameContains: "test-list-users",
},
}))
require.NoError(err)
require.Equal(4, len(res.Msg.Users))

// 3. List users with exact name match that should yield one user
res, err = client.ListUsers(ctx, connect.NewRequest(&v1alpha1.ListUsersRequest{
Filter: &v1alpha1.ListUsersRequest_Filter{
Name: "console-integration-test-list-users-3",
},
}))
require.NoError(err)
require.Equal(1, len(res.Msg.Users))
require.Equal("console-integration-test-list-users-3", res.Msg.Users[0].Name)
})
}
27 changes: 26 additions & 1 deletion backend/pkg/api/connect/service/user/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"context"
"errors"
"fmt"
"strings"

"connectrpc.com/connect"
"go.uber.org/zap"
Expand Down Expand Up @@ -58,7 +59,7 @@ func NewService(cfg *config.Config,
}

// ListUsers returns a list of all existing users.
func (s *Service) ListUsers(ctx context.Context, _ *connect.Request[v1alpha1.ListUsersRequest]) (*connect.Response[v1alpha1.ListUsersResponse], error) {
func (s *Service) ListUsers(ctx context.Context, req *connect.Request[v1alpha1.ListUsersRequest]) (*connect.Response[v1alpha1.ListUsersResponse], error) {
// 1. Check if we can list users
if !s.cfg.Redpanda.AdminAPI.Enabled {
return nil, apierrors.NewConnectError(
Expand All @@ -79,11 +80,35 @@ func (s *Service) ListUsers(ctx context.Context, _ *connect.Request[v1alpha1.Lis
)
}

// doesUserPassFilter returns true if either no filter is provided or
// if the given username passes the given filter criteria.
doesUserPassFilter := func(username string) bool {
if req.Msg.Filter == nil {
return true
}

if req.Msg.Filter.Name == username {
return true
}

if req.Msg.Filter.NameContains != "" && strings.Contains(username, req.Msg.Filter.NameContains) {
return true
}

return false
}

filteredUsers := make([]*v1alpha1.ListUsersResponse_User, 0)
for _, user := range users {
if s.isProtectedUserFn(user) {
continue
}

// Remove users that do not pass the filter criteria
if !doesUserPassFilter(user) {
continue
}

filteredUsers = append(filteredUsers, &v1alpha1.ListUsersResponse_User{
Name: user,
})
Expand Down
Loading

0 comments on commit 8a8bd0a

Please sign in to comment.