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:
parent
2bddfe7380
commit
7fc2269ba5
20 changed files with 943 additions and 3 deletions
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -288,4 +288,7 @@ public interface RealmResource {
|
|||
|
||||
@Path("client-policies/profiles")
|
||||
ClientPoliciesProfilesResource clientPoliciesProfilesResource();
|
||||
|
||||
@Path("organizations")
|
||||
OrganizationsResource organizations();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ model,6
|
|||
oauth,6
|
||||
oid4vc,6
|
||||
oidc,6
|
||||
organization,3
|
||||
policy,6
|
||||
providers,4
|
||||
runonserver,6
|
||||
|
|
Loading…
Reference in a new issue