Skip to content

OSAC-1283: Add functions and types for policies and permissions#665

Open
CrystalChun wants to merge 1 commit into
osac-project:mainfrom
CrystalChun:policies-permissions
Open

OSAC-1283: Add functions and types for policies and permissions#665
CrystalChun wants to merge 1 commit into
osac-project:mainfrom
CrystalChun:policies-permissions

Conversation

@CrystalChun

@CrystalChun CrystalChun commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Description

Adds functionality to CRUD policies and permissions in Keycloak for fine-grained access control for projects.

Testing

  • Build passes go build ./...
  • All unit tests successfully passed

Assisted-by: Claude Code noreply@anthropic.com

/cc @jhernand

Summary by CodeRabbit

  • Tests
    • Added and updated tests for authorization management capabilities.

Adds functionality to CRUD policies and permissions in Keycloak
for fine-grained access control for projects.

Assisted-by: Claude Code <noreply@anthropic.com>
@openshift-ci-robot

openshift-ci-robot commented Jun 9, 2026

Copy link
Copy Markdown

@CrystalChun: This pull request references OSAC-1283 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the sub-task to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Description

Adds functionality to CRUD policies and permissions in Keycloak for fine-grained access control for projects.

Testing

  • Build passes go build ./...
  • All unit tests successfully passed

Assisted-by: Claude Code noreply@anthropic.com

/cc @jhernand

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR extends the Keycloak IDP integration with authorization policy and scope permission management for project access control. It introduces domain types, client interface methods, REST API implementations, and integrates policy/permission creation into project authorization group setup with coordinated cleanup on failure.

Changes

Keycloak Authorization Policy and Scope Permission Management

Layer / File(s) Summary
Authorization domain types and client contracts
internal/idp/types.go, internal/idp/client.go, internal/idp/client_mock.go
AuthorizationPolicy and AuthorizationPermission struct types are defined with identifiers, names, logic/decision strategy, and group/resource/scope/policy associations. Client interface is extended with four methods for creating/deleting policies and permissions. GoMock wiring is generated for all new methods.
Keycloak authorization REST API implementation
internal/idp/keycloak/authz_policies.go, internal/idp/keycloak/types.go
Keycloak *Client implements policy creation (validates input, applies defaults, builds REST endpoints with realm/client UUIDs, POSTs and decodes responses). Scope permission creation resolves scope names to IDs via helper, converts single resource to array, restores names in response. Deletion methods execute DELETE requests. GetPolicyIDByName and GetPermissionIDByName helpers query by name. Keycloak types and converters map domain models to/from Keycloak representations.
Project authorization group setup and cleanup
internal/idp/resource_manager.go
createProjectAuthorizationGroups creates viewers/managers policies and project scope permissions; performs rollback cleanup on failure. deleteProjectAuthorizationGroups deletes permissions, policies, then groups in dependency order; resolves IDs by name and logs warnings on lookup failures. Helpers support optional name-to-ID lookup capability with explicit error handling.
Test verification
internal/idp/manager_test.go, internal/idp/resource_manager_test.go, internal/controllers/project/project_reconciler_function_test.go
mockClient stubs added for authorization methods. Resource manager test verifies policy/permission creation for viewers/managers with project scopes. Controller tests in four scenarios assert CreateGroupPolicy and CreateScopePermission calls with specific IDs and call counts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Possibly related PRs

  • osac-project/fulfillment-service#616: Extends the project authorization-group flow by updating resource manager and controller tests to create/delete authorization policies and scope permissions for viewers/managers groups.
  • osac-project/fulfillment-service#551: Introduces Keycloak project scope constants (ScopeViewProject, ScopeManageProject) that this PR uses in test expectations and permission creation.
  • osac-project/fulfillment-service#373: Establishes the Client interface foundation that this PR extends with authorization policy and scope permission methods.

Suggested labels

approved, lgtm


Suggested reviewers

  • jhernand
  • eranco74
  • adriengentil

Poem

🔐 Policies and scopes now take the stage,
Keycloak guards each authorization page,
With group-based rules and permission flows,
Project access blooms as the logic grows! ✨

🚥 Pre-merge checks | ✅ 10 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (10 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly reflects the primary change: adding functions and types for authorization policies and permissions across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Hardcoded-Secrets ✅ Passed No hardcoded secrets, API keys, tokens, passwords, or private keys found in any of the 9 modified files. Test data uses only synthetic IDs; pre-existing test credentials are not new to this PR.
No-Weak-Crypto ✅ Passed No weak cryptography patterns found in PR code—no MD5, SHA1, DES, RC4, 3DES, Blowfish, ECB usage, custom crypto implementations, or non-constant-time secret comparisons detected.
No-Injection-Vectors ✅ Passed No injection vectors found. All URL parameters properly escaped with url.PathEscape/QueryEscape. No SQL, shell execution, eval, yaml.load, or unsafe deserialization patterns.
Container-Privileges ✅ Passed PR contains only Go source code changes (authorization policy/permission functions). No container/K8s manifests were modified; existing manifests contain no privileged configurations.
No-Sensitive-Data-In-Logs ✅ Passed Logging contains only non-sensitive data: IDs, names, counts, and organization names. No passwords, tokens, API keys, credentials, PII, or session IDs exposed.
Ai-Attribution ✅ Passed Commit b835d19 includes proper "Assisted-by: Claude Code noreply@anthropic.com" trailer. No "Co-Authored-By" for AI tools found. Red Hat attribution verified.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/idp/keycloak/authz_policies.go`:
- Around line 51-52: Remove the no-op assignment that sets policy.GroupsClaim to
an empty string when it's already empty: delete the if block that checks if
policy.GroupsClaim == "" and reassigns it to ""; alternatively, if a default is
intended, set policy.GroupsClaim to a meaningful default (e.g., "groups") where
policy.GroupsClaim is initialized/validated so the code uses that default
instead of the redundant assignment.

In `@internal/idp/manager_test.go`:
- Around line 270-280: The mock CreateGroupPolicy implementation in mockClient
returns an AuthorizationPolicy but omits copying the Groups field from the
input, so update mockClient.CreateGroupPolicy to include Groups: policy.Groups
in the returned AuthorizationPolicy (preserving all other fields like ID, Name,
Type, Logic, DecisionStrategy, GroupsClaim) so the mock mirrors real behavior.

In `@internal/idp/resource_manager.go`:
- Around line 496-526: Both getPolicyIDByName and getPermissionIDByName
duplicate the same type-assert + call pattern; extract a generic helper (e.g.,
getResourceIDByName) on ResourceManager that accepts ctx, resourceType string,
name string and a lookupFunc func(context.Context,string)(string,error) and
returns (string,error); update getPolicyIDByName and getPermissionIDByName to
build their specific lookupFunc by type-asserting m.client to the existing
policyIDGetter/permissionIDGetter and pass that function to getResourceIDByName,
and ensure getResourceIDByName returns a clear error like "client does not
support getting <resourceType> ID by name" when lookupFunc is nil.
- Around line 293-304: The cleanupGroupsOnFailure helper currently swallows
errors from getGroupIDByPath and DeleteAuthorizationGroup; update
cleanupGroupsOnFailure to log debug-level messages when either
m.getGroupIDByPath or m.client.DeleteAuthorizationGroup return an error (include
the organizationName, group path, and the returned error), and log when a group
ID is empty to aid tracing; keep the "best-effort" behavior (do not return the
error) but ensure debug logs call the existing logger (e.g., m.logger or process
logger used elsewhere) so failures are visible during troubleshooting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 56714416-4519-4088-a5dd-4c7370b4ed22

📥 Commits

Reviewing files that changed from the base of the PR and between 38f6f71 and b835d19.

📒 Files selected for processing (9)
  • internal/controllers/project/project_reconciler_function_test.go
  • internal/idp/client.go
  • internal/idp/client_mock.go
  • internal/idp/keycloak/authz_policies.go
  • internal/idp/keycloak/types.go
  • internal/idp/manager_test.go
  • internal/idp/resource_manager.go
  • internal/idp/resource_manager_test.go
  • internal/idp/types.go

Comment on lines +51 to +52
if policy.GroupsClaim == "" {
policy.GroupsClaim = ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove no-op assignment.

Setting GroupsClaim to an empty string when it's already empty serves no purpose. Either remove this block or set a meaningful default value.

♻️ Suggested fix
-	if policy.GroupsClaim == "" {
-		policy.GroupsClaim = ""
-	}
📝 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
if policy.GroupsClaim == "" {
policy.GroupsClaim = ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/idp/keycloak/authz_policies.go` around lines 51 - 52, Remove the
no-op assignment that sets policy.GroupsClaim to an empty string when it's
already empty: delete the if block that checks if policy.GroupsClaim == "" and
reassigns it to ""; alternatively, if a default is intended, set
policy.GroupsClaim to a meaningful default (e.g., "groups") where
policy.GroupsClaim is initialized/validated so the code uses that default
instead of the redundant assignment.

Comment on lines +270 to +280
func (m *mockClient) CreateGroupPolicy(ctx context.Context, policy *AuthorizationPolicy) (*AuthorizationPolicy, error) {
// Return the policy with an ID assigned
return &AuthorizationPolicy{
ID: "test-policy-id",
Name: policy.Name,
Type: policy.Type,
Logic: policy.Logic,
DecisionStrategy: policy.DecisionStrategy,
GroupsClaim: policy.GroupsClaim,
}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mock should copy Groups field to match real behavior.

The CreateGroupPolicy mock copies most fields from the input policy but omits the Groups field (line 279). This could cause tests to miss issues with group path handling. Real implementations (like Keycloak) would preserve this field in the response.

🔧 Suggested fix to include Groups field
 func (m *mockClient) CreateGroupPolicy(ctx context.Context, policy *AuthorizationPolicy) (*AuthorizationPolicy, error) {
 	// Return the policy with an ID assigned
 	return &AuthorizationPolicy{
 		ID:               "test-policy-id",
 		Name:             policy.Name,
 		Type:             policy.Type,
 		Logic:            policy.Logic,
 		DecisionStrategy: policy.DecisionStrategy,
 		GroupsClaim:      policy.GroupsClaim,
+		Groups:           policy.Groups,
 	}, nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/idp/manager_test.go` around lines 270 - 280, The mock
CreateGroupPolicy implementation in mockClient returns an AuthorizationPolicy
but omits copying the Groups field from the input, so update
mockClient.CreateGroupPolicy to include Groups: policy.Groups in the returned
AuthorizationPolicy (preserving all other fields like ID, Name, Type, Logic,
DecisionStrategy, GroupsClaim) so the mock mirrors real behavior.

Comment on lines +293 to +304
// cleanupGroupsOnFailure is a helper to clean up groups when policy or permission creation fails.
func (m *ResourceManager) cleanupGroupsOnFailure(ctx context.Context, organizationName, viewersGroupPath, managersGroupPath string) {
viewersGroupID, _ := m.getGroupIDByPath(ctx, organizationName, viewersGroupPath)
if viewersGroupID != "" {
_ = m.client.DeleteAuthorizationGroup(ctx, organizationName, viewersGroupID)
}

managersGroupID, _ := m.getGroupIDByPath(ctx, organizationName, managersGroupPath)
if managersGroupID != "" {
_ = m.client.DeleteAuthorizationGroup(ctx, organizationName, managersGroupID)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider logging cleanup failures for debugging.

The best-effort cleanup silently ignores all errors. While this is appropriate for rollback scenarios, it could make debugging difficult if the cleanup fails. Consider adding debug-level logging when getGroupIDByPath or DeleteAuthorizationGroup fail.

💡 Optional enhancement to add debug logging
 func (m *ResourceManager) cleanupGroupsOnFailure(ctx context.Context, organizationName, viewersGroupPath, managersGroupPath string) {
-	viewersGroupID, _ := m.getGroupIDByPath(ctx, organizationName, viewersGroupPath)
+	viewersGroupID, err := m.getGroupIDByPath(ctx, organizationName, viewersGroupPath)
+	if err != nil {
+		m.logger.DebugContext(ctx, "Failed to get viewers group ID during cleanup",
+			slog.String("group_path", viewersGroupPath),
+			slog.Any("error", err),
+		)
+	}
 	if viewersGroupID != "" {
-		_ = m.client.DeleteAuthorizationGroup(ctx, organizationName, viewersGroupID)
+		if err := m.client.DeleteAuthorizationGroup(ctx, organizationName, viewersGroupID); err != nil {
+			m.logger.DebugContext(ctx, "Failed to delete viewers group during cleanup",
+				slog.String("group_id", viewersGroupID),
+				slog.Any("error", err),
+			)
+		}
 	}
 
-	managersGroupID, _ := m.getGroupIDByPath(ctx, organizationName, managersGroupPath)
+	managersGroupID, err := m.getGroupIDByPath(ctx, organizationName, managersGroupPath)
+	if err != nil {
+		m.logger.DebugContext(ctx, "Failed to get managers group ID during cleanup",
+			slog.String("group_path", managersGroupPath),
+			slog.Any("error", err),
+		)
+	}
 	if managersGroupID != "" {
-		_ = m.client.DeleteAuthorizationGroup(ctx, organizationName, managersGroupID)
+		if err := m.client.DeleteAuthorizationGroup(ctx, organizationName, managersGroupID); err != nil {
+			m.logger.DebugContext(ctx, "Failed to delete managers group during cleanup",
+				slog.String("group_id", managersGroupID),
+				slog.Any("error", err),
+			)
+		}
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/idp/resource_manager.go` around lines 293 - 304, The
cleanupGroupsOnFailure helper currently swallows errors from getGroupIDByPath
and DeleteAuthorizationGroup; update cleanupGroupsOnFailure to log debug-level
messages when either m.getGroupIDByPath or m.client.DeleteAuthorizationGroup
return an error (include the organizationName, group path, and the returned
error), and log when a group ID is empty to aid tracing; keep the "best-effort"
behavior (do not return the error) but ensure debug logs call the existing
logger (e.g., m.logger or process logger used elsewhere) so failures are visible
during troubleshooting.

Comment on lines +496 to +526
// getPolicyIDByName is a helper to get the policy ID from a policy name.
// This is a Keycloak-specific operation and may not be available on all IdP clients.
func (m *ResourceManager) getPolicyIDByName(ctx context.Context, policyName string) (string, error) {
// This relies on the Keycloak client implementation
// If the client doesn't support this, it will return an error
type policyIDGetter interface {
GetPolicyIDByName(ctx context.Context, policyName string) (string, error)
}

if getter, ok := m.client.(policyIDGetter); ok {
return getter.GetPolicyIDByName(ctx, policyName)
}

return "", fmt.Errorf("client does not support getting policy ID by name")
}

// getPermissionIDByName is a helper to get the permission ID from a permission name.
// This is a Keycloak-specific operation and may not be available on all IdP clients.
func (m *ResourceManager) getPermissionIDByName(ctx context.Context, permissionName string) (string, error) {
// This relies on the Keycloak client implementation
// If the client doesn't support this, it will return an error
type permissionIDGetter interface {
GetPermissionIDByName(ctx context.Context, permissionName string) (string, error)
}

if getter, ok := m.client.(permissionIDGetter); ok {
return getter.GetPermissionIDByName(ctx, permissionName)
}

return "", fmt.Errorf("client does not support getting permission ID by name")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting common lookup pattern to reduce duplication.

The getPolicyIDByName and getPermissionIDByName functions follow an identical pattern (interface definition, type assertion, method call). While the duplication is minimal and the current approach is clear, extracting a generic helper could improve maintainability if more lookup methods are added in the future.

🔄 Optional refactor using a generic helper

Note: This is a more complex change and may reduce clarity, so only consider if multiple similar lookup methods will be added.

// getResourceIDByName is a generic helper for Keycloak name-to-ID lookups.
func (m *ResourceManager) getResourceIDByName(
	ctx context.Context,
	resourceType string,
	name string,
	lookupFunc func(ctx context.Context, name string) (string, error),
) (string, error) {
	if lookupFunc == nil {
		return "", fmt.Errorf("client does not support getting %s ID by name", resourceType)
	}
	return lookupFunc(ctx, name)
}

// Then use it like:
func (m *ResourceManager) getPolicyIDByName(ctx context.Context, policyName string) (string, error) {
	type policyIDGetter interface {
		GetPolicyIDByName(ctx context.Context, policyName string) (string, error)
	}
	
	var lookupFunc func(context.Context, string) (string, error)
	if getter, ok := m.client.(policyIDGetter); ok {
		lookupFunc = getter.GetPolicyIDByName
	}
	
	return m.getResourceIDByName(ctx, "policy", policyName, lookupFunc)
}

However, the current explicit approach is clearer and has minimal duplication, so this refactoring is optional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/idp/resource_manager.go` around lines 496 - 526, Both
getPolicyIDByName and getPermissionIDByName duplicate the same type-assert +
call pattern; extract a generic helper (e.g., getResourceIDByName) on
ResourceManager that accepts ctx, resourceType string, name string and a
lookupFunc func(context.Context,string)(string,error) and returns
(string,error); update getPolicyIDByName and getPermissionIDByName to build
their specific lookupFunc by type-asserting m.client to the existing
policyIDGetter/permissionIDGetter and pass that function to getResourceIDByName,
and ensure getResourceIDByName returns a clear error like "client does not
support getting <resourceType> ID by name" when lookupFunc is nil.

@openshift-ci

openshift-ci Bot commented Jun 9, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: CrystalChun, jhernand

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved label Jun 9, 2026
@CrystalChun

Copy link
Copy Markdown
Contributor Author

/retest

@CrystalChun

Copy link
Copy Markdown
Contributor Author

/hold

@openshift-ci

openshift-ci Bot commented Jun 12, 2026

Copy link
Copy Markdown

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants