KEYCLOAK-2474 Polishing. Support for separate changelock table per changelog. Support for authenticated endpoint in domain-extension example

This commit is contained in:
mposolda 2016-06-20 18:05:55 +02:00
parent 44d7d776bc
commit c4513fdad9
35 changed files with 368 additions and 664 deletions

View file

@ -1 +0,0 @@
/target/

View file

@ -3,7 +3,8 @@ Example Domain Extension
To run, deploy as a module by running: To run, deploy as a module by running:
$KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.domain-extension-example --resources=target/domain-extension-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.keycloak.keycloak-server-spi,javax.ws.rs.api" $KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.domain-extension-example --resources=target/domain-extension-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.keycloak.keycloak-server-spi,javax.ws.rs.api,javax.persistence.api,org.hibernate,org.javassist"
Then registering the provider by editing keycloak-server.json and adding the module to the providers field: Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
@ -12,4 +13,32 @@ Then registering the provider by editing keycloak-server.json and adding the mod
"module:org.keycloak.examples.domain-extension-example" "module:org.keycloak.examples.domain-extension-example"
], ],
Then start (or restart) the server. Once started do xyz TODO. Then start (or restart) the server.
Testing
-------
First you can create some example companies with these CURL requests.
````
curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"foo company\" }" --header "Content-type: application/json"
curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"bar company\" }" --header "Content-type: application/json"
````
Then you can lookup all companies
````
curl -i --request GET http://localhost:8080/auth/realms/master/example/companies --header "Accept: application/json"
````
If you create realm `foo` in Keycloak admin console and then replace the realm name in the URI (for example like `http://localhost:8080/auth/realms/foo/example/companies` ) you will see
that companies are scoped per-realm. So you will see different companies for realm `master` and for realm `foo` .
Testing with authenticated access
---------------------------------
Example contains the endpoint, which is accessible just for authenticated users. REST request must be authenticated with bearer access token
of authenticated user and the user must be in realm role `admin` in order to access the resource. You can run bash script from the current directory:
````
./invoke-authenticated.sh
````
The script assumes user `admin` with password `admin` exists in realm `master`. Also it assumes that you have `curl` installed.

View file

@ -0,0 +1,19 @@
#!/bin/bash
export DIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli")
echo -e "\n\nSENT RESOURCE-OWNER-PASSWORD-CREDENTIALS-REQUEST. OUTPUT IS:\n\n";
echo $DIRECT_GRANT_RESPONSE;
export ACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');
echo -e "\n\nACCESS TOKEN IS \"$ACCESS_TOKEN\"";
echo -e "\n\nSENDING UN-AUTHENTICATED REQUEST. THIS SHOULD FAIL WITH 401: ";
curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json"
echo -e "\n\nSENDING AUTHENTICATED REQUEST. THIS SHOULD SUCCESSFULY CREATE COMPANY AND SUCCESS WITH 201: ";
curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";
echo -e "\n\nSEARCH COMPANIES: ";
curl -i --request GET http://localhost:8080/auth/realms/master/example/companies-auth --header "Accept: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";

View file

@ -0,0 +1,33 @@
package org.keycloak.examples.domainextension;
import org.keycloak.examples.domainextension.jpa.Company;
public class CompanyRepresentation {
private String id;
private String name;
public CompanyRepresentation() {
}
public CompanyRepresentation(Company company) {
id = company.getId();
name = company.getName();
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}

View file

@ -1,110 +0,0 @@
/*
* Copyright 2016 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.examples.domainextension.entities;
import java.util.Collections;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.UUID;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "EXAMPLE_COMPANY")
@NamedQueries({ @NamedQuery(name = "findAllCompanies", query = "from Company"),
@NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId") })
public class Company {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false)
private String name;
@Column(name = "REALM_ID", nullable = false)
private String realmId;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "company", orphanRemoval = true)
private final Set<Region> regions = new HashSet<>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "company", orphanRemoval = true)
private final Set<UserAccount> userAccounts = new HashSet<>();
@SuppressWarnings("unused")
private Company() {
}
public Company(String realmId, String name) {
this.id = UUID.randomUUID().toString();
this.realmId = realmId;
this.name = name;
}
public String getId() {
return id;
}
public String getRealmId() {
return realmId;
}
public String getName() {
return name;
}
public Set<UserAccount> getUserAccounts() {
return Collections.unmodifiableSet(userAccounts);
}
public void addUserAccount(UserAccount userAccount) {
userAccounts.add(userAccount);
}
public boolean removeUserAccount(UserAccount userAccount) {
return userAccounts.remove(userAccount);
}
public UserAccount getUserAccountByUsername(String username) {
for (UserAccount userAccount : userAccounts) {
if (userAccount.getUser().getUsername().equals(username)) {
return userAccount;
}
}
throw new NoSuchElementException("No user found with name '" + username + "'");
}
public void addRegion(Region region) {
regions.add(region);
}
public boolean removeRegion(Region region) {
return regions.remove(region);
}
}

View file

@ -1,73 +0,0 @@
/*
* Copyright 2016 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.examples.domainextension.entities;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.keycloak.models.jpa.entities.UserEntity;
/**
* The Class UserAccount.
*/
@Entity
@Table(name = "EXAMPLE_USER_ACCOUNT")
public class UserAccount {
@Id
@Column(name = "ID")
private String id;
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false)
private UserEntity user;
@ManyToOne
@JoinColumn(name = "COMPANY_ID", nullable = true)
private Company company;
@SuppressWarnings("unused")
private UserAccount() {
}
public UserAccount(String id, UserEntity userEntity, Company company) {
this.id = UUID.randomUUID().toString();
user = userEntity;
this.company = company;
}
public String getId() {
return id;
}
public UserEntity getUser() {
return user;
}
public Company getCompany() {
return company;
}
}

View file

@ -1,79 +0,0 @@
/*
* Copyright 2016 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.examples.domainextension.entities;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.keycloak.models.jpa.entities.RoleEntity;
@Entity
@Table(name = "EXAMPLE_USER_ACCOUNT_REGION_ROLE")
public class UserAccountRegionRole {
@Id
@Column(name = "ID")
private String id;
@ManyToOne
@JoinColumn(name = "USER_ACCOUNT_ID", nullable = false)
private UserAccount userAccount;
@ManyToOne
@JoinColumn(name = "REGION_ID", nullable = false)
private Region region;
@ManyToOne
@JoinColumn(name = "ROLE_ID", nullable = false)
private RoleEntity role;
@SuppressWarnings("unused")
private UserAccountRegionRole() {
}
public UserAccountRegionRole(UserAccount userAccount, Region region, RoleEntity role) {
this.id = UUID.randomUUID().toString();
this.userAccount = userAccount;
this.region = region;
this.role = role;
}
public String getId() {
return id;
}
public UserAccount getUserAccount() {
return userAccount;
}
public Region getRegion() {
return region;
}
public RoleEntity getRole() {
return role;
}
}

View file

@ -15,20 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.entities; package org.keycloak.examples.domainextension.jpa;
import java.util.UUID;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn; import javax.persistence.NamedQueries;
import javax.persistence.ManyToOne; import javax.persistence.NamedQuery;
import javax.persistence.Table; import javax.persistence.Table;
@Entity @Entity
@Table(name = "EXAMPLE_REGION") @Table(name = "EXAMPLE_COMPANY")
public class Region { @NamedQueries({ @NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId") })
public class Company {
@Id @Id
@Column(name = "ID") @Column(name = "ID")
@ -37,26 +37,30 @@ public class Region {
@Column(name = "NAME", nullable = false) @Column(name = "NAME", nullable = false)
private String name; private String name;
@ManyToOne @Column(name = "REALM_ID", nullable = false)
@JoinColumn(name = "COMPANY_ID", nullable = true) private String realmId;
private Company company;
@SuppressWarnings("unused")
private Region() {
}
public Region(String name) {
this.id = UUID.randomUUID().toString();
this.name = name;
}
public String getId() { public String getId() {
return id; return id;
} }
public String getRealmId() {
return realmId;
}
public String getName() { public String getName() {
return name; return name;
} }
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
} }

View file

@ -15,16 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.providers.entity; package org.keycloak.examples.domainextension.jpa;
import java.util.Arrays; import java.util.Collections;
import java.util.List; import java.util.List;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.examples.domainextension.entities.Company;
import org.keycloak.examples.domainextension.entities.Region;
import org.keycloak.examples.domainextension.entities.UserAccount;
import org.keycloak.examples.domainextension.entities.UserAccountRegionRole;
/** /**
* @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a> * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
@ -35,16 +31,20 @@ public class ExampleJpaEntityProvider implements JpaEntityProvider {
@Override @Override
public List<Class<?>> getEntities() { public List<Class<?>> getEntities() {
return Arrays.asList(Company.class, Region.class, UserAccount.class, UserAccountRegionRole.class); return Collections.<Class<?>>singletonList(Company.class);
} }
@Override @Override
public String getChangelogLocation() { public String getChangelogLocation() {
return "example-changelog.xml"; return "META-INF/example-changelog.xml";
} }
@Override @Override
public void close() { public void close() {
} }
@Override
public String getFactoryId() {
return ExampleJpaEntityProviderFactory.ID;
}
} }

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.providers.entity; package org.keycloak.examples.domainextension.jpa;
import org.keycloak.Config.Scope; import org.keycloak.Config.Scope;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
@ -30,7 +30,7 @@ import org.keycloak.models.KeycloakSessionFactory;
*/ */
public class ExampleJpaEntityProviderFactory implements JpaEntityProviderFactory { public class ExampleJpaEntityProviderFactory implements JpaEntityProviderFactory {
private static final String ID = "example-entity-provider"; protected static final String ID = "example-entity-provider";
@Override @Override
public JpaEntityProvider create(KeycloakSession session) { public JpaEntityProvider create(KeycloakSession session) {

View file

@ -1,21 +1,24 @@
package org.keycloak.examples.domainextension.rest; package org.keycloak.examples.domainextension.rest;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.keycloak.examples.domainextension.entities.Company; import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.examples.domainextension.rest.model.CompanyView; import org.keycloak.examples.domainextension.CompanyRepresentation;
import org.keycloak.examples.domainextension.services.ExampleService; import org.keycloak.examples.domainextension.spi.ExampleService;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
public class CompanyResource { public class CompanyResource {
private KeycloakSession session; private final KeycloakSession session;
public CompanyResource(KeycloakSession session) { public CompanyResource(KeycloakSession session) {
this.session = session; this.session = session;
@ -23,19 +26,27 @@ public class CompanyResource {
@GET @GET
@Path("") @Path("")
public Set<CompanyView> getMasterAccounts() { @NoCache
List<Company> companies = session.getProvider(ExampleService.class).listCompanies(); @Produces(MediaType.APPLICATION_JSON)
Set<CompanyView> companyViews = new HashSet<>(); public List<CompanyRepresentation> getCompanies() {
for (Company company : companies) { return session.getProvider(ExampleService.class).listCompanies();
companyViews.add(new CompanyView(company));
} }
return companyViews;
@POST
@Path("")
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
public Response createProviderInstance(CompanyRepresentation rep) {
session.getProvider(ExampleService.class).addCompany(rep);
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(rep.getId()).build()).build();
} }
@GET @GET
@NoCache
@Path("{id}") @Path("{id}")
public CompanyView getCompany(@PathParam("id") final String id) { @Produces(MediaType.APPLICATION_JSON)
return new CompanyView(session.getProvider(ExampleService.class).findCompany(id)); public CompanyRepresentation getCompany(@PathParam("id") final String id) {
return session.getProvider(ExampleService.class).findCompany(id);
} }
} }

View file

@ -15,9 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.providers.rest; package org.keycloak.examples.domainextension.rest;
import org.keycloak.examples.domainextension.rest.ExampleRestResource;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProvider;

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.providers.rest; package org.keycloak.examples.domainextension.rest;
import org.keycloak.Config.Scope; import org.keycloak.Config.Scope;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;

View file

@ -1,15 +1,22 @@
package org.keycloak.examples.domainextension.rest; package org.keycloak.examples.domainextension.rest;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
public class ExampleRestResource { public class ExampleRestResource {
private KeycloakSession session; private final KeycloakSession session;
private final AuthenticationManager.AuthResult auth;
public ExampleRestResource(KeycloakSession session) { public ExampleRestResource(KeycloakSession session) {
this.session = session; this.session = session;
this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
} }
@Path("companies") @Path("companies")
@ -17,4 +24,20 @@ public class ExampleRestResource {
return new CompanyResource(session); return new CompanyResource(session);
} }
// Same like "companies" endpoint, but REST endpoint is authenticated with Bearer token and user must be in realm role "admin"
// Just for illustration purposes
@Path("companies-auth")
public CompanyResource getCompanyResourceAuthenticated() {
checkRealmAdmin();
return new CompanyResource(session);
}
private void checkRealmAdmin() {
if (auth == null) {
throw new NotAuthorizedException("Bearer");
} else if (auth.getToken().getRealmAccess() == null || !auth.getToken().getRealmAccess().isUserInRole("admin")) {
throw new ForbiddenException("Does not have realm admin role");
}
}
} }

View file

@ -1,26 +0,0 @@
package org.keycloak.examples.domainextension.rest.model;
import org.keycloak.examples.domainextension.entities.Company;
public class CompanyView {
private String id;
private String name;
public CompanyView() {
}
public CompanyView(Company company) {
id = company.getId();
name = company.getName();
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}

View file

@ -1,51 +0,0 @@
/*
* Copyright 2016 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.examples.domainextension.services.repository;
import java.lang.reflect.ParameterizedType;
import javax.persistence.EntityManager;
public abstract class AbstractRepository<T> {
private final EntityManager entityManager;
private final Class<T> clazz;
@SuppressWarnings("unchecked")
public AbstractRepository(EntityManager entityManager) {
this.entityManager = entityManager;
clazz = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
protected EntityManager getEntityManager() {
return entityManager;
}
public T findById(String id) {
return entityManager.find(clazz, id);
}
public void remove(T entity) {
entityManager.remove(entity);
}
public void persist(T entity) {
entityManager.persist(entity);
}
}

View file

@ -1,41 +0,0 @@
/*
* Copyright 2016 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.examples.domainextension.services.repository;
import java.util.List;
import javax.persistence.EntityManager;
import org.keycloak.examples.domainextension.entities.Company;
public class CompanyRepository extends AbstractRepository<Company> {
public CompanyRepository(EntityManager entityManager) {
super(entityManager);
}
public List<Company> getAll() {
return getEntityManager().createNamedQuery("findAllCompanies", Company.class).getResultList();
}
public List<Company> getAll(String realmId) {
return getEntityManager().createNamedQuery("findByRealm", Company.class).setParameter("realmId", realmId)
.getResultList();
}
}

View file

@ -15,19 +15,19 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.services; package org.keycloak.examples.domainextension.spi;
import java.util.List; import java.util.List;
import org.keycloak.examples.domainextension.entities.Company; import org.keycloak.examples.domainextension.CompanyRepresentation;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
public interface ExampleService extends Provider { public interface ExampleService extends Provider {
List<Company> listCompanies(); List<CompanyRepresentation> listCompanies();
Company findCompany(String id); CompanyRepresentation findCompany(String id);
void addCompany(Company company); CompanyRepresentation addCompany(CompanyRepresentation company);
} }

View file

@ -15,9 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.services.spi; package org.keycloak.examples.domainextension.spi;
import org.keycloak.examples.domainextension.services.ExampleService;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleService> { public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleService> {

View file

@ -15,9 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.services.spi; package org.keycloak.examples.domainextension.spi;
import org.keycloak.examples.domainextension.services.ExampleService;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi; import org.keycloak.provider.Spi;

View file

@ -15,30 +15,30 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.services; package org.keycloak.examples.domainextension.spi.impl;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.examples.domainextension.entities.Company; import org.keycloak.examples.domainextension.jpa.Company;
import org.keycloak.examples.domainextension.services.repository.CompanyRepository; import org.keycloak.examples.domainextension.CompanyRepresentation;
import org.keycloak.examples.domainextension.spi.ExampleService;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
public class ExampleServiceImpl implements ExampleService { public class ExampleServiceImpl implements ExampleService {
private final KeycloakSession session; private final KeycloakSession session;
private CompanyRepository companyRepository;
public ExampleServiceImpl(KeycloakSession session) { public ExampleServiceImpl(KeycloakSession session) {
this.session = session; this.session = session;
if (getRealm() == null) { if (getRealm() == null) {
throw new IllegalStateException("The service cannot accept a session without a realm in it's context."); throw new IllegalStateException("The service cannot accept a session without a realm in it's context.");
} }
companyRepository = new CompanyRepository(getEntityManager());
} }
private EntityManager getEntityManager() { private EntityManager getEntityManager() {
@ -50,18 +50,35 @@ public class ExampleServiceImpl implements ExampleService {
} }
@Override @Override
public List<Company> listCompanies() { public List<CompanyRepresentation> listCompanies() {
return companyRepository.getAll(); List<Company> companyEntities = getEntityManager().createNamedQuery("findByRealm", Company.class)
.setParameter("realmId", getRealm().getId())
.getResultList();
List<CompanyRepresentation> result = new LinkedList<>();
for (Company entity : companyEntities) {
result.add(new CompanyRepresentation(entity));
}
return result;
} }
@Override @Override
public Company findCompany(String id) { public CompanyRepresentation findCompany(String id) {
return companyRepository.findById(id); Company entity = getEntityManager().find(Company.class, id);
return entity==null ? null : new CompanyRepresentation(entity);
} }
@Override @Override
public void addCompany(Company company) { public CompanyRepresentation addCompany(CompanyRepresentation company) {
companyRepository.persist(company); Company entity = new Company();
String id = company.getId()==null ? KeycloakModelUtils.generateId() : company.getId();
entity.setId(id);
entity.setName(company.getName());
entity.setRealmId(getRealm().getId());
getEntityManager().persist(entity);
company.setId(id);
return company;
} }
public void close() { public void close() {

View file

@ -15,11 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.examples.domainextension.services.spi; package org.keycloak.examples.domainextension.spi.impl;
import org.keycloak.Config.Scope; import org.keycloak.Config.Scope;
import org.keycloak.examples.domainextension.services.ExampleService; import org.keycloak.examples.domainextension.spi.ExampleService;
import org.keycloak.examples.domainextension.services.ExampleServiceImpl; import org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -47,7 +47,7 @@ public class ExampleServiceProviderFactoryImpl implements ExampleServiceProvider
@Override @Override
public String getId() { public String getId() {
return "exampleService"; return "exampleServiceImpl";
} }
} }

View file

@ -16,7 +16,7 @@
<addPrimaryKey <addPrimaryKey
constraintName="PK_COMPANY" constraintName="PK_COMPANY"
tableName="DOCDATA_COMPANY" tableName="EXAMPLE_COMPANY"
columnNames="ID" columnNames="ID"
/> />
@ -28,116 +28,6 @@
referencedColumnNames="ID" referencedColumnNames="ID"
/> />
<createTable tableName="EXAMPLE_REGION">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="COMPANY_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey
constraintName="PK_REGION"
tableName="EXAMPLE_REGION"
columnNames="ID"
/>
<addForeignKeyConstraint
constraintName="FK_REGION_COMPANY"
baseTableName="EXAMPLE_REGION"
baseColumnNames="COMPANY_ID"
referencedTableName="EXAMPLE_COMPANY"
referencedColumnNames="ID"
/>
<createTable tableName="EXAMPLE_USER_ACCOUNT">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="COMPANY_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey
constraintName="PK_USER_ACCOUNT"
tableName="EXAMPLE_USER_ACCOUNT"
columnNames="ID"
/>
<addUniqueConstraint
constraintName="UC_USER_ACCOUNT_USER_ID"
tableName="EXAMPLE_USER_ACCOUNT"
columnNames="USER_ID"
/>
<addForeignKeyConstraint
constraintName="FK_USER_ACCOUNT_USER_ENTITY"
baseTableName="EXAMPLE_USER_ACCOUNT"
baseColumnNames="USER_ID"
referencedTableName="USER_ENTITY"
referencedColumnNames="ID"
/>
<addForeignKeyConstraint
constraintName="FK_USER_ACCOUNT_COMPANY"
baseTableName="EXAMPLE_USER_ACCOUNT"
baseColumnNames="COMPANY_ID"
referencedTableName="EXAMPLE_COMPANY"
referencedColumnNames="ID"
/>
<createTable tableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
<column name="USER_ACCOUNT_ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
<column name="REGION_ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
<column name="ROLE_ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
</createTable>
<addPrimaryKey
constraintName="PK_USER_ACCOUNT_REGION_ROLE"
tableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
columnNames="ID"
/>
<addForeignKeyConstraint
constraintName="FK_USER_ACCOUNT_REGION_ROLE_USER_ACCOUNT"
baseTableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
baseColumnNames="USER_ACCOUNT_ID"
referencedTableName="EXAMPLE_USER_ACCOUNT"
referencedColumnNames="ID"
/>
<addForeignKeyConstraint
constraintName="FK_USER_ACCOUNT_REGION_ROLE_REGION"
baseTableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
baseColumnNames="REGION_ID"
referencedTableName="EXAMPLE_REGION"
referencedColumnNames="ID"
/>
<addForeignKeyConstraint
constraintName="FK_USER_ACCOUNT_REGION_ROLE_ROLE"
baseTableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
baseColumnNames="ROLE_ID"
referencedTableName="KEYCLOAK_ROLE"
referencedColumnNames="ID"
/>
</changeSet> </changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -15,4 +15,4 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.examples.domainextension.DomainExtensionProviderFactory org.keycloak.examples.domainextension.jpa.ExampleJpaEntityProviderFactory

View file

@ -15,4 +15,4 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.examples.domainextension.services.spi.ExampleServiceProviderFactoryImpl org.keycloak.examples.domainextension.spi.impl.ExampleServiceProviderFactoryImpl

View file

@ -15,4 +15,4 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.examples.domainextension.services.spi.ExampleSpi org.keycloak.examples.domainextension.spi.ExampleSpi

View file

@ -15,4 +15,4 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.examples.domainextension.rest.ExampleResourceProviderFactory org.keycloak.examples.domainextension.rest.ExampleRealmResourceProviderFactory

View file

@ -102,6 +102,11 @@
<artifactId>jackson-core</artifactId> <artifactId>jackson-core</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View file

@ -44,4 +44,10 @@ public interface JpaEntityProvider extends Provider {
*/ */
String getChangelogLocation(); String getChangelogLocation();
/**
* Return the ID of provider factory, which created this provider. Might be used to "compute" the table name of liquibase changelog table.
* @return ID of provider factory
*/
String getFactoryId();
} }

View file

@ -23,12 +23,17 @@ import liquibase.changelog.ChangeSet;
import liquibase.changelog.RanChangeSet; import liquibase.changelog.RanChangeSet;
import liquibase.exception.LiquibaseException; import liquibase.exception.LiquibaseException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.reflections.Reflections;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import java.lang.reflect.Method;
import java.sql.Connection; import java.sql.Connection;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -54,26 +59,21 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
ThreadLocalSessionContext.setCurrentSession(session); ThreadLocalSessionContext.setCurrentSession(session);
try { try {
Liquibase liquibase = getLiquibase(connection, defaultSchema); // Run update with keycloak master changelog first
Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
updateChangeSet(liquibase, liquibase.getChangeLogFile());
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null); // Run update for each custom JpaEntityProvider
if (!changeSets.isEmpty()) { Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
if (changeSets.get(0).getId().equals(FIRST_VERSION)) { for (JpaEntityProvider jpaProvider : jpaProviders) {
logger.info("Initializing database schema"); String customChangelog = jpaProvider.getChangelogLocation();
} else { if (customChangelog != null) {
if (logger.isDebugEnabled()) { String factoryId = jpaProvider.getFactoryId();
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList(); String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
logger.debugv("Updating database from {0} to {1}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId()); liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
} else { updateChangeSet(liquibase, liquibase.getChangeLogFile());
logger.infov("Updating database");
} }
} }
liquibase.update((Contexts) null);
logger.debug("Completed database update");
} else {
logger.debug("Database is up to date");
}
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to update database", e); throw new RuntimeException("Failed to update database", e);
} finally { } finally {
@ -81,21 +81,50 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
} }
} }
protected void updateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
if (!changeSets.isEmpty()) {
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
if (ranChangeSets.isEmpty()) {
logger.infov("Initializing database schema. Using changelog {0}", changelog);
} else {
if (logger.isDebugEnabled()) {
logger.debugv("Updating database from {0} to {1}. Using changelog {2}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
} else {
logger.infov("Updating database. Using changelog {0}", changelog);
}
}
liquibase.update((Contexts) null);
logger.debugv("Completed database update for changelog {0}", changelog);
} else {
logger.debugv("Database is up to date for changelog {0}", changelog);
// Needs to restart liquibase services to clear changeLogHistory.
Method resetServices = Reflections.findDeclaredMethod(Liquibase.class, "resetServices");
Reflections.invokeMethod(true, resetServices, liquibase);
}
}
@Override @Override
public void validate(Connection connection, String defaultSchema) { public void validate(Connection connection, String defaultSchema) {
logger.debug("Validating if database is updated"); logger.debug("Validating if database is updated");
try { try {
Liquibase liquibase = getLiquibase(connection, defaultSchema); // Validate with keycloak master changelog first
Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
validateChangeSet(liquibase, liquibase.getChangeLogFile());
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null); // Validate each custom JpaEntityProvider
if (!changeSets.isEmpty()) { Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList(); for (JpaEntityProvider jpaProvider : jpaProviders) {
String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database", String customChangelog = jpaProvider.getChangelogLocation();
ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId()); if (customChangelog != null) {
throw new RuntimeException(errorMessage); String factoryId = jpaProvider.getFactoryId();
} else { String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
logger.debug("Validation passed. Database is up-to-date"); liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
validateChangeSet(liquibase, liquibase.getChangeLogFile());
}
} }
} catch (LiquibaseException e) { } catch (LiquibaseException e) {
@ -103,11 +132,28 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
} }
} }
private Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException { protected void validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
if (!changeSets.isEmpty()) {
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database. Used changelog was %s",
ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
throw new RuntimeException(errorMessage);
} else {
logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog);
}
}
private Liquibase getLiquibaseForKeycloakUpdate(Connection connection, String defaultSchema) throws LiquibaseException {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class); LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
return liquibaseProvider.getLiquibase(connection, defaultSchema); return liquibaseProvider.getLiquibase(connection, defaultSchema);
} }
private Liquibase getLiquibaseForCustomProviderUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelogLocation, classloader, changelogTableName);
}
@Override @Override
public void close() { public void close() {
} }

View file

@ -18,25 +18,18 @@
package org.keycloak.connections.jpa.updater.liquibase.conn; package org.keycloak.connections.jpa.updater.liquibase.conn;
import java.sql.Connection; import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider; import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase; import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator; import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator; import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator;
import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService; import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import liquibase.Liquibase; import liquibase.Liquibase;
import liquibase.changelog.ChangeLogParameters;
import liquibase.changelog.ChangeSet; import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog; import liquibase.changelog.DatabaseChangeLog;
import liquibase.database.Database; import liquibase.database.Database;
@ -46,8 +39,6 @@ import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException; import liquibase.exception.LiquibaseException;
import liquibase.logging.LogFactory; import liquibase.logging.LogFactory;
import liquibase.logging.LogLevel; import liquibase.logging.LogLevel;
import liquibase.parser.ChangeLogParser;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.resource.ClassLoaderResourceAccessor; import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor; import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.ServiceLocator; import liquibase.servicelocator.ServiceLocator;
@ -62,11 +53,8 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
private volatile boolean initialized = false; private volatile boolean initialized = false;
private KeycloakSession keycloakSession;
@Override @Override
public LiquibaseConnectionProvider create(KeycloakSession session) { public LiquibaseConnectionProvider create(KeycloakSession session) {
this.keycloakSession = session;
if (!initialized) { if (!initialized) {
synchronized (this) { synchronized (this) {
if (!initialized) { if (!initialized) {
@ -143,63 +131,26 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
} }
String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG : LiquibaseJpaUpdaterProvider.CHANGELOG; String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG : LiquibaseJpaUpdaterProvider.CHANGELOG;
logger.debugf("Using changelog file: %s", changelog);
ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader()); ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
DatabaseChangeLog databaseChangeLog = generateDynamicChangeLog(changelog, resourceAccessor, database);
return new Liquibase(databaseChangeLog, resourceAccessor, database); logger.debugf("Using changelog file %s and changelogTableName %s", changelog, database.getDatabaseChangeLogTableName());
return new Liquibase(changelog, resourceAccessor, database);
} }
/** @Override
* We want to be able to provide extra changesets as an extension to the Keycloak data model. public Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
* But we do not want users to be able to not execute certain parts of the Keycloak internal data model. Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
* Therefore, we generate a dynamic changelog here that always contains the keycloak changelog file if (defaultSchema != null) {
* and optionally include the user extension changelog files. database.setDefaultSchemaName(defaultSchema);
*
* @param changelog the changelog file location
* @param resourceAccessor the resource accessor
* @param database the database
* @return
*/
private DatabaseChangeLog generateDynamicChangeLog(String changelog, ResourceAccessor resourceAccessor, Database database) throws LiquibaseException {
ChangeLogParameters changeLogParameters = new ChangeLogParameters(database);
ChangeLogParser parser = ChangeLogParserFactory.getInstance().getParser(changelog, resourceAccessor);
DatabaseChangeLog keycloakDatabaseChangeLog = parser.parse(changelog, changeLogParameters, resourceAccessor);
List<String> locations = new ArrayList<>();
Set<JpaEntityProvider> entityProviders = keycloakSession.getAllProviders(JpaEntityProvider.class);
for (JpaEntityProvider entityProvider : entityProviders) {
String location = entityProvider.getChangelogLocation();
if (location != null) {
locations.add(location);
}
} }
final DatabaseChangeLog dynamicMasterChangeLog; ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classloader);
if (locations.isEmpty()) { database.setDatabaseChangeLogTableName(changelogTableName);
// If there are no extra changelog locations, we'll just use the keycloak one.
dynamicMasterChangeLog = keycloakDatabaseChangeLog;
} else {
// A change log is essentially not much more than a (big) collection of changesets.
// The original (file) destination is not important. So we can just make one big dynamic change log that include all changesets.
dynamicMasterChangeLog = new DatabaseChangeLog();
dynamicMasterChangeLog.setChangeLogParameters(changeLogParameters);
for (ChangeSet changeSet : keycloakDatabaseChangeLog.getChangeSets()) {
dynamicMasterChangeLog.addChangeSet(changeSet);
}
ProxyClassLoader proxyClassLoader = new ProxyClassLoader(JpaUtils.getProvidedEntities(keycloakSession));
for (String location : locations) {
ResourceAccessor proxyResourceAccessor = new ClassLoaderResourceAccessor(proxyClassLoader);
ChangeLogParser locationParser = ChangeLogParserFactory.getInstance().getParser(location, proxyResourceAccessor);
DatabaseChangeLog locationDatabaseChangeLog = locationParser.parse(location, changeLogParameters, proxyResourceAccessor);
for (ChangeSet changeSet : locationDatabaseChangeLog.getChangeSets()) {
dynamicMasterChangeLog.addChangeSet(changeSet);
}
}
}
return dynamicMasterChangeLog; logger.debugf("Using changelog file %s and changelogTableName %s", changelogLocation, database.getDatabaseChangeLogTableName());
return new Liquibase(changelogLocation, resourceAccessor, database);
} }
private static class LogWrapper extends LogFactory { private static class LogWrapper extends LogFactory {

View file

@ -30,4 +30,6 @@ public interface LiquibaseConnectionProvider extends Provider {
Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException; Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException;
Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException;
} }

View file

@ -82,4 +82,16 @@ public class JpaUtils {
return providedEntityClasses; return providedEntityClasses;
} }
/**
* Get the name of custom table for liquibase updates for give ID of JpaEntityProvider
* @param jpaEntityProviderFactoryId
* @return table name
*/
public static String getCustomChangelogTableName(String jpaEntityProviderFactoryId) {
String upperCased = jpaEntityProviderFactoryId.toUpperCase();
upperCased = upperCased.replaceAll("-", "_");
upperCased = upperCased.replaceAll("[^A-Z_]", "");
return "DATABASECHANGELOG_" + upperCased.substring(0, Math.min(10, upperCased.length()));
}
} }

View file

@ -0,0 +1,36 @@
/*
* Copyright 2016 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.connections.jpa.util;
import org.junit.Assert;
import org.junit.Test;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JpaUtilsTest {
@Test
public void testConvertTableName() {
Assert.assertEquals("DATABASECHANGELOG_FOO", JpaUtils.getCustomChangelogTableName("foo"));
Assert.assertEquals("DATABASECHANGELOG_FOOBAR", JpaUtils.getCustomChangelogTableName("foo123bar"));
Assert.assertEquals("DATABASECHANGELOG_FOO_BAR", JpaUtils.getCustomChangelogTableName("foo_bar568"));
Assert.assertEquals("DATABASECHANGELOG_FOO_BAR_C", JpaUtils.getCustomChangelogTableName("foo-bar-c568"));
Assert.assertEquals("DATABASECHANGELOG_EXAMPLE_EN", JpaUtils.getCustomChangelogTableName("example-entity-provider"));
}
}

View file

@ -138,6 +138,10 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-core</artifactId> <artifactId>keycloak-saml-adapter-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-servlet-filter-adapter</artifactId> <artifactId>keycloak-saml-servlet-filter-adapter</artifactId>