grafana-iam: Skeleton of the resource permission api backend (#110218)

* Extract from #108753

Co-Authored-By: mohammad-hamid <mohammad.hamid@grafana.com>

* Tackle create

Co-Authored-By: mohammad-hamid <mohammad.hamid@grafana.com>

* WIP use identity store to resolve role names

* Commit empty service for now

* Clean

* For now only show name and created at

---------

Co-authored-by: mohammad-hamid <mohammad.hamid@grafana.com>
This commit is contained in:
Gabriel MABILLE
2025-08-27 15:00:09 +02:00
committed by GitHub
co-authored by mohammad-hamid
parent a0075ffdd6
commit b6226c6173
8 changed files with 194 additions and 17 deletions
@@ -72,6 +72,30 @@ var RoleInfo = utils.NewResourceInfo(GROUP, VERSION,
},
)
var ResourcePermissionInfo = utils.NewResourceInfo(GROUP, VERSION,
"resourcepermissions", "resourcepermission", "ResourcePermission",
func() runtime.Object { return &ResourcePermission{} },
func() runtime.Object { return &ResourcePermissionList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
perm, ok := obj.(*ResourcePermission)
if ok {
if perm != nil {
return []interface{}{
perm.Name,
perm.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
}
return nil, fmt.Errorf("expected resource permission")
},
},
)
var (
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
@@ -90,6 +114,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&CoreRoleList{},
&Role{},
&RoleList{},
&ResourcePermission{},
&ResourcePermissionList{},
// What is this about?
&metav1.PartialObjectMetadata{},
+1
View File
@@ -33,6 +33,7 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth
authorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = authorizer
resourceAuthorizer[iamv0.RoleInfo.GetName()] = authorizer
resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = authorizer
return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer}
}
+6 -4
View File
@@ -27,9 +27,10 @@ type RoleStorageBackend interface{ resource.StorageBackend }
// This is used just so wire has something unique to return
type IdentityAccessManagementAPIBuilder struct {
// Stores
store legacy.LegacyIdentityStore
coreRolesStorage CoreRoleStorageBackend
rolesStorage RoleStorageBackend
store legacy.LegacyIdentityStore
coreRolesStorage CoreRoleStorageBackend
rolesStorage RoleStorageBackend
resourcePermissionsStorage resource.StorageBackend
// Access Control
authorizer authorizer.Authorizer
@@ -47,7 +48,8 @@ type IdentityAccessManagementAPIBuilder struct {
sso ssosettings.Service
// Toggle for enabling authz management apis
enableAuthZApis bool
enableAuthZApis bool
enableResourcePermissionApis bool
// Toggle for enabling authn mutation
enableAuthnMutation bool
+25 -13
View File
@@ -29,6 +29,7 @@ import (
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db"
"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"
"github.com/grafana/grafana/pkg/registry/apis/iam/sso"
"github.com/grafana/grafana/pkg/registry/apis/iam/team"
@@ -53,23 +54,26 @@ func RegisterAPIService(
coreRolesStorage CoreRoleStorageBackend,
rolesStorage RoleStorageBackend,
) (*IdentityAccessManagementAPIBuilder, error) {
store := legacy.NewLegacySQLStores(legacysql.NewDatabaseProvider(sql))
dbProvider := legacysql.NewDatabaseProvider(sql)
store := legacy.NewLegacySQLStores(dbProvider)
legacyAccessClient := newLegacyAccessClient(ac, store)
authorizer := newIAMAuthorizer(accessClient, legacyAccessClient)
builder := &IdentityAccessManagementAPIBuilder{
store: store,
coreRolesStorage: coreRolesStorage,
rolesStorage: rolesStorage,
sso: ssoService,
authorizer: authorizer,
legacyAccessClient: legacyAccessClient,
accessClient: accessClient,
display: user.NewLegacyDisplayREST(store),
reg: reg,
enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis),
enableAuthnMutation: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation),
enableDualWriter: true,
store: store,
coreRolesStorage: coreRolesStorage,
rolesStorage: rolesStorage,
resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider),
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,
}
apiregistration.RegisterAPI(builder)
@@ -173,6 +177,14 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
storage[iamv0.RoleInfo.StoragePath()] = roleStore
}
if b.enableResourcePermissionApis {
resourcePermissionStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage)
if err != nil {
return err
}
storage[iamv0.ResourcePermissionInfo.StoragePath()] = resourcePermissionStore
}
apiGroupInfo.VersionedResourcesStorageMap[legacyiamv0.VERSION] = storage
return nil
}
@@ -0,0 +1,11 @@
package resourcepermission
import (
"errors"
"fmt"
)
var (
errNotImplemented = errors.New("not supported by this storage backend")
errEmptyName = fmt.Errorf("name cannot be empty")
)
@@ -0,0 +1,11 @@
package resourcepermission
// List
// Get
// Create
// Update
// Delete
@@ -0,0 +1,103 @@
package resourcepermission
import (
"context"
"fmt"
"iter"
"net/http"
"sync"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
var (
_ resource.StorageBackend = &ResourcePermSqlBackend{}
)
type ResourcePermSqlBackend struct {
dbProvider legacysql.LegacyDatabaseProvider
logger log.Logger
subscribers []chan *resource.WrittenEvent
mutex sync.Mutex
}
func ProvideStorageBackend(dbProvider legacysql.LegacyDatabaseProvider) *ResourcePermSqlBackend {
return &ResourcePermSqlBackend{
dbProvider: dbProvider,
logger: log.New("resourceperm_storage_backend"),
subscribers: make([]chan *resource.WrittenEvent, 0),
mutex: sync.Mutex{},
}
}
func (s *ResourcePermSqlBackend) GetResourceStats(ctx context.Context, namespace string, minCount int) ([]resource.ResourceStats, error) {
return []resource.ResourceStats{}, errNotImplemented
}
func (s *ResourcePermSqlBackend) ListHistory(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) {
return 0, errNotImplemented
}
func (s *ResourcePermSqlBackend) ListIterator(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) {
return 0, errNotImplemented
}
func (s *ResourcePermSqlBackend) ListModifiedSince(ctx context.Context, key resource.NamespacedResource, sinceRv int64) (int64, iter.Seq2[*resource.ModifiedResource, error]) {
return 0, func(yield func(*resource.ModifiedResource, error) bool) {
yield(nil, errNotImplemented)
}
}
func (s *ResourcePermSqlBackend) ReadResource(_ context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse {
return &resource.BackendReadResponse{
Key: req.GetKey(),
Error: &resourcepb.ErrorResult{Code: http.StatusForbidden, Message: errNotImplemented.Error()},
}
}
func (s *ResourcePermSqlBackend) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) {
stream := make(chan *resource.WrittenEvent, 10)
return stream, nil
}
func isValidKey(key *resourcepb.ResourceKey, requireName bool) error {
gr := v0alpha1.ResourcePermissionInfo.GroupResource()
if key.Group != gr.Group {
return fmt.Errorf("expecting group (%s != %s)", key.Group, gr.Group)
}
if key.Resource != gr.Resource {
return fmt.Errorf("expecting resource (%s != %s)", key.Resource, gr.Resource)
}
if requireName && key.Name == "" {
return fmt.Errorf("expecting name (uid): %w", errEmptyName)
}
return nil
}
func (s *ResourcePermSqlBackend) WriteEvent(ctx context.Context, event resource.WriteEvent) (rv int64, err error) {
ns, err := types.ParseNamespace(event.Key.Namespace)
if err != nil {
return 0, err
}
if ns.OrgID <= 0 {
return 0, apierrors.NewBadRequest("write requires a valid namespace")
}
if err := isValidKey(event.Key, true); err != nil {
return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid key %q: %v", event.Key, err.Error()))
}
switch event.Type {
default:
return 0, fmt.Errorf("unsupported event type: %v", event.Type)
}
}
@@ -0,0 +1,11 @@
package resourcepermission
// List
// Get
// Create
// Update
// Delete