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

List service accounts in the organization members API #244

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,7 @@ public Long getMembersCount() {
public Stream<UserModel> getMembersStream() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@xpg do we need to update getMembersCount?

I couldn't find the usage of /members/count endpoint

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this should be updated and tested also.

Copy link
Member

Choose a reason for hiding this comment

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

@kedare Please update the /members/count method also

Copy link
Author

Choose a reason for hiding this comment

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

Do you know if by default it already contains the service account ? Because it's a named query that counds entries on OrganizationMemberEntity so I guess yes ? (if not that would mean the service accounts are on another table?)

Copy link
Collaborator

Choose a reason for hiding this comment

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

By default the /members/count endpoint contains both users + service account users.

Copy link
Author

Choose a reason for hiding this comment

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

I guess I have to check if getServiceAccountClientLink() is null ?
Also I see there are no relation defined between OrganizationMemberEntity and UserEntity (just the ID field buy no FK), is there any reason for this ? Should this be added? It's not 100% clear how to join them via HQL, it looks like the usual SQL join syntax would not work on this case ?

Copy link
Member

Choose a reason for hiding this comment

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

You can join them on the id

Copy link
Author

Choose a reason for hiding this comment

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

What would be the best way to test HQL named queries ? Or I need to compile and run the whole test suite every time ?

Copy link
Author

Choose a reason for hiding this comment

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

Maybe someone could write the HQL query directly ? I guess it would take less than a few minutes for someone that knows Hibernate well.

Copy link
Member

Choose a reason for hiding this comment

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

Please let me know when you are able to find someone to complete this feature for you. I am closing the PR until that time.

return org.getMembers().stream()
.map(m -> m.getUserId())
.map(uid -> session.users().getUserById(realm, uid))
.filter(u -> u != null && u.getServiceAccountClientLink() == null);
.map(uid -> session.users().getUserById(realm, uid));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@ public MembersResource(OrganizationAdminResource parent, OrganizationModel organ
public Stream<UserRepresentation> getMembers(
@QueryParam("search") String searchQuery,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults) {
@QueryParam("max") Integer maxResults,
@QueryParam("includeServiceAccounts") Boolean includeServiceAccounts) {
log.debugf("Get members for %s %s [%s]", realm.getName(), organization.getId(), searchQuery);
firstResult = firstResult != null ? firstResult : 0;
boolean shouldIncludeServiceAccounts = includeServiceAccounts != null && includeServiceAccounts;
maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS;
return organization
.searchForMembersStream(searchQuery, firstResult, maxResults)
.filter(u -> shouldIncludeServiceAccounts || (u != null && u.getServiceAccountClientLink() == null))
.map(m -> toRepresentation(session, realm, m));
}

Expand Down
15 changes: 15 additions & 0 deletions src/test/java/io/phasetwo/service/Helpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import org.apache.http.impl.client.CloseableHttpClient;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
Expand All @@ -32,6 +34,8 @@ public class Helpers {
private static List<String> orgsTypes =
Arrays.stream(OrganizationResourceType.values()).map(Enum::toString).toList();

public static final String SERVICE_ACCOUNT_PREFIX = "service-account-";

static {
mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Expand Down Expand Up @@ -83,6 +87,17 @@ public static void deleteUser(Keycloak keycloak, String realm, String id) {
keycloak.realm(realm).users().delete(id);
}

public static UserRepresentation createServiceAccountClient(
Keycloak keycloak, String realm, String clientId) {
ClientRepresentation clientRepresentation = new ClientRepresentation();
clientRepresentation.setEnabled(true);
clientRepresentation.setClientId(clientId);
clientRepresentation.setServiceAccountsEnabled(true);
keycloak.realm(realm).clients().create(clientRepresentation);

return keycloak.realm(realm).users().searchByUsername(SERVICE_ACCOUNT_PREFIX+ clientId, true).get(0);
}

public static RealmEventsConfigRepresentation addEventListener(
Keycloak keycloak, String realm, String name) {
RealmResource realmResource = keycloak.realm(realm);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import static io.phasetwo.service.Helpers.createUser;
import static io.phasetwo.service.Helpers.createUserWithCredentials;
import static io.phasetwo.service.Helpers.createWebhook;
import static io.phasetwo.service.Helpers.createServiceAccountClient;
import static io.phasetwo.service.Helpers.deleteUser;
import static io.phasetwo.service.Helpers.deleteWebhook;
import static io.phasetwo.service.Helpers.objectMapper;
import static io.phasetwo.service.Helpers.removeEventListener;
import static io.phasetwo.service.Helpers.SERVICE_ACCOUNT_PREFIX;
import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION;
import static io.phasetwo.service.protocol.oidc.mappers.ActiveOrganizationMapper.INCLUDED_ORGANIZATION_PROPERTIES;
import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -2044,4 +2046,59 @@ private void validateActiveOrganizationNotVisibleFromAccount(Response response)
assertThat(attributeNode.hasNonNull(ACTIVE_ORGANIZATION), is(false));
}
}

@Test
void testIncludeServiceAccountsQueryParameter() throws IOException {
OrganizationRepresentation org = createDefaultOrg();
String id = org.getId();

Response response = getRequest(id, "members");
assertThat(response.statusCode(), is(Status.OK.getStatusCode()));

// create a user
UserRepresentation user1 = createUser(keycloak, REALM, "johndoe");
UserRepresentation user2 = createUser(keycloak, REALM, "johndow");
UserRepresentation user3 = createServiceAccountClient(keycloak, REALM, "client1");
UserRepresentation user4 = createServiceAccountClient(keycloak, REALM, "client2");

// add membership
response = putRequest("foo", org.getId(), "members", user1.getId());
assertThat(response.getStatusCode(), is(Status.CREATED.getStatusCode()));
response = putRequest("foo", org.getId(), "members", user2.getId());
assertThat(response.getStatusCode(), is(Status.CREATED.getStatusCode()));
response = putRequest("foo", org.getId(), "members", user3.getId());
assertThat(response.getStatusCode(), is(Status.CREATED.getStatusCode()));
response = putRequest("foo", org.getId(), "members", user4.getId());
assertThat(response.getStatusCode(), is(Status.CREATED.getStatusCode()));

// search members with query parameter
response = getRequest(id, "members?includeServiceAccounts=true");
assertThat(response.statusCode(), is(Status.OK.getStatusCode()));
List<UserRepresentation> members = objectMapper().readValue(response.getBody().asString(), new TypeReference<>() {});
assertThat(members, notNullValue());
assertThat(members, hasSize(5)); // including org admin default
assertThat(members, hasItem(hasProperty("username", is("johndoe"))));
assertThat(members, hasItem(hasProperty("username", is("johndow"))));
assertThat(members, hasItem(hasProperty("username", is(SERVICE_ACCOUNT_PREFIX + "client1"))));
assertThat(members, hasItem(hasProperty("username", is(SERVICE_ACCOUNT_PREFIX + "client2"))));

response = getRequest(id, "members?includeServiceAccounts=false");
assertThat(response.statusCode(), is(Status.OK.getStatusCode()));
members = objectMapper().readValue(response.getBody().asString(), new TypeReference<>() {});
assertThat(members, notNullValue());
assertThat(members, hasSize(3)); // including org admin default
assertThat(members, hasItem(hasProperty("username", is("johndoe"))));
assertThat(members, hasItem(hasProperty("username", is("johndow"))));

// delete user
deleteUser(keycloak, REALM, user1.getId());
deleteUser(keycloak, REALM, user2.getId());
deleteClient("client1");
deleteClient("client2");

// delete org
deleteOrganization(id);
}


}