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
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
return orgIDFromHeader(r.HTTPRequest)
|
||||
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 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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user