Manage a single identity provider for an organization

Closes #28272

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2024-04-03 20:57:46 +02:00 committed by Pedro Igor
parent 0327787645
commit 00ce3e34bd
12 changed files with 501 additions and 71 deletions

View file

@ -0,0 +1,46 @@
/*
* 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.POST;
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.IdentityProviderRepresentation;
public interface OrganizationIdentityProviderResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response create(IdentityProviderRepresentation idpRepresentation);
@GET
@Produces(MediaType.APPLICATION_JSON)
IdentityProviderRepresentation toRepresentation();
@PUT
@Consumes(MediaType.APPLICATION_JSON)
Response update(IdentityProviderRepresentation idpRepresentation);
@DELETE
Response delete();
}

View file

@ -42,4 +42,7 @@ public interface OrganizationResource {
@Path("members") @Path("members")
OrganizationMembersResource members(); OrganizationMembersResource members();
@Path("identity-provider")
OrganizationIdentityProviderResource identityProvider();
} }

View file

@ -28,8 +28,6 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapKeyColumn; import jakarta.persistence.MapKeyColumn;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.util.Map; import java.util.Map;
@ -38,9 +36,6 @@ import java.util.Map;
*/ */
@Entity @Entity
@Table(name="IDENTITY_PROVIDER") @Table(name="IDENTITY_PROVIDER")
@NamedQueries({
@NamedQuery(name="findIdentityProviderByAlias", query="select identityProvider from IdentityProviderEntity identityProvider where identityProvider.alias = :alias")
})
public class IdentityProviderEntity { public class IdentityProviderEntity {
@Id @Id

View file

@ -34,9 +34,12 @@ import jakarta.persistence.Table;
public class OrganizationEntity { public class OrganizationEntity {
@Id @Id
@Column(name="ID", length = 36) @Column(name = "ID", length = 36)
@Access(AccessType.PROPERTY) @Access(AccessType.PROPERTY)
protected String id; private String id;
@Column(name = "NAME")
private String name;
@Column(name = "REALM_ID") @Column(name = "REALM_ID")
private String realmId; private String realmId;
@ -44,8 +47,8 @@ public class OrganizationEntity {
@Column(name = "GROUP_ID") @Column(name = "GROUP_ID")
private String groupId; private String groupId;
@Column(name="NAME") @Column(name = "IPD_ALIAS")
protected String name; private String idpAlias;
public String getId() { public String getId() {
return id; return id;
@ -55,6 +58,10 @@ public class OrganizationEntity {
this.id = id; this.id = id;
} }
public void setName(String name) {
this.name = name;
}
public String getRealmId() { public String getRealmId() {
return realmId; return realmId;
} }
@ -75,8 +82,12 @@ public class OrganizationEntity {
return name; return name;
} }
public void setName(String name) { public String getIdpAlias() {
this.name = name; return idpAlias;
}
public void setIdpAlias(String idpAlias) {
this.idpAlias = idpAlias;
} }
@Override @Override

View file

@ -27,7 +27,9 @@ 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.GroupModel;
import org.keycloak.models.GroupProvider; import org.keycloak.models.GroupProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException; 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;
@ -71,15 +73,17 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override @Override
public boolean remove(OrganizationModel organization) { public boolean remove(OrganizationModel organization) {
OrganizationEntity entity = getEntity(organization.getId());
GroupModel group = getOrganizationGroup(organization); GroupModel group = getOrganizationGroup(organization);
//TODO: won't scale, requires a better mechanism for bulk deleting users //TODO: won't scale, requires a better mechanism for bulk deleting users
userProvider.getGroupMembersStream(realm, group).forEach(userModel -> userProvider.removeUser(realm, userModel)); userProvider.getGroupMembersStream(realm, group).forEach(userModel -> userProvider.removeUser(realm, userModel));
groupProvider.removeGroup(realm, group); groupProvider.removeGroup(realm, group);
OrganizationAdapter adapter = getAdapter(organization.getId()); realm.removeIdentityProviderByAlias(entity.getIdpAlias());
em.remove(adapter.getEntity()); em.remove(entity);
return true; return true;
} }
@ -92,12 +96,11 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override @Override
public boolean addMember(OrganizationModel organization, UserModel user) { public boolean addMember(OrganizationModel organization, UserModel user) {
throwExceptionIfOrganizationIsNull(organization); throwExceptionIfObjectIsNull(organization, "Organization");
if (user == null) { throwExceptionIfObjectIsNull(user, "User");
throw new ModelException("User can not be null");
} OrganizationEntity entity = getEntity(organization.getId());
OrganizationAdapter adapter = getAdapter(organization.getId()); GroupModel group = groupProvider.getGroupById(realm, entity.getGroupId());
GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId());
if (user.isMemberOf(group)) { if (user.isMemberOf(group)) {
return false; return false;
@ -108,14 +111,15 @@ public class JpaOrganizationProvider implements OrganizationProvider {
} }
user.joinGroup(group); user.joinGroup(group);
user.setSingleAttribute(USER_ORGANIZATION_ATTRIBUTE, adapter.getId()); user.setSingleAttribute(USER_ORGANIZATION_ATTRIBUTE, entity.getId());
return true; return true;
} }
@Override @Override
public OrganizationModel getById(String id) { public OrganizationModel getById(String id) {
return getAdapter(id, false); OrganizationEntity entity = getEntity(id, false);
return entity == null ? null : new OrganizationAdapter(realm, entity);
} }
@Override @Override
@ -129,16 +133,15 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override @Override
public Stream<UserModel> getMembersStream(OrganizationModel organization) { public Stream<UserModel> getMembersStream(OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization); throwExceptionIfObjectIsNull(organization, "Organization");
OrganizationAdapter adapter = getAdapter(organization.getId()); GroupModel group = getOrganizationGroup(organization);
GroupModel group = getOrganizationGroup(adapter);
return userProvider.getGroupMembersStream(realm, group); return userProvider.getGroupMembersStream(realm, group);
} }
@Override @Override
public UserModel getMemberById(OrganizationModel organization, String id) { public UserModel getMemberById(OrganizationModel organization, String id) {
throwExceptionIfOrganizationIsNull(organization); throwExceptionIfObjectIsNull(organization, "Organization");
UserModel user = userProvider.getUserById(realm, id); UserModel user = userProvider.getUserById(realm, id);
if (user == null) { if (user == null) {
@ -156,9 +159,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override @Override
public OrganizationModel getByMember(UserModel member) { public OrganizationModel getByMember(UserModel member) {
if (member == null) { throwExceptionIfObjectIsNull(member, "User");
throw new ModelException("User can not be null");
}
String orgId = member.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE); String orgId = member.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);
@ -169,16 +170,47 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return getById(orgId); return getById(orgId);
} }
@Override
public boolean addIdentityProvider(OrganizationModel organization, IdentityProviderModel identityProvider) {
throwExceptionIfObjectIsNull(organization, "Organization");
throwExceptionIfObjectIsNull(identityProvider, "Identity provider");
OrganizationEntity organizationEntity = getEntity(organization.getId());
organizationEntity.setIdpAlias(identityProvider.getAlias());
return true;
}
@Override
public IdentityProviderModel getIdentityProvider(OrganizationModel organization) {
throwExceptionIfObjectIsNull(organization, "Organization");
throwExceptionIfObjectIsNull(organization.getId(), "Organization ID");
OrganizationEntity organizationEntity = getEntity(organization.getId());
// realm and its IDPs are cached
return realm.getIdentityProviderByAlias(organizationEntity.getIdpAlias());
}
@Override
public boolean removeIdentityProvider(OrganizationModel organization) {
throwExceptionIfObjectIsNull(organization, "Organization");
OrganizationEntity organizationEntity = getEntity(organization.getId());
organizationEntity.setIdpAlias(null);
return true;
}
@Override @Override
public void close() { public void close() {
} }
private OrganizationAdapter getAdapter(String id) { /**
return getAdapter(id, true); * @throws ModelException if there is no entity with given {@code id}
*/
private OrganizationEntity getEntity(String id) {
return getEntity(id, true);
} }
private OrganizationAdapter getAdapter(String id, boolean failIfNotFound) { private OrganizationEntity getEntity(String id, boolean failIfNotFound) {
OrganizationEntity entity = em.find(OrganizationEntity.class, id); OrganizationEntity entity = em.find(OrganizationEntity.class, id);
if (entity == null) { if (entity == null) {
@ -192,19 +224,17 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throw new ModelException("Organization [" + entity.getId() + " does not belong to realm [" + realm.getId() + "]"); throw new ModelException("Organization [" + entity.getId() + " does not belong to realm [" + realm.getId() + "]");
} }
return new OrganizationAdapter(realm, entity); return entity;
} }
private GroupModel createOrganizationGroup(String name) { private GroupModel createOrganizationGroup(String name) {
if (name == null) { throwExceptionIfObjectIsNull(name, "Name of the group");
throw new ModelException("name can not be null");
}
String groupName = getCanonicalGroupName(name); String groupName = getCanonicalGroupName(name);
GroupModel group = groupProvider.getGroupByName(realm, null, name); GroupModel group = groupProvider.getGroupByName(realm, null, name);
if (group != null) { if (group != null) {
throw new ModelException("A group with the same name already exist and it is bound to different organization"); throw new ModelDuplicateException("A group with the same name already exist and it is bound to different organization");
} }
return groupProvider.createGroup(realm, groupName); return groupProvider.createGroup(realm, groupName);
@ -215,21 +245,21 @@ public class JpaOrganizationProvider implements OrganizationProvider {
} }
private GroupModel getOrganizationGroup(OrganizationModel organization) { private GroupModel getOrganizationGroup(OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization); throwExceptionIfObjectIsNull(organization, "Organization");
OrganizationAdapter adapter = getAdapter(organization.getId()); OrganizationEntity entity = getEntity(organization.getId());
GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId()); GroupModel group = groupProvider.getGroupById(realm, entity.getGroupId());
if (group == null) { if (group == null) {
throw new ModelException("Organization group " + adapter.getGroupId() + " not found"); throw new ModelException("Organization group " + entity.getGroupId() + " not found");
} }
return group; return group;
} }
private void throwExceptionIfOrganizationIsNull(OrganizationModel organization) { private void throwExceptionIfObjectIsNull(Object object, String objectName) {
if (organization == null) { if (object == null) {
throw new ModelException("organization can not be null"); throw new ModelException(String.format("%s cannot be null", objectName));
} }
} }
} }

View file

@ -85,6 +85,7 @@
<column name="GROUP_ID" type="VARCHAR(255)"> <column name="GROUP_ID" type="VARCHAR(255)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="IPD_ALIAS" type="VARCHAR(255)" />
<column name="NAME" type="VARCHAR(255)"> <column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>

View file

@ -17,7 +17,7 @@
package org.keycloak.organization; package org.keycloak.organization;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -29,7 +29,7 @@ import org.keycloak.provider.Provider;
public interface OrganizationProvider extends Provider { public interface OrganizationProvider extends Provider {
/** /**
* Creates a new organization with given {@code name} to the given realm. * Creates a new organization with given {@code name} to the realm.
* The internal ID of the organization will be created automatically. * The internal ID of the organization will be created automatically.
* @param name String name of the organization. * @param name String name of the organization.
* @throws ModelDuplicateException If there is already an organization with the given name * @throws ModelDuplicateException If there is already an organization with the given name
@ -41,60 +41,85 @@ public interface OrganizationProvider extends Provider {
* Returns a {@link OrganizationModel} by its {@code id}; * Returns a {@link OrganizationModel} by its {@code id};
* *
* @param id the id of an organization * @param id the id of an organization
* @return the organization with the given {@code id} * @return the organization with the given {@code id} or {@code null} if there is no such an organization.
*/ */
OrganizationModel getById(String id); OrganizationModel getById(String id);
/** /**
* Removes the given organization from the given realm. * Removes the given organization from the realm together with the data associated with it, e.g. its members etc.
* *
* @param organization Organization to be removed. * @param organization Organization to be removed.
* @return true if the organization was removed, false if group doesn't exist or doesn't belong to the given realm * @throws ModelException if the organization doesn't exist or doesn't belong to the realm.
* @return {@code true} if the organization was removed, {@code false} otherwise
*/ */
boolean remove(OrganizationModel organization); boolean remove(OrganizationModel organization);
/** /**
* Removes all organizations from the given realm. * Removes all organizations from the realm.
*/ */
void removeAll(); void removeAll();
/** /**
* Adds the give {@link UserModel} as a member of the given {@link OrganizationModel}. * Adds the given {@link UserModel} as a member of the given {@link OrganizationModel}.
* *
* @param organization the organization * @param organization the organization
* @param user the user * @param user the user
* @throws ModelException if the {@link UserModel} is member of different organization
* @return {@code true} if the user was added as a member. Otherwise, returns {@code false} * @return {@code true} if the user was added as a member. Otherwise, returns {@code false}
*/ */
boolean addMember(OrganizationModel organization, UserModel user); boolean addMember(OrganizationModel organization, UserModel user);
/** /**
* Returns the organizations of the given realm as a stream. * Returns the organizations of the realm as a stream.
* @return Stream of the organizations. Never returns {@code null}. * @return Stream of the organizations. Never returns {@code null}.
*/ */
Stream<OrganizationModel> getAllStream(); Stream<OrganizationModel> getAllStream();
/** /**
* Returns the members of a given {@code organization}. * Returns the members of a given {@link OrganizationModel}.
* *
* @param organization the organization * @param organization the organization
* @return the organization with the given {@code id} * @return Stream of the members. Never returns {@code null}.
*/ */
Stream<UserModel> getMembersStream(OrganizationModel organization); Stream<UserModel> getMembersStream(OrganizationModel organization);
/** /**
* Returns the member of an {@code organization} by its {@code id}. * Returns the member of the {@link OrganizationModel} by its {@code id}.
* *
* @param organization the organization * @param organization the organization
* @param id the member id * @param id the member id
* @return the organization with the given {@code id} * @return the member of the {@link OrganizationModel} with the given {@code id}
*/ */
UserModel getMemberById(OrganizationModel organization, String id); UserModel getMemberById(OrganizationModel organization, String id);
/** /**
* Returns the {@link OrganizationModel} that a {@code member} belongs to. * Returns the {@link OrganizationModel} that the {@code member} belongs to.
* *
* @param member the member of a organization * @param member the member of a organization
* @return the organization the {@code member} belongs to * @return the organization the {@code member} belongs to or {@code null} if the user doesn't belong to any.
*/ */
OrganizationModel getByMember(UserModel member); OrganizationModel getByMember(UserModel member);
/**
* Associate the given {@link IdentityProviderModel} with the given {@link OrganizationModel}.
*
* @param organization the organization
* @param identityProvider the identityProvider
* @return {@code true} if the identityProvider was associated with the organization. Otherwise, returns {@code false}
*/
boolean addIdentityProvider(OrganizationModel organization, IdentityProviderModel identityProvider);
/**
* @param organization the organization
* @return The identityProvider associated with a given {@code organization} or {@code null} if there is none.
*/
IdentityProviderModel getIdentityProvider(OrganizationModel organization);
/**
* Removes the link between the given {@link OrganizationModel} and identity provider associated with it if such a link exists.
*
* @param organization the organization
* @return {@code true} if the link was removed, {@code false} otherwise
*/
boolean removeIdentityProvider(OrganizationModel organization);
} }

View file

@ -0,0 +1,176 @@
/*
* 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 jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
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 java.util.Objects;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.IdentityProviderResource;
import org.keycloak.services.resources.admin.IdentityProvidersResource;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@Provider
public class OrganizationIdentityProviderResource {
private final KeycloakSession session;
private final RealmModel realm;
private final OrganizationProvider organizationProvider;
private final OrganizationModel organization;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent;
public OrganizationIdentityProviderResource() {
// needed for registering to the JAX-RS stack
this(null, null, null, null);
}
public OrganizationIdentityProviderResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session;
this.realm = session == null ? null : session.getContext().getRealm();
this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class);
this.organization = organization;
this.auth = auth;
this.adminEvent = adminEvent;
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addIdentityProvider(IdentityProviderRepresentation providerRep) {
IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization);
if (identityProvider != null) {
throw ErrorResponse.error("Organization already assigned with an identity provider.", Status.BAD_REQUEST);
}
//create IdP within the realm
Response response = new IdentityProvidersResource(realm, session, auth, adminEvent).create(providerRep);
if (Status.CREATED.getStatusCode() == response.getStatus()) {
//get the created IdP from session
identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias());
String errorMessage;
try {
if (organizationProvider.addIdentityProvider(organization, identityProvider)) {
return response;
}
errorMessage = "Assigning the Identity provider with the organization was not succesful.";
} catch (ModelException me) {
errorMessage = me.getMessage();
}
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
}
return response;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public IdentityProviderRepresentation getIdentityProvider() {
IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization);
return identityProvider == null ? null : toRepresentation(identityProvider);
}
@DELETE
public Response delete() {
IdentityProviderModel identityProvider = getIdentityProviderModel();
Response response = getIdentityProviderResource(identityProvider).delete();
// remove link between IdP and the organization if the IdP deletetion was successful
if (Status.NO_CONTENT.getStatusCode() == response.getStatus()) {
String errorMessage;
try {
if (organizationProvider.removeIdentityProvider(organization)) {
return response;
}
errorMessage = "Removing the Identity provider from the organization was not succesful.";
} catch (ModelException me) {
errorMessage = me.getMessage();
}
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
}
return response;
}
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response update(IdentityProviderRepresentation providerRep) {
IdentityProviderModel identityProvider = getIdentityProviderModel();
Response response = getIdentityProviderResource(identityProvider).update(providerRep);
//update link between IdP and the organization if the update of IdP was successful and the IdP alias differs
if (Status.NO_CONTENT.getStatusCode() == response.getStatus() &&
! Objects.equals(identityProvider.getAlias(), providerRep.getAlias())) {
//get the updated IdP from session
identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias());
String errorMessage;
try {
if (organizationProvider.removeIdentityProvider(organization) &&
organizationProvider.addIdentityProvider(organization, identityProvider)) {
return response;
}
errorMessage = "Updating the Identity provider was not succesful.";
} catch (ModelException me) {
errorMessage = me.getMessage();
}
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
}
return response;
}
private IdentityProviderRepresentation toRepresentation(IdentityProviderModel idp) {
return ModelToRepresentation.toRepresentation(realm, idp);
}
private IdentityProviderResource getIdentityProviderResource(IdentityProviderModel idp) {
return new IdentityProviderResource(auth, realm, session, idp, adminEvent);
}
private IdentityProviderModel getIdentityProviderModel() {
IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization);
if (identityProvider == null) {
throw ErrorResponse.error("Organization doesn't have assigned an identity provider.", Status.NOT_FOUND);
}
return identityProvider;
}
}

View file

@ -19,7 +19,6 @@ package org.keycloak.organization.admin.resource;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes; 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;
@ -39,11 +38,11 @@ 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.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
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.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.UserResource; import org.keycloak.services.resources.admin.UserResource;
import org.keycloak.services.resources.admin.UsersResource; import org.keycloak.services.resources.admin.UsersResource;
@ -82,26 +81,26 @@ public class OrganizationMemberResource {
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public Response addMember(UserRepresentation rep) { public Response addMember(UserRepresentation rep) {
if (rep == null || !Objects.equals(rep.getUsername(), rep.getEmail())) { if (rep == null || !Objects.equals(rep.getUsername(), rep.getEmail())) {
throw new BadRequestException("To add a member to the organization it is expected the username and the email is the same."); throw ErrorResponse.error("To add a member to the organization it is expected the username and the email is the same.", Status.BAD_REQUEST);
} }
UsersResource usersResource = new UsersResource(session, auth, adminEvent); UsersResource usersResource = new UsersResource(session, auth, adminEvent);
Response response = usersResource.createUser(rep); Response response = usersResource.createUser(rep);
if (Status.CREATED.getStatusCode() == response.getStatus()) { if (Status.CREATED.getStatusCode() == response.getStatus()) {
RealmModel realm = session.getContext().getRealm();
UserModel member = session.users().getUserByUsername(realm, rep.getEmail());
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
UserModel member = session.users().getUserByUsername(realm, rep.getEmail());
String errorMessage;
try { try {
if (provider.addMember(organization, member)) { if (provider.addMember(organization, member)) {
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(member.getId()).build()).build(); return response;
} }
errorMessage = "Assigning the User as member of the organization was not succesful.";
} catch (ModelException me) { } catch (ModelException me) {
throw new BadRequestException(me.getMessage()); errorMessage = me.getMessage();
} }
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
throw new BadRequestException();
} }
return response; return response;
@ -118,7 +117,7 @@ public class OrganizationMemberResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public UserRepresentation get(@PathParam("id") String id) { public UserRepresentation get(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) { if (StringUtil.isBlank(id)) {
throw new BadRequestException(); throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
} }
return toRepresentation(getMember(id)); return toRepresentation(getMember(id));
@ -128,7 +127,7 @@ public class OrganizationMemberResource {
@DELETE @DELETE
public Response delete(@PathParam("id") String id) { public Response delete(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) { if (StringUtil.isBlank(id)) {
throw new BadRequestException(); throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
} }
UserModel member = getMember(id); UserModel member = getMember(id);
@ -148,7 +147,7 @@ public class OrganizationMemberResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public OrganizationRepresentation getOrganization(@PathParam("id") String id) { public OrganizationRepresentation getOrganization(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) { if (StringUtil.isBlank(id)) {
throw new BadRequestException(); throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
} }
UserModel member = getMember(id); UserModel member = getMember(id);

View file

@ -119,6 +119,11 @@ public class OrganizationResource {
return new OrganizationMemberResource(session, organization, auth, adminEvent); return new OrganizationMemberResource(session, organization, auth, adminEvent);
} }
@Path("{id}/identity-provider")
public OrganizationIdentityProviderResource identityProvider(@PathParam("id") String id) {
return new OrganizationIdentityProviderResource(session, getOrganization(id), auth, adminEvent);
}
private OrganizationModel getOrganization(String id) { private OrganizationModel getOrganization(String id) {
if (id == null) { if (id == null) {
throw new BadRequestException(); throw new BadRequestException();

View file

@ -0,0 +1,138 @@
/*
* 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.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import jakarta.ws.rs.core.Response;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.ORGANIZATION)
public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
private final String idpAlias = "org-identity-provider";
@Before
public void addCleanups() {
addCleanupIdP(idpAlias);
}
@Test
public void testCRUD() {
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider();
//create, read
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
try (Response response = orgIdPResource.create(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
}
idpRepresentation = orgIdPResource.toRepresentation();
assertThat(idpRepresentation.getAlias(), equalTo(idpAlias));
String updatedIdpAlias = "updated-org-identity-provider";
//update
idpRepresentation.setAlias(updatedIdpAlias);
try (Response response = orgIdPResource.update(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
addCleanupIdP(updatedIdpAlias);
}
assertThat(orgIdPResource.toRepresentation().getAlias(), equalTo(updatedIdpAlias));
//delete
try (Response response = orgIdPResource.delete()) {
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
}
assertThat(orgIdPResource.toRepresentation(), nullValue());
}
@Test
public void tryCreateSecondIdp() {
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider();
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
try (Response response = orgIdPResource.create(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
}
idpRepresentation.setAlias("another-idp");
try (Response response = orgIdPResource.create(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.BAD_REQUEST.getStatusCode()));
}
}
@Test(expected = jakarta.ws.rs.NotFoundException.class)
public void removingOrgShouldRemoveIdP() {
OrganizationRepresentation orgRep = createOrganization();
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
try (Response response = orgIdPResource.create(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
}
try (Response response = orgResource.delete()) {
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
}
testRealm().identityProviders().get(idpAlias).toRepresentation();
}
@Test
public void tryUpdateAndRemoveIdPNotAssignedToOrg() {
OrganizationRepresentation orgRep = createOrganization();
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
//create IdP in realm not bound to Org
testRealm().identityProviders().create(idpRepresentation).close();
try (Response response = orgIdPResource.update(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
}
try (Response response = orgIdPResource.delete()) {
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
}
}
private IdentityProviderRepresentation createRep(String alias, String providerId) {
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
idp.setAlias(alias);
idp.setDisplayName(alias);
idp.setProviderId(providerId);
idp.setEnabled(true);
return idp;
}
private void addCleanupIdP(String alias) {
getCleanup().addCleanup(() -> testRealm().identityProviders().get(alias).remove());
}
}

View file

@ -17,3 +17,4 @@ TransactionsTest
UserProfileTest UserProfileTest
org.keycloak.testsuite.admin.** org.keycloak.testsuite.admin.**
org.keycloak.testsuite.authz.**ManagementTest org.keycloak.testsuite.authz.**ManagementTest
org.keycloak.testsuite.organization.admin.**