mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Add integration test infra, and fix authz patterns (#77218)
This commit is contained in:
parent
9a0af13dbc
commit
c122ffc72b
@ -48,7 +48,7 @@ func (auth OrgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut
|
||||
|
||||
// Quick check that the same org is used
|
||||
if signedInUser.OrgID == info.OrgID {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
// Check if the user has access to the specified org
|
||||
@ -60,7 +60,7 @@ func (auth OrgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut
|
||||
|
||||
for _, org := range result {
|
||||
if org.OrgID == info.OrgID {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,25 +28,25 @@ func (auth OrgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib
|
||||
|
||||
switch signedInUser.OrgRole {
|
||||
case org.RoleAdmin:
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
case org.RoleEditor:
|
||||
switch a.GetVerb() {
|
||||
case "get", "list", "watch", "create", "update", "patch", "delete", "put", "post":
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
default:
|
||||
return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil
|
||||
}
|
||||
case org.RoleViewer:
|
||||
switch a.GetVerb() {
|
||||
case "get", "list", "watch":
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
default:
|
||||
return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil
|
||||
}
|
||||
case org.RoleNone:
|
||||
return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil
|
||||
}
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
}
|
||||
|
||||
func errorMessageForGrafanaOrgRole(grafanaOrgRole string, a authorizer.Attributes) string {
|
||||
|
@ -28,7 +28,11 @@ func ProvideAuthorizer(
|
||||
authorizers = append(authorizers, orgIDAuthorizer)
|
||||
}
|
||||
|
||||
authorizers = append(authorizers, orgRoleAuthorizer)
|
||||
authorizers = append(authorizers,
|
||||
orgRoleAuthorizer,
|
||||
|
||||
// Add this last so that if nothing says authorizer.DecisionDeny, it will pass
|
||||
authorizerfactory.NewAlwaysAllowAuthorizer(),
|
||||
)
|
||||
return union.New(authorizers...)
|
||||
}
|
||||
|
@ -52,5 +52,5 @@ func (auth StackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib
|
||||
return authorizer.DecisionDeny, "user must be in org 1", nil
|
||||
}
|
||||
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
3
pkg/tests/apis/README.md
Normal file
3
pkg/tests/apis/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# K8s integration tests
|
||||
|
||||
This directory contains integration tests the k8s api services
|
293
pkg/tests/apis/helper.go
Normal file
293
pkg/tests/apis/helper.go
Normal file
@ -0,0 +1,293 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/server"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
)
|
||||
|
||||
type K8sTestHelper struct {
|
||||
t *testing.T
|
||||
env server.TestEnv
|
||||
namespacer request.NamespaceMapper
|
||||
|
||||
Org1 OrgUsers
|
||||
Org2 OrgUsers
|
||||
|
||||
// // Registered groups
|
||||
groups []metav1.APIGroup
|
||||
|
||||
// Used to build the URL paths
|
||||
selectedGVR schema.GroupVersionResource
|
||||
}
|
||||
|
||||
func NewK8sTestHelper(t *testing.T) *K8sTestHelper {
|
||||
t.Helper()
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: true, // do not start extra port 6443
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServer,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||
},
|
||||
})
|
||||
|
||||
_, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
c := &K8sTestHelper{
|
||||
env: *env,
|
||||
t: t,
|
||||
namespacer: request.GetNamespaceMapper(nil),
|
||||
}
|
||||
|
||||
c.Org1 = c.createTestUsers(int64(1))
|
||||
c.Org2 = c.createTestUsers(int64(2))
|
||||
|
||||
// Read the API groups
|
||||
rsp := doRequest(c, RequestParams{
|
||||
User: c.Org1.Viewer,
|
||||
Path: "/apis",
|
||||
// Accept: "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json",
|
||||
}, &metav1.APIGroupList{})
|
||||
c.groups = rsp.Result.Groups
|
||||
return c
|
||||
}
|
||||
|
||||
type OrgUsers struct {
|
||||
Admin User
|
||||
Editor User
|
||||
Viewer User
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Identity identity.Requester
|
||||
password string
|
||||
}
|
||||
|
||||
type RequestParams struct {
|
||||
User User
|
||||
Method string // GET, POST, PATCH, etc
|
||||
Path string
|
||||
Body []byte
|
||||
ContentType string
|
||||
Accept string
|
||||
}
|
||||
|
||||
type K8sResponse[T any] struct {
|
||||
Response *http.Response
|
||||
Body []byte
|
||||
Result *T
|
||||
Status *metav1.Status
|
||||
}
|
||||
|
||||
type AnyResourceResponse = K8sResponse[AnyResource]
|
||||
type AnyResourceListResponse = K8sResponse[AnyResourceList]
|
||||
|
||||
// This will set the expected Group/Version/Resource and return the discovery info if found
|
||||
func (c *K8sTestHelper) SetGroupVersionResource(gvr schema.GroupVersionResource) {
|
||||
c.t.Helper()
|
||||
|
||||
c.selectedGVR = gvr
|
||||
}
|
||||
|
||||
func (c *K8sTestHelper) PostResource(user User, resource string, payload AnyResource) AnyResourceResponse {
|
||||
c.t.Helper()
|
||||
|
||||
namespace := payload.Namespace
|
||||
if namespace == "" {
|
||||
namespace = c.namespacer(user.Identity.GetOrgID())
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/apis/%s/namespaces/%s/%s",
|
||||
payload.APIVersion, namespace, resource)
|
||||
if payload.Name != "" {
|
||||
path = fmt.Sprintf("%s/%s", path, payload.Name)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
return doRequest(c, RequestParams{
|
||||
Method: http.MethodPost,
|
||||
Path: path,
|
||||
User: user,
|
||||
Body: body,
|
||||
}, &AnyResource{})
|
||||
}
|
||||
|
||||
func (c *K8sTestHelper) PutResource(user User, resource string, payload AnyResource) AnyResourceResponse {
|
||||
c.t.Helper()
|
||||
|
||||
path := fmt.Sprintf("/apis/%s/namespaces/%s/%s/%s",
|
||||
payload.APIVersion, payload.Namespace, resource, payload.Name)
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
return doRequest(c, RequestParams{
|
||||
Method: http.MethodPut,
|
||||
Path: path,
|
||||
User: user,
|
||||
Body: body,
|
||||
}, &AnyResource{})
|
||||
}
|
||||
|
||||
func (c *K8sTestHelper) List(user User, namespace string) AnyResourceListResponse {
|
||||
c.t.Helper()
|
||||
|
||||
return doRequest(c, RequestParams{
|
||||
User: user,
|
||||
Path: fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s",
|
||||
c.selectedGVR.Group,
|
||||
c.selectedGVR.Version,
|
||||
namespace,
|
||||
c.selectedGVR.Resource),
|
||||
}, &AnyResourceList{})
|
||||
}
|
||||
|
||||
func doRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResponse[T] {
|
||||
c.t.Helper()
|
||||
|
||||
if params.Method == "" {
|
||||
params.Method = http.MethodGet
|
||||
}
|
||||
|
||||
// Get the URL
|
||||
addr := c.env.Server.HTTPServer.Listener.Addr()
|
||||
baseUrl := fmt.Sprintf("http://%s", addr)
|
||||
login := params.User.Identity.GetLogin()
|
||||
if login != "" && params.User.password != "" {
|
||||
baseUrl = fmt.Sprintf("http://%s:%s@%s", login, params.User.password, addr)
|
||||
}
|
||||
|
||||
contentType := params.ContentType
|
||||
var body io.Reader
|
||||
if params.Body != nil {
|
||||
body = bytes.NewReader(params.Body)
|
||||
if contentType == "" && json.Valid(params.Body) {
|
||||
contentType = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(params.Method, fmt.Sprintf(
|
||||
"%s%s",
|
||||
baseUrl,
|
||||
params.Path,
|
||||
), body)
|
||||
require.NoError(c.t, err)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
if params.Accept != "" {
|
||||
req.Header.Set("Accept", params.Accept)
|
||||
}
|
||||
rsp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
r := K8sResponse[T]{
|
||||
Response: rsp,
|
||||
Result: result,
|
||||
}
|
||||
defer func() {
|
||||
_ = rsp.Body.Close() // ignore any close errors
|
||||
}()
|
||||
r.Body, _ = io.ReadAll(rsp.Body)
|
||||
if json.Valid(r.Body) {
|
||||
_ = json.Unmarshal(r.Body, r.Result)
|
||||
|
||||
s := &metav1.Status{}
|
||||
err := json.Unmarshal(r.Body, s)
|
||||
if err == nil && s.Kind == "Status" { // Usually an error!
|
||||
r.Status = s
|
||||
r.Result = nil
|
||||
}
|
||||
} else {
|
||||
_ = yaml.Unmarshal(r.Body, r.Result)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (c K8sTestHelper) createTestUsers(orgId int64) OrgUsers {
|
||||
c.t.Helper()
|
||||
|
||||
store := c.env.SQLStore
|
||||
store.Cfg.AutoAssignOrg = true
|
||||
store.Cfg.AutoAssignOrgId = int(orgId)
|
||||
quotaService := quotaimpl.ProvideService(store, store.Cfg)
|
||||
|
||||
orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService)
|
||||
require.NoError(c.t, err)
|
||||
|
||||
gotID, err := orgService.GetOrCreate(context.Background(), fmt.Sprintf("Org%d", orgId))
|
||||
require.NoError(c.t, err)
|
||||
require.Equal(c.t, orgId, gotID)
|
||||
|
||||
teamSvc := teamimpl.ProvideService(store, store.Cfg)
|
||||
cache := localcache.ProvideService()
|
||||
userSvc, err := userimpl.ProvideService(store,
|
||||
orgService, store.Cfg, teamSvc, cache, quotaService,
|
||||
supportbundlestest.NewFakeBundleService())
|
||||
require.NoError(c.t, err)
|
||||
|
||||
createUser := func(key string, role org.RoleType) User {
|
||||
u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{
|
||||
DefaultOrgRole: string(role),
|
||||
Password: key,
|
||||
Login: fmt.Sprintf("%s%d", key, orgId),
|
||||
OrgID: orgId,
|
||||
})
|
||||
require.NoError(c.t, err)
|
||||
require.Equal(c.t, orgId, u.OrgID)
|
||||
require.True(c.t, u.ID > 0)
|
||||
|
||||
s, err := userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
|
||||
UserID: u.ID,
|
||||
Login: u.Login,
|
||||
Email: u.Email,
|
||||
OrgID: orgId,
|
||||
})
|
||||
require.NoError(c.t, err)
|
||||
require.Equal(c.t, orgId, s.OrgID)
|
||||
require.Equal(c.t, role, s.OrgRole) // make sure the role was set properly
|
||||
return User{
|
||||
Identity: s,
|
||||
password: key,
|
||||
}
|
||||
}
|
||||
return OrgUsers{
|
||||
Admin: createUser("admin", org.RoleAdmin),
|
||||
Editor: createUser("editor", org.RoleEditor),
|
||||
Viewer: createUser("viewer", org.RoleViewer),
|
||||
}
|
||||
}
|
||||
|
||||
func (c K8sTestHelper) CreateDS(cmd *datasources.AddDataSourceCommand) *datasources.DataSource {
|
||||
c.t.Helper()
|
||||
|
||||
dataSource, err := c.env.Server.HTTPServer.DataSourcesService.AddDataSource(context.Background(), cmd)
|
||||
require.NoError(c.t, err)
|
||||
return dataSource
|
||||
}
|
45
pkg/tests/apis/playlist/playlist_test.go
Normal file
45
pkg/tests/apis/playlist/playlist_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package playlist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
)
|
||||
|
||||
func TestPlaylist(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
helper := apis.NewK8sTestHelper(t)
|
||||
helper.SetGroupVersionResource(
|
||||
schema.GroupVersionResource{
|
||||
Group: "playlist.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "playlists",
|
||||
})
|
||||
|
||||
t.Run("Check List from different org users", func(t *testing.T) {
|
||||
// Check view permissions
|
||||
rsp := helper.List(helper.Org1.Viewer, "default")
|
||||
require.Equal(t, 200, rsp.Response.StatusCode)
|
||||
require.NotNil(t, rsp.Result)
|
||||
require.Empty(t, rsp.Result.Items)
|
||||
require.Nil(t, rsp.Status)
|
||||
|
||||
// Check view permissions
|
||||
rsp = helper.List(helper.Org2.Viewer, "default")
|
||||
require.Equal(t, 403, rsp.Response.StatusCode) // Org2 can not see default namespace
|
||||
require.Nil(t, rsp.Result)
|
||||
require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason)
|
||||
|
||||
// Check view permissions
|
||||
rsp = helper.List(helper.Org2.Viewer, "org-22")
|
||||
require.Equal(t, 403, rsp.Response.StatusCode) // Unknown/not a member
|
||||
require.Nil(t, rsp.Result)
|
||||
require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason)
|
||||
})
|
||||
}
|
45
pkg/tests/apis/types.go
Normal file
45
pkg/tests/apis/types.go
Normal file
@ -0,0 +1,45 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type AnyResource struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Generic object
|
||||
Spec map[string]any `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type AnyResourceList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []map[string]any `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// Read local JSON or YAML file into a resource
|
||||
func (c *K8sTestHelper) LoadAnyResource(fpath string) AnyResource {
|
||||
c.t.Helper()
|
||||
|
||||
//nolint:gosec
|
||||
raw, err := os.ReadFile(fpath)
|
||||
require.NoError(c.t, err)
|
||||
require.NotEmpty(c.t, raw)
|
||||
|
||||
res := &AnyResource{}
|
||||
if json.Valid(raw) {
|
||||
err = json.Unmarshal(raw, res)
|
||||
} else {
|
||||
err = yaml.Unmarshal(raw, res)
|
||||
}
|
||||
require.NoError(c.t, err)
|
||||
return *res
|
||||
}
|
Loading…
Reference in New Issue
Block a user