mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Add access token to in-proc communication and ServiceIdentity (#98926)
Use fake access token for in-proc grpc and add ServiceIdentity --------- Co-authored-by: gamab <gabriel.mabille@grafana.com> Co-authored-by: Karl Persson <23356117+kalleep@users.noreply.github.com>
This commit is contained in:
parent
eb2d276a42
commit
437b7a565d
@ -5,14 +5,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
"github.com/grafana/authlib/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ctxUserKey struct{}
|
type ctxUserKey struct{}
|
||||||
|
|
||||||
// WithRequester attaches the requester to the context.
|
// WithRequester attaches the requester to the context.
|
||||||
func WithRequester(ctx context.Context, usr Requester) context.Context {
|
func WithRequester(ctx context.Context, usr Requester) context.Context {
|
||||||
ctx = claims.WithAuthInfo(ctx, usr) // also set the upstream auth info claims
|
ctx = types.WithAuthInfo(ctx, usr) // also set the upstream auth info claims
|
||||||
return context.WithValue(ctx, ctxUserKey{}, usr)
|
return context.WithValue(ctx, ctxUserKey{}, usr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,3 +29,54 @@ func GetRequester(ctx context.Context) (Requester, error) {
|
|||||||
func checkNilRequester(r Requester) bool {
|
func checkNilRequester(r Requester) bool {
|
||||||
return r == nil || (reflect.ValueOf(r).Kind() == reflect.Ptr && reflect.ValueOf(r).IsNil())
|
return r == nil || (reflect.ValueOf(r).Kind() == reflect.Ptr && reflect.ValueOf(r).IsNil())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serviceName = "service"
|
||||||
|
|
||||||
|
// WithServiceIdentitiy sets creates an identity representing the service itself in provided org and store it in context.
|
||||||
|
// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with
|
||||||
|
// static permissions so it can be used in legacy code paths.
|
||||||
|
func WithServiceIdentitiy(ctx context.Context, orgID int64) (context.Context, Requester) {
|
||||||
|
r := &StaticRequester{
|
||||||
|
Type: types.TypeAccessPolicy,
|
||||||
|
Name: serviceName,
|
||||||
|
UserUID: serviceName,
|
||||||
|
AuthID: serviceName,
|
||||||
|
Login: serviceName,
|
||||||
|
OrgRole: RoleAdmin,
|
||||||
|
IsGrafanaAdmin: true,
|
||||||
|
OrgID: orgID,
|
||||||
|
Permissions: map[int64]map[string][]string{
|
||||||
|
orgID: serviceIdentityPermissions,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return WithRequester(ctx, r), r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWildcardPermissions(actions ...string) map[string][]string {
|
||||||
|
permissions := make(map[string][]string, len(actions))
|
||||||
|
for _, a := range actions {
|
||||||
|
permissions[a] = []string{"*"}
|
||||||
|
}
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceIdentityPermissions is a list of wildcard permissions for provided actions.
|
||||||
|
// We should add every action required "internally" here.
|
||||||
|
var serviceIdentityPermissions = getWildcardPermissions(
|
||||||
|
"folders:read",
|
||||||
|
"folders:write",
|
||||||
|
"folders:create",
|
||||||
|
"dashboards:read",
|
||||||
|
"dashboards:write",
|
||||||
|
"dashboards:create",
|
||||||
|
"datasources:read",
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsServiceIdentity(ctx context.Context) bool {
|
||||||
|
ident, err := GetRequester(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ident.GetUID() == types.NewTypeID(types.TypeAccessPolicy, serviceName)
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@ import (
|
|||||||
func WithRequester(handler http.Handler) http.Handler {
|
func WithRequester(handler http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
requester, err := identity.GetRequester(ctx)
|
_, err := identity.GetRequester(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
handler.ServeHTTP(w, req)
|
handler.ServeHTTP(w, req)
|
||||||
return
|
return
|
||||||
@ -24,54 +24,27 @@ func WithRequester(handler http.Handler) http.Handler {
|
|||||||
|
|
||||||
// Find the kubernetes user info
|
// Find the kubernetes user info
|
||||||
info, ok := request.UserFrom(ctx)
|
info, ok := request.UserFrom(ctx)
|
||||||
if ok {
|
if !ok {
|
||||||
if info.GetName() == user.Anonymous {
|
handler.ServeHTTP(w, req)
|
||||||
requester = &identity.StaticRequester{
|
return
|
||||||
Type: claims.TypeAnonymous,
|
|
||||||
Name: info.GetName(),
|
|
||||||
Login: info.GetName(),
|
|
||||||
Permissions: map[int64]map[string][]string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.GetName() == user.APIServerUser ||
|
|
||||||
slices.Contains(info.GetGroups(), user.SystemPrivilegedGroup) {
|
|
||||||
orgId := int64(1)
|
|
||||||
requester = &identity.StaticRequester{
|
|
||||||
Type: claims.TypeServiceAccount, // system:apiserver
|
|
||||||
UserID: 1,
|
|
||||||
OrgID: orgId,
|
|
||||||
Name: info.GetName(),
|
|
||||||
Login: info.GetName(),
|
|
||||||
OrgRole: identity.RoleAdmin,
|
|
||||||
|
|
||||||
IsGrafanaAdmin: true,
|
|
||||||
Namespace: "default",
|
|
||||||
|
|
||||||
Permissions: map[int64]map[string][]string{
|
|
||||||
orgId: {
|
|
||||||
"*": {"*"}, // all resources, all scopes
|
|
||||||
// FIXME(kalleep): We don't support wildcard actions so we need to list all possible actions
|
|
||||||
// for this user. This is not scalable and we should look into how to fix this.
|
|
||||||
"org.users:read": {"*"},
|
|
||||||
// Dashboards do not support wildcard action
|
|
||||||
// dashboards.ActionDashboardsRead: {"*"},
|
|
||||||
// dashboards.ActionDashboardsCreate: {"*"},
|
|
||||||
// dashboards.ActionDashboardsWrite: {"*"},
|
|
||||||
// dashboards.ActionDashboardsDelete: {"*"},
|
|
||||||
// dashboards.ActionFoldersCreate: {"*"},
|
|
||||||
// dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, // access to read all folders
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if requester != nil {
|
|
||||||
req = req.WithContext(identity.WithRequester(ctx, requester))
|
|
||||||
} else {
|
|
||||||
klog.V(5).Info("unable to map the k8s user to grafana requester", "user", info)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ok && info.GetName() == user.Anonymous {
|
||||||
|
req = req.WithContext(identity.WithRequester(ctx, &identity.StaticRequester{
|
||||||
|
Type: claims.TypeAnonymous,
|
||||||
|
Name: info.GetName(),
|
||||||
|
Login: info.GetName(),
|
||||||
|
Permissions: map[int64]map[string][]string{},
|
||||||
|
}))
|
||||||
|
} else if ok && info.GetName() == user.APIServerUser ||
|
||||||
|
slices.Contains(info.GetGroups(), user.SystemPrivilegedGroup) {
|
||||||
|
// For system:apiserver we use the identity of the service itself
|
||||||
|
ctx, _ = identity.WithServiceIdentitiy(ctx, 1)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
} else {
|
||||||
|
klog.V(5).Info("unable to map the k8s user to grafana requester", "user", info)
|
||||||
|
}
|
||||||
|
|
||||||
handler.ServeHTTP(w, req)
|
handler.ServeHTTP(w, req)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
)
|
)
|
||||||
@ -159,11 +158,8 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig) (b
|
|||||||
log.Info("starting legacyToUnifiedStorageDataSyncer")
|
log.Info("starting legacyToUnifiedStorageDataSyncer")
|
||||||
startSync := time.Now()
|
startSync := time.Now()
|
||||||
|
|
||||||
// Add a claim to the context to allow the background job to use the underlying access_token permissions.
|
|
||||||
orgId := int64(1)
|
|
||||||
|
|
||||||
ctx = klog.NewContext(ctx, log)
|
ctx = klog.NewContext(ctx, log)
|
||||||
ctx = identity.WithRequester(ctx, getSyncRequester(orgId))
|
ctx, _ = identity.WithServiceIdentitiy(ctx, 0)
|
||||||
ctx = request.WithNamespace(ctx, cfg.RequestInfo.Namespace)
|
ctx = request.WithNamespace(ctx, cfg.RequestInfo.Namespace)
|
||||||
ctx = request.WithRequestInfo(ctx, cfg.RequestInfo)
|
ctx = request.WithRequestInfo(ctx, cfg.RequestInfo)
|
||||||
|
|
||||||
@ -298,23 +294,6 @@ func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig) (b
|
|||||||
return everythingSynced, err
|
return everythingSynced, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSyncRequester(orgId int64) *identity.StaticRequester {
|
|
||||||
return &identity.StaticRequester{
|
|
||||||
Type: claims.TypeServiceAccount, // system:apiserver
|
|
||||||
UserID: 1,
|
|
||||||
OrgID: orgId,
|
|
||||||
Name: "admin",
|
|
||||||
Login: "admin",
|
|
||||||
OrgRole: identity.RoleAdmin,
|
|
||||||
IsGrafanaAdmin: true,
|
|
||||||
Permissions: map[int64]map[string][]string{
|
|
||||||
orgId: {
|
|
||||||
"*": {"*"}, // all resources, all scopes
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalversion.ListOptions) ([]runtime.Object, error) {
|
func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalversion.ListOptions) ([]runtime.Object, error) {
|
||||||
ll, err := obj.List(ctx, listOptions)
|
ll, err := obj.List(ctx, listOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -5,12 +5,11 @@ import (
|
|||||||
|
|
||||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,18 +70,11 @@ func folderTreeCollector(folderService folder.Service) legacyTupleCollector {
|
|||||||
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector")
|
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
user := &user.SignedInUser{
|
ctx, ident := identity.WithServiceIdentitiy(ctx, orgID)
|
||||||
Login: "folder-tree-collector",
|
|
||||||
OrgRole: "Admin",
|
|
||||||
IsGrafanaAdmin: true,
|
|
||||||
IsServiceAccount: true,
|
|
||||||
Permissions: map[int64]map[string][]string{orgID: {dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}}},
|
|
||||||
OrgID: orgID,
|
|
||||||
}
|
|
||||||
|
|
||||||
q := folder.GetFoldersQuery{
|
q := folder.GetFoldersQuery{
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
SignedInUser: user,
|
SignedInUser: ident,
|
||||||
}
|
}
|
||||||
|
|
||||||
folders, err := folderService.GetFolders(ctx, q)
|
folders, err := folderService.GetFolders(ctx, q)
|
||||||
|
@ -22,7 +22,6 @@ func NewInProcGrpcAuthenticator() *authnlib.GrpcAuthenticator {
|
|||||||
// In proc grpc ID token signature verification can be skipped
|
// In proc grpc ID token signature verification can be skipped
|
||||||
return authnlib.NewUnsafeGrpcAuthenticator(
|
return authnlib.NewUnsafeGrpcAuthenticator(
|
||||||
&authnlib.GrpcAuthenticatorConfig{},
|
&authnlib.GrpcAuthenticatorConfig{},
|
||||||
authnlib.WithDisableAccessTokenAuthOption(),
|
|
||||||
authnlib.WithIDTokenAuthOption(false),
|
authnlib.WithIDTokenAuthOption(false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
61
pkg/services/authn/grpcutils/inproc_exchanger.go
Normal file
61
pkg/services/authn/grpcutils/inproc_exchanger.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package grpcutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
|
"github.com/grafana/authlib/authn"
|
||||||
|
"github.com/grafana/authlib/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inProcExchanger struct {
|
||||||
|
tokenResponse *authn.TokenExchangeResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProvideInProcExchanger() *inProcExchanger {
|
||||||
|
tokenResponse, err := createInProcToken()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &inProcExchanger{tokenResponse}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *inProcExchanger) Exchange(ctx context.Context, r authn.TokenExchangeRequest) (*authn.TokenExchangeResponse, error) {
|
||||||
|
return e.tokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createInProcToken() (*authn.TokenExchangeResponse, error) {
|
||||||
|
claims := authn.Claims[authn.AccessTokenClaims]{
|
||||||
|
Claims: jwt.Claims{
|
||||||
|
Audience: []string{"resourceStore"},
|
||||||
|
Issuer: "grafana",
|
||||||
|
Subject: types.NewTypeID(types.TypeAccessPolicy, "grafana"),
|
||||||
|
},
|
||||||
|
Rest: authn.AccessTokenClaims{
|
||||||
|
Namespace: "*",
|
||||||
|
Permissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"},
|
||||||
|
DelegatedPermissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := json.Marshal(map[string]string{
|
||||||
|
"alg": "none",
|
||||||
|
"typ": authn.TokenTypeAccess,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &authn.TokenExchangeResponse{
|
||||||
|
Token: fmt.Sprintf("%s.%s.", base64.RawURLEncoding.EncodeToString(header), base64.RawURLEncoding.EncodeToString(payload)),
|
||||||
|
}, nil
|
||||||
|
}
|
@ -37,7 +37,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||||
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
|
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
@ -54,14 +53,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
provisionerPermissions = []accesscontrol.Permission{
|
|
||||||
{Action: dashboards.ActionFoldersCreate, Scope: dashboards.ScopeFoldersAll},
|
|
||||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll},
|
|
||||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
|
||||||
{Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll},
|
|
||||||
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll},
|
|
||||||
{Action: datasources.ActionRead, Scope: datasources.ScopeAll},
|
|
||||||
}
|
|
||||||
// DashboardServiceImpl implements the DashboardService interface
|
// DashboardServiceImpl implements the DashboardService interface
|
||||||
_ dashboards.DashboardService = (*DashboardServiceImpl)(nil)
|
_ dashboards.DashboardService = (*DashboardServiceImpl)(nil)
|
||||||
_ dashboards.DashboardProvisioningService = (*DashboardServiceImpl)(nil)
|
_ dashboards.DashboardProvisioningService = (*DashboardServiceImpl)(nil)
|
||||||
@ -164,7 +155,7 @@ func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.Sc
|
|||||||
|
|
||||||
total := int64(0)
|
total := int64(0)
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(org.ID))
|
ctx, _ := identity.WithServiceIdentitiy(ctx, org.ID)
|
||||||
orgDashboards, err := dr.CountDashboardsInOrg(ctx, org.ID)
|
orgDashboards, err := dr.CountDashboardsInOrg(ctx, org.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -228,21 +219,6 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
|||||||
return limits, nil
|
return limits, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDashboardBackgroundRequester(orgId int64) *identity.StaticRequester {
|
|
||||||
return &identity.StaticRequester{
|
|
||||||
Type: claims.TypeServiceAccount,
|
|
||||||
UserID: 1,
|
|
||||||
OrgID: orgId,
|
|
||||||
Name: "dashboard-background",
|
|
||||||
Login: "dashboard-background",
|
|
||||||
Permissions: map[int64]map[string][]string{
|
|
||||||
orgId: {
|
|
||||||
"*": {"*"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) {
|
func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) {
|
||||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||||
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
||||||
@ -581,6 +557,7 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
|
ctx, _ := identity.WithServiceIdentitiy(ctx, org.ID)
|
||||||
// find all dashboards in the org that have a file repo set that is not in the given readers list
|
// find all dashboards in the org that have a file repo set that is not in the given readers list
|
||||||
foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
||||||
ProvisionedReposNotIn: cmd.ReaderNames,
|
ProvisionedReposNotIn: cmd.ReaderNames,
|
||||||
@ -592,7 +569,6 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.
|
|||||||
|
|
||||||
// delete them
|
// delete them
|
||||||
for _, foundDash := range foundDashs {
|
for _, foundDash := range foundDashs {
|
||||||
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(org.ID))
|
|
||||||
if err = dr.deleteDashboard(ctx, foundDash.DashboardID, foundDash.DashboardUID, org.ID, false); err != nil {
|
if err = dr.deleteDashboard(ctx, foundDash.DashboardID, foundDash.DashboardUID, org.ID, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -682,8 +658,8 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
|
|||||||
dto.Dashboard.Data.Set("refresh", dr.cfg.MinRefreshInterval)
|
dto.Dashboard.Data.Set("refresh", dr.cfg.MinRefreshInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions)
|
ctx, ident := identity.WithServiceIdentitiy(ctx, dto.OrgID)
|
||||||
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(dto.OrgID))
|
dto.User = ident
|
||||||
|
|
||||||
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false)
|
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -722,8 +698,8 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C
|
|||||||
ctx, span := tracer.Start(ctx, "dashboards.service.SaveFolderForProvisionedDashboards")
|
ctx, span := tracer.Start(ctx, "dashboards.service.SaveFolderForProvisionedDashboards")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
dto.SignedInUser = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions)
|
ctx, ident := identity.WithServiceIdentitiy(ctx, dto.OrgID)
|
||||||
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(dto.OrgID))
|
dto.SignedInUser = ident
|
||||||
|
|
||||||
f, err := dr.folderService.Create(ctx, dto)
|
f, err := dr.folderService.Create(ctx, dto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -867,7 +843,7 @@ func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, das
|
|||||||
|
|
||||||
// DeleteProvisionedDashboard removes dashboard from the DB even if it is provisioned.
|
// DeleteProvisionedDashboard removes dashboard from the DB even if it is provisioned.
|
||||||
func (dr *DashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
|
func (dr *DashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
|
||||||
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(orgId))
|
ctx, _ = identity.WithServiceIdentitiy(ctx, orgId)
|
||||||
return dr.deleteDashboard(ctx, dashboardId, "", orgId, false)
|
return dr.deleteDashboard(ctx, dashboardId, "", orgId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -949,7 +925,7 @@ func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashbo
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(org.ID))
|
ctx, _ = identity.WithServiceIdentitiy(ctx, org.ID)
|
||||||
dash, err := dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: org.ID, ID: dashboardId})
|
dash, err := dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: org.ID, ID: dashboardId})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if we can't find it in this org, try the next one
|
// if we can't find it in this org, try the next one
|
||||||
@ -1720,7 +1696,7 @@ type dashboardProvisioningWithUID struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]*dashboardProvisioningWithUID, error) {
|
func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]*dashboardProvisioningWithUID, error) {
|
||||||
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(query.OrgId))
|
ctx, _ = identity.WithServiceIdentitiy(ctx, query.OrgId)
|
||||||
|
|
||||||
if query.ProvisionedRepo != "" {
|
if query.ProvisionedRepo != "" {
|
||||||
query.ProvisionedRepo = provisionedFileNameWithPrefix(query.ProvisionedRepo)
|
query.ProvisionedRepo = provisionedFileNameWithPrefix(query.ProvisionedRepo)
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
@ -45,7 +44,7 @@ type sqlStatsService struct {
|
|||||||
func (ss *sqlStatsService) getDashboardCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
|
func (ss *sqlStatsService) getDashboardCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
|
||||||
count := int64(0)
|
count := int64(0)
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
ctx = identity.WithRequester(ctx, getStatsRequester(org.ID))
|
ctx, _ = identity.WithServiceIdentitiy(ctx, org.ID)
|
||||||
dashsCount, err := ss.dashSvc.CountDashboardsInOrg(ctx, org.ID)
|
dashsCount, err := ss.dashSvc.CountDashboardsInOrg(ctx, org.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -59,7 +58,7 @@ func (ss *sqlStatsService) getDashboardCount(ctx context.Context, orgs []*org.Or
|
|||||||
func (ss *sqlStatsService) getTagCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
|
func (ss *sqlStatsService) getTagCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
|
||||||
total := 0
|
total := 0
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
ctx = identity.WithRequester(ctx, getStatsRequester(org.ID))
|
ctx, _ = identity.WithServiceIdentitiy(ctx, org.ID)
|
||||||
tags, err := ss.dashSvc.GetDashboardTags(ctx, &dashboards.GetDashboardTagsQuery{
|
tags, err := ss.dashSvc.GetDashboardTags(ctx, &dashboards.GetDashboardTagsQuery{
|
||||||
OrgID: org.ID,
|
OrgID: org.ID,
|
||||||
})
|
})
|
||||||
@ -75,11 +74,10 @@ func (ss *sqlStatsService) getTagCount(ctx context.Context, orgs []*org.OrgDTO)
|
|||||||
func (ss *sqlStatsService) getFolderCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
|
func (ss *sqlStatsService) getFolderCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
|
||||||
total := 0
|
total := 0
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
backgroundUser := getStatsRequester(org.ID)
|
ctx, ident := identity.WithServiceIdentitiy(ctx, org.ID)
|
||||||
ctx = identity.WithRequester(ctx, backgroundUser)
|
|
||||||
folders, err := ss.folderSvc.GetFolders(ctx, folder.GetFoldersQuery{
|
folders, err := ss.folderSvc.GetFolders(ctx, folder.GetFoldersQuery{
|
||||||
OrgID: org.ID,
|
OrgID: org.ID,
|
||||||
SignedInUser: backgroundUser,
|
SignedInUser: ident,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -438,18 +436,3 @@ func addToStats(base stats.UserStats, role org.RoleType, count int64) stats.User
|
|||||||
|
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatsRequester(orgId int64) *identity.StaticRequester {
|
|
||||||
return &identity.StaticRequester{
|
|
||||||
Type: claims.TypeServiceAccount,
|
|
||||||
UserID: 1,
|
|
||||||
OrgID: orgId,
|
|
||||||
Name: "stats-requester",
|
|
||||||
Login: "stats-requester",
|
|
||||||
Permissions: map[int64]map[string][]string{
|
|
||||||
orgId: {
|
|
||||||
"*": {"*"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fullstorydev/grpchan"
|
"github.com/fullstorydev/grpchan"
|
||||||
@ -12,7 +13,8 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
authnlib "github.com/grafana/authlib/authn"
|
authnlib "github.com/grafana/authlib/authn"
|
||||||
claims "github.com/grafana/authlib/types"
|
"github.com/grafana/authlib/types"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/authn/grpcutils"
|
"github.com/grafana/grafana/pkg/services/authn/grpcutils"
|
||||||
@ -70,8 +72,8 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clientInt, _ := authnlib.NewGrpcClientInterceptor(
|
clientInt, _ := authnlib.NewGrpcClientInterceptor(
|
||||||
&authnlib.GrpcClientConfig{},
|
&authnlib.GrpcClientConfig{TokenRequest: &authnlib.TokenExchangeRequest{}},
|
||||||
authnlib.WithDisableAccessTokenOption(),
|
authnlib.WithTokenClientOption(grpcutils.ProvideInProcExchanger()),
|
||||||
authnlib.WithIDTokenExtractorOption(idTokenExtractor),
|
authnlib.WithIDTokenExtractorOption(idTokenExtractor),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -131,15 +133,28 @@ func NewCloudResourceClient(tracer tracing.Tracer, conn *grpc.ClientConn, cfg au
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authLogger = slog.Default().With("logger", "resource-client-auth-interceptor")
|
||||||
|
|
||||||
func idTokenExtractor(ctx context.Context) (string, error) {
|
func idTokenExtractor(ctx context.Context) (string, error) {
|
||||||
authInfo, ok := claims.AuthInfoFrom(ctx)
|
if identity.IsServiceIdentity(ctx) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info, ok := types.AuthInfoFrom(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("no claims found")
|
return "", fmt.Errorf("no claims found")
|
||||||
}
|
}
|
||||||
|
|
||||||
extra := authInfo.GetExtra()
|
if token := info.GetIDToken(); len(token) != 0 {
|
||||||
if token, exists := extra["id-token"]; exists && len(token) != 0 && token[0] != "" {
|
return token, nil
|
||||||
return token[0], nil
|
}
|
||||||
|
|
||||||
|
if !types.IsIdentityType(info.GetIdentityType(), types.TypeAccessPolicy) {
|
||||||
|
authLogger.Warn(
|
||||||
|
"calling resource store as the service without id token or marking it as the service identity",
|
||||||
|
"subject", info.GetSubject(),
|
||||||
|
"uid", info.GetUID(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,11 +15,8 @@ func TestIDTokenExtractor(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Empty(t, token)
|
assert.Empty(t, token)
|
||||||
})
|
})
|
||||||
t.Run("should return an empty token for static requester of type service account as grafana admin ", func(t *testing.T) {
|
t.Run("should return an empty token when grafana identity is set", func(t *testing.T) {
|
||||||
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
|
ctx, _ := identity.WithServiceIdentitiy(context.Background(), 0)
|
||||||
Type: claims.TypeServiceAccount,
|
|
||||||
IsGrafanaAdmin: true,
|
|
||||||
})
|
|
||||||
token, err := idTokenExtractor(ctx)
|
token, err := idTokenExtractor(ctx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, token)
|
assert.Empty(t, token)
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
claims "github.com/grafana/authlib/types"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -233,13 +232,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make this cancelable
|
// Make this cancelable
|
||||||
ctx, cancel := context.WithCancel(claims.WithAuthInfo(context.Background(),
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
&identity.StaticRequester{
|
|
||||||
Type: claims.TypeServiceAccount,
|
|
||||||
Login: "watcher", // admin user for watch
|
|
||||||
UserID: 1,
|
|
||||||
IsGrafanaAdmin: true,
|
|
||||||
}))
|
|
||||||
s := &server{
|
s := &server{
|
||||||
tracer: opts.Tracer,
|
tracer: opts.Tracer,
|
||||||
log: logger,
|
log: logger,
|
||||||
|
@ -5,15 +5,16 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
"github.com/grafana/authlib/authn"
|
||||||
|
"github.com/grafana/authlib/types"
|
||||||
"github.com/grafana/dskit/services"
|
"github.com/grafana/dskit/services"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
@ -70,15 +71,13 @@ func TestIntegrationBackendHappyPath(t *testing.T) {
|
|||||||
t.Skip("skipping integration test")
|
t.Skip("skipping integration test")
|
||||||
}
|
}
|
||||||
|
|
||||||
testUserA := &identity.StaticRequester{
|
ctx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||||
Type: claims.TypeUser,
|
Claims: jwt.Claims{
|
||||||
Login: "testuser",
|
Subject: "testuser",
|
||||||
UserID: 123,
|
},
|
||||||
UserUID: "u123",
|
Rest: authn.AccessTokenClaims{},
|
||||||
OrgRole: identity.RoleAdmin,
|
}))
|
||||||
IsGrafanaAdmin: true, // can do anything
|
|
||||||
}
|
|
||||||
ctx := identity.WithRequester(context.Background(), testUserA)
|
|
||||||
backend, server := newServer(t, nil)
|
backend, server := newServer(t, nil)
|
||||||
|
|
||||||
stream, err := backend.WatchWriteEvents(context.Background()) // Using a different context to avoid canceling the stream after the DefaultContextTimeout
|
stream, err := backend.WatchWriteEvents(context.Background()) // Using a different context to avoid canceling the stream after the DefaultContextTimeout
|
||||||
@ -420,15 +419,12 @@ func TestClientServer(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
var client resource.ResourceStoreClient
|
var client resource.ResourceStoreClient
|
||||||
|
|
||||||
// Test with an admin identity
|
clientCtx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||||
clientCtx := identity.WithRequester(ctx, &identity.StaticRequester{
|
Claims: jwt.Claims{
|
||||||
Type: claims.TypeUser,
|
Subject: "testuser",
|
||||||
Login: "testuser",
|
},
|
||||||
UserID: 123,
|
Rest: authn.AccessTokenClaims{},
|
||||||
UserUID: "u123",
|
}))
|
||||||
OrgRole: identity.RoleAdmin,
|
|
||||||
IsGrafanaAdmin: true, // can do anything
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Start and stop service", func(t *testing.T) {
|
t.Run("Start and stop service", func(t *testing.T) {
|
||||||
err = services.StartAndAwaitRunning(ctx, svc)
|
err = services.StartAndAwaitRunning(ctx, svc)
|
||||||
|
Loading…
Reference in New Issue
Block a user