Apiserver: Refactor authenticator and authorizers (#101449)

* Clean up authenticator

* Cleanup authorizers and replace org_id and stack_id with namespace authorizer

* Remove dependency on org service

* Extract orgID from /apis/ urls and validate stack id
pull/101667/head^2
Karl Persson 5 months ago committed by GitHub
parent 0a24a7cd4c
commit 43f56c5ca1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 30
      pkg/services/apiserver/auth/authenticator/authenticator.go
  2. 63
      pkg/services/apiserver/auth/authenticator/authenticator_test.go
  3. 11
      pkg/services/apiserver/auth/authenticator/provider.go
  4. 24
      pkg/services/apiserver/auth/authenticator/signedinuser.go
  5. 88
      pkg/services/apiserver/auth/authenticator/signedinuser_test.go
  6. 25
      pkg/services/apiserver/auth/authorizer/authorizer.go
  7. 4
      pkg/services/apiserver/auth/authorizer/impersonation.go
  8. 52
      pkg/services/apiserver/auth/authorizer/namespace.go
  9. 87
      pkg/services/apiserver/auth/authorizer/org_id.go
  10. 13
      pkg/services/apiserver/auth/authorizer/role.go
  11. 66
      pkg/services/apiserver/auth/authorizer/stack_id.go
  12. 4
      pkg/services/apiserver/service.go
  13. 98
      pkg/services/authn/authnimpl/service.go
  14. 41
      pkg/services/authn/authnimpl/service_test.go
  15. 125
      pkg/tests/apis/helper.go
  16. 4
      pkg/tests/apis/iam/iam_test.go

@ -0,0 +1,30 @@
package authenticator
import (
"net/http"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/union"
"k8s.io/klog/v2"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
func NewAuthenticator(authRequestHandlers ...authenticator.Request) authenticator.Request {
handlers := append([]authenticator.Request{authenticator.RequestFunc(identityAuthenticator)}, authRequestHandlers...)
return union.New(handlers...)
}
var _ authenticator.RequestFunc = identityAuthenticator
// identityAuthenticator check if we have any identity set in context.
// If not we delegate authentication to next authenticator in the chain.
func identityAuthenticator(req *http.Request) (*authenticator.Response, bool, error) {
ident, err := identity.GetRequester(req.Context())
if err != nil {
klog.V(5).Info("no idenitty in context", "err", err)
return nil, false, nil
}
return &authenticator.Response{User: ident}, true, nil
}

@ -0,0 +1,63 @@
package authenticator
import (
"context"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/authenticator"
)
func TestAuthenticator(t *testing.T) {
t.Run("should call next authenticator if identity is not set in context", func(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil)
require.NoError(t, err)
mockAuthenticator := &mockAuthenticator{}
auth := NewAuthenticator(mockAuthenticator)
res, ok, err := auth.AuthenticateRequest(req)
require.NoError(t, err)
require.False(t, ok)
require.Nil(t, res)
require.True(t, mockAuthenticator.called)
})
t.Run("should authenticate when identity is set in context", func(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil)
require.NoError(t, err)
ident := &user.SignedInUser{
Name: "admin",
UserID: 1,
UserUID: "xyz",
Teams: []int64{1, 2},
}
req = req.WithContext(identity.WithRequester(context.Background(), ident))
mockAuthenticator := &mockAuthenticator{}
auth := NewAuthenticator(mockAuthenticator)
res, ok, err := auth.AuthenticateRequest(req)
require.NoError(t, err)
require.True(t, ok)
require.False(t, mockAuthenticator.called)
require.Equal(t, ident.GetName(), res.User.GetName())
require.Equal(t, ident.GetUID(), res.User.GetUID())
require.Equal(t, []string{"1", "2"}, res.User.GetGroups())
require.Empty(t, res.User.GetExtra()["id-token"])
})
}
var _ authenticator.Request = (*mockAuthenticator)(nil)
type mockAuthenticator struct {
called bool
}
func (a *mockAuthenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
a.called = true
return nil, false, nil
}

@ -1,11 +0,0 @@
package authenticator
import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/union"
)
func NewAuthenticator(authRequestHandlers ...authenticator.Request) authenticator.Request {
handlers := append([]authenticator.Request{authenticator.RequestFunc(signedInUserAuthenticator)}, authRequestHandlers...)
return union.New(handlers...)
}

@ -1,24 +0,0 @@
package authenticator
import (
"net/http"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/klog/v2"
)
var _ authenticator.RequestFunc = signedInUserAuthenticator
func signedInUserAuthenticator(req *http.Request) (*authenticator.Response, bool, error) {
ctx := req.Context()
signedInUser, err := identity.GetRequester(ctx)
if err != nil {
klog.V(5).Info("failed to get signed in user", "err", err)
return nil, false, nil
}
return &authenticator.Response{
User: signedInUser,
}, true, nil
}

@ -1,88 +0,0 @@
package authenticator
import (
"context"
"net/http"
"testing"
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/union"
)
func TestSignedInUser(t *testing.T) {
t.Run("should call next authenticator if SignedInUser is not set", func(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil)
require.NoError(t, err)
mockAuthenticator := &mockAuthenticator{}
all := union.New(authenticator.RequestFunc(signedInUserAuthenticator), mockAuthenticator)
res, ok, err := all.AuthenticateRequest(req)
require.NoError(t, err)
require.False(t, ok)
require.Nil(t, res)
require.True(t, mockAuthenticator.called)
})
t.Run("should set user and group", func(t *testing.T) {
u := &user.SignedInUser{
Name: "admin",
UserID: 1,
UserUID: "xyz",
Teams: []int64{1, 2},
}
ctx := identity.WithRequester(context.Background(), u)
req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil)
require.NoError(t, err)
req = req.WithContext(ctx)
mockAuthenticator := &mockAuthenticator{}
all := union.New(authenticator.RequestFunc(signedInUserAuthenticator), mockAuthenticator)
res, ok, err := all.AuthenticateRequest(req)
require.NoError(t, err)
require.True(t, ok)
require.False(t, mockAuthenticator.called)
require.Equal(t, u.GetName(), res.User.GetName())
require.Equal(t, u.GetUID(), res.User.GetUID())
require.Equal(t, []string{"1", "2"}, res.User.GetGroups())
require.Empty(t, res.User.GetExtra()["id-token"])
})
t.Run("should set ID token when available", func(t *testing.T) {
u := &user.SignedInUser{
Name: "admin",
UserID: 1,
UserUID: uuid.New().String(),
Teams: []int64{1, 2},
IDToken: "test-id-token",
}
ctx := identity.WithRequester(context.Background(), u)
req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil)
require.NoError(t, err)
req = req.WithContext(ctx)
mockAuthenticator := &mockAuthenticator{}
all := union.New(authenticator.RequestFunc(signedInUserAuthenticator), mockAuthenticator)
res, ok, err := all.AuthenticateRequest(req)
require.NoError(t, err)
require.True(t, ok)
require.False(t, mockAuthenticator.called)
require.Equal(t, u.GetName(), res.User.GetName())
require.Equal(t, u.GetUID(), res.User.GetUID())
require.Equal(t, []string{"1", "2"}, res.User.GetGroups())
require.Equal(t, "test-id-token", res.User.GetExtra()["id-token"][0])
})
}
var _ authenticator.Request = (*mockAuthenticator)(nil)
type mockAuthenticator struct {
called bool
}
func (a *mockAuthenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
a.called = true
return nil, false, nil
}

@ -3,7 +3,6 @@ package authorizer
import (
"context"
orgsvc "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"k8s.io/apimachinery/pkg/runtime/schema"
k8suser "k8s.io/apiserver/pkg/authentication/user"
@ -19,17 +18,21 @@ type GrafanaAuthorizer struct {
auth authorizer.Authorizer
}
func NewGrafanaAuthorizer(cfg *setting.Cfg, orgService orgsvc.Service) *GrafanaAuthorizer {
// NewGrafanaAuthorizer returns an authorizer configured for a grafana instance.
// This authorizer is a chain of smaller authorizers that together form the decision if
// access should be granted.
// 1. We deny all impersonate request.
// 2. We allow all identities that belongs to `system:masters` group, regular grafana identities cannot
// be part of this group
// 3. We check that identity is allowed to make a request for namespace.
// 4. We check authorizer that is configured speficially for an api.
// 5. As a last fallback we check Role, this will only happen if an api have not configured
// an authorizer or return authorizer.DecisionNoOpinion
func NewGrafanaAuthorizer(cfg *setting.Cfg) *GrafanaAuthorizer {
authorizers := []authorizer.Authorizer{
&impersonationAuthorizer{},
newImpersonationAuthorizer(),
authorizerfactory.NewPrivilegedGroups(k8suser.SystemPrivilegedGroup),
}
// In Hosted grafana, the StackID replaces the orgID as a valid namespace
if cfg.StackID != "" {
authorizers = append(authorizers, newStackIDAuthorizer(cfg))
} else {
authorizers = append(authorizers, newOrgIDAuthorizer(orgService))
newNamespaceAuthorizer(),
}
// Individual services may have explicit implementations
@ -38,7 +41,7 @@ func NewGrafanaAuthorizer(cfg *setting.Cfg, orgService orgsvc.Service) *GrafanaA
// org role is last -- and will return allow for verbs that match expectations
// The apiVersion flavors will run first and can return early when FGAC has appropriate rules
authorizers = append(authorizers, newOrgRoleAuthorizer(orgService))
authorizers = append(authorizers, newRoleAuthorizer())
return &GrafanaAuthorizer{
apis: apis,
auth: union.New(authorizers...),

@ -8,6 +8,10 @@ import (
var _ authorizer.Authorizer = (*impersonationAuthorizer)(nil)
func newImpersonationAuthorizer() *impersonationAuthorizer {
return &impersonationAuthorizer{}
}
// ImpersonationAuthorizer denies all impersonation requests.
type impersonationAuthorizer struct{}

@ -0,0 +1,52 @@
package authorizer
import (
"context"
"fmt"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
type namespaceAuthorizer struct {
}
func newNamespaceAuthorizer() *namespaceAuthorizer {
return &namespaceAuthorizer{}
}
func (auth namespaceAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
ident, err := identity.GetRequester(ctx)
if err != nil {
return authorizer.DecisionDeny, "missing auth info", fmt.Errorf("missing auth info: %w", err)
}
if ident.GetIsGrafanaAdmin() {
return authorizer.DecisionNoOpinion, "", nil
}
if !a.IsResourceRequest() {
return authorizer.DecisionNoOpinion, "", nil
}
// If we have an anonymous user, let the next authorizers decide.
if types.IsIdentityType(ident.GetIdentityType(), types.TypeAnonymous) {
return authorizer.DecisionNoOpinion, "", nil
}
ns, err := types.ParseNamespace(a.GetNamespace())
if err != nil {
return authorizer.DecisionDeny, "invalid namespace", err
}
if ns.OrgID != ident.GetOrgID() {
return authorizer.DecisionDeny, "invalid org", nil
}
if !types.NamespaceMatches(ident.GetNamespace(), a.GetNamespace()) {
return authorizer.DecisionDeny, "invalid namespace", nil
}
return authorizer.DecisionNoOpinion, "", nil
}

@ -1,87 +0,0 @@
package authorizer
import (
"context"
"fmt"
"k8s.io/apiserver/pkg/authorization/authorizer"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/org"
)
var _ authorizer.Authorizer = &orgIDAuthorizer{}
type orgIDAuthorizer struct {
log log.Logger
org org.Service
}
func newOrgIDAuthorizer(orgService org.Service) *orgIDAuthorizer {
return &orgIDAuthorizer{
log: log.New("grafana-apiserver.authorizer.orgid"),
org: orgService,
}
}
func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
signedInUser, err := identity.GetRequester(ctx)
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil
}
info, err := claims.ParseNamespace(a.GetNamespace())
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error reading namespace: %v", err), nil
}
// No opinion when the namespace is empty
if info.Value == "" {
return authorizer.DecisionNoOpinion, "", nil
}
// Grafana super admins can see things in every org
if signedInUser.GetIsGrafanaAdmin() {
return authorizer.DecisionNoOpinion, "", nil
}
if info.OrgID == -1 {
return authorizer.DecisionDeny, "org id is required", nil
}
if info.StackID != 0 {
return authorizer.DecisionDeny, "using a stack namespace requires deployment with a fixed stack id", nil
}
// Quick check that the same org is used
if signedInUser.GetOrgID() == info.OrgID {
return authorizer.DecisionNoOpinion, "", nil
}
// If we have an anonymous user, let the next authorizers decide.
if signedInUser.GetIdentityType() == claims.TypeAnonymous {
return authorizer.DecisionNoOpinion, "", nil
}
// Check if the user has access to the specified org
// nolint:staticcheck
userId, err := signedInUser.GetInternalID()
if err != nil {
return authorizer.DecisionDeny, "unable to get userId", err
}
query := org.GetUserOrgListQuery{UserID: userId}
result, err := auth.org.GetUserOrgList(ctx, &query)
if err != nil {
return authorizer.DecisionDeny, "error getting user org list", err
}
for _, org := range result {
if org.OrgID == info.OrgID {
return authorizer.DecisionNoOpinion, "", nil
}
}
return authorizer.DecisionDeny, fmt.Sprintf("user %d is not a member of org %d", userId, info.OrgID), nil
}

@ -5,22 +5,19 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/org"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
var _ authorizer.Authorizer = &orgRoleAuthorizer{}
var _ authorizer.Authorizer = &roleAuthorizer{}
type orgRoleAuthorizer struct {
log log.Logger
}
type roleAuthorizer struct{}
func newOrgRoleAuthorizer(orgService org.Service) *orgRoleAuthorizer {
return &orgRoleAuthorizer{log: log.New("grafana-apiserver.authorizer.orgrole")}
func newRoleAuthorizer() *roleAuthorizer {
return &roleAuthorizer{}
}
func (auth orgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
func (auth roleAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
signedInUser, err := identity.GetRequester(ctx)
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil

@ -1,66 +0,0 @@
package authorizer
import (
"context"
"fmt"
"strconv"
"k8s.io/apiserver/pkg/authorization/authorizer"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
)
var _ authorizer.Authorizer = &stackIDAuthorizer{}
type stackIDAuthorizer struct {
log log.Logger
stackID int64
}
func newStackIDAuthorizer(cfg *setting.Cfg) *stackIDAuthorizer {
stackID, err := strconv.ParseInt(cfg.StackID, 10, 64)
if err != nil {
return nil
}
return &stackIDAuthorizer{
log: log.New("grafana-apiserver.authorizer.stackid"),
stackID: stackID, // this lets a single tenant grafana validate stack id (rather than orgs)
}
}
func (auth stackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
signedInUser, err := identity.GetRequester(ctx)
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil
}
// If we have an anonymous user, let the next authorizers decide.
if signedInUser.GetIdentityType() == claims.TypeAnonymous {
return authorizer.DecisionNoOpinion, "", nil
}
info, err := claims.ParseNamespace(a.GetNamespace())
if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error reading namespace: %v", err), nil
}
// No opinion when the namespace is empty
if info.Value == "" {
return authorizer.DecisionNoOpinion, "", nil
}
if info.StackID != auth.stackID {
msg := fmt.Sprintf("wrong stack id is selected (expected: %d, found %d)", auth.stackID, info.StackID)
return authorizer.DecisionDeny, msg, nil
}
if info.OrgID != 1 {
return authorizer.DecisionDeny, "cloud instance requires org 1", nil
}
if signedInUser.GetOrgID() != 1 {
return authorizer.DecisionDeny, "user must be in org 1", nil
}
return authorizer.DecisionNoOpinion, "", nil
}

@ -46,7 +46,6 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/utils"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
@ -158,7 +157,6 @@ func ProvideService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
rr routing.RouteRegister,
orgService org.Service,
tracing *tracing.TracingService,
serverLockService *serverlock.ServerLockService,
db db.DB,
@ -178,7 +176,7 @@ func ProvideService(
rr: rr,
stopCh: make(chan struct{}),
builders: []builder.APIGroupBuilder{},
authorizer: authorizer.NewGrafanaAuthorizer(cfg, orgService),
authorizer: authorizer.NewGrafanaAuthorizer(cfg),
tracing: tracing,
db: db, // For Unified storage
metrics: metrics.ProvideRegisterer(),

@ -12,7 +12,7 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
claims "github.com/grafana/authlib/types"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@ -34,6 +34,7 @@ const (
)
var (
errInvalidNamespace = errutil.Forbidden("authn.invalid-namespace", errutil.WithPublicMessage("invalid namespace"))
errCantAuthenticateReq = errutil.Unauthorized("auth.unauthorized")
errDisabledIdentity = errutil.Unauthorized("identity.disabled")
)
@ -57,9 +58,12 @@ func ProvideService(
cfg *setting.Cfg, tracer tracing.Tracer, sessionService auth.UserTokenService,
usageStats usagestats.Service, registerer prometheus.Registerer, authTokenService login.AuthInfoService,
) *Service {
stackID, _ := strconv.ParseInt(cfg.StackID, 10, 64)
s := &Service{
log: log.New("authn.service"),
cfg: cfg,
stackID: stackID,
clients: make(map[string]authn.Client),
clientQueue: newQueue[authn.ContextAwareClient](),
idenityResolverClients: make(map[string]authn.IdentityResolverClient),
@ -77,8 +81,9 @@ func ProvideService(
}
type Service struct {
log log.Logger
cfg *setting.Cfg
log log.Logger
cfg *setting.Cfg
stackID int64
clients map[string]authn.Client
clientQueue *queue[authn.ContextAwareClient]
@ -103,7 +108,11 @@ func (s *Service) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id
ctx, span := s.tracer.Start(ctx, "authn.Authenticate")
defer span.End()
r.OrgID = orgIDFromRequest(r)
orgID, err := s.orgIDFromRequest(r)
if err != nil {
return nil, err
}
r.OrgID = orgID
var authErr error
for _, item := range s.clientQueue.items {
@ -204,7 +213,11 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i
))
defer span.End()
r.OrgID = orgIDFromRequest(r)
orgID, err := s.orgIDFromRequest(r)
if err != nil {
return nil, err
}
r.OrgID = orgID
defer func() {
for _, hook := range s.postLoginHooks.items {
@ -226,7 +239,7 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i
}
// Login is only supported for users
if !id.IsIdentityType(claims.TypeUser) {
if !id.IsIdentityType(types.TypeUser) {
s.metrics.failedLogin.WithLabelValues(client).Inc()
return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", id.GetIdentityType())
}
@ -292,7 +305,7 @@ func (s *Service) Logout(ctx context.Context, user identity.Requester, sessionTo
redirect.URL = s.cfg.SignoutRedirectUrl
}
if !user.IsIdentityType(claims.TypeUser) {
if !user.IsIdentityType(types.TypeUser) {
return redirect, nil
}
@ -350,7 +363,7 @@ func (s *Service) ResolveIdentity(ctx context.Context, orgID int64, typedID stri
identity, err := s.resolveIdenity(ctx, orgID, typedID)
if err != nil {
if errors.Is(err, claims.ErrInvalidTypedID) {
if errors.Is(err, types.ErrInvalidTypedID) {
return nil, authn.ErrUnsupportedIdentity.Errorf("invalid identity type")
}
@ -409,16 +422,16 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, typedID strin
ctx, span := s.tracer.Start(ctx, "authn.resolveIdentity")
defer span.End()
t, i, err := claims.ParseTypeID(typedID)
t, i, err := types.ParseTypeID(typedID)
if err != nil {
return nil, err
}
if claims.IsIdentityType(t, claims.TypeUser) {
if types.IsIdentityType(t, types.TypeUser) {
return &authn.Identity{
OrgID: orgID,
ID: i,
Type: claims.TypeUser,
Type: types.TypeUser,
ClientParams: authn.ClientParams{
AllowGlobalOrg: true,
FetchSyncedUser: true,
@ -427,10 +440,10 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, typedID strin
}, nil
}
if claims.IsIdentityType(t, claims.TypeServiceAccount) {
if types.IsIdentityType(t, types.TypeServiceAccount) {
return &authn.Identity{
ID: i,
Type: claims.TypeServiceAccount,
Type: types.TypeServiceAccount,
OrgID: orgID,
ClientParams: authn.ClientParams{
AllowGlobalOrg: true,
@ -462,17 +475,66 @@ func (s *Service) errorLogFunc(ctx context.Context, err error) func(msg string,
return l.Warn
}
func orgIDFromRequest(r *authn.Request) int64 {
func (s *Service) orgIDFromRequest(r *authn.Request) (int64, error) {
if r.HTTPRequest == nil {
return 0
return 0, nil
}
orgID, err := s.orgIDFromNamespace(r.HTTPRequest)
if err != nil {
return 0, err
}
orgID := orgIDFromQuery(r.HTTPRequest)
if orgID > 0 {
return orgID
return orgID, nil
}
orgID = orgIDFromQuery(r.HTTPRequest)
if orgID > 0 {
return orgID, nil
}
return orgIDFromHeader(r.HTTPRequest), nil
}
func (s *Service) orgIDFromNamespace(req *http.Request) (int64, error) {
if !strings.HasPrefix(req.URL.Path, "/apis") {
return 0, nil
}
namespace := parseNamespace(req.URL.Path)
if namespace == "" {
return 0, nil
}
info, err := types.ParseNamespace(namespace)
if err != nil {
return 0, nil
}
if info.StackID != s.stackID {
return 0, errInvalidNamespace.Errorf("invalid namespace")
}
return info.OrgID, nil
}
func parseNamespace(path string) string {
// Pretty navie parsing of namespace but it should do the job
// Possbile url paths can be found here:
// https://github.com/kubernetes/kubernetes/blob/803e9d64952407981b3815b1d749cc96a39ba3c6/staging/src/k8s.io/apiserver/pkg/endpoints/request/requestinfo.go#L104-L127
const namespacePath = "/namespaces/"
index := strings.Index(path, namespacePath)
if index == -1 {
return ""
}
parts := strings.Split(path[index+len(namespacePath):], "/")
if len(parts) == 0 {
return ""
}
return orgIDFromHeader(r.HTTPRequest)
return parts[0]
}
// name of query string used to target specific org for request

@ -265,7 +265,9 @@ func TestService_OrgID(t *testing.T) {
type TestCase struct {
desc string
req *authn.Request
stackID int64
expectedOrgID int64
expectedErr error
}
tests := []TestCase{
@ -301,12 +303,48 @@ func TestService_OrgID(t *testing.T) {
}},
expectedOrgID: 0,
},
{
desc: "should set org id from default namespace",
req: &authn.Request{HTTPRequest: &http.Request{
Header: map[string][]string{},
URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/default/folders"),
}},
expectedOrgID: 1,
},
{
desc: "should set org id from namespace",
req: &authn.Request{HTTPRequest: &http.Request{
Header: map[string][]string{},
URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/org-2/folders"),
}},
expectedOrgID: 2,
},
{
desc: "should set set org 1 for stack namespace",
req: &authn.Request{HTTPRequest: &http.Request{
Header: map[string][]string{},
URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/stacks-100/folders"),
}},
stackID: 100,
expectedOrgID: 1,
},
{
desc: "should error for wrong stack namespace",
req: &authn.Request{HTTPRequest: &http.Request{
Header: map[string][]string{},
URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/stacks-100/folders"),
}},
stackID: 101,
expectedOrgID: 0,
expectedErr: errInvalidNamespace,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
var calledWith int64
s := setupTests(t, func(svc *Service) {
svc.stackID = tt.stackID
svc.RegisterClient(authntest.MockClient{
AuthenticateFunc: func(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
calledWith = r.OrgID
@ -316,7 +354,8 @@ func TestService_OrgID(t *testing.T) {
})
})
_, _ = s.Authenticate(context.Background(), tt.req)
_, err := s.Authenticate(context.Background(), tt.req)
assert.ErrorIs(t, tt.expectedErr, err)
assert.Equal(t, tt.expectedOrgID, calledWith)
})
}

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
goerrors "errors"
"fmt"
"io"
"net/http"
@ -49,7 +50,10 @@ import (
"github.com/grafana/grafana/pkg/tests/testinfra"
)
const Org1 = "Org1"
const (
Org1 = "Org1"
Org2 = "OrgB"
)
type K8sTestHelper struct {
t *testing.T
@ -61,6 +65,10 @@ type K8sTestHelper struct {
// // Registered groups
groups []metav1.APIGroup
orgSvc org.Service
teamSvc team.Service
userSvc user.Service
}
func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper {
@ -85,8 +93,27 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper {
Namespacer: request.GetNamespaceMapper(nil),
}
quotaService := quotaimpl.ProvideService(c.env.SQLStore, c.env.Cfg)
orgSvc, err := orgimpl.ProvideService(c.env.SQLStore, c.env.Cfg, quotaService)
require.NoError(c.t, err)
c.orgSvc = orgSvc
teamSvc, err := teamimpl.ProvideService(c.env.SQLStore, c.env.Cfg, tracing.NewNoopTracerService())
require.NoError(c.t, err)
c.teamSvc = teamSvc
userSvc, err := userimpl.ProvideService(
c.env.SQLStore, orgSvc, c.env.Cfg, teamSvc,
localcache.ProvideService(), tracing.NewNoopTracerService(), quotaService,
supportbundlestest.NewFakeBundleService())
require.NoError(c.t, err)
c.userSvc = userSvc
_ = c.CreateOrg(Org1)
_ = c.CreateOrg(Org2)
c.Org1 = c.createTestUsers(Org1)
c.OrgB = c.createTestUsers("OrgB")
c.OrgB = c.createTestUsers(Org2)
c.loadAPIGroups()
@ -455,6 +482,7 @@ func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers {
Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil),
Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil),
}
users.Staff = c.CreateTeam("staff", "staff@"+orgName, users.Admin.Identity.GetOrgID())
// Add Admin and Editor to Staff team as Admin and Member, respectively.
@ -464,61 +492,67 @@ func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers {
return users
}
func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.RoleType, permissions []resourcepermissions.SetResourcePermissionCommand) User {
c.t.Helper()
func (c *K8sTestHelper) CreateOrg(name string) int64 {
if name == Org1 {
return 1
}
store := c.env.SQLStore
oldAssing := c.env.Cfg.AutoAssignOrg
defer func() {
c.env.Cfg.AutoAssignOrg = false
c.env.Cfg.AutoAssignOrgId = 1 // the default
c.env.Cfg.AutoAssignOrg = oldAssing
}()
quotaService := quotaimpl.ProvideService(store, c.env.Cfg)
orgService, err := orgimpl.ProvideService(store, c.env.Cfg, quotaService)
require.NoError(c.t, err)
orgId := int64(1)
if orgName != Org1 {
o, err := orgService.GetByName(context.Background(), &org.GetOrgByNameQuery{Name: orgName})
if err != nil {
if !org.ErrOrgNotFound.Is(err) {
require.NoError(c.t, err)
}
orgId, err = orgService.GetOrCreate(context.Background(), orgName)
require.NoError(c.t, err)
} else {
orgId = o.ID
}
c.env.Cfg.AutoAssignOrg = false
o, err := c.orgSvc.GetByName(context.Background(), &org.GetOrgByNameQuery{
Name: name,
})
if goerrors.Is(err, org.ErrOrgNotFound) {
id, err := c.orgSvc.GetOrCreate(context.Background(), name)
require.NoError(c.t, err)
return id
}
c.env.Cfg.AutoAssignOrg = true
c.env.Cfg.AutoAssignOrgId = int(orgId)
teamSvc, err := teamimpl.ProvideService(store, c.env.Cfg, tracing.InitializeTracerForTest())
require.NoError(c.t, err)
return o.ID
}
cache := localcache.ProvideService()
userSvc, err := userimpl.ProvideService(
store, orgService, c.env.Cfg, teamSvc,
cache, tracing.InitializeTracerForTest(), quotaService,
supportbundlestest.NewFakeBundleService())
require.NoError(c.t, err)
func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.RoleType, permissions []resourcepermissions.SetResourcePermissionCommand) User {
c.t.Helper()
orgId := c.CreateOrg(orgName)
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr())
u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{
// make org1 admins grafana admins
isGrafanaAdmin := basicRole == identity.RoleAdmin && orgId == 1
u, err := c.userSvc.Create(context.Background(), &user.CreateUserCommand{
DefaultOrgRole: string(basicRole),
Password: user.Password(name),
Login: fmt.Sprintf("%s-%d", name, orgId),
OrgID: orgId,
IsAdmin: basicRole == identity.RoleAdmin && orgId == 1, // make org1 admins grafana admins
IsAdmin: isGrafanaAdmin,
})
// for tests to work we need to add grafana admins to every org
if isGrafanaAdmin {
orgs, err := c.orgSvc.Search(context.Background(), &org.SearchOrgsQuery{})
require.NoError(c.t, err)
for _, o := range orgs {
_ = c.orgSvc.AddOrgUser(context.Background(), &org.AddOrgUserCommand{
Role: identity.RoleAdmin,
OrgID: o.ID,
UserID: u.ID,
})
}
}
require.NoError(c.t, err)
require.Equal(c.t, orgId, u.OrgID)
require.True(c.t, u.ID > 0)
// should this always return a user with ID token?
s, err := userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
s, err := c.userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
UserID: u.ID,
Login: u.Login,
Email: u.Email,
@ -563,19 +597,6 @@ func (c *K8sTestHelper) SetPermissions(user User, permissions []resourcepermissi
}
func (c *K8sTestHelper) AddOrUpdateTeamMember(user User, teamID int64, permission team.PermissionType) {
teamSvc, err := teamimpl.ProvideService(c.env.SQLStore, c.env.Cfg, tracing.InitializeTracerForTest())
require.NoError(c.t, err)
orgService, err := orgimpl.ProvideService(c.env.SQLStore, c.env.Cfg, c.env.Server.HTTPServer.QuotaService)
require.NoError(c.t, err)
cache := localcache.ProvideService()
userSvc, err := userimpl.ProvideService(
c.env.SQLStore, orgService, c.env.Cfg, teamSvc,
cache, tracing.InitializeTracerForTest(), c.env.Server.HTTPServer.QuotaService,
supportbundlestest.NewFakeBundleService())
require.NoError(c.t, err)
teampermissionSvc, err := ossaccesscontrol.ProvideTeamPermissions(
c.env.Cfg,
c.env.FeatureToggles,
@ -584,8 +605,8 @@ func (c *K8sTestHelper) AddOrUpdateTeamMember(user User, teamID int64, permissio
c.env.Server.HTTPServer.AccessControl,
c.env.Server.HTTPServer.License,
c.env.Server.HTTPServer.AlertNG.AccesscontrolService,
teamSvc,
userSvc,
c.teamSvc,
c.userSvc,
resourcepermissions.NewActionSetService(c.env.FeatureToggles),
)
require.NoError(c.t, err)
@ -662,7 +683,7 @@ func (c *K8sTestHelper) CreateDS(cmd *datasources.AddDataSourceCommand) *datasou
func (c *K8sTestHelper) CreateTeam(name, email string, orgID int64) team.Team {
c.t.Helper()
team, err := c.env.Server.HTTPServer.TeamService.CreateTeam(context.Background(), name, email, orgID)
team, err := c.teamSvc.CreateTeam(context.Background(), name, email, orgID)
require.NoError(c.t, err)
return team
}

@ -110,6 +110,10 @@ func TestIntegrationIdentity(t *testing.T) {
// Get just the specs (avoids values that change with each deployment)
found = teamClient.SpecJSON(rsp)
require.JSONEq(t, `[
{
"email": "admin-1",
"login": "admin-1"
},
{
"email": "admin-3",
"login": "admin-3"

Loading…
Cancel
Save