POC/Provisioning: Add service for client with authorization (#96755)
This commit is contained in:
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user