More improvements

This commit is contained in:
Roberto Jimenez Sanchez
2025-12-16 12:43:44 +01:00
parent fa86386564
commit 30219176e7
2 changed files with 165 additions and 304 deletions
+2 -1
View File
@@ -105,7 +105,8 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime.
return
}
folders := resources.NewFolderManager(readWriter, folderClient, resources.NewEmptyFolderTree())
dualReadWriter := resources.NewDualReadWriter(readWriter, parser, folders, c.access)
authorizer := resources.NewRepositoryAuthorizer(repo.Config(), c.access)
dualReadWriter := resources.NewDualReadWriter(readWriter, parser, folders, authorizer)
query := r.URL.Query()
opts := resources.DualWriteOptions{
Ref: query.Get("ref"),
@@ -7,9 +7,9 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
@@ -19,180 +19,13 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
/*
DualReadWriter Authorization and Operation Behavior
DualReadWriter manages file operations for provisioning repositories, handling both repository writes
and Grafana DB synchronization. It performs resource-level authorization checks using the access checker.
Authorization Context:
- Standard provisioning Authorizer validates repository-level access before DualReadWriter is called
- DualReadWriter performs additional resource-level authorization (e.g., folder permissions for dashboards)
- Authorization checks use the access checker with the requester's identity and folder context
- Folder context comes from the EXISTING resource's folder (if it exists) or the NEW resource's folder
Branch Behavior:
- Configured Branch (empty ref or matches repo default): Updates both repository and Grafana DB
- Other Branches: Only updates repository, Grafana DB remains unchanged
- Some operations (directory move/delete) are restricted on configured branch
Operation Authorization and Behavior Table:
┌───────────────────┬──────────────────┬──────────────────────────────────────────────────────────────────────────┐
│ Operation │ Branch Type │ Authorization & Behavior │
├───────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Read (GET) │ Any Branch │ Auth: VerbGet on resource in its folder │
│ │ │ Folder: From parsed resource metadata │
│ │ │ Behavior: │
│ │ │ 1. Read file from repository │
│ │ │ 2. Parse file │
│ │ │ 3. Run DryRun validation │
│ │ │ 4. Authorize (VerbGet) │
│ │ │ 5. Return parsed resource │
├───────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ CreateResource │ Configured │ Auth: VerbCreate if new resource, VerbUpdate if exists │
│ (POST file) │ Branch │ Folder: Existing resource's folder OR new resource's folder │
│ │ │ Behavior: │
│ │ │ 1. Parse file content │
│ │ │ 2. Run DryRun validation │
│ │ │ 3. Check if resource exists (ensureExisting) │
│ │ │ 4. Authorize (VerbCreate if new, VerbUpdate if exists) │
│ │ │ 5. Create file in repository │
│ │ │ 6. Create folder path in Grafana (if needed) │
│ │ │ 7. Run resource create/update in Grafana DB │
│ ├──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ │ Other Branches │ Auth: Same as configured branch │
│ │ │ Behavior: Same as configured branch BUT skip steps 6-7 │
│ │ │ (Grafana DB not updated) │
├───────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ UpdateResource │ Configured │ Auth: VerbUpdate if exists, VerbCreate if new │
│ (PUT file) │ Branch │ Folder: Existing resource's folder OR new resource's folder │
│ │ │ Behavior: │
│ │ │ 1. Parse file content │
│ │ │ 2. Run DryRun validation │
│ │ │ 3. Check if resource exists (ensureExisting) │
│ │ │ 4. Authorize (VerbUpdate if exists, VerbCreate if new) │
│ │ │ 5. Update file in repository │
│ │ │ 6. Create folder path in Grafana (if needed) │
│ │ │ 7. Run resource create/update in Grafana DB │
│ ├──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ │ Other Branches │ Auth: Same as configured branch │
│ │ │ Behavior: Same as configured branch BUT skip steps 6-7 │
│ │ │ (Grafana DB not updated) │
├───────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Delete (file) │ Configured │ Auth: VerbDelete on resource in its folder │
│ (DELETE file) │ Branch │ Folder: Existing resource's folder (via ensureExisting) │
│ │ │ Behavior: │
│ │ │ 1. Read file from repository │
│ │ │ 2. Parse file │
│ │ │ 3. Check if resource exists (ensureExisting) │
│ │ │ 4. Authorize (VerbDelete) │
│ │ │ 5. Set Action to Delete │
│ │ │ 6. Run DryRun validation (unless skipped) │
│ │ │ 7. Delete file from repository │
│ │ │ 8. Run resource delete in Grafana DB │
│ ├──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ │ Other Branches │ Auth: Same as configured branch │
│ │ │ Behavior: Same as configured branch BUT skip step 8 │
│ │ │ (Grafana DB not updated) │
├───────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ CreateFolder │ Configured │ Auth: VerbCreate on folder │
│ (POST dir/) │ Branch │ Folder: Parent folder context (currently empty) │
│ │ │ Behavior: │
│ │ │ 1. Validate path is directory │
│ │ │ 2. Authorize folder creation (VerbCreate) │
│ │ │ 3. Create folder in repository │
│ │ │ 4. Ensure folder path exists in Grafana │
│ │ │ 5. Return folder info with URLs │
│ ├──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ │ Other Branches │ Auth: Same as configured branch │
│ │ │ Behavior: Same as configured branch BUT skip steps 4-5 │
│ │ │ (Grafana DB not updated) │
├───────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Delete (folder) │ Configured │ NOT ALLOWED - Returns HTTP 405 Method Not Allowed │
│ (DELETE dir/) │ Branch │ Error: "directory delete operations are not available for configured │
│ │ │ branch. Use bulk delete operations via the jobs API instead" │
│ ├──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ │ Other Branches │ Auth: VerbDelete on folder │
│ │ │ Folder: The folder path itself │
│ │ │ Behavior: │
│ │ │ 1. Authorize folder deletion (VerbDelete) │
│ │ │ 2. Delete folder from repository │
│ │ │ 3. Return folder delete response │
│ │ │ Note: Grafana DB not updated (branch operation) │
├───────────────────┼──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ MoveResource │ Configured │ NOT ALLOWED for directories - Returns HTTP 405 Method Not Allowed │
│ (POST with │ Branch │ Error: "directory move operations are not available for configured │
│ originalPath) │ │ branch. Use bulk move operations via the jobs API instead" │
│ │ │ │
│ (directory move) │ │ ALLOWED for files: │
│ │ │ Auth: Two checks required: │
│ │ │ 1. VerbDelete on original resource in its existing folder │
│ │ │ 2. VerbCreate (if new) or VerbUpdate (if exists) on destination │
│ │ │ Folder: Existing resource's folder for both source and destination │
│ │ │ Behavior: │
│ │ │ 1. Read and parse original file │
│ │ │ 2. Check if original resource exists (ensureExisting) │
│ │ │ 3. Authorize delete on original (VerbDelete) │
│ │ │ 4. Parse destination file (original or updated content) │
│ │ │ 5. Run DryRun on destination │
│ │ │ 6. Check if destination resource exists (ensureExisting) │
│ │ │ 7. Authorize destination (VerbCreate if new, VerbUpdate if exists) │
│ │ │ 8. Perform move in repository (or delete+create if content changes) │
│ │ │ 9. Create folder path in Grafana (if needed) │
│ │ │ 10. Delete old resource from Grafana (if name changed) │
│ │ │ 11. Create/update new resource in Grafana DB │
│ ├──────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ │ Other Branches │ Auth (files): Same as configured branch │
│ │ │ Behavior (files): Same BUT skip steps 9-11 (Grafana DB not updated) │
│ │ │ │
│ │ │ Auth (directories): │
│ │ │ 1. VerbDelete on original folder │
│ │ │ 2. VerbCreate on destination folder │
│ │ │ Behavior (directories): │
│ │ │ 1. Authorize delete on original folder (VerbDelete) │
│ │ │ 2. Authorize create on destination folder (VerbCreate) │
│ │ │ 3. Perform move in repository │
│ │ │ 4. Return folder move response │
│ │ │ Note: Grafana DB not updated (branch operation) │
└───────────────────┴──────────────────┴──────────────────────────────────────────────────────────────────────────┘
Authorization Details:
- authorize() checks permissions on resources (dashboards, etc.)
* Uses parsed.Existing.folder if resource exists, otherwise uses parsed.Meta.folder
* Calls access.Check() with resource Group/Resource/Namespace/Name/Verb and folder context
* Returns Forbidden (403) if not allowed
- authorizeFolder() checks permissions on folders
* Used for folder create/delete operations
* Calls access.Check() with Folder resource and verb
* For create operations, uses empty name (checking parent folder permissions)
* Returns Forbidden (403) if not allowed
Key Concepts:
- Configured Branch: The default branch set in repository config (empty ref or explicit match)
- Other Branches: Any non-default branch specified via "ref" query parameter
- ensureExisting: Populates parsed.Existing by querying Grafana DB for resource by name
- DryRun: Validates resource before applying (checks schema, required fields, etc.)
- Grafana DB Update: Only happens on configured branch; ensures repository and DB stay in sync
- Provisioning Identity: All write operations use provisioning service identity for Grafana DB changes
- Folder Context: Authorization checks include folder path to validate granular permissions
- Move Operations: Require authorization at both source (delete) and destination (create/update)
- Bulk Operations: Directory moves/deletes on configured branch must use Jobs API for safety
Restrictions:
- Directory delete on configured branch: Not allowed (use Jobs API)
- Directory move on configured branch: Not allowed (use Jobs API)
- PUT on directory: Returns Method Not Supported (405)
- Operations on unhealthy repositories: Write operations require healthy repository
*/
// DualReadWriter is a wrapper around a repository that can read from and write resources
// into both the Git repository as well as in Grafana. It isn't a dual writer in the sense of what unistore handling calls dual writing.
type DualReadWriter struct {
repo repository.ReaderWriter
parser Parser
folders *FolderManager
access authlib.AccessChecker
repo repository.ReaderWriter
parser Parser
folders *FolderManager
authorizer Authorizer
}
type DualWriteOptions struct {
@@ -208,8 +41,8 @@ type DualWriteOptions struct {
Branch string // Configured default branch
}
func NewDualReadWriter(repo repository.ReaderWriter, parser Parser, folders *FolderManager, access authlib.AccessChecker) *DualReadWriter {
return &DualReadWriter{repo: repo, parser: parser, folders: folders, access: access}
func NewDualReadWriter(repo repository.ReaderWriter, parser Parser, folders *FolderManager, authorizer Authorizer) *DualReadWriter {
return &DualReadWriter{repo: repo, parser: parser, folders: folders, authorizer: authorizer}
}
func (r *DualReadWriter) Read(ctx context.Context, path string, ref string) (*ParsedResource, error) {
@@ -237,8 +70,7 @@ func (r *DualReadWriter) Read(ctx context.Context, path string, ref string) (*Pa
return nil, fmt.Errorf("error running dryRun: %w", err)
}
// Authorize based on the existing resource
if err = r.authorize(ctx, parsed, utils.VerbGet); err != nil {
if err = r.authorizer.AuthorizeResource(ctx, parsed, utils.VerbGet); err != nil {
return nil, err
}
@@ -246,7 +78,7 @@ func (r *DualReadWriter) Read(ctx context.Context, path string, ref string) (*Pa
}
func (r *DualReadWriter) Delete(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
if err := repository.IsWriteAllowed(r.repo.Config(), opts.Ref); err != nil {
if err := r.authorizer.AuthorizeWrite(ctx, opts.Ref); err != nil {
return nil, err
}
@@ -272,15 +104,7 @@ func (r *DualReadWriter) Delete(ctx context.Context, opts DualWriteOptions) (*Pa
return nil, fmt.Errorf("parse file: %w", err)
}
// Populate the existing resource to ensure we check permissions in the correct folder
if err = r.ensureExisting(ctx, parsed); err != nil {
return nil, err
}
// Authorize after simple checks but before performing the operation
// This checks against the existing resource's folder (if it exists) to ensure
// we validate permissions where the resource actually lives
if err = r.authorize(ctx, parsed, utils.VerbDelete); err != nil {
if err = r.authorizer.AuthorizeResource(ctx, parsed, utils.VerbDelete); err != nil {
return nil, err
}
@@ -312,7 +136,7 @@ func (r *DualReadWriter) Delete(ctx context.Context, opts DualWriteOptions) (*Pa
// CreateFolder creates a new folder in the repository
// FIXME: fix signature to return ParsedResource
func (r *DualReadWriter) CreateFolder(ctx context.Context, opts DualWriteOptions) (*provisioning.ResourceWrapper, error) {
if err := repository.IsWriteAllowed(r.repo.Config(), opts.Ref); err != nil {
if err := r.authorizer.AuthorizeWrite(ctx, opts.Ref); err != nil {
return nil, err
}
@@ -320,9 +144,12 @@ func (r *DualReadWriter) CreateFolder(ctx context.Context, opts DualWriteOptions
return nil, fmt.Errorf("not a folder path")
}
if err := r.authorizeFolder(ctx, opts.Path, utils.VerbCreate); err != nil {
// For create operations, use empty name to check parent folder permissions
folderParsed := folderParsedResource(opts.Path, opts.Ref, r.repo.Config(), "")
if err := r.authorizer.AuthorizeResource(ctx, folderParsed, utils.VerbCreate); err != nil {
return nil, err
}
// TODO: authorized to create folders under first existing ancestor folder
// Now actually create the folder
if err := r.repo.Create(ctx, opts.Path, opts.Ref, nil, opts.Message); err != nil {
@@ -370,17 +197,90 @@ func (r *DualReadWriter) CreateFolder(ctx context.Context, opts DualWriteOptions
// CreateResource creates a new resource in the repository
func (r *DualReadWriter) CreateResource(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
return r.createOrUpdate(ctx, true, opts)
if err := r.authorizer.AuthorizeWrite(ctx, opts.Ref); err != nil {
return nil, err
}
info := &repository.FileInfo{
Data: opts.Data,
Path: opts.Path,
Ref: opts.Ref,
}
parsed, err := r.parser.Parse(ctx, info)
if err != nil {
return nil, err
}
// TODO: check if the resource does not exist in the database.
// Make sure the value is valid
if !opts.SkipDryRun {
if err := parsed.DryRun(ctx); err != nil {
logger := logging.FromContext(ctx).With("path", opts.Path, "name", parsed.Obj.GetName(), "ref", opts.Ref)
logger.Warn("failed to dry run resource on create", "error", err)
return nil, fmt.Errorf("error running dryRun: %w", err)
}
}
if len(parsed.Errors) > 0 {
// Now returns BadRequest (400) for validation errors
return nil, fmt.Errorf("errors while parsing file [%v]", parsed.Errors)
}
// TODO: is this the right way?
// Check if resource already exists - create should fail if it does
if err = r.ensureExisting(ctx, parsed); err != nil {
return nil, err
}
if parsed.Existing != nil {
return nil, apierrors.NewConflict(parsed.GVR.GroupResource(), parsed.Obj.GetName(),
fmt.Errorf("resource already exists"))
}
// Authorization check: Check if we can create the resource in the folder from the file
if err = r.authorizer.AuthorizeResource(ctx, parsed, utils.VerbCreate); err != nil {
return nil, err
}
// TODO: authorized to create folders under first existing ancestor folder
data, err := parsed.ToSaveBytes()
if err != nil {
return nil, err
}
// Always use the provisioning identity when writing
ctx, _, err = identity.WithProvisioningIdentity(ctx, parsed.Obj.GetNamespace())
if err != nil {
return nil, fmt.Errorf("unable to use provisioning identity %w", err)
}
// TODO: handle the error repository.ErrFileAlreadyExists
err = r.repo.Create(ctx, opts.Path, opts.Ref, data, opts.Message)
if err != nil {
return nil, err // raw error is useful
}
// Directly update the grafana database
// Behaves the same running sync after writing
// FIXME: to make sure if behaves in the same way as in sync, we should
// we should refactor the code to use the same function.
if r.shouldUpdateGrafanaDB(opts, parsed) {
if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil {
return nil, fmt.Errorf("ensure folder path exists: %w", err)
}
err = parsed.Run(ctx)
}
return parsed, err
}
// UpdateResource updates a resource in the repository
func (r *DualReadWriter) UpdateResource(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
return r.createOrUpdate(ctx, false, opts)
}
// Create or updates a resource in the repository
func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts DualWriteOptions) (*ParsedResource, error) {
if err := repository.IsWriteAllowed(r.repo.Config(), opts.Ref); err != nil {
if err := r.authorizer.AuthorizeWrite(ctx, opts.Ref); err != nil {
return nil, err
}
@@ -399,7 +299,7 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D
if !opts.SkipDryRun {
if err := parsed.DryRun(ctx); err != nil {
logger := logging.FromContext(ctx).With("path", opts.Path, "name", parsed.Obj.GetName(), "ref", opts.Ref)
logger.Warn("failed to dry run resource on create", "error", err)
logger.Warn("failed to dry run resource on update", "error", err)
return nil, fmt.Errorf("error running dryRun: %w", err)
}
@@ -410,20 +310,15 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D
return nil, fmt.Errorf("errors while parsing file [%v]", parsed.Errors)
}
// Populate existing resource if it exists
// Populate existing resource to check permissions in the correct folder
if err = r.ensureExisting(ctx, parsed); err != nil {
return nil, err
}
// Authorization check:
// - If resource exists (parsed.Existing != nil): Check if we can update it in its current folder
// - If resource doesn't exist: Check if we can create it in the folder from the file
// This prevents unauthorized overwrites of existing resources and ensures proper folder permissions
verb := utils.VerbUpdate
if parsed.Existing == nil && parsed.Action == provisioning.ResourceActionCreate {
verb = utils.VerbCreate
}
if err = r.authorize(ctx, parsed, verb); err != nil {
// TODO: what to do with a name or kind change?
// Authorization check: Check if we can update the existing resource in its current folder
if err = r.authorizer.AuthorizeResource(ctx, parsed, utils.VerbUpdate); err != nil {
return nil, err
}
@@ -438,12 +333,7 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D
return nil, fmt.Errorf("unable to use provisioning identity %w", err)
}
// Create or update
if create {
err = r.repo.Create(ctx, opts.Path, opts.Ref, data, opts.Message)
} else {
err = r.repo.Update(ctx, opts.Path, opts.Ref, data, opts.Message)
}
err = r.repo.Update(ctx, opts.Path, opts.Ref, data, opts.Message)
if err != nil {
return nil, err // raw error is useful
}
@@ -465,7 +355,7 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D
// MoveResource moves a resource from one path to another in the repository
func (r *DualReadWriter) MoveResource(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
if err := repository.IsWriteAllowed(r.repo.Config(), opts.Ref); err != nil {
if err := r.authorizer.AuthorizeWrite(ctx, opts.Ref); err != nil {
return nil, err
}
@@ -505,12 +395,15 @@ func (r *DualReadWriter) moveDirectory(ctx context.Context, opts DualWriteOption
}
// Check permissions to delete the original folder
if err := r.authorizeFolder(ctx, opts.OriginalPath, utils.VerbDelete); err != nil {
originalFolderID := ParseFolder(opts.OriginalPath, r.repo.Config().Name).ID
originalFolderParsed := folderParsedResource(opts.OriginalPath, opts.Ref, r.repo.Config(), originalFolderID)
if err := r.authorizer.AuthorizeResource(ctx, originalFolderParsed, utils.VerbDelete); err != nil {
return nil, fmt.Errorf("not authorized to move from original folder: %w", err)
}
// Check permissions to create at the new folder location
if err := r.authorizeFolder(ctx, opts.Path, utils.VerbCreate); err != nil {
// Check permissions to create at the new folder location (empty name for create)
newFolderParsed := folderParsedResource(opts.Path, opts.Ref, r.repo.Config(), "")
if err := r.authorizer.AuthorizeResource(ctx, newFolderParsed, utils.VerbCreate); err != nil {
return nil, fmt.Errorf("not authorized to move to new folder: %w", err)
}
@@ -570,7 +463,7 @@ func (r *DualReadWriter) moveFile(ctx context.Context, opts DualWriteOptions) (*
}
// Authorize delete on the original path (checks existing resource's folder if it exists)
if err = r.authorize(ctx, parsed, utils.VerbDelete); err != nil {
if err = r.authorizer.AuthorizeResource(ctx, parsed, utils.VerbDelete); err != nil {
return nil, fmt.Errorf("not authorized to delete original file: %w", err)
}
@@ -617,10 +510,10 @@ func (r *DualReadWriter) moveFile(ctx context.Context, opts DualWriteOptions) (*
// - If resource exists at destination: Check if we can update it in its folder
// - If no resource at destination: Check if we can create in the new folder
verb := utils.VerbUpdate
if newParsed.Existing == nil && newParsed.Action == provisioning.ResourceActionCreate {
if newParsed.Existing == nil {
verb = utils.VerbCreate
}
if err = r.authorize(ctx, newParsed, verb); err != nil {
if err = r.authorizer.AuthorizeResource(ctx, newParsed, verb); err != nil {
return nil, fmt.Errorf("not authorized for destination: %w", err)
}
@@ -679,53 +572,6 @@ func (r *DualReadWriter) moveFile(ctx context.Context, opts DualWriteOptions) (*
return newParsed, nil
}
// authorize checks if the requester has permission to perform the specified verb on the resource.
// The provisioning service operates with admin privileges and validates based on resource-level permissions.
//
// IMPORTANT: If parsed.Existing is set, this checks permissions against the EXISTING resource's folder.
// Otherwise, it checks against the NEW resource's folder (from parsed.Meta).
// This ensures we validate permissions in the correct folder where the resource actually exists or will be created.
func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, verb string) error {
id, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized(err.Error())
}
// Determine which resource and folder to use for authorization
var resourceName string
var folder string
if parsed.Existing != nil {
// Check against existing resource in its current folder
resourceName = parsed.Existing.GetName()
if meta, err := utils.MetaAccessor(parsed.Existing); err == nil {
folder = meta.GetFolder()
} else {
// Fallback to file metadata if we can't extract from existing
folder = parsed.Meta.GetFolder()
}
} else {
// Check against new resource in the folder from the file
resourceName = parsed.Obj.GetName()
folder = parsed.Meta.GetFolder()
}
// Check resource-level permissions via access checker
rsp, err := r.access.Check(ctx, id, authlib.CheckRequest{
Group: parsed.GVR.Group,
Resource: parsed.GVR.Resource,
Namespace: id.GetNamespace(),
Name: resourceName,
Verb: verb,
}, folder)
if err != nil || !rsp.Allowed {
return apierrors.NewForbidden(parsed.GVR.GroupResource(), resourceName,
fmt.Errorf("no access to perform %s on the resource in folder '%s'", verb, folder))
}
return nil
}
// ensureExisting populates parsed.Existing if a resource with the given name exists in storage.
// Returns nil if no resource exists, if Client is nil, or if Existing is already populated.
// This is used before authorization checks to ensure we validate permissions against the actual
@@ -747,42 +593,6 @@ func (r *DualReadWriter) ensureExisting(ctx context.Context, parsed *ParsedResou
return nil
}
// authorizeFolder validates that the requester has permission to perform the specified verb on a folder.
// This is used for folder operations (create, delete, update) where we don't have a ParsedResource.
func (r *DualReadWriter) authorizeFolder(ctx context.Context, path string, verb string) error {
id, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized(err.Error())
}
// For folder operations, we need to determine the parent folder for permission checks
parentFolder := ""
// TODO: Extract parent folder from path if needed for hierarchical permission checks
// For create operations, use empty name (checking if we can create in the parent)
// For other operations, the folder path is the resource name
name := path
if verb == utils.VerbCreate {
name = "" // Empty name for creation checks
}
// Check folder permissions via access checker
rsp, err := r.access.Check(ctx, id, authlib.CheckRequest{
Group: FolderResource.Group,
Resource: FolderResource.Resource,
Namespace: id.GetNamespace(),
Name: name,
Verb: verb,
}, parentFolder)
if err != nil || !rsp.Allowed {
return apierrors.NewForbidden(FolderResource.GroupResource(), path,
fmt.Errorf("no access to perform %s on folder", verb))
}
return nil
}
func (r *DualReadWriter) deleteFolder(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
// Reject directory delete operations for configured branch - use bulk operations instead
if r.isConfiguredBranch(opts) {
@@ -797,7 +607,9 @@ func (r *DualReadWriter) deleteFolder(ctx context.Context, opts DualWriteOptions
}
// Check permissions to delete the folder
if err := r.authorizeFolder(ctx, opts.Path, utils.VerbDelete); err != nil {
folderID := ParseFolder(opts.Path, r.repo.Config().Name).ID
folderParsed := folderParsedResource(opts.Path, opts.Ref, r.repo.Config(), folderID)
if err := r.authorizer.AuthorizeResource(ctx, folderParsed, utils.VerbDelete); err != nil {
return nil, err
}
@@ -829,6 +641,54 @@ func getPathType(isDir bool) string {
return "file (no trailing '/')"
}
// folderParsedResource creates a ParsedResource for a folder path.
// This is used for authorization checks on folder operations.
// For create operations, name should be empty string to check parent permissions.
// For other operations, name should be the folder ID derived from the path.
func folderParsedResource(path, ref string, repo *provisioning.Repository, name string) *ParsedResource {
folderObj := &unstructured.Unstructured{}
folderObj.SetName(name)
folderObj.SetNamespace(repo.Namespace)
// TODO: which parent? top existing ancestor.
meta, _ := utils.MetaAccessor(folderObj)
if meta != nil {
// Set parent folder for folder operations
parentFolder := ""
if path != "" {
parentPath := safepath.Dir(path)
if parentPath != "" {
parentFolder = ParseFolder(parentPath, repo.Name).ID
} else {
parentFolder = RootFolder(repo)
}
}
meta.SetFolder(parentFolder)
}
return &ParsedResource{
Info: &repository.FileInfo{
Path: path,
Ref: ref,
},
Obj: folderObj,
Meta: meta,
GVK: schema.GroupVersionKind{
Group: FolderResource.Group,
Version: FolderResource.Version,
Kind: "Folder",
},
GVR: FolderResource,
Repo: provisioning.ResourceRepositoryInfo{
Type: repo.Spec.Type,
Namespace: repo.Namespace,
Name: repo.Name,
Title: repo.Spec.Title,
},
}
}
func folderDeleteResponse(ctx context.Context, path, ref string, repo repository.Repository) (*ParsedResource, error) {
urls, err := getFolderURLs(ctx, path, ref, repo)
if err != nil {