grafana-iam: Populate Zanzana on resource permission creation (#111654)
* `grafana-iam`: Populate Zanzana on resource permission creation * use zanzana const * Adding a toggle * Add a new feature toggle to manage zanzana sync * wire * wire * WIP * Fix hook issue * comments and tests * Account for PR feedback * Add a timeout to writes * Check tuples len * comment * validate basic role * shorter error * object reads better than entry
This commit is contained in:
@@ -1030,6 +1030,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
kubernetesAuthzResourcePermissionApis?: boolean;
|
||||
/**
|
||||
* Enable sync of Zanzana authorization store on AuthZ CRD mutations
|
||||
*/
|
||||
kubernetesAuthzZanzanaSync?: boolean;
|
||||
/**
|
||||
* Enables create, delete, and update mutations for resources owned by IAM identity
|
||||
*/
|
||||
kubernetesAuthnMutation?: boolean;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
)
|
||||
|
||||
var (
|
||||
errEmptyName = errors.New("name cannot be empty")
|
||||
errInvalidBasicRole = errors.New("invalid basic role")
|
||||
errUnknownKind = errors.New("unknown permission kind")
|
||||
|
||||
defaultWriteTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
func toZanzanaSubject(kind iamv0.ResourcePermissionSpecPermissionKind, name string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errEmptyName
|
||||
}
|
||||
switch kind {
|
||||
case iamv0.ResourcePermissionSpecPermissionKindUser:
|
||||
return zanzana.NewTupleEntry(zanzana.TypeUser, name, ""), nil
|
||||
case iamv0.ResourcePermissionSpecPermissionKindServiceAccount:
|
||||
return zanzana.NewTupleEntry(zanzana.TypeServiceAccount, name, ""), nil
|
||||
case iamv0.ResourcePermissionSpecPermissionKindTeam:
|
||||
return zanzana.NewTupleEntry(zanzana.TypeTeam, name, ""), nil
|
||||
case iamv0.ResourcePermissionSpecPermissionKindBasicRole:
|
||||
basicRole := zanzana.TranslateBasicRole(name)
|
||||
if basicRole == "" {
|
||||
return "", fmt.Errorf("%w: %s", errInvalidBasicRole, name)
|
||||
}
|
||||
|
||||
// e.g role:basic_viewer#assignee
|
||||
return zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, zanzana.RelationAssignee), nil
|
||||
}
|
||||
|
||||
// should not happen since we are after create
|
||||
// validation webhook should have caught invalid kinds
|
||||
return "", errUnknownKind
|
||||
}
|
||||
|
||||
func toZanzanaType(apiGroup string) string {
|
||||
if apiGroup == "folder.grafana.app" {
|
||||
return zanzana.TypeFolder
|
||||
}
|
||||
return zanzana.TypeResource
|
||||
}
|
||||
|
||||
func NewResourceTuple(object string, resource iamv0.ResourcePermissionspecResource, perm iamv0.ResourcePermissionspecPermission) (*v1.TupleKey, error) {
|
||||
// Typ is "folder" or "resource"
|
||||
typ := toZanzanaType(resource.ApiGroup)
|
||||
|
||||
// subject
|
||||
subject, err := toZanzanaSubject(perm.Kind, perm.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &v1.TupleKey{
|
||||
// e.g. "user:{uid}", "serviceaccount:{uid}", "team:{uid}", "basicrole:{viewer|editor|admin}"
|
||||
User: subject,
|
||||
// "view", "edit", "admin"
|
||||
Relation: strings.ToLower(perm.Verb),
|
||||
// e.g. "folder:{name}" or "resource:{apiGroup}/{resource}/{name}"
|
||||
Object: object,
|
||||
}
|
||||
|
||||
// For resources we add a condition to filter by apiGroup/resource
|
||||
// e.g "group_filter": {"group_resource": "dashboards.grafana.app/dashboards"}
|
||||
if typ == zanzana.TypeResource {
|
||||
key.Condition = &v1.RelationshipCondition{
|
||||
Name: "group_filter",
|
||||
Context: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"group_resource": structpb.NewStringValue(
|
||||
resource.ApiGroup + "/" + resource.Resource,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// AfterResourcePermissionCreate is a post-create hook that writes the resource permission to Zanzana (openFGA)
|
||||
func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj runtime.Object, _ *metav1.CreateOptions) {
|
||||
if b.zClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rp, ok := obj.(*iamv0.ResourcePermission)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
resource := rp.Spec.Resource
|
||||
permissions := rp.Spec.Permissions
|
||||
|
||||
object := zanzana.NewObjectEntry(toZanzanaType(resource.ApiGroup), resource.ApiGroup, resource.Resource, "", resource.Name)
|
||||
|
||||
tuples := make([]*v1.TupleKey, 0, len(permissions))
|
||||
for _, p := range permissions {
|
||||
tuple, err := NewResourceTuple(object, resource, p)
|
||||
if err != nil {
|
||||
b.logger.Error("failed to create resource permission tuple",
|
||||
"namespace", rp.Namespace,
|
||||
"object", object,
|
||||
"err", err,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
tuples = append(tuples, tuple)
|
||||
}
|
||||
|
||||
// Avoid writing if there are no valid tuples
|
||||
if len(tuples) == 0 {
|
||||
b.logger.Warn("no valid tuples to write", "namespace", rp.Namespace, "resource", object)
|
||||
return
|
||||
}
|
||||
|
||||
b.logger.Debug("writing resource permission to zanzana",
|
||||
"namespace", rp.Namespace,
|
||||
"object", object,
|
||||
"tuplesCnt", len(tuples),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
|
||||
defer cancel()
|
||||
|
||||
err := b.zClient.Write(ctx, &v1.WriteRequest{
|
||||
Namespace: rp.Namespace,
|
||||
Writes: &v1.WriteRequestWrites{
|
||||
TupleKeys: tuples,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
b.logger.Error("failed to write resource permission to zanzana",
|
||||
"err", err,
|
||||
"namespace", rp.Namespace,
|
||||
"object", object,
|
||||
"tuplesCnt", len(tuples),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type FakeZanzanaClient struct {
|
||||
zanzana.Client
|
||||
writeCallback func(context.Context, *v1.WriteRequest) error
|
||||
}
|
||||
|
||||
// Write implements zanzana.Client.
|
||||
func (f *FakeZanzanaClient) Write(ctx context.Context, req *v1.WriteRequest) error {
|
||||
return f.writeCallback(ctx, req)
|
||||
}
|
||||
|
||||
func TestAfterResourcePermissionCreate(t *testing.T) {
|
||||
b := &IdentityAccessManagementAPIBuilder{
|
||||
logger: log.NewNopLogger(),
|
||||
}
|
||||
t.Run("should create zanzana entries for folder resource permissions", func(t *testing.T) {
|
||||
folderPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-2",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"},
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "Edit"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testFolderEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, req.Writes)
|
||||
require.Len(t, req.Writes.TupleKeys, 2)
|
||||
require.Equal(t, req.Namespace, "org-2")
|
||||
require.Equal(
|
||||
t,
|
||||
req.Writes.TupleKeys[0],
|
||||
&v1.TupleKey{User: "user:u1", Relation: "view", Object: "folder:fold1"},
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Writes.TupleKeys[1],
|
||||
&v1.TupleKey{User: "role:basic_editor#assignee", Relation: "edit", Object: "folder:fold1"},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testFolderEntries}
|
||||
b.AfterResourcePermissionCreate(&folderPerm, nil)
|
||||
})
|
||||
|
||||
t.Run("should create zanzana entries for dashboard resource permissions", func(t *testing.T) {
|
||||
dashPerm := iamv0.ResourcePermission{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: iamv0.ResourcePermissionSpec{
|
||||
Resource: iamv0.ResourcePermissionspecResource{
|
||||
ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1",
|
||||
},
|
||||
Permissions: []iamv0.ResourcePermissionspecPermission{
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "View"},
|
||||
{Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "Edit"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testDashEntries := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
object := "resource:dashboard.grafana.app/dashboards/dash1"
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, req.Writes)
|
||||
require.Len(t, req.Writes.TupleKeys, 2)
|
||||
require.Equal(t, req.Namespace, "default")
|
||||
|
||||
tuple1 := req.Writes.TupleKeys[0]
|
||||
require.NotNil(t, tuple1.Condition)
|
||||
require.Equal(t, "group_filter", tuple1.Condition.Name)
|
||||
tuple1.Condition = nil
|
||||
require.Equal(
|
||||
t,
|
||||
tuple1,
|
||||
&v1.TupleKey{User: "service-account:sa1", Relation: "view", Object: object},
|
||||
)
|
||||
|
||||
tuple2 := req.Writes.TupleKeys[1]
|
||||
require.NotNil(t, tuple2.Condition)
|
||||
require.Equal(t, "group_filter", tuple2.Condition.Name)
|
||||
tuple2.Condition = nil
|
||||
require.Equal(
|
||||
t,
|
||||
tuple2,
|
||||
&v1.TupleKey{User: "team:team1", Relation: "edit", Object: object},
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testDashEntries}
|
||||
b.AfterResourcePermissionCreate(&dashPerm, nil)
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
@@ -44,8 +48,14 @@ type IdentityAccessManagementAPIBuilder struct {
|
||||
legacyAccessClient types.AccessClient
|
||||
// accessClient is used for the core role apis
|
||||
accessClient types.AccessClient
|
||||
// zClient is used to populate Zanzana with:
|
||||
// - roles
|
||||
// - permissions
|
||||
// - assignments
|
||||
zClient zanzana.Client
|
||||
|
||||
reg prometheus.Registerer
|
||||
reg prometheus.Registerer
|
||||
logger log.Logger
|
||||
|
||||
// non-k8s api route
|
||||
display *user.LegacyDisplayREST
|
||||
@@ -54,11 +64,7 @@ type IdentityAccessManagementAPIBuilder struct {
|
||||
sso ssosettings.Service
|
||||
|
||||
// Toggle for enabling authz management apis
|
||||
enableAuthZApis bool
|
||||
enableResourcePermissionApis bool
|
||||
|
||||
// Toggle for enabling authn mutation
|
||||
enableAuthnMutation bool
|
||||
features featuremgmt.FeatureToggles
|
||||
|
||||
// Toggle for enabling dual writer
|
||||
enableDualWriter bool
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
@@ -26,8 +27,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
@@ -51,6 +53,7 @@ func RegisterAPIService(
|
||||
sql db.DB,
|
||||
ac accesscontrol.AccessControl,
|
||||
accessClient types.AccessClient,
|
||||
zClient zanzana.Client,
|
||||
reg prometheus.Registerer,
|
||||
coreRolesStorage CoreRoleStorageBackend,
|
||||
rolesStorage RoleStorageBackend,
|
||||
@@ -62,40 +65,45 @@ func RegisterAPIService(
|
||||
authorizer := newIAMAuthorizer(accessClient, legacyAccessClient)
|
||||
|
||||
builder := &IdentityAccessManagementAPIBuilder{
|
||||
store: store,
|
||||
coreRolesStorage: coreRolesStorage,
|
||||
rolesStorage: rolesStorage,
|
||||
resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider),
|
||||
roleBindingsStorage: roleBindingsStorage,
|
||||
sso: ssoService,
|
||||
authorizer: authorizer,
|
||||
legacyAccessClient: legacyAccessClient,
|
||||
accessClient: accessClient,
|
||||
display: user.NewLegacyDisplayREST(store),
|
||||
reg: reg,
|
||||
enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis),
|
||||
enableResourcePermissionApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis),
|
||||
enableAuthnMutation: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation),
|
||||
enableDualWriter: true,
|
||||
store: store,
|
||||
coreRolesStorage: coreRolesStorage,
|
||||
rolesStorage: rolesStorage,
|
||||
resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider),
|
||||
roleBindingsStorage: roleBindingsStorage,
|
||||
sso: ssoService,
|
||||
authorizer: authorizer,
|
||||
legacyAccessClient: legacyAccessClient,
|
||||
accessClient: accessClient,
|
||||
zClient: zClient,
|
||||
display: user.NewLegacyDisplayREST(store),
|
||||
reg: reg,
|
||||
logger: log.New("iam.apis"),
|
||||
features: features,
|
||||
// enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis),
|
||||
// enableResourcePermissionApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis),
|
||||
// enableAuthnMutation: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation),
|
||||
enableDualWriter: true,
|
||||
}
|
||||
apiregistration.RegisterAPI(builder)
|
||||
|
||||
return builder, nil
|
||||
}
|
||||
|
||||
// TODO zClient, reg
|
||||
func NewAPIService(
|
||||
accessClient types.AccessClient,
|
||||
dbProvider legacysql.LegacyDatabaseProvider,
|
||||
enabledApis map[string]bool,
|
||||
features featuremgmt.FeatureToggles,
|
||||
) *IdentityAccessManagementAPIBuilder {
|
||||
store := legacy.NewLegacySQLStores(dbProvider)
|
||||
resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider)
|
||||
resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
|
||||
return &IdentityAccessManagementAPIBuilder{
|
||||
store: store,
|
||||
display: user.NewLegacyDisplayREST(store),
|
||||
resourcePermissionsStorage: resourcePermissionsStorage,
|
||||
enableResourcePermissionApis: enabledApis["resourcepermissions"],
|
||||
store: store,
|
||||
display: user.NewLegacyDisplayREST(store),
|
||||
resourcePermissionsStorage: resourcePermissionsStorage,
|
||||
logger: log.New("iam.apis"),
|
||||
features: features,
|
||||
authorizer: authorizer.AuthorizerFunc(
|
||||
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
// For now only authorize resourcepermissions resource
|
||||
@@ -120,12 +128,12 @@ func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersi
|
||||
}
|
||||
|
||||
func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
if b.enableAuthZApis {
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) {
|
||||
if err := iamv0.AddAuthZKnownTypes(scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if b.enableResourcePermissionApis {
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
|
||||
if err := iamv0.AddResourcePermissionKnownTypes(scheme, iamv0.SchemeGroupVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -152,6 +160,8 @@ func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string
|
||||
|
||||
func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
||||
storage := map[string]rest.Storage{}
|
||||
enableAuthnMutation := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation)
|
||||
enableZanzanaSync := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzZanzanaSync)
|
||||
|
||||
// teams + users must have shorter names because they are often used as part of another name
|
||||
opts.StorageOptsRegister(iamv0.TeamResourceInfo.GroupResource(), apistore.StorageOptions{
|
||||
@@ -162,7 +172,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
})
|
||||
|
||||
teamResource := iamv0.TeamResourceInfo
|
||||
teamLegacyStore := team.NewLegacyStore(b.store, b.legacyAccessClient, b.enableAuthnMutation)
|
||||
teamLegacyStore := team.NewLegacyStore(b.store, b.legacyAccessClient, enableAuthnMutation)
|
||||
storage[teamResource.StoragePath()] = teamLegacyStore
|
||||
storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store)
|
||||
|
||||
@@ -185,7 +195,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
|
||||
// User store registration
|
||||
userResource := iamv0.UserResourceInfo
|
||||
legacyStore := user.NewLegacyStore(b.store, b.accessClient, b.enableAuthnMutation)
|
||||
legacyStore := user.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation)
|
||||
storage[userResource.StoragePath()] = legacyStore
|
||||
|
||||
if b.enableDualWriter {
|
||||
@@ -206,7 +216,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
|
||||
// Service Accounts store registration
|
||||
serviceAccountResource := iamv0.ServiceAccountResourceInfo
|
||||
saLegacyStore := serviceaccount.NewLegacyStore(b.store, b.accessClient, b.enableAuthnMutation)
|
||||
saLegacyStore := serviceaccount.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation)
|
||||
storage[serviceAccountResource.StoragePath()] = saLegacyStore
|
||||
|
||||
if b.enableDualWriter {
|
||||
@@ -230,7 +240,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso)
|
||||
}
|
||||
|
||||
if b.enableAuthZApis {
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) {
|
||||
// v0alpha1
|
||||
coreRoleStore, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage)
|
||||
if err != nil {
|
||||
@@ -251,11 +261,15 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore
|
||||
}
|
||||
|
||||
if b.enableResourcePermissionApis {
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
|
||||
resourcePermissionStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enableZanzanaSync {
|
||||
b.logger.Info("Enabling AfterCreate hook for ResourcePermission to sync to Zanzana")
|
||||
resourcePermissionStore.AfterCreate = b.AfterResourcePermissionCreate
|
||||
}
|
||||
storage[iamv0.ResourcePermissionInfo.StoragePath()] = resourcePermissionStore
|
||||
}
|
||||
|
||||
@@ -404,7 +418,7 @@ func (b *IdentityAccessManagementAPIBuilder) Mutate(ctx context.Context, a admis
|
||||
}
|
||||
|
||||
func NewLocalStore(resourceInfo utils.ResourceInfo, scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter,
|
||||
reg prometheus.Registerer, ac types.AccessClient, storageBackend resource.StorageBackend) (grafanarest.Storage, error) {
|
||||
reg prometheus.Registerer, ac types.AccessClient, storageBackend resource.StorageBackend) (*registry.Store, error) {
|
||||
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
|
||||
Backend: storageBackend,
|
||||
Reg: reg,
|
||||
|
||||
@@ -813,7 +813,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
}
|
||||
folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient)
|
||||
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl)
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1417,7 +1417,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
}
|
||||
folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient)
|
||||
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl)
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -81,6 +81,21 @@ func NewTupleEntry(objectType, name, relation string) string {
|
||||
return obj
|
||||
}
|
||||
|
||||
func NewObjectEntry(objectType, group, resource, subresource, name string) string {
|
||||
if objectType == TypeFolder {
|
||||
return TypeFolder + ":" + name
|
||||
}
|
||||
|
||||
obj := fmt.Sprintf("%s:%s/%s", objectType, group, resource)
|
||||
if subresource != "" {
|
||||
obj = fmt.Sprintf("%s/%s", obj, subresource)
|
||||
}
|
||||
if name != "" {
|
||||
obj = fmt.Sprintf("%s/%s", obj, name)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) {
|
||||
translation, ok := resourceTranslations[kind]
|
||||
|
||||
|
||||
@@ -1781,6 +1781,14 @@ var (
|
||||
HideFromAdminPage: true,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesAuthzZanzanaSync",
|
||||
Description: "Enable sync of Zanzana authorization store on AuthZ CRD mutations",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: identityAccessTeam,
|
||||
HideFromAdminPage: true,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesAuthnMutation",
|
||||
Description: "Enables create, delete, and update mutations for resources owned by IAM identity",
|
||||
|
||||
@@ -231,6 +231,7 @@ alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
|
||||
kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false
|
||||
kubernetesAuthZHandlerRedirect,experimental,@grafana/identity-access-team,false,false,false
|
||||
kubernetesAuthzResourcePermissionApis,experimental,@grafana/identity-access-team,false,false,false
|
||||
kubernetesAuthzZanzanaSync,experimental,@grafana/identity-access-team,false,false,false
|
||||
kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,false
|
||||
restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
skipTokenRotationIfRecent,GA,@grafana/identity-access-team,false,false,false
|
||||
|
||||
|
@@ -934,6 +934,10 @@ const (
|
||||
// Registers AuthZ resource permission /apis endpoints
|
||||
FlagKubernetesAuthzResourcePermissionApis = "kubernetesAuthzResourcePermissionApis"
|
||||
|
||||
// FlagKubernetesAuthzZanzanaSync
|
||||
// Enable sync of Zanzana authorization store on AuthZ CRD mutations
|
||||
FlagKubernetesAuthzZanzanaSync = "kubernetesAuthzZanzanaSync"
|
||||
|
||||
// FlagKubernetesAuthnMutation
|
||||
// Enables create, delete, and update mutations for resources owned by IAM identity
|
||||
FlagKubernetesAuthnMutation = "kubernetesAuthnMutation"
|
||||
|
||||
@@ -2171,6 +2171,23 @@
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesAuthzZanzanaSync",
|
||||
"resourceVersion": "1758887751768",
|
||||
"creationTimestamp": "2025-09-26T09:35:02Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-09-26 11:55:51.768754 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable sync of Zanzana authorization store on AuthZ CRD mutations",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/identity-access-team",
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesCorrelations",
|
||||
@@ -2257,6 +2274,21 @@
|
||||
"requiresRestart": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesZanzanaPopulate",
|
||||
"resourceVersion": "1758879077577",
|
||||
"creationTimestamp": "2025-09-26T09:31:17Z",
|
||||
"deletionTimestamp": "2025-09-26T09:35:02Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Populate Zanzana on AuthZ CRDs creation or update",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/identity-access-team",
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesStars",
|
||||
|
||||
Reference in New Issue
Block a user