Manage a single identity provider for an organization
Closes #28272 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
parent
0327787645
commit
00ce3e34bd
12 changed files with 501 additions and 71 deletions
|
@ -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();
|
||||
}
|
|
@ -42,4 +42,7 @@ public interface OrganizationResource {
|
|||
|
||||
@Path("members")
|
||||
OrganizationMembersResource members();
|
||||
|
||||
@Path("identity-provider")
|
||||
OrganizationIdentityProviderResource identityProvider();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -17,3 +17,4 @@ TransactionsTest
|
|||
UserProfileTest
|
||||
org.keycloak.testsuite.admin.**
|
||||
org.keycloak.testsuite.authz.**ManagementTest
|
||||
org.keycloak.testsuite.organization.admin.**
|
||||
|
|
Loading…
Reference in a new issue