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")
OrganizationMembersResource members();
@Path("identity-provider")
OrganizationIdentityProviderResource identityProvider();
}

View file

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

View file

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

View file

@ -27,7 +27,9 @@ import jakarta.persistence.TypedQuery;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
@ -71,15 +73,17 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override
public boolean remove(OrganizationModel organization) {
OrganizationEntity entity = getEntity(organization.getId());
GroupModel group = getOrganizationGroup(organization);
//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);
OrganizationAdapter adapter = getAdapter(organization.getId());
realm.removeIdentityProviderByAlias(entity.getIdpAlias());
em.remove(adapter.getEntity());
em.remove(entity);
return true;
}
@ -92,12 +96,11 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override
public boolean addMember(OrganizationModel organization, UserModel user) {
throwExceptionIfOrganizationIsNull(organization);
if (user == null) {
throw new ModelException("User can not be null");
}
OrganizationAdapter adapter = getAdapter(organization.getId());
GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId());
throwExceptionIfObjectIsNull(organization, "Organization");
throwExceptionIfObjectIsNull(user, "User");
OrganizationEntity entity = getEntity(organization.getId());
GroupModel group = groupProvider.getGroupById(realm, entity.getGroupId());
if (user.isMemberOf(group)) {
return false;
@ -108,14 +111,15 @@ public class JpaOrganizationProvider implements OrganizationProvider {
}
user.joinGroup(group);
user.setSingleAttribute(USER_ORGANIZATION_ATTRIBUTE, adapter.getId());
user.setSingleAttribute(USER_ORGANIZATION_ATTRIBUTE, entity.getId());
return true;
}
@Override
public OrganizationModel getById(String id) {
return getAdapter(id, false);
OrganizationEntity entity = getEntity(id, false);
return entity == null ? null : new OrganizationAdapter(realm, entity);
}
@Override
@ -129,16 +133,15 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override
public Stream<UserModel> getMembersStream(OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter adapter = getAdapter(organization.getId());
GroupModel group = getOrganizationGroup(adapter);
throwExceptionIfObjectIsNull(organization, "Organization");
GroupModel group = getOrganizationGroup(organization);
return userProvider.getGroupMembersStream(realm, group);
}
@Override
public UserModel getMemberById(OrganizationModel organization, String id) {
throwExceptionIfOrganizationIsNull(organization);
throwExceptionIfObjectIsNull(organization, "Organization");
UserModel user = userProvider.getUserById(realm, id);
if (user == null) {
@ -156,9 +159,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override
public OrganizationModel getByMember(UserModel member) {
if (member == null) {
throw new ModelException("User can not be null");
}
throwExceptionIfObjectIsNull(member, "User");
String orgId = member.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);
@ -169,16 +170,47 @@ public class JpaOrganizationProvider implements OrganizationProvider {
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
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);
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() + "]");
}
return new OrganizationAdapter(realm, entity);
return entity;
}
private GroupModel createOrganizationGroup(String name) {
if (name == null) {
throw new ModelException("name can not be null");
}
throwExceptionIfObjectIsNull(name, "Name of the group");
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");
throw new ModelDuplicateException("A group with the same name already exist and it is bound to different organization");
}
return groupProvider.createGroup(realm, groupName);
@ -215,21 +245,21 @@ public class JpaOrganizationProvider implements OrganizationProvider {
}
private GroupModel getOrganizationGroup(OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter adapter = getAdapter(organization.getId());
throwExceptionIfObjectIsNull(organization, "Organization");
OrganizationEntity entity = getEntity(organization.getId());
GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId());
GroupModel group = groupProvider.getGroupById(realm, entity.getGroupId());
if (group == null) {
throw new ModelException("Organization group " + adapter.getGroupId() + " not found");
throw new ModelException("Organization group " + entity.getGroupId() + " not found");
}
return group;
}
private void throwExceptionIfOrganizationIsNull(OrganizationModel organization) {
if (organization == null) {
throw new ModelException("organization can not be null");
private void throwExceptionIfObjectIsNull(Object object, String objectName) {
if (object == null) {
throw new ModelException(String.format("%s cannot be null", objectName));
}
}
}

View file

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

View file

@ -17,7 +17,7 @@
package org.keycloak.organization;
import java.util.stream.Stream;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel;
@ -29,7 +29,7 @@ import org.keycloak.provider.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.
* @param name String name of the organization.
* @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};
*
* @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);
/**
* 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.
* @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);
/**
* Removes all organizations from the given realm.
* Removes all organizations from the realm.
*/
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 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}
*/
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}.
*/
Stream<OrganizationModel> getAllStream();
/**
* Returns the members of a given {@code organization}.
* Returns the members of a given {@link OrganizationModel}.
*
* @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);
/**
* 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 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);
/**
* 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
* @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);
/**
* 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 jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
@ -39,11 +38,11 @@ import org.keycloak.models.ModelException;
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.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.UserResource;
import org.keycloak.services.resources.admin.UsersResource;
@ -82,26 +81,26 @@ public class OrganizationMemberResource {
@Consumes(MediaType.APPLICATION_JSON)
public Response addMember(UserRepresentation rep) {
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);
Response response = usersResource.createUser(rep);
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);
String errorMessage;
try {
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) {
throw new BadRequestException(me.getMessage());
errorMessage = me.getMessage();
}
throw new BadRequestException();
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
}
return response;
@ -118,7 +117,7 @@ public class OrganizationMemberResource {
@Produces(MediaType.APPLICATION_JSON)
public UserRepresentation get(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
}
return toRepresentation(getMember(id));
@ -128,7 +127,7 @@ public class OrganizationMemberResource {
@DELETE
public Response delete(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
}
UserModel member = getMember(id);
@ -148,7 +147,7 @@ public class OrganizationMemberResource {
@Produces(MediaType.APPLICATION_JSON)
public OrganizationRepresentation getOrganization(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
}
UserModel member = getMember(id);

View file

@ -119,6 +119,11 @@ public class OrganizationResource {
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) {
if (id == null) {
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
org.keycloak.testsuite.admin.**
org.keycloak.testsuite.authz.**ManagementTest
org.keycloak.testsuite.organization.admin.**