Allow managing members for an organization
Closes #27934 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
4154d27941
commit
32541f19a3
15 changed files with 749 additions and 58 deletions
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -21,6 +21,7 @@ import jakarta.ws.rs.Consumes;
|
|||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
@ -38,4 +39,7 @@ public interface OrganizationResource {
|
|||
|
||||
@DELETE
|
||||
Response delete();
|
||||
|
||||
@Path("members")
|
||||
OrganizationMembersResource members();
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import jakarta.persistence.Table;
|
|||
@Table(name="ORGANIZATION")
|
||||
@Entity
|
||||
@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")
|
||||
})
|
||||
public class OrganizationEntity {
|
||||
|
@ -42,6 +41,9 @@ public class OrganizationEntity {
|
|||
@Column(name = "REALM_ID")
|
||||
private String realmId;
|
||||
|
||||
@Column(name = "GROUP_ID")
|
||||
private String groupId;
|
||||
|
||||
@Column(name="NAME")
|
||||
protected String name;
|
||||
|
||||
|
@ -61,6 +63,14 @@ public class OrganizationEntity {
|
|||
this.realmId = realm;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(String groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
|
|
@ -17,17 +17,22 @@
|
|||
|
||||
package org.keycloak.organization.jpa;
|
||||
|
||||
import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE;
|
||||
import static org.keycloak.utils.StreamsUtil.closing;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
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.ModelException;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
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.utils.KeycloakModelUtils;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
|
@ -35,59 +40,84 @@ import org.keycloak.organization.OrganizationProvider;
|
|||
public class JpaOrganizationProvider implements OrganizationProvider {
|
||||
|
||||
private final EntityManager em;
|
||||
private final GroupProvider groupProvider;
|
||||
private final KeycloakSession session;
|
||||
private final UserProvider userProvider;
|
||||
|
||||
public JpaOrganizationProvider(KeycloakSession session) {
|
||||
JpaConnectionProvider jpaProvider = session.getProvider(JpaConnectionProvider.class);
|
||||
this.em = jpaProvider.getEntityManager();
|
||||
this.session = session;
|
||||
em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
groupProvider = session.groups();
|
||||
userProvider = session.users();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrganizationModel createOrganization(RealmModel realm, String name) {
|
||||
throwExceptionIfRealmIsNull(realm);
|
||||
GroupModel group = createOrganizationGroup(realm, name);
|
||||
OrganizationEntity entity = new OrganizationEntity();
|
||||
|
||||
entity.setId(KeycloakModelUtils.generateId());
|
||||
entity.setGroupId(group.getId());
|
||||
entity.setRealmId(realm.getId());
|
||||
entity.setName(name);
|
||||
|
||||
em.persist(entity);
|
||||
|
||||
return new OrganizationAdapter(entity);
|
||||
return new OrganizationAdapter(entity, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeOrganization(RealmModel realm, OrganizationModel organization) {
|
||||
throwExceptionIfRealmIsNull(realm);
|
||||
throwExceptionIfOrganizationIsNull(organization);
|
||||
OrganizationAdapter toRemove = getAdapter(realm, organization.getId());
|
||||
throwExceptionIfOrganizationIsNull(toRemove);
|
||||
GroupModel group = getOrganizationGroup(realm, organization);
|
||||
|
||||
if (!toRemove.getRealm().equals(realm.getId())) {
|
||||
throw new IllegalArgumentException("Organization [" + organization.getId() + " does not belong to realm [" + realm.getId() + "]");
|
||||
}
|
||||
//TODO: won't scale, requires a better mechanism for bulk deleting users
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeOrganizations(RealmModel realm) {
|
||||
throwExceptionIfRealmIsNull(realm);
|
||||
Query query = em.createNamedQuery("deleteByRealm");
|
||||
//TODO: won't scale, requires a better mechanism for bulk deleting organizations within a realm
|
||||
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
|
||||
public OrganizationModel getOrganizationById(RealmModel realm, String id) {
|
||||
return getAdapter(realm, id);
|
||||
return getAdapter(realm, id, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<OrganizationModel> getOrganizationsStream(RealmModel realm) {
|
||||
throwExceptionIfRealmIsNull(realm);
|
||||
TypedQuery<String> query = em.createNamedQuery("getByRealm", String.class);
|
||||
|
||||
query.setParameter("realmId", realm.getId());
|
||||
|
@ -95,34 +125,119 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
|||
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
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (entity == null) {
|
||||
if (failIfNotFound) {
|
||||
throw new ModelException("Organization [" + id + "] does not exist");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (organization == null) {
|
||||
throw new IllegalArgumentException("organization can not be null");
|
||||
throw new ModelException("organization can not be null");
|
||||
}
|
||||
}
|
||||
|
||||
private void throwExceptionIfRealmIsNull(RealmModel realm) {
|
||||
if (realm == null) {
|
||||
throw new IllegalArgumentException("realm can not be null");
|
||||
throw new ModelException("realm can not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,16 +17,19 @@
|
|||
|
||||
package org.keycloak.organization.jpa;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.jpa.JpaModel;
|
||||
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 KeycloakSession session;
|
||||
|
||||
public OrganizationAdapter(OrganizationEntity entity) {
|
||||
public OrganizationAdapter(OrganizationEntity entity, KeycloakSession session) {
|
||||
this.entity = entity;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -38,6 +41,10 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel<Organiza
|
|||
return entity.getRealmId();
|
||||
}
|
||||
|
||||
String getGroupId() {
|
||||
return entity.getGroupId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
entity.setName(name);
|
||||
|
@ -52,4 +59,17 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel<Organiza
|
|||
public OrganizationEntity getEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder()
|
||||
.append("id=")
|
||||
.append(getId())
|
||||
.append(",")
|
||||
.append("name=")
|
||||
.append(getName())
|
||||
.append(",")
|
||||
.append("groupId=")
|
||||
.append(getGroupId()).toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@
|
|||
<column name="REALM_ID" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="GROUP_ID" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
@ -32,6 +35,7 @@
|
|||
|
||||
<addPrimaryKey columnNames="ID" tableName="ORGANIZATION"/>
|
||||
<addUniqueConstraint tableName="ORGANIZATION" columnNames="REALM_ID, NAME" constraintName="UK_ORG_NAME"/>
|
||||
<addUniqueConstraint tableName="ORGANIZATION" columnNames="GROUP_ID" constraintName="UK_ORG_GROUP"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="keycloak" id="unique-consentuser">
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.stream.Stream;
|
|||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface OrganizationProvider extends Provider {
|
||||
|
@ -35,6 +36,13 @@ public interface OrganizationProvider extends Provider {
|
|||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
|
@ -52,10 +60,48 @@ public interface OrganizationProvider extends Provider {
|
|||
*/
|
||||
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.
|
||||
* @param realm Realm.
|
||||
* @return Stream of the organizations. Never returns {@code null}.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -19,10 +19,11 @@ package org.keycloak.models;
|
|||
|
||||
public interface OrganizationModel {
|
||||
|
||||
String USER_ORGANIZATION_ATTRIBUTE = "kc.org";
|
||||
|
||||
String getId();
|
||||
|
||||
void setName(String name);
|
||||
|
||||
String getName();
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -37,6 +37,8 @@ import org.keycloak.models.OrganizationModel;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
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;
|
||||
|
||||
@Provider
|
||||
|
@ -44,15 +46,19 @@ public class OrganizationResource {
|
|||
|
||||
private final KeycloakSession session;
|
||||
private final OrganizationProvider provider;
|
||||
private final AdminPermissionEvaluator auth;
|
||||
private final AdminEventBuilder adminEvent;
|
||||
|
||||
public OrganizationResource() {
|
||||
// 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.provider = session == null ? null : session.getProvider(OrganizationProvider.class);
|
||||
this.auth = auth;
|
||||
this.adminEvent = adminEvent;
|
||||
}
|
||||
|
||||
@POST
|
||||
|
@ -110,6 +116,14 @@ public class OrganizationResource {
|
|||
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) {
|
||||
if (id == null) {
|
||||
throw new BadRequestException();
|
||||
|
|
|
@ -27,7 +27,7 @@ public class OrganizationResourceProvider implements AdminRealmResourceProvider
|
|||
|
||||
@Override
|
||||
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||
return new OrganizationResource(session);
|
||||
return new OrganizationResource(session, auth, adminEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.")));
|
||||
}
|
||||
}
|
|
@ -34,16 +34,14 @@ import org.junit.Test;
|
|||
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
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;
|
||||
|
||||
@EnableFeature(Feature.ORGANIZATION)
|
||||
public class OrganizationTest extends AbstractAdminTest {
|
||||
public class OrganizationTest extends AbstractOrganizationTest {
|
||||
|
||||
@Test
|
||||
public void testUpdate() {
|
||||
OrganizationRepresentation expected = createRepresentation();
|
||||
OrganizationRepresentation expected = createOrganization();
|
||||
|
||||
assertEquals("neworg", expected.getName());
|
||||
expected.setName("acme");
|
||||
|
@ -61,7 +59,7 @@ public class OrganizationTest extends AbstractAdminTest {
|
|||
|
||||
@Test
|
||||
public void testGet() {
|
||||
OrganizationRepresentation expected = createRepresentation();
|
||||
OrganizationRepresentation expected = createOrganization();
|
||||
OrganizationRepresentation existing = testRealm().organizations().get(expected.getId()).toRepresentation();
|
||||
assertNotNull(existing);
|
||||
assertEquals(expected.getId(), existing.getId());
|
||||
|
@ -73,7 +71,7 @@ public class OrganizationTest extends AbstractAdminTest {
|
|||
List<OrganizationRepresentation> expected = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
expected.add(createRepresentation("org-" + i));
|
||||
expected.add(createOrganization("kc.org." + i));
|
||||
}
|
||||
|
||||
List<OrganizationRepresentation> existing = testRealm().organizations().getAll();
|
||||
|
@ -83,7 +81,7 @@ public class OrganizationTest extends AbstractAdminTest {
|
|||
|
||||
@Test
|
||||
public void testDelete() {
|
||||
OrganizationRepresentation expected = createRepresentation();
|
||||
OrganizationRepresentation expected = createOrganization();
|
||||
OrganizationResource organization = testRealm().organizations().get(expected.getId());
|
||||
|
||||
try (Response response = organization.delete()) {
|
||||
|
@ -95,26 +93,4 @@ public class OrganizationTest extends AbstractAdminTest {
|
|||
fail("should be deleted");
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue