The bare minimum implementation for organization

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

Co-authored-by: vramik <vramik@redhat.com>
This commit is contained in:
Pedro Igor 2024-03-14 18:13:14 -03:00
parent 2bddfe7380
commit 7fc2269ba5
20 changed files with 943 additions and 3 deletions

View file

@ -0,0 +1,59 @@
/*
* 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.representations.idm;
public class OrganizationRepresentation {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof OrganizationRepresentation)) return false;
OrganizationRepresentation that = (OrganizationRepresentation) o;
return id != null && id.equals(that.getId());
}
@Override
public int hashCode() {
if (id == null) {
return super.hashCode();
}
return id.hashCode();
}
}

View file

@ -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.OrganizationRepresentation;
public interface OrganizationResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
OrganizationRepresentation toRepresentation();
@PUT
@Consumes(MediaType.APPLICATION_JSON)
Response update(OrganizationRepresentation organization);
@DELETE
Response delete();
}

View file

@ -0,0 +1,44 @@
/*
* 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;
public interface OrganizationsResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response create(OrganizationRepresentation organization);
@Path("{id}")
OrganizationResource get(@PathParam("id") String id);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<OrganizationRepresentation> getAll();
}

View file

@ -288,4 +288,7 @@ public interface RealmResource {
@Path("client-policies/profiles")
ClientPoliciesProfilesResource clientPoliciesProfilesResource();
@Path("organizations")
OrganizationsResource organizations();
}

View file

@ -0,0 +1,90 @@
/*
* 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.models.jpa.entities;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
@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 {
@Id
@Column(name="ID", length = 36)
@Access(AccessType.PROPERTY)
protected String id;
@Column(name = "REALM_ID")
private String realmId;
@Column(name="NAME")
protected String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realm) {
this.realmId = realm;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof OrganizationEntity)) return false;
OrganizationEntity that = (OrganizationEntity) o;
return id != null && id.equals(that.getId());
}
@Override
public int hashCode() {
if (id == null) {
return super.hashCode();
}
return id.hashCode();
}
}

View file

@ -0,0 +1,128 @@
/*
* 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.jpa;
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.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
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;
public JpaOrganizationProvider(KeycloakSession session) {
JpaConnectionProvider jpaProvider = session.getProvider(JpaConnectionProvider.class);
this.em = jpaProvider.getEntityManager();
}
@Override
public OrganizationModel createOrganization(RealmModel realm, String name) {
throwExceptionIfRealmIsNull(realm);
OrganizationEntity entity = new OrganizationEntity();
entity.setId(KeycloakModelUtils.generateId());
entity.setRealmId(realm.getId());
entity.setName(name);
em.persist(entity);
return new OrganizationAdapter(entity);
}
@Override
public boolean removeOrganization(RealmModel realm, OrganizationModel organization) {
throwExceptionIfRealmIsNull(realm);
throwExceptionIfOrganizationIsNull(organization);
OrganizationAdapter toRemove = getAdapter(realm, organization.getId());
throwExceptionIfOrganizationIsNull(toRemove);
if (!toRemove.getRealm().equals(realm.getId())) {
throw new IllegalArgumentException("Organization [" + organization.getId() + " does not belong to realm [" + realm.getId() + "]");
}
em.remove(toRemove.getEntity());
return true;
}
@Override
public void removeOrganizations(RealmModel realm) {
throwExceptionIfRealmIsNull(realm);
Query query = em.createNamedQuery("deleteByRealm");
query.setParameter("realmId", realm.getId());
query.executeUpdate();
}
@Override
public OrganizationModel getOrganizationById(RealmModel realm, String id) {
return getAdapter(realm, id);
}
@Override
public Stream<OrganizationModel> getOrganizationsStream(RealmModel 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 void close() {
}
private OrganizationAdapter getAdapter(RealmModel realm, String id) {
OrganizationEntity entity = em.find(OrganizationEntity.class, id);
if (entity == null) {
return null;
}
if (!realm.getId().equals(entity.getRealmId())) {
return null;
}
return new OrganizationAdapter(entity);
}
private void throwExceptionIfOrganizationIsNull(OrganizationModel organization) {
if (organization == null) {
throw new IllegalArgumentException("organization can not be null");
}
}
private void throwExceptionIfRealmIsNull(RealmModel realm) {
if (realm == null) {
throw new IllegalArgumentException("realm can not be null");
}
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.jpa;
import org.keycloak.Config.Scope;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmModel.RealmRemovedEvent;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.OrganizationProviderFactory;
import org.keycloak.provider.ProviderEvent;
public class JpaOrganizationProviderFactory implements OrganizationProviderFactory {
@Override
public OrganizationProvider create(KeycloakSession session) {
return new JpaOrganizationProvider(session);
}
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
factory.register(this::handleRealmRemovedEvent);
}
@Override
public void close() {
}
@Override
public String getId() {
return "jpa";
}
private void handleRealmRemovedEvent(ProviderEvent event) {
if (event instanceof RealmRemovedEvent) {
KeycloakSession session = ((RealmRemovedEvent) event).getKeycloakSession();
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
RealmModel realm = ((RealmRemovedEvent) event).getRealm();
provider.removeOrganizations(realm);
}
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.jpa;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.jpa.JpaModel;
import org.keycloak.models.jpa.entities.OrganizationEntity;
public class OrganizationAdapter implements OrganizationModel, JpaModel<OrganizationEntity> {
private final OrganizationEntity entity;
public OrganizationAdapter(OrganizationEntity entity) {
this.entity = entity;
}
@Override
public String getId() {
return entity.getId();
}
String getRealm() {
return entity.getRealmId();
}
@Override
public void setName(String name) {
entity.setName(name);
}
@Override
public String getName() {
return entity.getName();
}
@Override
public OrganizationEntity getEntity() {
return entity;
}
}

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="25.0.0-org">
<createTable tableName="ORGANIZATION">
<column name="ID" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="REALM_ID" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="ID" tableName="ORGANIZATION"/>
<addUniqueConstraint tableName="ORGANIZATION" columnNames="REALM_ID, NAME" constraintName="UK_ORG_NAME"/>
</changeSet>
</databaseChangeLog>

View file

@ -81,5 +81,6 @@
<include file="META-INF/jpa-changelog-23.0.0.xml"/>
<include file="META-INF/jpa-changelog-24.0.0.xml"/>
<include file="META-INF/jpa-changelog-24.0.2.xml"/>
<include file="META-INF/jpa-changelog-25.0.0.xml"/>
</databaseChangeLog>

View file

@ -0,0 +1,18 @@
#
# 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.
#
org.keycloak.organization.jpa.JpaOrganizationProviderFactory

View file

@ -84,6 +84,9 @@
<class>org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity</class>
<!-- Organization -->
<class>org.keycloak.models.jpa.entities.OrganizationEntity</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>

View file

@ -17,6 +17,7 @@
package org.keycloak.organization;
import java.util.stream.Stream;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
@ -29,11 +30,13 @@ public interface OrganizationProvider extends Provider {
* The internal ID of the organization will be created automatically.
* @param realm Realm owning this 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
* @return Model of the created organization.
*/
OrganizationModel createOrganization(RealmModel realm, String name);
OrganizationModel getOrganizationById(RealmModel realm, String id);
/**
* Removes the given organization from the given realm.
*
@ -55,5 +58,4 @@ public interface OrganizationProvider extends Provider {
* @return Stream of the organizations. Never returns {@code null}.
*/
Stream<OrganizationModel> getOrganizationsStream(RealmModel realm);
}

View file

@ -16,7 +16,16 @@
*/
package org.keycloak.organization;
import org.keycloak.Config.Scope;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderFactory;
public interface OrganizationProviderFactory extends ProviderFactory<OrganizationProvider> {
public interface OrganizationProviderFactory extends ProviderFactory<OrganizationProvider>, EnvironmentDependentProviderFactory {
@Override
default boolean isSupported(Scope config) {
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.organization.admin.resource;
import java.util.stream.Stream;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.utils.StringUtil;
@Provider
public class OrganizationResource {
private final KeycloakSession session;
private final OrganizationProvider provider;
public OrganizationResource() {
// needed for registering to the JAX-RS stack
this(null);
}
public OrganizationResource(KeycloakSession session) {
this.session = session;
this.provider = session == null ? null : session.getProvider(OrganizationProvider.class);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(OrganizationRepresentation organization) {
if (organization == null) {
throw new BadRequestException();
}
RealmModel realm = session.getContext().getRealm();
OrganizationModel model = provider.createOrganization(realm, organization.getName());
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Stream<OrganizationRepresentation> get() {
return provider.getOrganizationsStream(session.getContext().getRealm()).map(this::toRepresentation);
}
@Path("{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public OrganizationRepresentation get(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
}
return toRepresentation(getOrganization(session.getContext().getRealm(), id));
}
@Path("{id}")
@DELETE
public Response delete(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw new BadRequestException();
}
RealmModel realm = session.getContext().getRealm();
provider.removeOrganization(realm, getOrganization(realm, id));
return Response.noContent().build();
}
@Path("{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response update(@PathParam("id") String id, OrganizationRepresentation organization) {
RealmModel realm = session.getContext().getRealm();
OrganizationModel model = getOrganization(realm, id);
toModel(organization, model);
return Response.noContent().build();
}
private OrganizationModel getOrganization(RealmModel realm, String id) {
if (id == null) {
throw new BadRequestException();
}
OrganizationModel model = provider.getOrganizationById(realm, id);
if (model == null) {
throw new NotFoundException();
}
return model;
}
private OrganizationRepresentation toRepresentation(OrganizationModel model) {
if (model == null) {
return null;
}
OrganizationRepresentation rep = new OrganizationRepresentation();
rep.setId(model.getId());
rep.setName(model.getName());
return rep;
}
private OrganizationModel toModel(OrganizationRepresentation rep, OrganizationModel model) {
if (rep == null) {
return null;
}
model.setName(rep.getName());
return model;
}
}

View file

@ -0,0 +1,62 @@
/*
* 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 org.keycloak.Config.Scope;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider;
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory;
public class OrganizationResourceFactory implements AdminRealmResourceProviderFactory, EnvironmentDependentProviderFactory {
private OrganizationResourceProvider PROVIDER_INSTANCE;
@Override
public AdminRealmResourceProvider create(KeycloakSession session) {
return PROVIDER_INSTANCE;
}
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
PROVIDER_INSTANCE = new OrganizationResourceProvider();
}
@Override
public void close() {
}
@Override
public String getId() {
return "organizations";
}
@Override
public boolean isSupported(Scope config) {
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
}
}

View file

@ -0,0 +1,37 @@
/*
* 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 org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
public class OrganizationResourceProvider implements AdminRealmResourceProvider {
@Override
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
return new OrganizationResource(session);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,17 @@
#
# 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.
#
org.keycloak.organization.admin.resource.OrganizationResourceFactory

View file

@ -0,0 +1,120 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.organization.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.ORGANIZATION)
public class OrganizationTest extends AbstractAdminTest {
@Test
public void testUpdate() {
OrganizationRepresentation expected = createRepresentation();
assertEquals("neworg", expected.getName());
expected.setName("acme");
OrganizationResource organization = testRealm().organizations().get(expected.getId());
try (Response response = organization.update(expected)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
OrganizationRepresentation existing = organization.toRepresentation();
assertEquals(expected.getId(), existing.getId());
assertEquals(expected.getName(), existing.getName());
}
@Test
public void testGet() {
OrganizationRepresentation expected = createRepresentation();
OrganizationRepresentation existing = testRealm().organizations().get(expected.getId()).toRepresentation();
assertNotNull(existing);
assertEquals(expected.getId(), existing.getId());
assertEquals(expected.getName(), existing.getName());
}
@Test
public void testGetAll() {
List<OrganizationRepresentation> expected = new ArrayList<>();
for (int i = 0; i < 5; i++) {
expected.add(createRepresentation("org-" + i));
}
List<OrganizationRepresentation> existing = testRealm().organizations().getAll();
assertFalse(existing.isEmpty());
MatcherAssert.assertThat(expected, Matchers.containsInAnyOrder(existing.toArray()));
}
@Test
public void testDelete() {
OrganizationRepresentation expected = createRepresentation();
OrganizationResource organization = testRealm().organizations().get(expected.getId());
try (Response response = organization.delete()) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
try {
organization.toRepresentation();
fail("should be deleted");
} catch (NotFoundException ignore) {}
}
private OrganizationRepresentation createRepresentation() {
return createRepresentation("neworg");
}
private OrganizationRepresentation createRepresentation(String name) {
OrganizationRepresentation org = new OrganizationRepresentation();
org.setName(name);
String id;
try (Response response = testRealm().organizations().create(org)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
id = ApiUtil.getCreatedId(response);
}
org.setId(id);
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());
return org;
}
}

View file

@ -29,6 +29,7 @@ model,6
oauth,6
oid4vc,6
oidc,6
organization,3
policy,6
providers,4
runonserver,6