Allow managing members for an organization

Closes #27934

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-03-19 12:44:15 -03:00
parent 4154d27941
commit 32541f19a3
15 changed files with 749 additions and 58 deletions

View file

@ -0,0 +1,41 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.admin.client.resource;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.UserRepresentation;
public interface OrganizationMemberResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
UserRepresentation toRepresentation();
@PUT
@Consumes(MediaType.APPLICATION_JSON)
Response update(UserRepresentation organization);
@DELETE
Response delete();
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.admin.client.resource;
import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
public interface OrganizationMembersResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response addMember(UserRepresentation member);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> getAll();
@Path("{id}/organization")
@GET
@Produces(MediaType.APPLICATION_JSON)
OrganizationRepresentation getOrganization(@PathParam("id") String id);
@Path("{id}")
OrganizationMemberResource member(@PathParam("id") String id);
}

View file

@ -21,6 +21,7 @@ import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT; import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
@ -38,4 +39,7 @@ public interface OrganizationResource {
@DELETE @DELETE
Response delete(); Response delete();
@Path("members")
OrganizationMembersResource members();
} }

View file

@ -29,7 +29,6 @@ import jakarta.persistence.Table;
@Table(name="ORGANIZATION") @Table(name="ORGANIZATION")
@Entity @Entity
@NamedQueries({ @NamedQueries({
@NamedQuery(name="deleteByRealm", query="delete from OrganizationEntity o where o.realmId = :realmId"),
@NamedQuery(name="getByRealm", query="select o.id from OrganizationEntity o where o.realmId = :realmId") @NamedQuery(name="getByRealm", query="select o.id from OrganizationEntity o where o.realmId = :realmId")
}) })
public class OrganizationEntity { public class OrganizationEntity {
@ -42,6 +41,9 @@ public class OrganizationEntity {
@Column(name = "REALM_ID") @Column(name = "REALM_ID")
private String realmId; private String realmId;
@Column(name = "GROUP_ID")
private String groupId;
@Column(name="NAME") @Column(name="NAME")
protected String name; protected String name;
@ -61,6 +63,14 @@ public class OrganizationEntity {
this.realmId = realm; this.realmId = realm;
} }
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getName() { public String getName() {
return name; return name;
} }

View file

@ -17,17 +17,22 @@
package org.keycloak.organization.jpa; package org.keycloak.organization.jpa;
import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE;
import static org.keycloak.utils.StreamsUtil.closing; import static org.keycloak.utils.StreamsUtil.closing;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.OrganizationEntity; import org.keycloak.models.jpa.entities.OrganizationEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
@ -35,59 +40,84 @@ import org.keycloak.organization.OrganizationProvider;
public class JpaOrganizationProvider implements OrganizationProvider { public class JpaOrganizationProvider implements OrganizationProvider {
private final EntityManager em; private final EntityManager em;
private final GroupProvider groupProvider;
private final KeycloakSession session;
private final UserProvider userProvider;
public JpaOrganizationProvider(KeycloakSession session) { public JpaOrganizationProvider(KeycloakSession session) {
JpaConnectionProvider jpaProvider = session.getProvider(JpaConnectionProvider.class); this.session = session;
this.em = jpaProvider.getEntityManager(); em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
groupProvider = session.groups();
userProvider = session.users();
} }
@Override @Override
public OrganizationModel createOrganization(RealmModel realm, String name) { public OrganizationModel createOrganization(RealmModel realm, String name) {
throwExceptionIfRealmIsNull(realm); GroupModel group = createOrganizationGroup(realm, name);
OrganizationEntity entity = new OrganizationEntity(); OrganizationEntity entity = new OrganizationEntity();
entity.setId(KeycloakModelUtils.generateId()); entity.setId(KeycloakModelUtils.generateId());
entity.setGroupId(group.getId());
entity.setRealmId(realm.getId()); entity.setRealmId(realm.getId());
entity.setName(name); entity.setName(name);
em.persist(entity); em.persist(entity);
return new OrganizationAdapter(entity); return new OrganizationAdapter(entity, session);
} }
@Override @Override
public boolean removeOrganization(RealmModel realm, OrganizationModel organization) { public boolean removeOrganization(RealmModel realm, OrganizationModel organization) {
throwExceptionIfRealmIsNull(realm); GroupModel group = getOrganizationGroup(realm, organization);
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter toRemove = getAdapter(realm, organization.getId());
throwExceptionIfOrganizationIsNull(toRemove);
if (!toRemove.getRealm().equals(realm.getId())) { //TODO: won't scale, requires a better mechanism for bulk deleting users
throw new IllegalArgumentException("Organization [" + organization.getId() + " does not belong to realm [" + realm.getId() + "]"); userProvider.getGroupMembersStream(realm, group).forEach(userModel -> userProvider.removeUser(realm, userModel));
} groupProvider.removeGroup(realm, group);
em.remove(toRemove.getEntity()); OrganizationAdapter adapter = getAdapter(realm, organization.getId());
em.remove(adapter.getEntity());
return true; return true;
} }
@Override @Override
public void removeOrganizations(RealmModel realm) { public void removeOrganizations(RealmModel realm) {
throwExceptionIfRealmIsNull(realm); //TODO: won't scale, requires a better mechanism for bulk deleting organizations within a realm
Query query = em.createNamedQuery("deleteByRealm"); getOrganizationsStream(realm).forEach(organization -> removeOrganization(realm, organization));
}
query.setParameter("realmId", realm.getId()); @Override
public boolean addOrganizationMember(RealmModel realm, OrganizationModel organization, UserModel user) {
throwExceptionIfOrganizationIsNull(organization);
if (user == null) {
throw new ModelException("User can not be null");
}
OrganizationAdapter adapter = getAdapter(realm, organization.getId());
GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId());
query.executeUpdate(); if (user.isMemberOf(group)) {
return false;
}
if (user.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE) != null) {
throw new ModelException("User [" + user.getId() + "] is a member of a different organization");
}
user.joinGroup(group);
user.setSingleAttribute(USER_ORGANIZATION_ATTRIBUTE, adapter.getId());
return true;
} }
@Override @Override
public OrganizationModel getOrganizationById(RealmModel realm, String id) { public OrganizationModel getOrganizationById(RealmModel realm, String id) {
return getAdapter(realm, id); return getAdapter(realm, id, false);
} }
@Override @Override
public Stream<OrganizationModel> getOrganizationsStream(RealmModel realm) { public Stream<OrganizationModel> getOrganizationsStream(RealmModel realm) {
throwExceptionIfRealmIsNull(realm);
TypedQuery<String> query = em.createNamedQuery("getByRealm", String.class); TypedQuery<String> query = em.createNamedQuery("getByRealm", String.class);
query.setParameter("realmId", realm.getId()); query.setParameter("realmId", realm.getId());
@ -95,34 +125,119 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return closing(query.getResultStream().map(id -> getAdapter(realm, id))); return closing(query.getResultStream().map(id -> getAdapter(realm, id)));
} }
@Override
public Stream<UserModel> getMembersStream(RealmModel realm, OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter adapter = getAdapter(realm, organization.getId());
GroupModel group = getOrganizationGroup(realm, adapter);
return userProvider.getGroupMembersStream(realm, group);
}
@Override
public UserModel getMemberById(RealmModel realm, OrganizationModel organization, String id) {
throwExceptionIfRealmIsNull(realm);
throwExceptionIfOrganizationIsNull(organization);
UserModel user = userProvider.getUserById(realm, id);
if (user == null) {
return null;
}
String orgId = user.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);
if (organization.getId().equals(orgId)) {
return user;
}
return null;
}
@Override
public OrganizationModel getOrganizationByMember(RealmModel realm, UserModel member) {
throwExceptionIfRealmIsNull(realm);
if (member == null) {
throw new ModelException("User can not be null");
}
String orgId = member.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);
if (orgId == null) {
return null;
}
return getOrganizationById(realm, orgId);
}
@Override @Override
public void close() { public void close() {
} }
private OrganizationAdapter getAdapter(RealmModel realm, String id) { private OrganizationAdapter getAdapter(RealmModel realm, String id) {
return getAdapter(realm, id, true);
}
private OrganizationAdapter getAdapter(RealmModel realm, String id, boolean failIfNotFound) {
throwExceptionIfRealmIsNull(realm);
OrganizationEntity entity = em.find(OrganizationEntity.class, id); OrganizationEntity entity = em.find(OrganizationEntity.class, id);
if (entity == null) { if (entity == null) {
if (failIfNotFound) {
throw new ModelException("Organization [" + id + "] does not exist");
}
return null; return null;
} }
if (!realm.getId().equals(entity.getRealmId())) { if (!realm.getId().equals(entity.getRealmId())) {
return null; throw new ModelException("Organization [" + entity.getId() + " does not belong to realm [" + realm.getId() + "]");
} }
return new OrganizationAdapter(entity); return new OrganizationAdapter(entity, session);
}
private GroupModel createOrganizationGroup(RealmModel realm, String name) {
throwExceptionIfRealmIsNull(realm);
if (name == null) {
throw new ModelException("name can not be null");
}
String groupName = getCanonicalGroupName(name);
GroupModel group = groupProvider.getGroupByName(realm, null, name);
if (group != null) {
throw new ModelException("A group with the same name already exist and it is bound to different organization");
}
return groupProvider.createGroup(realm, KeycloakModelUtils.generateId(), groupName);
}
private String getCanonicalGroupName(String name) {
return "kc.org." + name;
}
private GroupModel getOrganizationGroup(RealmModel realm, OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter adapter = getAdapter(realm, organization.getId());
GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId());
if (group == null) {
throw new ModelException("Organization group " + adapter.getGroupId() + " not found");
}
return group;
} }
private void throwExceptionIfOrganizationIsNull(OrganizationModel organization) { private void throwExceptionIfOrganizationIsNull(OrganizationModel organization) {
if (organization == null) { if (organization == null) {
throw new IllegalArgumentException("organization can not be null"); throw new ModelException("organization can not be null");
} }
} }
private void throwExceptionIfRealmIsNull(RealmModel realm) { private void throwExceptionIfRealmIsNull(RealmModel realm) {
if (realm == null) { if (realm == null) {
throw new IllegalArgumentException("realm can not be null"); throw new ModelException("realm can not be null");
} }
} }
} }

View file

@ -17,16 +17,19 @@
package org.keycloak.organization.jpa; package org.keycloak.organization.jpa;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.jpa.JpaModel; import org.keycloak.models.jpa.JpaModel;
import org.keycloak.models.jpa.entities.OrganizationEntity; import org.keycloak.models.jpa.entities.OrganizationEntity;
public class OrganizationAdapter implements OrganizationModel, JpaModel<OrganizationEntity> { public final class OrganizationAdapter implements OrganizationModel, JpaModel<OrganizationEntity> {
private final OrganizationEntity entity; private final OrganizationEntity entity;
private final KeycloakSession session;
public OrganizationAdapter(OrganizationEntity entity) { public OrganizationAdapter(OrganizationEntity entity, KeycloakSession session) {
this.entity = entity; this.entity = entity;
this.session = session;
} }
@Override @Override
@ -38,6 +41,10 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel<Organiza
return entity.getRealmId(); return entity.getRealmId();
} }
String getGroupId() {
return entity.getGroupId();
}
@Override @Override
public void setName(String name) { public void setName(String name) {
entity.setName(name); entity.setName(name);
@ -52,4 +59,17 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel<Organiza
public OrganizationEntity getEntity() { public OrganizationEntity getEntity() {
return entity; return entity;
} }
@Override
public String toString() {
return new StringBuilder()
.append("id=")
.append(getId())
.append(",")
.append("name=")
.append(getName())
.append(",")
.append("groupId=")
.append(getGroupId()).toString();
}
} }

View file

@ -25,6 +25,9 @@
<column name="REALM_ID" type="VARCHAR(255)"> <column name="REALM_ID" type="VARCHAR(255)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="GROUP_ID" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)"> <column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
@ -32,6 +35,7 @@
<addPrimaryKey columnNames="ID" tableName="ORGANIZATION"/> <addPrimaryKey columnNames="ID" tableName="ORGANIZATION"/>
<addUniqueConstraint tableName="ORGANIZATION" columnNames="REALM_ID, NAME" constraintName="UK_ORG_NAME"/> <addUniqueConstraint tableName="ORGANIZATION" columnNames="REALM_ID, NAME" constraintName="UK_ORG_NAME"/>
<addUniqueConstraint tableName="ORGANIZATION" columnNames="GROUP_ID" constraintName="UK_ORG_GROUP"/>
</changeSet> </changeSet>
<changeSet author="keycloak" id="unique-consentuser"> <changeSet author="keycloak" id="unique-consentuser">

View file

@ -21,6 +21,7 @@ import java.util.stream.Stream;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
public interface OrganizationProvider extends Provider { public interface OrganizationProvider extends Provider {
@ -35,6 +36,13 @@ public interface OrganizationProvider extends Provider {
*/ */
OrganizationModel createOrganization(RealmModel realm, String name); OrganizationModel createOrganization(RealmModel realm, String name);
/**
* Returns a {@link OrganizationModel} by its {@code id};
*
* @param realm the realm
* @param id the id of an organization
* @return the organization with the given {@code id}
*/
OrganizationModel getOrganizationById(RealmModel realm, String id); OrganizationModel getOrganizationById(RealmModel realm, String id);
/** /**
@ -52,10 +60,48 @@ public interface OrganizationProvider extends Provider {
*/ */
void removeOrganizations(RealmModel realm); void removeOrganizations(RealmModel realm);
/**
* Adds the give {@link UserModel} as a member of the given {@link OrganizationModel}.
*
* @param realm the realm
* @param organization the organization
* @param user the user
* @return {@code true} if the user was added as a member. Otherwise, returns {@code false}
*/
boolean addOrganizationMember(RealmModel realm, OrganizationModel organization, UserModel user);
/** /**
* Returns the organizations of the given realm as a stream. * Returns the organizations of the given realm as a stream.
* @param realm Realm. * @param realm Realm.
* @return Stream of the organizations. Never returns {@code null}. * @return Stream of the organizations. Never returns {@code null}.
*/ */
Stream<OrganizationModel> getOrganizationsStream(RealmModel realm); Stream<OrganizationModel> getOrganizationsStream(RealmModel realm);
/**
* Returns the members of a given {@code organization}.
*
* @param realm the realm
* @param organization the organization
* @return the organization with the given {@code id}
*/
Stream<UserModel> getMembersStream(RealmModel realm, OrganizationModel organization);
/**
* Returns the member of an {@code organization} by its {@code id}.
*
* @param realm the realm
* @param organization the organization
* @param id the member id
* @return the organization with the given {@code id}
*/
UserModel getMemberById(RealmModel realm, OrganizationModel organization, String id);
/**
* Returns the {@link OrganizationModel} that a {@code member} belongs to.
*
* @param realm the realm
* @param member the member of a organization
* @return the organization the {@code member} belongs to
*/
OrganizationModel getOrganizationByMember(RealmModel realm, UserModel member);
} }

View file

@ -19,10 +19,11 @@ package org.keycloak.models;
public interface OrganizationModel { public interface OrganizationModel {
String USER_ORGANIZATION_ATTRIBUTE = "kc.org";
String getId(); String getId();
void setName(String name); void setName(String name);
String getName(); String getName();
} }

View file

@ -0,0 +1,168 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.organization.admin.resource;
import java.util.stream.Stream;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.Provider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.UserResource;
import org.keycloak.services.resources.admin.UsersResource;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.utils.StringUtil;
@Provider
public class OrganizationMemberResource {
private final KeycloakSession session;
private final RealmModel realm;
private final OrganizationProvider provider;
private final OrganizationModel organization;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent;
public OrganizationMemberResource() {
this.session = null;
this.realm = null;
this.provider = null;
this.organization = null;
this.auth = null;
this.adminEvent = null;
}
public OrganizationMemberResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session;
this.realm = session.getContext().getRealm();
this.provider = session.getProvider(OrganizationProvider.class);
this.organization = organization;
this.auth = auth;
this.adminEvent = adminEvent;
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addMember(UserRepresentation rep) {
UsersResource usersResource = new UsersResource(session, auth, adminEvent);
Response response = usersResource.createUser(rep);
if (Status.CREATED.getStatusCode() == response.getStatus()) {
return KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), session.getContext(), session -> {
RealmModel realm = session.getContext().getRealm();
UserModel member = session.users().getUserByUsername(realm, rep.getEmail());
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
if (provider.addOrganizationMember(realm, organization, member)) {
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(member.getId()).build()).build();
}
throw new BadRequestException();
});
}
return response;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Stream<UserRepresentation> getMembers() {
return provider.getMembersStream(realm, organization).map(this::toRepresentation);
}
@Path("{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public UserRepresentation get(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
}
return toRepresentation(getMember(id));
}
@Path("{id}")
@DELETE
public Response delete(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
}
UserModel member = getMember(id);
return new UserResource(session, member, auth, adminEvent).deleteUser();
}
@Path("{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response update(@PathParam("id") String id, UserRepresentation user) {
return new UserResource(session, getMember(id), auth, adminEvent).updateUser(user);
}
@Path("{id}/organization")
@GET
@Produces(MediaType.APPLICATION_JSON)
public OrganizationRepresentation getOrganization(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
}
UserModel member = getMember(id);
OrganizationModel organization = provider.getOrganizationByMember(realm, member);
OrganizationRepresentation rep = new OrganizationRepresentation();
rep.setId(organization.getId());
return rep;
}
private UserModel getMember(String id) {
UserModel member = provider.getMemberById(realm, organization, id);
if (member == null) {
throw new NotFoundException();
}
return member;
}
private UserRepresentation toRepresentation(UserModel member) {
return ModelToRepresentation.toRepresentation(session, realm, member);
}
}

View file

@ -37,6 +37,8 @@ import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
@Provider @Provider
@ -44,15 +46,19 @@ public class OrganizationResource {
private final KeycloakSession session; private final KeycloakSession session;
private final OrganizationProvider provider; private final OrganizationProvider provider;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent;
public OrganizationResource() { public OrganizationResource() {
// needed for registering to the JAX-RS stack // needed for registering to the JAX-RS stack
this(null); this(null, null, null);
} }
public OrganizationResource(KeycloakSession session) { public OrganizationResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session; this.session = session;
this.provider = session == null ? null : session.getProvider(OrganizationProvider.class); this.provider = session == null ? null : session.getProvider(OrganizationProvider.class);
this.auth = auth;
this.adminEvent = adminEvent;
} }
@POST @POST
@ -110,6 +116,14 @@ public class OrganizationResource {
return Response.noContent().build(); return Response.noContent().build();
} }
@Path("{id}/members")
public OrganizationMemberResource members(@PathParam("id") String id) {
RealmModel realm = session.getContext().getRealm();
OrganizationModel model = getOrganization(realm, id);
return new OrganizationMemberResource(session, model, auth, adminEvent);
}
private OrganizationModel getOrganization(RealmModel realm, String id) { private OrganizationModel getOrganization(RealmModel realm, String id) {
if (id == null) { if (id == null) {
throw new BadRequestException(); throw new BadRequestException();

View file

@ -27,7 +27,7 @@ public class OrganizationResourceProvider implements AdminRealmResourceProvider
@Override @Override
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
return new OrganizationResource(session); return new OrganizationResource(session, auth, adminEvent);
} }
@Override @Override

View file

@ -0,0 +1,81 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.organization.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.admin.ApiUtil;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public abstract class AbstractOrganizationTest extends AbstractAdminTest {
protected OrganizationRepresentation createOrganization() {
return createOrganization("neworg");
}
protected OrganizationRepresentation createOrganization(String name) {
OrganizationRepresentation org = new OrganizationRepresentation();
org.setName(name);
String id;
try (Response response = testRealm().organizations().create(org)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
id = ApiUtil.getCreatedId(response);
}
org.setId(id);
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());
return org;
}
protected UserRepresentation addMember(OrganizationResource organization) {
return addMember(organization, "jdoe@neworg.org");
}
protected UserRepresentation addMember(OrganizationResource organization, String email) {
UserRepresentation expected = new UserRepresentation();
expected.setEmail(email);
expected.setUsername(expected.getEmail());
try (Response response = organization.members().addMember(expected)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
String id = ApiUtil.getCreatedId(response);
UserRepresentation actual = organization.members().member(id).toRepresentation();
assertNotNull(expected);
assertEquals(id, actual.getId());
assertEquals(expected.getUsername(), actual.getUsername());
assertEquals(expected.getEmail(), actual.getEmail());
return actual;
}
}
}

View file

@ -0,0 +1,161 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.organization.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationMemberResource;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.ORGANIZATION)
public class OrganizationMemberTest extends AbstractOrganizationTest {
@Test
public void testUpdate() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization);
expected.setFirstName("f");
expected.setLastName("l");
OrganizationMemberResource member = organization.members().member(expected.getId());
try (Response response = member.update(expected)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
UserRepresentation existing = member.toRepresentation();
assertEquals(expected.getId(), existing.getId());
assertEquals(expected.getUsername(), existing.getUsername());
assertEquals(expected.getEmail(), existing.getEmail());
assertEquals(expected.getFirstName(), existing.getFirstName());
assertEquals(expected.getLastName(), existing.getLastName());
}
@Test
public void testGet() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization);
UserRepresentation existing = organization.members().member(expected.getId()).toRepresentation();
assertEquals(expected.getId(), existing.getId());
assertEquals(expected.getUsername(), existing.getUsername());
assertEquals(expected.getEmail(), existing.getEmail());
}
@Test
public void testGetMemberOrganization() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation member = addMember(organization);
OrganizationRepresentation expected = organization.toRepresentation();
OrganizationRepresentation actual = organization.members().getOrganization(member.getId());
assertNotNull(actual);
assertEquals(expected.getId(), actual.getId());
}
@Test
public void testGetAll() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
List<UserRepresentation> expected = new ArrayList<>();
for (int i = 0; i < 5; i++) {
expected.add(addMember(organization, "member-" + i + "@neworg.org"));
}
List<UserRepresentation> existing = organization.members().getAll();;
assertFalse(existing.isEmpty());
assertEquals(expected.size(), existing.size());
for (UserRepresentation expectedRep : expected) {
UserRepresentation existingRep = existing.stream().filter(member -> member.getId().equals(expectedRep.getId())).findAny().orElse(null);
assertNotNull(existingRep);
assertEquals(expectedRep.getId(), existingRep.getId());
assertEquals(expectedRep.getUsername(), existingRep.getUsername());
assertEquals(expectedRep.getEmail(), existingRep.getEmail());
assertEquals(expectedRep.getFirstName(), existingRep.getFirstName());
assertEquals(expectedRep.getLastName(), existingRep.getLastName());
}
}
@Test
public void testDelete() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization);
OrganizationMemberResource member = organization.members().member(expected.getId());
try (Response response = member.delete()) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
try {
member.toRepresentation();
fail("should be deleted");
} catch (NotFoundException ignore) {}
}
@Test
public void testDeleteMembersOnOrganizationRemoval() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
List<UserRepresentation> expected = new ArrayList<>();
for (int i = 0; i < 5; i++) {
expected.add(addMember(organization, "member-" + i + "@neworg.org"));
}
organization.delete().close();
for (UserRepresentation member : expected) {
try {
organization.members().member(member.getId()).toRepresentation();
fail("should be deleted");
} catch (NotFoundException ignore) {}
}
for (UserRepresentation member : expected) {
try {
testRealm().users().get(member.getId()).toRepresentation();
fail("should be deleted");
} catch (NotFoundException ignore) {}
}
}
@Test
public void testDeleteGroupOnOrganizationRemoval() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
addMember(organization);
assertTrue(testRealm().groups().groups().stream().anyMatch(group -> group.getName().startsWith("kc.org.")));
organization.delete().close();
assertFalse(testRealm().groups().groups().stream().anyMatch(group -> group.getName().startsWith("kc.org.")));
}
}

View file

@ -34,16 +34,14 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature; import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.ORGANIZATION) @EnableFeature(Feature.ORGANIZATION)
public class OrganizationTest extends AbstractAdminTest { public class OrganizationTest extends AbstractOrganizationTest {
@Test @Test
public void testUpdate() { public void testUpdate() {
OrganizationRepresentation expected = createRepresentation(); OrganizationRepresentation expected = createOrganization();
assertEquals("neworg", expected.getName()); assertEquals("neworg", expected.getName());
expected.setName("acme"); expected.setName("acme");
@ -61,7 +59,7 @@ public class OrganizationTest extends AbstractAdminTest {
@Test @Test
public void testGet() { public void testGet() {
OrganizationRepresentation expected = createRepresentation(); OrganizationRepresentation expected = createOrganization();
OrganizationRepresentation existing = testRealm().organizations().get(expected.getId()).toRepresentation(); OrganizationRepresentation existing = testRealm().organizations().get(expected.getId()).toRepresentation();
assertNotNull(existing); assertNotNull(existing);
assertEquals(expected.getId(), existing.getId()); assertEquals(expected.getId(), existing.getId());
@ -73,7 +71,7 @@ public class OrganizationTest extends AbstractAdminTest {
List<OrganizationRepresentation> expected = new ArrayList<>(); List<OrganizationRepresentation> expected = new ArrayList<>();
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
expected.add(createRepresentation("org-" + i)); expected.add(createOrganization("kc.org." + i));
} }
List<OrganizationRepresentation> existing = testRealm().organizations().getAll(); List<OrganizationRepresentation> existing = testRealm().organizations().getAll();
@ -83,7 +81,7 @@ public class OrganizationTest extends AbstractAdminTest {
@Test @Test
public void testDelete() { public void testDelete() {
OrganizationRepresentation expected = createRepresentation(); OrganizationRepresentation expected = createOrganization();
OrganizationResource organization = testRealm().organizations().get(expected.getId()); OrganizationResource organization = testRealm().organizations().get(expected.getId());
try (Response response = organization.delete()) { try (Response response = organization.delete()) {
@ -95,26 +93,4 @@ public class OrganizationTest extends AbstractAdminTest {
fail("should be deleted"); fail("should be deleted");
} catch (NotFoundException ignore) {} } catch (NotFoundException ignore) {}
} }
private OrganizationRepresentation createRepresentation() {
return createRepresentation("neworg");
}
private OrganizationRepresentation createRepresentation(String name) {
OrganizationRepresentation org = new OrganizationRepresentation();
org.setName(name);
String id;
try (Response response = testRealm().organizations().create(org)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
id = ApiUtil.getCreatedId(response);
}
org.setId(id);
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());
return org;
}
} }