8000 Allow managing members of an organization by pedroigor · Pull Request #28057 · keycloak/keycloak · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Allow managing members of an organization #28057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.admin.client.resource;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.UserRepresentation;

public interface OrganizationMemberResource {

@GET
@Produces(MediaType.APPLICATION_JSON)
UserRepresentation toRepresentation();

@PUT
@Consumes(MediaType.APPLICATION_JSON)
Response update(UserRepresentation organization);

@DELETE
Response delete();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.admin.client.resource;

import java.util.List;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;

public interface OrganizationMembersResource {

@POST
@Consumes(MediaType.APPLICATION_JSON)
Response addMember(UserRepresentation member);

@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> getAll();

@Path("{id}/organization")
@GET
@Produces(MediaType.APPLICATION_JSON)
OrganizationRepresentation getOrganization(@PathParam("id") String id);

@Path("{id}")
OrganizationMemberResource member(@PathParam("id") String id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
Expand All @@ -38,4 +39,7 @@ public interface OrganizationResource {

@DELETE
Response delete();

@Path("members")
OrganizationMembersResource members();
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
@Table(name="ORGANIZATION")
@Entity
@NamedQueries({
@NamedQuery(name="deleteByRealm", query="delete from OrganizationEntity o where o.realmId = :realmId"),
@NamedQuery(name="getByRealm", query="select o.id from OrganizationEntity o where o.realmId = :realmId")
})
public class OrganizationEntity {
Expand All @@ -42,6 +41,9 @@ public class OrganizationEntity {
@Column(name = "REALM_ID")
private String realmId;

@Column(name = "GROUP_ID")
private String groupId;

@Column(name="NAME")
protected String name;

Expand All @@ -61,6 +63,14 @@ public void setRealmId(String realm) {
this.realmId = realm;
}

public String getGroupId() {
return groupId;
}

public void setGroupId(String groupId) {
this.groupId = groupId;
}

public String getName() {
return name;
}
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the IllegalArgumentException should be replaced by ModelException in the file.

Original file line number Diff line number Diff line change
Expand Up @@ -17,112 +17,227 @@

package org.keycloak.organization.jpa;

import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE;
import static org.keycloak.utils.StreamsUtil.closing;

import java.util.stream.Stream;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.OrganizationEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.OrganizationProvider;

public class JpaOrganizationProvider implements OrganizationProvider {

private final EntityManager em;
private final GroupProvider groupProvider;
private final KeycloakSession session;
private final UserProvider userProvider;

public JpaOrganizationProvider(KeycloakSession session) {
JpaConnectionProvider jpaProvider = session.getProvider(JpaConnectionProvider.class);
this.em = jpaProvider.getEntityManager();
this.session = session;
em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
groupProvider = session.groups();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed the groupProvider is used within createdOrganizationGroup method, while in removeOrganization there is session.groups() used. I'd suggest to either use one or another approach.

userProvider = session.users();
}

@Override
public OrganizationModel createOrganization(RealmModel realm, String name) {
throwExceptionIfRealmIsNull(realm);
GroupModel group = createOrganizationGroup(realm, name);
OrganizationEntity entity = new OrganizationEntity();

entity.setId(KeycloakModelUtils.generateId());
entity.setGroupId(group.getId());
entity.setRealmId(realm.getId());
entity.setName(name);

em.persist(entity);

return new OrganizationAdapter(entity);
return new OrganizationAdapter(entity, session);
}

@Override
public boolean removeOrganization(RealmModel realm, OrganizationModel organization) {
throwExceptionIfRealmIsNull(realm);
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter toRemove = getAdapter(realm, organization.getId());
throwExceptionIfOrganizationIsNull(toRemove);
GroupModel group = getOrganizationGroup(realm, organization);

if (!toRemove.getRealm().equals(realm.getId())) {
throw new IllegalArgumentException("Organization [" + organization.getId() + " does not belong to realm [" + realm.getId() + "]");
}
//TODO: won't scale, requires a better mechanism for bulk deleting users
userProvider.getGroupMembersStream(realm, group).forEach(userModel -> userProvider.removeUser(realm, userModel));
groupProvider.removeGroup(realm, group);

OrganizationAdapter adapter = getAdapter(realm, organization.getId());

em.remove(toRemove.getEntity());
em.remove(adapter.getEntity());

return true;
}

@Override
public void removeOrganizations(RealmModel realm) {
throwExceptionIfRealmIsNull(realm);
Query query = em.createNamedQuery("deleteByRealm");
//TODO: won't scale, requires a better mechanism for bulk deleting organizations within a realm
getOrganizationsStream(realm).forEach(organization -> removeOrganization(realm, organization));
}

query.setParameter("realmId", realm.getId());
@Override
public boolean addOrganizationMember(RealmModel realm, OrganizationModel organization, UserModel user) {
throwExceptionIfOrganizationIsNull(organization);
if (user == null) {
throw new ModelException("User can not be null");
}
OrganizationAdapter adapter = getAdapter(realm, organization.getId());
GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId());

if (user.isMemberOf(group)) {
return false;
}

query.executeUpdate();
if (user.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE) != null) {
throw new ModelException("User [" + user.getId() + "] is a member of a different organization");
}

user.joinGroup(group);
user.setSingleAttribute(USER_ORGANIZATION_ATTRIBUTE, adapter.getId());

return true;
}

@Override
public OrganizationModel getOrganizationById(RealmModel realm, String id) {
return getAdapter(realm, id);
return getAdapter(realm, id, false);
}

@Override
public Stream<OrganizationModel> getOrganizationsStream(RealmModel realm) {
throwExceptionIfRealmIsNull(realm);
TypedQuery<String> query = em.createNamedQuery("getByRealm", String.class);

query.setParameter("realmId", realm.getId());

return closing(query.getResultStream().map(id -> getAdapter(realm, id)));
}

@Override
public Stream<UserModel> getMembersStream(RealmModel realm, OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter adapter = getAdapter(realm, organization.getId());
GroupModel group = getOrganizationGroup(realm, adapter);

return userProvider.getGroupMembersStream(realm, group);
}

@Override
public UserModel getMemberById(RealmModel realm, OrganizationModel organization, String id) {
throwExceptionIfRealmIsNull(realm);
throwExceptionIfOrganizationIsNull(organization);
UserModel user = userProvider.getUserById(realm, id);

if (user == null) {
return null;
}

String orgId = user.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);

if (organization.getId().equals(orgId)) {
return user;
}

return null;
}

@Override
public OrganizationModel getOrganizationByMember(RealmModel realm, UserModel member) {
throwExceptionIfRealmIsNull(realm);
if (member == null) {
throw new ModelException("User can not be null");
}

String orgId = member.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);

if (orgId == null) {
return null;
}

return getOrganizationById(realm, orgId);
}

@Override
public void close() {

}

private OrganizationAdapter getAdapter(RealmModel realm, String id) {
return getAdapter(realm, id, true);
}

private OrganizationAdapter getAdapter(RealmModel realm, String id, boolean failIfNotFound) {
throwExceptionIfRealmIsNull(realm);
OrganizationEntity entity = em.find(OrganizationEntity.class, id);

if (entity == null) {
if (failIfNotFound) {
throw new ModelException("Organization [" + id + "] does not exist");
}
return null;
}

if (!realm.getId().equals(entity.getRealmId())) {
return null;
throw new ModelException("Organization [" + entity.getId() + " does not belong to realm [" + realm.getId() + "]");
}

return new OrganizationAdapter(entity, session);
}

private GroupModel createOrganizationGroup(RealmModel realm, String name) {
throwExceptionIfRealmIsNull(realm);
if (name == null) {
throw new ModelException("name can not be null");
}

String groupName = getCanonicalGroupName(name);
GroupModel group = groupProvider.getGroupByName(realm, null, name);

if (group != null) {
throw new ModelException("A group with the same name already exist and it is bound to different organization");
}

return groupProvider.createGroup(realm, KeycloakModelUtils.generateId(), groupName);
}

private String getCanonicalGroupName(String name) {
return "kc.org." + name;
}

private GroupModel getOrganizationGroup(RealmModel realm, OrganizationModel organization) {
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter adapter = getAdapter(realm, organization.getId());

GroupModel group = groupProvider.getGroupById(realm, adapter.getGroupId());

if (group == null) {
throw new ModelException("Organization group " + adapter.getGroupId() + " not found");
}

return new OrganizationAdapter(entity);
return group;
}

private void throwExceptionIfOrganizationIsNull(OrganizationModel organization) {
if (organization == null) {
throw new IllegalArgumentException("organization can not be null");
throw new ModelException("organization can not be null");
}
}

private void throwExceptionIfRealmIsNull(RealmModel realm) {
if (realm == null) {
throw new IllegalArgumentException("realm can not be null");
throw new ModelException("realm can not be null");
}
}
}
Loading
0