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.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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue