8000 Introduce a capability allowing to change current membershipType for … by martin-kanis · Pull Request #52370 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Introduce a capability allowing to change current membershipType for … - #52370

Merged
pedroigor merged 1 commit into
keycloak:mainfrom
martin-kanis:issue-48913
Sep 9, 2026
Merged

pedroigor merged 1 commit into
keycloak:mainfrom
martin-kanis:issue-48913

Conversation

@martin-kanis
Copy link
Copy Markdown
Member

…existing organization members

Closes #48913

Copilot AI balanced review requested due to automatic review settings September 3, 2026 12:40
@martin-kanis
martin-kanis requested review from a team as code owners September 3, 2026 12:40
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The provider return contract conflicts with the implemented and tested idempotent behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds Admin API support for changing an existing organization member between managed and unmanaged lifecycle modes.

Changes:

  • Adds the REST endpoint and admin-client method.
  • Implements persistence and cache invalidation.
  • Adds lifecycle tests and documentation.
File summaries
File Description
OrganizationMemberTest.java Tests updates, events, errors, and lifecycle behavior.
OrganizationMemberResource.java (services) Implements the secured REST endpoint.
OrganizationProvider.java Adds the provider API contract.
JpaOrganizationProvider.java Persists membership-type changes.
InfinispanOrganizationProvider.java Invalidates membership caches.
OrganizationMemberResource.java (admin client) Exposes the endpoint to clients.
managing-members.adoc Documents usage and lifecycle implications.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort l 8000 evel: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI review requested due to automatic review settings September 3, 2026 13:08
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

return Response.noContent().build();
}

throw ErrorResponse.error("Not a member of the organization", Status.BAD_REQUEST);

UserEntity userEntity = em.find(UserEntity.class, member.getId());
if (userEntity == null) {
return false;
Comment on lines +268 to +275
logger.warnf("Changed membership type to MANAGED for user '%s' in organization '%s'. "
+ "Removing this member or deleting the organization will also remove the user from the realm.",
member.getUsername(), organization.getName());
} else {
logger.warnf("Changed membership type to UNMANAGED for user '%s' in organization '%s'. "
+ "The user may lack local credentials if originally federated via an identity provider.",
member.getUsername(), organization.getName());
}
Comment on lines +256 to +258
if (StringUtil.isBlank(memberId)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
}
Comment on lines +946 to +959
adminEvents.clear();
try (Response response = organization.members().member(member.getId()).updateMembershipType(MembershipType.MANAGED)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}

MemberRepresentation updated = organization.members().member(member.getId()).toRepresentation();
assertEquals(MembershipType.MANAGED, updated.getMembershipType());

// Verify admin event
AdminEventRepresentation event = adminEvents.poll();
AdminEventAssertion.assertSuccess(event)
.operationType(OperationType.UPDATE)
.resourceType(ResourceType.ORGANIZATION_MEMBERSHIP);
assertEquals(MembershipType.MANAGED.name(), event.getDetails().get(MembershipType.NAME));
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The endpoint is consistently implemented across storage, caching, client, documentation, and integration tests.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostEncTest

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@keycloak-github-bot keycloak-github-bot 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.

Unreported flaky test detected, please review

…existing organization members

Closes keycloak#48913

Signed-off-by: Martin Kanis <mkanis@ibm.com>
Copilot AI review requested due to automatic review settings September 8, 2026 09:56
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The JPA implementation mishandles federated members and does not atomically enforce the single-managed-organization invariant.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +204 to +205
if (MembershipType.MANAGED.equals(membershipType)) {
throwIfManagedByAnotherOrg(organization, member);
Comment on lines +208 to +211
UserEntity userEntity = em.find(UserEntity.class, member.getId());
if (userEntity == null) {
return false;
}
@vramik vramik left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@martin-kanis Nice work on this one! The SPI contract is clean — the Javadoc on updateMembershipType clearly documents the idempotent return semantics. Cache invalidation via registerMemberInvalidation is correctly placed before delegating in the Infinispan layer, which is easy to get wrong. The test coverage is very solid too — especially the lifecycle test that verifies UNMANAGED→MANAGED followed by member removal actually deletes the user from the realm. And the documentation warning about lifecycle implications is a good addition.

One concern:

(As you also point out in the docs) Being a MANAGED member means the organization owns the user's lifecycle — removing the member or deleting the org also deletes the user from the realm. Having a user MANAGED in multiple orgs is nonsensical: which org "owns" the lifecycle? And it's dangerous: removing the user from org-A calls userProvider.removeUser(), which deletes them from the realm entirely, silently removing them from org-B as well.

This state might be already reachable today via the IdP broker flow (IdpAddOrganizationMemberAuthenticator unconditionally calls addManagedMember), but this PR adds another path to it via updateMembershipType.

I think updateMembershipType should reject changing to MANAGED if the user is already a MANAGED member of another organization (409 Conflict ??).

Ideally the same guard should also be added to JpaOrganizationProvider.addMember(org, user, metadata) — the private method that both addManagedMember and RegistrationUserCreation.addOrganizationMember use — so all runtime paths are covered in one place. That could be handled in a separate PR, if you'd prefer so.

@pedroigor
Copy link
Copy Markdown
Contributor

+1 to throwIfManagedByAnotherOrg.

@vramik vramik left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @martin-kanis

@pedroigor
pedroigor merged commit 2c0fd93 into keycloak:main Sep 9, 2026
94 checks passed
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.

Introduce a capability allowing to change current membershipType for existing organization members

4 participants

0