POC/Provisioning: Add service for client with authorization (#96755)

This commit is contained in:
Ryan McKinley authored and GitHub committed 2024-11-20 15:49:42 +03:00
1 parent b55cd2438c
commit fbcc82ab17
6 files changed
+303 -6

No files matched your search

@@ -0,0 +1,172 @@
package auth
import (
"errors"
"fmt"
"log/slog"
"sync"
"golang.org/x/net/context"
"k8s.io/client-go/rest"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/user"
)
type BackgroundIdentityService interface {
WorkerIdentity(ctx context.Context, namespace string) (identity.Requester, error)
// This will return a rest.Config with a worker identity attached in the context
RestConfigForNamespace(ctx context.Context, namespace string) (*rest.Config, error)
}
func ProvideProvisioningIdentityService(
serviceAccounts serviceaccounts.Service,
authn authn.Service,
clientConfigProvider apiserver.DirectRestConfigProvider,
// HACK, for now we will use a global grafana admin user
// service accounts need permissions granted (as far as i can tell)
users user.Service,
orgs org.Service,
) BackgroundIdentityService {
prefix := "provisioning-background-worker"
return &backgroundIdentities{
serviceAccountNamePrefix: prefix,
role: org.RoleAdmin,
clientConfigProvider: clientConfigProvider,
log: slog.Default().With("logger", "background-identities", "prefix", prefix),
accounts: make(map[int64]string),
serviceAccounts: serviceAccounts,
authn: authn,
users: users,
orgs: orgs,
}
}
type backgroundIdentities struct {
log *slog.Logger
serviceAccountNamePrefix string
role org.RoleType
clientConfigProvider apiserver.DirectRestConfigProvider
// typed ids
accounts map[int64]string
mutex sync.Mutex
users user.Service
serviceAccounts serviceaccounts.Service
authn authn.Service
orgs org.Service
}
func (o *backgroundIdentities) WorkerIdentity(ctx context.Context, namespace string) (identity.Requester, error) {
info, err := claims.ParseNamespace(namespace)
if err != nil {
return nil, err
}
o.mutex.Lock()
defer o.mutex.Unlock()
id, ok := o.accounts[info.OrgID]
if !ok {
if false { // <<<<< HACK
id, err = o.makeAdminUser(ctx, info.OrgID)
} else {
id, err = o.verifyServiceAccount(ctx, info.OrgID)
}
if err != nil {
return nil, err
}
o.accounts[info.OrgID] = id
}
return o.authn.ResolveIdentity(ctx, info.OrgID, id)
}
func (o *backgroundIdentities) makeAdminUser(ctx context.Context, orgId int64) (string, error) {
found, err := o.users.GetByLogin(ctx, &user.GetUserByLoginQuery{
LoginOrEmail: o.serviceAccountNamePrefix,
})
if found == nil {
// Same user across all orgs??
found, err = o.users.Create(ctx, &user.CreateUserCommand{
Login: o.serviceAccountNamePrefix,
Name: o.serviceAccountNamePrefix,
Email: o.serviceAccountNamePrefix,
OrgID: orgId,
IsAdmin: true,
DefaultOrgRole: string(org.RoleAdmin),
})
}
if err != nil {
return "", err
}
// Make sure the user exists in the org
err = o.orgs.UpdateOrgUser(ctx, &org.UpdateOrgUserCommand{
Role: identity.RoleAdmin,
OrgID: orgId,
UserID: found.ID,
})
return claims.NewTypeID(claims.TypeUser, found.UID), err
}
func (o *backgroundIdentities) verifyServiceAccount(ctx context.Context, orgId int64) (string, error) {
serviceAccountName := fmt.Sprintf("%s-org-%d", o.serviceAccountNamePrefix, orgId)
saForm := serviceaccounts.CreateServiceAccountForm{
Name: serviceAccountName,
Role: &o.role,
}
serviceAccount, err := o.serviceAccounts.CreateServiceAccount(ctx, orgId, &saForm)
if serviceAccount == nil {
accountAlreadyExists := errors.Is(err, serviceaccounts.ErrServiceAccountAlreadyExists)
if accountAlreadyExists {
accountId, err := o.serviceAccounts.RetrieveServiceAccountIdByName(ctx, orgId, serviceAccountName)
if err != nil {
o.log.Error("Failed to retrieve service account", "err", err, "accountName", serviceAccountName)
return "", err
}
// update org_role to make sure everything works properly if someone has changed the role since SA's original creation
dto, err := o.serviceAccounts.UpdateServiceAccount(ctx, orgId, accountId, &serviceaccounts.UpdateServiceAccountForm{
Name: &serviceAccountName,
Role: &o.role,
})
if err != nil {
o.log.Error("Failed to update service account", "err", err, "accountName", serviceAccountName)
return "", err
}
serviceAccount = &serviceaccounts.ServiceAccountDTO{
Id: dto.Id,
UID: dto.UID,
}
}
}
if serviceAccount == nil {
o.log.Error("Failed to retrieve service account", "err", err, "accountName", serviceAccountName)
return "", err
}
return claims.NewTypeID(claims.TypeServiceAccount, fmt.Sprintf("%d", serviceAccount.Id)), nil
}
func (o *backgroundIdentities) RestConfigForNamespace(ctx context.Context, namespace string) (*rest.Config, error) {
id, err := o.WorkerIdentity(ctx, namespace)
if err != nil {
return nil, err
}
return o.clientConfigProvider.GetRestConfigForBackgroundWorker(id), nil
}
+68
View File
@@ -0,0 +1,68 @@
package provisioning
import (
"context"
"sync"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/auth"
)
type resourceClient struct {
identities auth.BackgroundIdentityService
client map[string]*dynamic.DynamicClient
clientMu sync.Mutex
kinds map[schema.GroupVersionKind]schema.GroupVersionResource
kindMu sync.Mutex
}
func newResourceClient(identities auth.BackgroundIdentityService) *resourceClient {
return &resourceClient{
identities: identities,
client: make(map[string]*dynamic.DynamicClient),
}
}
func (c *resourceClient) Client(ns string) (*dynamic.DynamicClient, error) {
c.clientMu.Lock()
defer c.clientMu.Unlock()
client, ok := c.client[ns]
if ok {
return client, nil
}
config, err := c.identities.RestConfigForNamespace(context.Background(), ns)
if err != nil {
return nil, err
}
client, err = dynamic.NewForConfig(config)
if err != nil {
return nil, err
}
c.client[ns] = client
return client, nil
}
func (c *resourceClient) GVR(ns string, gvk schema.GroupVersionKind) (schema.GroupVersionResource, bool) {
c.kindMu.Lock()
defer c.kindMu.Unlock()
// TODO... this needs to find the right GVR for a GVK
ok := true
var gvr schema.GroupVersionResource
switch gvk.Kind {
case "Dashboard":
gvr = gvk.GroupVersion().WithResource("dashboards")
case "Playlist":
gvr = gvk.GroupVersion().WithResource("playlists")
default:
ok = false
}
return gvr, ok
}
+31
View File
@@ -8,14 +8,18 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
type helloWorldSubresource struct {
getter rest.Getter
statusUpdater rest.Updater
// for our hacky test access right now
parent *ProvisioningAPIBuilder
}
func (*helloWorldSubresource) New() runtime.Object {
@@ -89,6 +93,33 @@ func (s *helloWorldSubresource) Connect(ctx context.Context, name string, opts r
slog.InfoContext(ctx, "the conspicuous boolean", "bool", b)
}
write := r.URL.Query().Get("list")
if write != "" {
gvr, ok := s.parent.client.GVR(repo.Namespace, schema.GroupVersionKind{
Group: v1alpha1.GROUP, // dashboard.grafana.app
Version: v1alpha1.VERSION, // "v0alpha1",
Kind: "Dashboard",
})
if !ok {
responder.Error(fmt.Errorf("no dashboard gvr"))
return
}
client, err := s.parent.client.Client(repo.Namespace)
if err != nil {
responder.Error(err)
return
}
ttt, err := client.Resource(gvr).Namespace(repo.Namespace).List(ctx, metav1.ListOptions{})
if err != nil {
responder.Error(err)
return
}
responder.Object(200, ttt) // proxy the list
return
}
slog.InfoContext(ctx, "Got a repository",
"name", repo.Name,
"type", repo.Spec.Type,
+12 -6
View File
@@ -30,6 +30,7 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/auth"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
@@ -42,12 +43,13 @@ var (
// This is used just so wire has something unique to return
type ProvisioningAPIBuilder struct {
urlProvider func(namespace string) string
webhookSecreteKey string
urlProvider func(namespace string) string
webhookSecretKey string
getter rest.Getter
localFileResolver *LocalFolderResolver
logger *slog.Logger
client *resourceClient
}
// This constructor will be called when building a multi-tenant apiserveer
@@ -57,21 +59,24 @@ func NewProvisioningAPIBuilder(
local *LocalFolderResolver,
urlProvider func(namespace string) string,
webhookSecreteKey string,
identities auth.BackgroundIdentityService,
) *ProvisioningAPIBuilder {
return &ProvisioningAPIBuilder{
urlProvider: urlProvider,
localFileResolver: local,
logger: slog.Default().With("logger", "provisioning-api-builder"),
webhookSecreteKey: webhookSecreteKey,
webhookSecretKey: webhookSecreteKey,
client: newResourceClient(identities),
}
}
func RegisterAPIService(
// It is OK to use settigns.Cfg here -- this is only used when running single tenant with a full setup
// It is OK to use setting.Cfg here -- this is only used when running single tenant with a full setup
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
reg prometheus.Registerer,
identities auth.BackgroundIdentityService,
) *ProvisioningAPIBuilder {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
@@ -81,7 +86,7 @@ func RegisterAPIService(
DevenvPath: filepath.Join(cfg.HomePath, "devenv"),
}, func(namespace string) string {
return cfg.AppURL
}, cfg.SecretKey)
}, cfg.SecretKey, identities)
apiregistration.RegisterAPI(builder)
return builder
}
@@ -136,6 +141,7 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv
helloWorld := &helloWorldSubresource{
getter: repositoryStorage,
statusUpdater: repositoryStatusStorage,
parent: b,
}
storage := map[string]rest.Storage{}
@@ -508,7 +514,7 @@ spec:
// using the configured secret key. The generated secret is consistent.
// TODO: this must be replaced by a app platform secrets once we have that.
func (b *ProvisioningAPIBuilder) generateWebhookSecret(token string) string {
secretKey := []byte(b.webhookSecreteKey)
secretKey := []byte(b.webhookSecretKey)
h := hmac.New(sha256.New, secretKey)
h.Write([]byte(token))
+2
View File
@@ -36,6 +36,7 @@ import (
"github.com/grafana/grafana/pkg/middleware/csrf"
"github.com/grafana/grafana/pkg/middleware/loggermw"
apiregistry "github.com/grafana/grafana/pkg/registry/apis"
provisioningauth "github.com/grafana/grafana/pkg/registry/apis/provisioning/auth"
appregistry "github.com/grafana/grafana/pkg/registry/apps"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
@@ -228,6 +229,7 @@ var wireBasicSet = wire.NewSet(
authinfoimpl.ProvideService,
wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)),
authinfoimpl.ProvideStore,
provisioningauth.ProvideProvisioningIdentityService,
datasourceproxy.ProvideService,
search.ProvideService,
searchV2.ProvideService,
+18
View File
@@ -95,6 +95,9 @@ type DirectRestConfigProvider interface {
// creating clients that map legacy API handlers to k8s backed services
GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config
// Get a rest client with the given identity
GetRestConfigForBackgroundWorker(requester identity.Requester) *clientrest.Config
// This can be used to rewrite incoming requests to path now supported under /apis
DirectlyServeHTTP(w http.ResponseWriter, r *http.Request)
}
@@ -514,6 +517,21 @@ func (s *service) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Co
}
}
func (s *service) GetRestConfigForBackgroundWorker(requester identity.Requester) *clientrest.Config {
return &clientrest.Config{
Transport: &roundTripperFunc{
fn: func(req *http.Request) (*http.Response, error) {
if err := s.NamedService.AwaitRunning(req.Context()); err != nil {
return nil, err
}
ctx := identity.WithRequester(context.Background(), requester)
wrapped := grafanaresponsewriter.WrapHandler(s.handler)
return wrapped(req.WithContext(ctx))
},
},
}
}
func (s *service) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := s.NamedService.AwaitRunning(r.Context()); err != nil {
return