-
Notifications
You must be signed in to change notification settings - Fork 15
Fixes user to group syncing #160
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
Open
edwardofclt
wants to merge
8
commits into
mitodl:main
Choose a base branch
from
edwardofclt:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
9c20978
try this copilot fix
edwardofclt e6e18e2
store artifact
edwardofclt 98f794a
log the group members
08b22e3
try this
edwardofclt aa581d1
log this no matter what
edwardofclt 1210a2f
this seemingly works!
edwardofclt fff67fe
put workflow back
edwardofclt 37829ce
put it back
edwardofclt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,32 @@ | ||
| name: Build keycloak-scim and release | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - 'main' | ||
| - "main" | ||
| tags: | ||
| - 'v*' | ||
|
|
||
| - "v*" | ||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-22.04 | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 | ||
| - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 | ||
|
|
||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4 | ||
| with: | ||
| java-version: "17" | ||
| distribution: "temurin" | ||
|
|
||
| - name: Build with Gradle | ||
| run: gradle shadowJar | ||
| - name: Build with Gradle | ||
| run: gradle shadowJar | ||
|
|
||
| - name: Create Release with jar file | ||
| uses: "marvinpinto/action-automatic-releases@latest" | ||
| with: | ||
| repo_token: "${{ secrets.GITHUB_TOKEN }}" | ||
| automatic_release_tag: "latest" | ||
| prerelease: false | ||
| title: "keycloak-scim release" | ||
| files: | | ||
| ./build/libs/*.jar | ||
| - name: Create Release with jar file | ||
| uses: "marvinpinto/action-automatic-releases@latest" | ||
| with: | ||
| repo_token: "${{ secrets.GITHUB_TOKEN }}" | ||
| automatic_release_tag: "latest" | ||
| prerelease: false | ||
| title: "keycloak-scim release" | ||
| files: | | ||
| ./build/libs/*.jar |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,8 @@ public void apply(GroupModel group) { | |
| .getGroupMembersStream(session.getContext().getRealm(), group) | ||
| .map(x -> x.getId()) | ||
| .collect(Collectors.toSet()); | ||
| LOGGER.info(String.format("Collected %d members for group %s (id=%s)", | ||
| this.members.size(), group.getName(), group.getId())); | ||
| this.skip = StringUtils.equals(group.getFirstAttribute("scim-skip"), "true"); | ||
| } | ||
|
|
||
|
|
@@ -61,14 +63,30 @@ public void apply(Group group) { | |
| setExternalId(group.getId().get()); | ||
| setDisplayName(group.getDisplayName().get()); | ||
| var groupMembers = group.getMembers(); | ||
| this.members = new HashSet<String>(); | ||
| if (groupMembers != null && groupMembers.size() > 0) { | ||
| this.members = new HashSet<String>(); | ||
| LOGGER.info(String.format("Processing %d incoming members for group %s", | ||
| groupMembers.size(), getDisplayName())); | ||
| for (var groupMember : groupMembers) { | ||
| var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User") | ||
| .getSingleResult(); | ||
| this.members.add(userMapping.getId()); | ||
| try { | ||
| String memberValue = groupMember.getValue().get(); | ||
| LOGGER.info(String.format("Looking up user with externalId: %s", memberValue)); | ||
| var userMapping = this.query("findByExternalId", memberValue, "User") | ||
| .getSingleResult(); | ||
| LOGGER.info(String.format("Found user mapping: id=%s, externalId=%s", | ||
| userMapping.getId(), userMapping.getExternalId())); | ||
| this.members.add(userMapping.getId()); | ||
| } catch (NoResultException e) { | ||
| LOGGER.error(String.format("No user mapping found for externalId: %s", | ||
| groupMember.getValue().get())); | ||
| } catch (Exception e) { | ||
| LOGGER.error(String.format("Failed to process incoming group member: %s - Error: %s", | ||
| groupMember.getValue().get(), e.getMessage()), e); | ||
| } | ||
| } | ||
| } | ||
| LOGGER.info(String.format("Processed incoming SCIM group %s with %d mapped members", | ||
| getDisplayName(), this.members.size())); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -77,28 +95,42 @@ public Group toSCIM(Boolean addMeta) { | |
| group.setId(externalId); | ||
| group.setExternalId(id); | ||
| group.setDisplayName(displayName); | ||
| if (members.size() > 0) { | ||
| var groupMembers = new ArrayList<Member>(); | ||
| for (var member : members) { | ||
| var groupMember = new Member(); | ||
| try { | ||
| var userMapping = this.query("findById", member, "User").getSingleResult(); | ||
| groupMember.setValue(userMapping.getExternalId()); | ||
| var ref = new URI(String.format("Users/%s", userMapping.getExternalId())); | ||
| groupMember.setRef(ref.toString()); | ||
| groupMembers.add(groupMember); | ||
| } catch (Exception e) { | ||
| LOGGER.error(e); | ||
|
|
||
| List<Member> groupMembers = new ArrayList<>(); | ||
| LOGGER.info(String.format("Processing %d members for SCIM group %s", members.size(), displayName)); | ||
|
|
||
| for (String memberId : members) { | ||
| try { | ||
| // Try to ensure the user has a mapping | ||
| String externalId = ensureUserMapping(memberId); | ||
|
|
||
| if (externalId == null) { | ||
| LOGGER.error(String.format("Could not get or create mapping for user %s, skipping", memberId)); | ||
| continue; | ||
|
Comment on lines
+107
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| LOGGER.info(String.format("Adding member with externalId %s to group %s", externalId, displayName)); | ||
| var groupMember = new Member(); | ||
| groupMember.setValue(externalId); | ||
| groupMember.setType("User"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| var ref = new URI(String.format("Users/%s", externalId)); | ||
| groupMember.setRef(ref.toString()); | ||
| groupMembers.add(groupMember); | ||
| } catch (Exception e) { | ||
| LOGGER.error("Failed to process group member: " + memberId, e); | ||
| } | ||
| group.setMembers(groupMembers); | ||
| } | ||
|
|
||
| group.setMembers(groupMembers); | ||
| LOGGER.info(String.format("Final SCIM group %s has %d members", displayName, groupMembers.size())); | ||
|
|
||
| if (addMeta) { | ||
| var meta = new Meta(); | ||
| try { | ||
| var uri = new URI("Groups/" + externalId); | ||
| meta.setLocation(uri.toString()); | ||
| } catch (URISyntaxException e) { | ||
| LOGGER.error("Failed to create meta URI", e); | ||
| } | ||
| group.setMeta(meta); | ||
| } | ||
|
|
@@ -131,15 +163,20 @@ public Boolean tryToMap() { | |
| public void createEntity() { | ||
| var group = session.groups().createGroup(realm, displayName); | ||
| this.id = group.getId(); | ||
| LOGGER.info(String.format("Created new group: %s (id=%s)", displayName, this.id)); | ||
|
|
||
| for (String mId : members) { | ||
| try { | ||
| LOGGER.info(String.format("Attempting to add user with id=%s to group %s", mId, displayName)); | ||
| var user = session.users().getUserById(realm, mId); | ||
| if (user == null) { | ||
| throw new NoResultException(); | ||
| LOGGER.warn(String.format("User with id=%s not found in Keycloak", mId)); | ||
| continue; | ||
| } | ||
| LOGGER.info(String.format("Adding user %s to group %s", user.getUsername(), displayName)); | ||
| user.joinGroup(group); | ||
| } catch (Exception e) { | ||
| LOGGER.warn(e); | ||
| LOGGER.warn(String.format("Failed to add user with id=%s to group: %s", mId, e.getMessage()), e); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -158,42 +195,133 @@ public Boolean skipRefresh() { | |
| public PatchBuilder<Group> toPatchBuilder(ScimRequestBuilder scimRequestBuilder, String url) { | ||
| List<Member> groupMembers = new ArrayList<>(); | ||
| PatchBuilder<Group> patchBuilder; | ||
| patchBuilder = scimRequestBuilder.patch(url, Group.class); | ||
| if (members.size() > 0) { | ||
| for (String member : members) { | ||
| var userMapping = this.query("findById", member, "User").getSingleResult(); | ||
| groupMembers.add(Member.builder().value(userMapping.getExternalId()).build()); | ||
| try { | ||
| LOGGER.info(String.format("Creating PATCH request to URL: %s", url)); | ||
| patchBuilder = scimRequestBuilder.patch(url, Group.class); | ||
|
|
||
| if (members.size() > 0) { | ||
| for (String member : members) { | ||
| try { | ||
| LOGGER.info(String.format("Looking for member with id: %s", member)); | ||
|
|
||
| // Try to ensure the user has a mapping | ||
| String externalId = ensureUserMapping(member); | ||
|
|
||
| if (externalId == null) { | ||
| LOGGER.error( | ||
| String.format("Could not get or create mapping for user %s, skipping", member)); | ||
| continue; | ||
|
Comment on lines
+210
to
+213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| LOGGER.info(String.format("Using externalId %s for user %s", externalId, member)); | ||
|
|
||
| Member memberNode = Member.builder() | ||
| .value(externalId) | ||
| .type("User") | ||
| .build(); | ||
| groupMembers.add(memberNode); | ||
| LOGGER.info(String.format("Added member %s to PATCH request", externalId)); | ||
| } catch (Exception e) { | ||
| LOGGER.error(String.format("Failed to process member %s: %s - Stack trace: ", | ||
| member, e.getMessage()), e); | ||
| } | ||
| } | ||
|
|
||
| // Debug the members being sent | ||
| LOGGER.info(String.format("Adding %d members to PATCH request", groupMembers.size())); | ||
|
|
||
| patchBuilder.addOperation() | ||
| .path("members") | ||
| .op(PatchOp.REPLACE) | ||
| .valueNodes(groupMembers) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("displayName") | ||
| .value(displayName) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("externalId") | ||
| .value(id) | ||
| .build(); | ||
| } else { | ||
| LOGGER.info("No members to add, using REMOVE operation"); | ||
| patchBuilder.addOperation() | ||
| .path("members") | ||
| .op(PatchOp.REMOVE) | ||
| .value(null) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("displayName") | ||
| .value(displayName) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("externalId") | ||
| .value(id) | ||
| .build(); | ||
| } | ||
| patchBuilder.addOperation() | ||
| .path("members") | ||
| .op(PatchOp.REPLACE) | ||
| .valueNodes(groupMembers) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("displayName") | ||
| .value(displayName) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("externalId") | ||
| .value(id) | ||
| .build(); | ||
| } else { | ||
| patchBuilder.addOperation() | ||
| .path("members") | ||
| .op(PatchOp.REMOVE) | ||
| .value(null) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("displayName") | ||
| .value(displayName) | ||
| .next() | ||
| .op(PatchOp.REPLACE) | ||
| .path("externalId") | ||
| .value(id) | ||
| .build(); | ||
|
|
||
| // Log the entire request for debugging | ||
| LOGGER.info("Final PATCH request payload: " + patchBuilder.getResource()); | ||
| return patchBuilder; | ||
|
|
||
| } catch (Exception e) { | ||
| LOGGER.error(String.format("Failed to create patch request to %s: %s", url, e.getMessage()), e); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Ensures a user has a SCIM mapping, creating one if needed | ||
| * | ||
| * @param userId The Keycloak user ID | ||
| * @return The external SCIM ID for the user, or null if mapping failed | ||
| */ | ||
| private String ensureUserMapping(String userId) { | ||
| try { | ||
| // First try to get the existing mapping | ||
| LOGGER.info(String.format("Checking if user %s already has a SCIM mapping", userId)); | ||
| var userMappingQuery = this.query("findById", userId, "User"); | ||
| try { | ||
| var existingMapping = userMappingQuery.getSingleResult(); | ||
| LOGGER.info(String.format("Found existing user mapping: id=%s, externalId=%s", | ||
| existingMapping.getId(), existingMapping.getExternalId())); | ||
| return existingMapping.getExternalId(); | ||
| } catch (NoResultException e) { | ||
| // No mapping found, need to create one | ||
| LOGGER.info(String.format("No SCIM mapping found for user %s, creating one", userId)); | ||
|
|
||
| // Get the user from Keycloak | ||
| var user = session.users().getUserById(realm, userId); | ||
| if (user == null) { | ||
| LOGGER.error(String.format("Cannot create mapping: User %s not found in Keycloak", userId)); | ||
| return null; | ||
| } | ||
|
|
||
| // Create a new UserAdapter to handle the mapping | ||
| LOGGER.info(String.format("Creating UserAdapter for user %s (%s)", userId, user.getUsername())); | ||
| try { | ||
| // Create a new User adapter and generate a mapping | ||
| var userAdapter = new UserAdapter(session, this.componentId); | ||
| userAdapter.setId(userId); | ||
|
|
||
| // Generate a new externalId (UUID) for this user | ||
| String externalId = java.util.UUID.randomUUID().toString(); | ||
| userAdapter.setExternalId(externalId); | ||
|
|
||
| // Persist the mapping | ||
| LOGGER.info(String.format("Persisting new user mapping: id=%s, externalId=%s", userId, externalId)); | ||
| userAdapter.saveMapping(); | ||
|
|
||
| return externalId; | ||
| } catch (Exception ex) { | ||
| LOGGER.error(String.format("Failed to create user mapping for %s: %s", userId, ex.getMessage()), | ||
| ex); | ||
| return null; | ||
| } | ||
| } | ||
| LOGGER.info(patchBuilder.getResource()); | ||
| return patchBuilder; | ||
| } catch (Exception e) { | ||
| LOGGER.error(String.format("Error ensuring user mapping for %s: %s", userId, e.getMessage()), e); | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
NoResultExceptionis caught, but only an error is logged. Consider adding more context to the log message, or potentially retrying the operation, or even skipping the user and logging a warning if the user is not found. Also, consider using a more specific exception type if possible.