mirror of https://github.com/grafana/grafana
Auth: Add anonymous authn client (#59637)
* Authn: Add Client interface and Reqeust and Identity structures * Authn: Implement Authenticate method in service * Authn: Add tracing * Authn: Add logger * AuthN: Implement Anonymous clientpull/57193/head
parent
c52d4e2a64
commit
22be025284
@ -1,4 +1,45 @@ |
||||
package authn |
||||
|
||||
import ( |
||||
"context" |
||||
"net/http" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
"github.com/grafana/grafana/pkg/services/user" |
||||
) |
||||
|
||||
const ( |
||||
ClientAnonymous = "auth.anonymous" |
||||
) |
||||
|
||||
type Service interface { |
||||
Authenticate(ctx context.Context, client string, r *Request) (*Identity, error) |
||||
} |
||||
|
||||
type Client interface { |
||||
Authenticate(ctx context.Context, r *Request) (*Identity, error) |
||||
} |
||||
|
||||
type Request struct { |
||||
HTTPRequest *http.Request |
||||
} |
||||
|
||||
type Identity struct { |
||||
OrgID int64 |
||||
OrgName string |
||||
IsAnonymous bool |
||||
OrgRoles map[int64]org.RoleType |
||||
} |
||||
|
||||
func (i *Identity) Role() org.RoleType { |
||||
return i.OrgRoles[i.OrgID] |
||||
} |
||||
|
||||
func (i *Identity) SignedInUser() *user.SignedInUser { |
||||
return &user.SignedInUser{ |
||||
OrgID: i.OrgID, |
||||
OrgName: i.OrgName, |
||||
OrgRole: i.Role(), |
||||
IsAnonymous: i.IsAnonymous, |
||||
} |
||||
} |
||||
|
||||
@ -1,8 +1,62 @@ |
||||
package authnimpl |
||||
|
||||
import "github.com/grafana/grafana/pkg/services/authn" |
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/infra/tracing" |
||||
"github.com/grafana/grafana/pkg/services/authn" |
||||
"github.com/grafana/grafana/pkg/services/authn/clients" |
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"go.opentelemetry.io/otel/attribute" |
||||
) |
||||
|
||||
var _ authn.Service = new(Service) |
||||
|
||||
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service) *Service { |
||||
s := &Service{ |
||||
log: log.New("authn.service"), |
||||
cfg: cfg, |
||||
clients: make(map[string]authn.Client), |
||||
tracer: tracer, |
||||
} |
||||
|
||||
if s.cfg.AnonymousEnabled { |
||||
s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService) |
||||
} |
||||
|
||||
return s |
||||
} |
||||
|
||||
type Service struct { |
||||
log log.Logger |
||||
cfg *setting.Cfg |
||||
clients map[string]authn.Client |
||||
|
||||
tracer tracing.Tracer |
||||
} |
||||
|
||||
func (s *Service) Authenticate(ctx context.Context, clientName string, r *authn.Request) (*authn.Identity, error) { |
||||
ctx, span := s.tracer.Start(ctx, "authn.Authenticate") |
||||
defer span.End() |
||||
|
||||
span.SetAttributes("authn.client", clientName, attribute.Key("authn.client").String(clientName)) |
||||
|
||||
client, ok := s.clients[clientName] |
||||
if !ok { |
||||
s.log.FromContext(ctx).Warn("auth client not found", "client", clientName) |
||||
span.AddEvents([]string{"message"}, []tracing.EventValue{{Str: "auth client is not configured"}}) |
||||
return nil, authn.ErrClientNotFound |
||||
} |
||||
|
||||
// FIXME: We want to perform common authentication operations here.
|
||||
// We will add them as we start to implement clients that requires them.
|
||||
// Those operations can be Syncing user, syncing teams, create a session etc.
|
||||
// We would need to check what operations a client support and also if they are requested
|
||||
// because for e.g. basic auth we want to create a session if the call is coming from the
|
||||
// login handler, but if we want to perform basic auth during a request (called from contexthandler) we don't
|
||||
// want a session to be created.
|
||||
|
||||
return client.Authenticate(ctx, r) |
||||
} |
||||
|
||||
@ -0,0 +1,62 @@ |
||||
package authnimpl |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/infra/tracing" |
||||
"github.com/grafana/grafana/pkg/services/authn" |
||||
"github.com/grafana/grafana/pkg/services/authn/authntest" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
func TestService_Authenticate(t *testing.T) { |
||||
type TestCase struct { |
||||
desc string |
||||
clientName string |
||||
expectedErr error |
||||
} |
||||
|
||||
tests := []TestCase{ |
||||
{ |
||||
desc: "should succeed with authentication for configured client", |
||||
clientName: "fake", |
||||
}, |
||||
{ |
||||
desc: "should fail when client is not configured", |
||||
clientName: "gitlab", |
||||
expectedErr: authn.ErrClientNotFound, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
svc := setupTests(t, func(svc *Service) { |
||||
svc.clients["fake"] = &authntest.FakeClient{} |
||||
}) |
||||
|
||||
_, err := svc.Authenticate(context.Background(), tt.clientName, &authn.Request{}) |
||||
assert.ErrorIs(t, tt.expectedErr, err) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func setupTests(t *testing.T, opts ...func(svc *Service)) *Service { |
||||
t.Helper() |
||||
|
||||
s := &Service{ |
||||
log: log.NewNopLogger(), |
||||
cfg: setting.NewCfg(), |
||||
clients: map[string]authn.Client{}, |
||||
tracer: tracing.InitializeTracerForTest(), |
||||
} |
||||
|
||||
for _, o := range opts { |
||||
o(s) |
||||
} |
||||
|
||||
return s |
||||
} |
||||
@ -1,7 +1,22 @@ |
||||
package authntest |
||||
|
||||
import "github.com/grafana/grafana/pkg/services/authn" |
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/authn" |
||||
) |
||||
|
||||
type FakeService struct { |
||||
authn.Service |
||||
} |
||||
|
||||
var _ authn.Client = new(FakeClient) |
||||
|
||||
type FakeClient struct { |
||||
ExpectedErr error |
||||
ExpectedIdentity *authn.Identity |
||||
} |
||||
|
||||
func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { |
||||
return f.ExpectedIdentity, f.ExpectedErr |
||||
} |
||||
|
||||
@ -0,0 +1,41 @@ |
||||
package clients |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/services/authn" |
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
var _ authn.Client = new(Anonymous) |
||||
|
||||
func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service) *Anonymous { |
||||
return &Anonymous{ |
||||
cfg: cfg, |
||||
log: log.New("authn.anonymous"), |
||||
orgService: orgService, |
||||
} |
||||
} |
||||
|
||||
type Anonymous struct { |
||||
cfg *setting.Cfg |
||||
log log.Logger |
||||
orgService org.Service |
||||
} |
||||
|
||||
func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { |
||||
o, err := a.orgService.GetByName(ctx, &org.GetOrgByNameQuery{Name: a.cfg.AnonymousOrgName}) |
||||
if err != nil { |
||||
a.log.FromContext(ctx).Error("failed to find organization", "name", a.cfg.AnonymousOrgName, "error", err) |
||||
return nil, err |
||||
} |
||||
|
||||
return &authn.Identity{ |
||||
OrgID: o.ID, |
||||
OrgName: o.Name, |
||||
OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)}, |
||||
IsAnonymous: true, |
||||
}, nil |
||||
} |
||||
@ -0,0 +1,66 @@ |
||||
package clients |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/services/authn" |
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
"github.com/grafana/grafana/pkg/services/org/orgtest" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestAnonymous_Authenticate(t *testing.T) { |
||||
type TestCase struct { |
||||
desc string |
||||
org *org.Org |
||||
cfg *setting.Cfg |
||||
err error |
||||
} |
||||
|
||||
tests := []TestCase{ |
||||
{ |
||||
desc: "should success with valid org configured", |
||||
org: &org.Org{ID: 1, Name: "some org"}, |
||||
cfg: &setting.Cfg{ |
||||
AnonymousOrgName: "some org", |
||||
AnonymousOrgRole: "Viewer", |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "should return error if any error occurs during org lookup", |
||||
err: fmt.Errorf("some error"), |
||||
cfg: &setting.Cfg{ |
||||
AnonymousOrgName: "some org", |
||||
AnonymousOrgRole: "Viewer", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
c := Anonymous{ |
||||
cfg: tt.cfg, |
||||
log: log.NewNopLogger(), |
||||
orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err}, |
||||
} |
||||
|
||||
identity, err := c.Authenticate(context.Background(), &authn.Request{}) |
||||
if err != nil { |
||||
require.Error(t, err) |
||||
require.Nil(t, identity) |
||||
} else { |
||||
require.Nil(t, err) |
||||
|
||||
assert.Equal(t, true, identity.IsAnonymous) |
||||
assert.Equal(t, tt.org.ID, identity.OrgID) |
||||
assert.Equal(t, tt.org.Name, identity.OrgName) |
||||
assert.Equal(t, tt.cfg.AnonymousOrgRole, string(identity.Role())) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
package authn |
||||
|
||||
import "github.com/grafana/grafana/pkg/util/errutil" |
||||
|
||||
var ErrClientNotFound = errutil.NewBase(errutil.StatusNotFound, "auth.client.notConfigured") |
||||
Loading…
Reference in new issue