Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add policy file generation and check permission function #53

Merged
merged 13 commits into from
Nov 7, 2024

Conversation

akiyatomohiro
Copy link
Contributor

@akiyatomohiro akiyatomohiro commented Oct 30, 2024

  • add check permission function(sends graphql requests to the dashboard server)
  • add policy file generation

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a Client struct for interacting with a GraphQL API for permission checks.
    • Added a PermissionChecker struct for managing permission checks.
    • Implemented resource and action management with ResourceBuilder and related types.
    • Enabled generation of Cerbos policies in YAML format.
  • Bug Fixes

    • Enhanced error handling in permission checks and policy generation.
  • Tests

    • Added comprehensive unit tests for permission checking and resource management functionalities.
    • Improved test coverage for policy generation scenarios.
  • Dependencies

    • Updated to include a direct dependency on gopkg.in/yaml.v2 v2.4.0.

Copy link

coderabbitai bot commented Oct 30, 2024

Walkthrough

The changes introduce several new files and functionalities related to permission checking and resource management in the Cerbos client. A Client struct is added for interacting with a GraphQL API, along with methods for checking permissions. A PermissionChecker struct is also introduced to manage permission checks more effectively. Additionally, resource and policy management functionalities are encapsulated in new files, along with comprehensive unit tests to validate the new features and ensure robustness.

Changes

File Path Change Summary
cerbos/client/check.go Introduced Client struct with methods NewClient and CheckPermission. Added types CheckPermissionInput, CheckPermissionResponse, and GraphQLQuery for permission checks via GraphQL API.
cerbos/client/check_test.go Added unit tests for NewClient and CheckPermission methods, covering various scenarios including success and error handling.
cerbos/client/permission_checker.go Introduced PermissionChecker struct with method CheckPermission for managing permission checks, using the Client for GraphQL interactions.
cerbos/generator/builder.go Added types ResourceDefinition, ActionDefinition, ResourceBuilder, and associated functions for managing resource definitions and actions.
cerbos/generator/builder_test.go Introduced unit tests for ResourceBuilder functionalities, ensuring correct behavior of resource and action definitions.
cerbos/generator/policy.go Added types CerbosPolicy, ResourcePolicy, and Rule. Introduced GeneratePolicies function for generating Cerbos policies in YAML format.
cerbos/generator/policy_test.go Added unit tests for GeneratePolicies function, covering various scenarios including successful and error cases in policy generation.
go.mod Updated to add a direct dependency on gopkg.in/yaml.v2 v2.4.0 and removed the indirect dependency on the same, while retaining gopkg.in/yaml.v3 v3.0.1 as an indirect dependency.

Poem

In the meadow where permissions play,
A rabbit hops on a bright new day.
With checks and balances, all in line,
The paths are clear, the rules divine.
So let us cheer for the changes made,
In our Cerbos world, new joys cascade! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@akiyatomohiro akiyatomohiro changed the title feat: add policy file generation feat: add policy file generation and check permission function Oct 31, 2024
@akiyatomohiro akiyatomohiro marked this pull request as ready for review November 7, 2024 09:04
@akiyatomohiro akiyatomohiro requested a review from pyshx November 7, 2024 09:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Outside diff range and nitpick comments (16)
cerbos/client/permission_checker.go (1)

10-13: Add documentation to the struct and its fields.

The struct and its fields should be documented to explain their purpose, any validation rules, and usage constraints.

+// PermissionChecker handles permission verification against a dashboard service.
 type PermissionChecker struct {
+	// Service identifies the service name for permission checks
 	Service      string
+	// DashboardURL is the endpoint URL for the permission verification service
 	DashboardURL string
 }
cerbos/generator/builder.go (2)

3-11: Add documentation comments for exported types and fields.

The exported types ResourceDefinition and ActionDefinition should have documentation comments explaining their purpose and usage. This helps with API documentation and code clarity.

Apply this diff:

+// ResourceDefinition represents a resource and its associated actions
 type ResourceDefinition struct {
+    // Resource is the fully qualified resource name
     Resource string
+    // Actions is a list of permitted actions on this resource
     Actions  []ActionDefinition
 }

+// ActionDefinition represents an action that can be performed on a resource
+// and the roles permitted to perform it
 type ActionDefinition struct {
+    // Action is the name of the permitted action
     Action string
+    // Roles is a list of roles allowed to perform this action
     Roles  []string
 }

37-46: Consider sorting results for consistent output.

The Build method should sort the results to ensure consistent output order across multiple builds.

Apply this diff:

+import "sort"

 func (b *ResourceBuilder) Build() []ResourceDefinition {
     result := make([]ResourceDefinition, 0, len(b.resources))
     for resource, actions := range b.resources {
         result = append(result, ResourceDefinition{
             Resource: b.serviceName + ":" + resource,
             Actions:  actions,
         })
     }
+    // Sort for consistent output
+    sort.Slice(result, func(i, j int) bool {
+        return result[i].Resource < result[j].Resource
+    })
     return result
 }
cerbos/client/check.go (2)

24-34: Enhance client configuration with timeout and validation.

The client initialization could be improved with:

  1. HTTP client timeout configuration
  2. Dashboard URL validation
  3. Option to customize HTTP client settings

Consider this enhancement:

 type Client struct {
     httpClient   *http.Client
     dashboardURL string
 }

+type ClientOption func(*Client)
+
+func WithHTTPClient(client *http.Client) ClientOption {
+    return func(c *Client) {
+        c.httpClient = client
+    }
+}
+
+func WithTimeout(timeout time.Duration) ClientOption {
+    return func(c *Client) {
+        c.httpClient.Timeout = timeout
+    }
+}
+
-func NewClient(dashboardURL string) *Client {
+func NewClient(dashboardURL string, opts ...ClientOption) (*Client, error) {
+    if _, err := url.Parse(dashboardURL); err != nil {
+        return nil, fmt.Errorf("invalid dashboard URL: %w", err)
+    }
+
+    c := &Client{
+        httpClient: &http.Client{
+            Timeout: 30 * time.Second,
+        },
+        dashboardURL: dashboardURL,
+    }
+
+    for _, opt := range opts {
+        opt(c)
+    }
+
-    return &Client{
-        httpClient:   &http.Client{},
-        dashboardURL: dashboardURL,
-    }
+    return c, nil
 }

55-66: Enhance CheckPermission with timeout, retries, and rate limiting.

The permission check could be more robust with:

  1. Context timeout handling
  2. Retry mechanism for transient failures
  3. Rate limiting to prevent API abuse

Consider implementing a circuit breaker pattern and rate limiting:

+type ClientConfig struct {
+    MaxRetries  int
+    RetryDelay  time.Duration
+    RateLimit   rate.Limit
+    BurstLimit  int
+}
+
+func (c *Client) CheckPermission(ctx context.Context, authInfo *appx.AuthInfo, input CheckPermissionInput) (bool, error) {
+    // Add rate limiting
+    if err := c.rateLimiter.Wait(ctx); err != nil {
+        return false, fmt.Errorf("rate limit exceeded: %w", err)
+    }
+
     if err := c.validateInput(authInfo); err != nil {
         return false, err
     }
 
-    req, err := c.createRequest(ctx, authInfo, input)
-    if err != nil {
-        return false, err
-    }
+    var result bool
+    err := retry.Do(
+        func() error {
+            req, err := c.createRequest(ctx, authInfo, input)
+            if err != nil {
+                return err
+            }
+            
+            result, err = c.executeRequest(req)
+            return err
+        },
+        retry.Context(ctx),
+        retry.Attempts(c.config.MaxRetries),
+        retry.Delay(c.config.RetryDelay),
+        retry.OnRetry(func(n uint, err error) {
+            log.Printf("Retrying permission check after error: %v (attempt %d)", err, n+1)
+        }),
+    )
 
-    return c.executeRequest(req)
+    return result, err
 }
cerbos/generator/policy_test.go (2)

11-112: Well-structured table-driven tests with comprehensive test cases.

The test cases effectively cover the main scenarios including successful generations and error cases.

Consider adding these additional test cases to improve coverage:

  1. Empty actions list
  2. Duplicate resource names
  3. Resource names with special characters

Example test case:

{
    name:        "duplicate resource names",
    serviceName: "flow",
    defineResources: func(builder *ResourceBuilder) []ResourceDefinition {
        return builder.
            AddResource("project", []ActionDefinition{
                NewActionDefinition("read", []string{"owner"}),
            }).
            AddResource("project", []ActionDefinition{
                NewActionDefinition("write", []string{"owner"}),
            }).
            Build()
    },
    wantErr: "duplicate resource name: project",
},

114-142: Test execution logic is robust with proper cleanup.

The implementation properly handles file operations and verifies both content and file count.

Consider using filepath.FromSlash() when constructing expected file paths to ensure cross-platform compatibility:

-				content, err := os.ReadFile(filepath.Join(testDir, filename))
+				content, err := os.ReadFile(filepath.Join(testDir, filepath.FromSlash(filename)))
cerbos/client/check_test.go (2)

14-21: Consider adding edge cases to the NewClient test.

While the basic initialization test is good, consider adding test cases for:

  • Malformed dashboard URLs
  • Custom HTTP client configurations
  • Empty/invalid dashboard URLs
// Example additional test cases:
func TestClient_NewClient(t *testing.T) {
    tests := []struct {
        name         string
        dashboardURL string
        wantErr     bool
    }{
        {
            name: "valid URL",
            dashboardURL: "http://test-dashboard",
            wantErr: false,
        },
        {
            name: "invalid URL",
            dashboardURL: "://invalid",
            wantErr: true,
        },
        {
            name: "empty URL",
            dashboardURL: "",
            wantErr: true,
        },
    }
    // ... test execution
}

23-109: Enhance test coverage with additional scenarios.

The current test cases cover the basic flows well, but consider adding tests for:

  1. Invalid service/resource/action values
  2. Context cancellation handling
  3. Request timeout scenarios
  4. Malformed server responses
// Example additional test cases to add:
{
    name: "error - invalid service",
    authInfo: &appx.AuthInfo{Token: "test-token"},
    input: CheckPermissionInput{
        Service: "",  // empty service
        Resource: "project",
        Action: "read",
    },
    wantErr: "invalid service",
},
{
    name: "error - context cancelled",
    authInfo: &appx.AuthInfo{Token: "test-token"},
    input: CheckPermissionInput{
        Service: "flow",
        Resource: "project",
        Action: "read",
    },
    useContext: context.WithCancel, // implement context cancellation
    wantErr: "context canceled",
},
cerbos/generator/builder_test.go (3)

9-28: Consider adding more test cases for better coverage.

While the basic test case is good, consider adding cases for:

  • Empty serviceName
  • Special characters in serviceName
  • Very long serviceName

Example addition:

 tests := []struct {
     name        string
     serviceName string
 }{
     {
         name:        "create new builder",
         serviceName: "flow",
     },
+    {
+        name:        "empty service name",
+        serviceName: "",
+    },
+    {
+        name:        "special characters in service name",
+        serviceName: "[email protected]",
+    },
+    {
+        name:        "very long service name",
+        serviceName: strings.Repeat("a", 256),
+    },
 }

30-54: Enhance test coverage with edge cases.

Consider adding test cases for:

  • Empty action string
  • Empty roles array
  • Duplicate roles
  • Invalid role names

Example addition:

 tests := []struct {
     name     string
     action   string
     roles    []string
     expected ActionDefinition
 }{
     {
         name:   "create action definition",
         action: "read",
         roles:  []string{"owner", "reader"},
         expected: ActionDefinition{
             Action: "read",
             Roles:  []string{"owner", "reader"},
         },
     },
+    {
+        name:   "empty action",
+        action: "",
+        roles:  []string{"owner"},
+        expected: ActionDefinition{
+            Action: "",
+            Roles:  []string{"owner"},
+        },
+    },
+    {
+        name:   "empty roles",
+        action: "write",
+        roles:  []string{},
+        expected: ActionDefinition{
+            Action: "write",
+            Roles:  []string{},
+        },
+    },
+    {
+        name:   "duplicate roles",
+        action: "read",
+        roles:  []string{"owner", "owner"},
+        expected: ActionDefinition{
+            Action: "read",
+            Roles:  []string{"owner", "owner"},
+        },
+    },
 }

94-182: LGTM! Well-structured test cases with good coverage.

The test cases cover both single and multiple resources with various action combinations. The resource name formatting (flow:project) is consistently tested.

Minor suggestion: Consider adding a test case for resource names with special characters to verify the formatting behavior.

Example addition:

     {
         name:        "build multiple resources",
         serviceName: "flow",
         // ... existing test case ...
     },
+    {
+        name:        "resource name with special characters",
+        serviceName: "flow-service",
+        resources: map[string][]ActionDefinition{
+            "project/v1": {
+                {
+                    Action: "read",
+                    Roles:  []string{"owner"},
+                },
+            },
+        },
+        expected: []ResourceDefinition{
+            {
+                Resource: "flow-service:project/v1",
+                Actions: []ActionDefinition{
+                    {
+                        Action: "read",
+                        Roles:  []string{"owner"},
+                    },
+                },
+            },
+        },
+    },
cerbos/generator/policy.go (4)

33-33: Clarify the error message when defineResources is nil

The error message could be more precise. Consider updating it to clearly indicate which parameter is nil.

Apply this diff to improve the error message:

-		return fmt.Errorf("define resources function is required")
+		return fmt.Errorf("defineResources function cannot be nil")

41-41: Include resource name in the error message for better debugging

Including the invalid resource name in the error message can help with debugging.

Apply this diff to enhance the error message:

-			return fmt.Errorf("invalid resource name")
+			return fmt.Errorf("invalid resource name: %s", resource.Resource)

56-56: Consider making Effect configurable

The Effect field is currently hardcoded as "EFFECT_ALLOW". For future flexibility, consider making this configurable to support different effects like "EFFECT_DENY".

You can update the Rule struct and ActionDefinition to include an Effect field or determine the effect based on context.

 type Rule struct {
 	Actions []string `yaml:"actions"`
-	Effect  string   `yaml:"effect"`
+	Effect  string   `yaml:"effect"` // Could be set per action
 	Roles   []string `yaml:"roles"`
 }

 // Within the loop over actions
 for _, action := range resource.Actions {
 	rule := Rule{
 		Actions: []string{action.Action},
-		Effect:  "EFFECT_ALLOW",
+		Effect:  action.Effect, // Assume ActionDefinition has an Effect field
 		Roles:   action.Roles,
 	}

74-76: Handle partial failures when writing policy files

Currently, if an error occurs while writing a policy file, the function returns immediately, potentially leaving some policies unwritten. Consider logging the error and continuing to process the remaining resources or accumulating errors to return a comprehensive error report.

Modify the code to collect errors:

+	var errs []error

 	for _, resource := range resources {
 		// ... existing code ...

 		if err := os.WriteFile(outputPath, data, 0644); err != nil {
-			return fmt.Errorf("failed to write file: %w", err)
+			errs = append(errs, fmt.Errorf("failed to write file '%s': %w", outputPath, err))
+			continue
 		}
 	}

+	if len(errs) > 0 {
+		return fmt.Errorf("encountered errors during policy generation: %v", errs)
+	}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f01a05d and 60843e8.

📒 Files selected for processing (8)
  • cerbos/client/check.go (1 hunks)
  • cerbos/client/check_test.go (1 hunks)
  • cerbos/client/permission_checker.go (1 hunks)
  • cerbos/generator/builder.go (1 hunks)
  • cerbos/generator/builder_test.go (1 hunks)
  • cerbos/generator/policy.go (1 hunks)
  • cerbos/generator/policy_test.go (1 hunks)
  • go.mod (1 hunks)
🔇 Additional comments (6)
cerbos/client/permission_checker.go (1)

1-9: LGTM! Clean package structure and imports.

The package structure is well-organized with minimal necessary imports.

cerbos/generator/policy_test.go (2)

1-9: LGTM! Appropriate imports for testing requirements.

The package name and imports are well-structured, using standard testing packages and the testify/assert package for clean assertions.


33-44: Verify YAML structure against Cerbos policy schema.

While the YAML structure appears correct, it would be beneficial to validate it against the official Cerbos policy schema.

Also applies to: 62-78, 79-91

✅ Verification successful

YAML structure is validated through Go struct definitions

The YAML structure is implicitly validated through Go struct type definitions in cerbos/generator/policy.go. The CerbosPolicy, ResourcePolicy, and Rule structs enforce the schema structure during YAML unmarshaling. The code also includes basic validation like checking for empty resource names.

Key validations in place:

  • Required fields through struct tags
  • Type safety for fields (string, arrays)
  • Predefined API version ("api.cerbos.dev/v1")
  • Non-empty resource name check
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for Cerbos schema validation in tests
# Expected: Find schema validation code or consider adding it

# Look for schema validation
rg -g '!vendor' -g '*.go' 'yaml.v[23]?.Unmarshal|policy.*schema|cerbos.*schema'

Length of output: 81


Script:

#!/bin/bash
# Let's check for YAML validation in test files and policy-related code
rg -g '*.go' 'yaml\.' -A 3

# Also check for any policy validation or verification code
rg -g '*.go' 'ValidatePolicy|Validate.*Policy|policy.*validation' -A 3

# Look for test helper functions that might handle policy validation
rg -g '*_test.go' 'func.*Test.*Policy|func.*policy' -A 3

Length of output: 1872


Script:

#!/bin/bash
# Let's check the policy.go file to understand the policy handling
cat cerbos/generator/policy.go

# Also check if there are any validation tests
rg -g '*_test.go' 'invalid.*policy|policy.*invalid|policy.*error' -A 3

# Look for any schema or validation-related files
fd -e yaml -e json schema

Length of output: 2074

cerbos/client/check_test.go (1)

1-12: LGTM! Clean import organization.

The imports are well-organized and all dependencies are properly utilized throughout the test file.

cerbos/generator/builder_test.go (1)

1-8: LGTM! Clean and minimal imports.

The package name matches the directory and imports are appropriate for the test file.

go.mod (1)

47-47: 🛠️ Refactor suggestion

Consider consolidating YAML libraries.

The codebase already uses github.com/goccy/go-yaml v1.11.3. Adding gopkg.in/yaml.v2 as another YAML parser could lead to:

  • Inconsistent YAML parsing behavior across the codebase
  • Increased maintenance overhead
  • Potential confusion for developers

Let's check the YAML parser usage in the codebase:

Consider one of these approaches:

  1. Use the existing github.com/goccy/go-yaml for the new policy file generation
  2. If yaml.v2 has specific features needed, consider migrating the entire codebase to use it consistently

Comment on lines +15 to +20
func NewPermissionChecker(service string, dashboardURL string) *PermissionChecker {
return &PermissionChecker{
Service: service,
DashboardURL: dashboardURL,
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and documentation to the constructor.

The constructor should validate its inputs and be properly documented.

+// NewPermissionChecker creates a new permission checker instance.
+// Returns an error if service is empty or dashboardURL is invalid.
 func NewPermissionChecker(service string, dashboardURL string) *PermissionChecker {
+	if service == "" {
+		return nil, fmt.Errorf("service name cannot be empty")
+	}
+	if dashboardURL == "" {
+		return nil, fmt.Errorf("dashboard URL cannot be empty")
+	}
+	if _, err := url.Parse(dashboardURL); err != nil {
+		return nil, fmt.Errorf("invalid dashboard URL: %w", err)
+	}
 	return &PermissionChecker{
 		Service:      service,
 		DashboardURL: dashboardURL,
-	}
+	}, nil
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +22 to +39
func (p *PermissionChecker) CheckPermission(ctx context.Context, authInfo *appx.AuthInfo, resource string, action string) (bool, error) {
if p == nil {
return false, fmt.Errorf("permission checker not found")
}

if authInfo == nil {
return false, fmt.Errorf("auth info not found")
}

input := CheckPermissionInput{
Service: p.Service,
Resource: resource,
Action: action,
}

client := NewClient(p.DashboardURL)
return client.CheckPermission(ctx, authInfo, input)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Several improvements needed for the CheckPermission method.

  1. Creating a new client for each permission check is inefficient. Consider storing the client as a field in the PermissionChecker struct.
  2. The method lacks proper documentation and parameter validation.
  3. No timeout management for the context.
+// CheckPermission verifies if the authenticated user has permission to perform
+// the specified action on the given resource.
+// Returns false and an error if the check fails or times out.
 func (p *PermissionChecker) CheckPermission(ctx context.Context, authInfo *appx.AuthInfo, resource string, action string) (bool, error) {
 	if p == nil {
-		return false, fmt.Errorf("permission checker not found")
+		return false, fmt.Errorf("permission checker is nil")
 	}
 
 	if authInfo == nil {
-		return false, fmt.Errorf("auth info not found")
+		return false, fmt.Errorf("authentication info is required")
+	}
+
+	if resource == "" {
+		return false, fmt.Errorf("resource cannot be empty")
+	}
+
+	if action == "" {
+		return false, fmt.Errorf("action cannot be empty")
 	}
 
 	input := CheckPermissionInput{
@@ -34,6 +38,11 @@
 		Action:   action,
 	}
 
-	client := NewClient(p.DashboardURL)
-	return client.CheckPermission(ctx, authInfo, input)
+	// Add timeout if not already set
+	if _, ok := ctx.Deadline(); !ok {
+		var cancel context.CancelFunc
+		ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
+		defer cancel()
+	}
+
+	return p.client.CheckPermission(ctx, authInfo, input)
 }

Additionally, update the struct to include the client:

 type PermissionChecker struct {
 	Service      string
 	DashboardURL string
+	client       *Client
 }

 func NewPermissionChecker(service string, dashboardURL string) (*PermissionChecker, error) {
+	client := NewClient(dashboardURL)
 	return &PermissionChecker{
 		Service:      service,
 		DashboardURL: dashboardURL,
+		client:       client,
 	}, nil
 }

Committable suggestion skipped: line range outside the PR's diff.


💡 Codebase verification

Based on the verification results, I can now provide the final response:

Security implementation in permission checking is adequate but needs HTTPS enforcement

The implementation shows proper security handling:

  • Authentication validation is present with proper error handling
  • GraphQL client properly validates auth info before making requests
  • Error cases are properly handled and returned

However, there are two security recommendations:

  1. The client accepts any URL scheme (both HTTP and HTTPS) as shown in the test cases. The NewClient function should validate and enforce HTTPS URLs in production environments.
  2. Consider adding logging for permission check failures to aid in security auditing.

Example locations that need attention:

  • cerbos/client/check.go: Add URL scheme validation in NewClient
  • cerbos/client/check.go: Add logging when permission checks fail
🔗 Analysis chain

Verify security implications of permission checking.

Since this method is crucial for security decisions, we should verify:

  1. How authentication failures are handled in the GraphQL client
  2. If there's proper logging of permission check failures
  3. If the dashboard URL is using HTTPS

Let me generate additional verification scripts to check the client implementation and URL validation specifically.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the client handles authentication and uses HTTPS

# Look for authentication error handling in the client
echo "Checking authentication error handling:"
ast-grep --pattern 'func (c *Client) CheckPermission($ctx, $auth, $input) {
  $$$
  if $err != nil {
    $$$
  }
  $$$
}'

# Check if there's logging for permission failures
echo "Checking logging implementation:"
rg -A 2 "log\." 

# Verify HTTPS usage
echo "Checking HTTPS URL validation:"
rg -A 2 "url\.Parse"

Length of output: 9420


Script:

#!/bin/bash
# Check client implementation and URL validation

# Look for the Client implementation and permission checking
echo "Checking Client implementation:"
ast-grep --pattern 'type Client struct {
  $$$
}'

ast-grep --pattern 'func NewClient($url) *Client {
  $$$
}'

# Check URL validation in the client
echo "Checking URL validation in client:"
rg -A 5 "NewClient" cerbos/client/

# Check error handling in permission checking
echo "Checking error handling in permission checking:"
rg -A 5 "CheckPermission" cerbos/client/

Length of output: 9887

Comment on lines +32 to +35
func (b *ResourceBuilder) AddResource(resource string, actions []ActionDefinition) *ResourceBuilder {
b.resources[resource] = actions
return b
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for AddResource method.

The method should validate its inputs to prevent invalid resource definitions.

Apply this diff:

+// AddResource adds a resource and its associated actions to the builder.
+// It panics if resource is empty or if no actions are provided.
 func (b *ResourceBuilder) AddResource(resource string, actions []ActionDefinition) *ResourceBuilder {
+    if resource == "" {
+        panic("resource cannot be empty")
+    }
+    if len(actions) == 0 {
+        panic("at least one action must be specified")
+    }
     b.resources[resource] = actions
     return b
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (b *ResourceBuilder) AddResource(resource string, actions []ActionDefinition) *ResourceBuilder {
b.resources[resource] = actions
return b
}
// AddResource adds a resource and its associated actions to the builder.
// It panics if resource is empty or if no actions are provided.
func (b *ResourceBuilder) AddResource(resource string, actions []ActionDefinition) *ResourceBuilder {
if resource == "" {
panic("resource cannot be empty")
}
if len(actions) == 0 {
panic("at least one action must be specified")
}
b.resources[resource] = actions
return b
}

Comment on lines +25 to +30
func NewActionDefinition(action string, roles []string) ActionDefinition {
return ActionDefinition{
Action: action,
Roles: roles,
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and documentation for NewActionDefinition.

The constructor should validate its inputs and include documentation.

Apply this diff:

+// NewActionDefinition creates a new ActionDefinition with the specified action
+// and permitted roles. It panics if action is empty or no roles are provided.
 func NewActionDefinition(action string, roles []string) ActionDefinition {
+    if action == "" {
+        panic("action cannot be empty")
+    }
+    if len(roles) == 0 {
+        panic("at least one role must be specified")
+    }
     return ActionDefinition{
         Action: action,
         Roles:  roles,
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewActionDefinition(action string, roles []string) ActionDefinition {
return ActionDefinition{
Action: action,
Roles: roles,
}
}
// NewActionDefinition creates a new ActionDefinition with the specified action
// and permitted roles. It panics if action is empty or no roles are provided.
func NewActionDefinition(action string, roles []string) ActionDefinition {
if action == "" {
panic("action cannot be empty")
}
if len(roles) == 0 {
panic("at least one role must be specified")
}
return ActionDefinition{
Action: action,
Roles: roles,
}
}

Comment on lines +13 to +23
type ResourceBuilder struct {
serviceName string
resources map[string][]ActionDefinition
}

func NewResourceBuilder(serviceName string) *ResourceBuilder {
return &ResourceBuilder{
serviceName: serviceName,
resources: make(map[string][]ActionDefinition),
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and documentation for ResourceBuilder.

The ResourceBuilder type and its constructor need documentation. Additionally, the constructor should validate the serviceName parameter.

Apply this diff:

+// ResourceBuilder facilitates the construction of resource definitions
+// for a specific service
 type ResourceBuilder struct {
     serviceName string
     resources   map[string][]ActionDefinition
 }

+// NewResourceBuilder creates a new ResourceBuilder for the specified service.
+// It returns an error if serviceName is empty.
 func NewResourceBuilder(serviceName string) *ResourceBuilder {
+    if serviceName == "" {
+        panic("serviceName cannot be empty")
+    }
     return &ResourceBuilder{
         serviceName: serviceName,
         resources:   make(map[string][]ActionDefinition),
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type ResourceBuilder struct {
serviceName string
resources map[string][]ActionDefinition
}
func NewResourceBuilder(serviceName string) *ResourceBuilder {
return &ResourceBuilder{
serviceName: serviceName,
resources: make(map[string][]ActionDefinition),
}
}
// ResourceBuilder facilitates the construction of resource definitions
// for a specific service
type ResourceBuilder struct {
serviceName string
resources map[string][]ActionDefinition
}
// NewResourceBuilder creates a new ResourceBuilder for the specified service.
// It returns an error if serviceName is empty.
func NewResourceBuilder(serviceName string) *ResourceBuilder {
if serviceName == "" {
panic("serviceName cannot be empty")
}
return &ResourceBuilder{
serviceName: serviceName,
resources: make(map[string][]ActionDefinition),
}
}

Comment on lines +1 to +149
CheckPermission: struct {
Allowed bool "json:\"allowed\""
}{
Allowed: true,
},
},
},
wantAllowed: true,
},
{
name: "success - permission denied",
authInfo: &appx.AuthInfo{
Token: "test-token",
},
input: CheckPermissionInput{
Service: "flow",
Resource: "project",
Action: "write",
},
serverStatus: http.StatusOK,
serverResp: CheckPermissionResponse{
Data: struct {
CheckPermission struct {
Allowed bool "json:\"allowed\""
} "json:\"checkPermission\""
}{
CheckPermission: struct {
Allowed bool "json:\"allowed\""
}{
Allowed: false,
},
},
},
wantAllowed: false,
},
{
name: "error - nil auth info",
authInfo: nil,
input: CheckPermissionInput{
Service: "flow",
Resource: "project",
Action: "read",
},
wantErr: "auth info is required",
},
{
name: "error - server error",
authInfo: &appx.AuthInfo{
Token: "test-token",
},
input: CheckPermissionInput{
Service: "flow",
Resource: "project",
Action: "read",
},
serverStatus: http.StatusInternalServerError,
wantErr: "failed to decode response",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/graphql", r.URL.Path)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
if tt.authInfo != nil {
assert.Equal(t, "Bearer "+tt.authInfo.Token, r.Header.Get("Authorization"))
}

var gqlRequest GraphQLQuery
err := json.NewDecoder(r.Body).Decode(&gqlRequest)
assert.NoError(t, err)
assert.Contains(t, gqlRequest.Query, "query CheckPermission")
assert.Contains(t, gqlRequest.Query, "checkPermission")

w.WriteHeader(tt.serverStatus)
if tt.serverStatus == http.StatusOK {
if err := json.NewEncoder(w).Encode(tt.serverResp); err != nil {
t.Fatalf("failed to encode response: %v", err)
return
}
}
}))
defer server.Close()

client := NewClient(server.URL)
allowed, err := client.CheckPermission(context.Background(), tt.authInfo, tt.input)

if tt.wantErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}

assert.NoError(t, err)
assert.Equal(t, tt.wantAllowed, allowed)
})
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Test coverage needs improvement

The current test file only covers Client methods but misses testing for the PermissionChecker type. Key missing test coverage:

  • NewPermissionChecker constructor
  • PermissionChecker.CheckPermission method including its edge cases:
    • nil checker
    • nil auth info
    • successful permission check
    • permission denied case
🔗 Analysis chain

Verify test coverage against implementation.

The test file provides good coverage, but let's verify it covers all methods and edge cases from the implementation.


Let me try a different approach to verify the test coverage.


Now I can see the complete implementation and test files. Let me verify one more aspect about the permission_checker.go file to ensure we have a complete picture.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if all exported methods in check.go are covered by tests

# Find all exported methods in check.go
echo "Checking for exported methods in check.go:"
ast-grep --pattern 'func ($c Client) $method($$$) {$$$}' cerbos/client/check.go

# Find corresponding test methods
echo "Checking for corresponding test methods:"
ast-grep --pattern 'func Test$$$($$$) {$$$}' cerbos/client/check_test.go

Length of output: 342


Script:

#!/bin/bash
# Let's first check if check.go exists and see its content
echo "Content of check.go:"
cat cerbos/client/check.go

# Also check for any other potential implementation files
echo -e "\nListing all go files in cerbos/client:"
fd -e go . cerbos/client/

Length of output: 3005


Script:

#!/bin/bash
# Check the content of permission_checker.go to see if it contains any additional functionality that needs testing
echo "Content of permission_checker.go:"
cat cerbos/client/permission_checker.go

Length of output: 936

Comment on lines +56 to +92
func TestResourceBuilder_AddResource(t *testing.T) {
tests := []struct {
name string
serviceName string
resource string
actions []ActionDefinition
expected map[string][]ActionDefinition
}{
{
name: "add single resource",
serviceName: "flow",
resource: "project",
actions: []ActionDefinition{
{
Action: "read",
Roles: []string{"owner", "reader"},
},
},
expected: map[string][]ActionDefinition{
"project": {
{
Action: "read",
Roles: []string{"owner", "reader"},
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := NewResourceBuilder(tt.serviceName)
builder.AddResource(tt.resource, tt.actions)
assert.Equal(t, tt.expected, builder.resources)
})
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test cases for resource management scenarios.

The current test only covers adding a single resource. Consider adding test cases for:

  • Adding multiple resources
  • Overwriting an existing resource
  • Adding a resource with no actions
  • Adding a resource with duplicate actions

Example addition:

 tests := []struct {
     name        string
     serviceName string
     resource    string
     actions     []ActionDefinition
     expected    map[string][]ActionDefinition
 }{
     {
         name:        "add single resource",
         serviceName: "flow",
         resource:    "project",
         actions: []ActionDefinition{
             {
                 Action: "read",
                 Roles:  []string{"owner", "reader"},
             },
         },
         expected: map[string][]ActionDefinition{
             "project": {
                 {
                     Action: "read",
                     Roles:  []string{"owner", "reader"},
                 },
             },
         },
     },
+    {
+        name:        "overwrite existing resource",
+        serviceName: "flow",
+        resource:    "project",
+        actions: []ActionDefinition{
+            {
+                Action: "write",
+                Roles:  []string{"owner"},
+            },
+        },
+        expected: map[string][]ActionDefinition{
+            "project": {
+                {
+                    Action: "write",
+                    Roles:  []string{"owner"},
+                },
+            },
+        },
+    },
+    {
+        name:        "add resource with no actions",
+        serviceName: "flow",
+        resource:    "project",
+        actions:     []ActionDefinition{},
+        expected: map[string][]ActionDefinition{
+            "project": {},
+        },
+    },
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +62 to +63
filename := strings.ReplaceAll(resource.Resource, ":", "_")
outputPath := filepath.Join(outputDir, fmt.Sprintf("%s.yaml", filename))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Sanitize filenames to prevent invalid or unsafe characters

Replacing colons with underscores might not be sufficient to ensure the filename is safe. Consider further sanitizing resource.Resource to remove or replace other potentially problematic characters, such as slashes, backslashes, or special characters.

You can use a function like this to sanitize filenames:

import "regexp"

// Helper function to sanitize filenames
func sanitizeFilename(name string) string {
	// Replace any non-word character with an underscore
	reg := regexp.MustCompile(`[^\w]+`)
	return reg.ReplaceAllString(name, "_")
}

Then update the filename assignment:

-		filename := strings.ReplaceAll(resource.Resource, ":", "_")
+		filename := sanitizeFilename(resource.Resource)

Comment on lines +65 to +67
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Move directory creation outside of the loop to improve efficiency

Calling os.MkdirAll(outputDir, 0755) inside the loop can result in redundant system calls since outputDir remains constant. Move this directory creation before the loop to avoid unnecessary overhead.

Apply this diff to move the directory creation:

 func GeneratePolicies(serviceName string, defineResources DefineResourcesFunc, outputDir string) error {
+	if err := os.MkdirAll(outputDir, 0755); err != nil {
+		return fmt.Errorf("failed to create directory: %w", err)
+	}

 	for _, resource := range resources {
-		if err := os.MkdirAll(outputDir, 0755); err != nil {
-			return fmt.Errorf("failed to create directory: %w", err)
-		}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +53 to +60
for _, action := range resource.Actions {
rule := Rule{
Actions: []string{action.Action},
Effect: "EFFECT_ALLOW",
Roles: action.Roles,
}
policy.ResourcePolicy.Rules = append(policy.ResourcePolicy.Rules, rule)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Check for empty actions to prevent generating policies without rules

If resource.Actions is empty, the generated policy will have no rules, resulting in potentially invalid policy files. Consider adding a check to ensure resource.Actions is not empty before proceeding.

Apply this diff to add the check:

+		if len(resource.Actions) == 0 {
+			return fmt.Errorf("no actions defined for resource: %s", resource.Resource)
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, action := range resource.Actions {
rule := Rule{
Actions: []string{action.Action},
Effect: "EFFECT_ALLOW",
Roles: action.Roles,
}
policy.ResourcePolicy.Rules = append(policy.ResourcePolicy.Rules, rule)
}
if len(resource.Actions) == 0 {
return fmt.Errorf("no actions defined for resource: %s", resource.Resource)
}
for _, action := range resource.Actions {
rule := Rule{
Actions: []string{action.Action},
Effect: "EFFECT_ALLOW",
Roles: action.Roles,
}
policy.ResourcePolicy.Rules = append(policy.ResourcePolicy.Rules, rule)
}

@pyshx pyshx merged commit cd5d2cb into main Nov 7, 2024
2 checks passed
@pyshx pyshx deleted the feat/add-policy-file-generation branch November 7, 2024 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants