KEYCLOAK-2474 Polishing. Support for separate changelock table per changelog. Support for authenticated endpoint in domain-extension example
This commit is contained in:
parent
44d7d776bc
commit
c4513fdad9
35 changed files with 368 additions and 664 deletions
|
@ -1 +0,0 @@
|
|||
/target/
|
|
@ -3,7 +3,8 @@ Example Domain Extension
|
|||
|
||||
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:
|
||||
|
||||
|
@ -12,4 +13,32 @@ Then registering the provider by editing keycloak-server.json and adding the mod
|
|||
"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.
|
19
examples/providers/domain-extension/invoke-authenticated.sh
Executable file
19
examples/providers/domain-extension/invoke-authenticated.sh
Executable 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";
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,21 +15,21 @@
|
|||
* 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.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.NamedQueries;
|
||||
import javax.persistence.NamedQuery;
|
||||
import javax.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "EXAMPLE_REGION")
|
||||
public class Region {
|
||||
|
||||
@Table(name = "EXAMPLE_COMPANY")
|
||||
@NamedQueries({ @NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId") })
|
||||
public class Company {
|
||||
|
||||
@Id
|
||||
@Column(name = "ID")
|
||||
private String id;
|
||||
|
@ -37,26 +37,30 @@ public class Region {
|
|||
@Column(name = "NAME", nullable = false)
|
||||
private String name;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "COMPANY_ID", nullable = true)
|
||||
private Company company;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Region() {
|
||||
|
||||
}
|
||||
|
||||
public Region(String name) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.name = name;
|
||||
}
|
||||
@Column(name = "REALM_ID", nullable = false)
|
||||
private String realmId;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
public String getRealmId() {
|
||||
return realmId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -15,16 +15,12 @@
|
|||
* 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 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>
|
||||
|
@ -35,16 +31,20 @@ public class ExampleJpaEntityProvider implements JpaEntityProvider {
|
|||
|
||||
@Override
|
||||
public List<Class<?>> getEntities() {
|
||||
return Arrays.asList(Company.class, Region.class, UserAccount.class, UserAccountRegionRole.class);
|
||||
return Collections.<Class<?>>singletonList(Company.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChangelogLocation() {
|
||||
return "example-changelog.xml";
|
||||
return "META-INF/example-changelog.xml";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFactoryId() {
|
||||
return ExampleJpaEntityProviderFactory.ID;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
* 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.connections.jpa.entityprovider.JpaEntityProvider;
|
||||
|
@ -30,7 +30,7 @@ import org.keycloak.models.KeycloakSessionFactory;
|
|||
*/
|
||||
public class ExampleJpaEntityProviderFactory implements JpaEntityProviderFactory {
|
||||
|
||||
private static final String ID = "example-entity-provider";
|
||||
protected static final String ID = "example-entity-provider";
|
||||
|
||||
@Override
|
||||
public JpaEntityProvider create(KeycloakSession session) {
|
|
@ -1,21 +1,24 @@
|
|||
package org.keycloak.examples.domainextension.rest;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
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.keycloak.examples.domainextension.rest.model.CompanyView;
|
||||
import org.keycloak.examples.domainextension.services.ExampleService;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.examples.domainextension.CompanyRepresentation;
|
||||
import org.keycloak.examples.domainextension.spi.ExampleService;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public class CompanyResource {
|
||||
|
||||
private KeycloakSession session;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public CompanyResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
|
@ -23,19 +26,27 @@ public class CompanyResource {
|
|||
|
||||
@GET
|
||||
@Path("")
|
||||
public Set<CompanyView> getMasterAccounts() {
|
||||
List<Company> companies = session.getProvider(ExampleService.class).listCompanies();
|
||||
Set<CompanyView> companyViews = new HashSet<>();
|
||||
for (Company company : companies) {
|
||||
companyViews.add(new CompanyView(company));
|
||||
}
|
||||
return companyViews;
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<CompanyRepresentation> getCompanies() {
|
||||
return session.getProvider(ExampleService.class).listCompanies();
|
||||
}
|
||||
|
||||
@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
|
||||
@NoCache
|
||||
@Path("{id}")
|
||||
public CompanyView getCompany(@PathParam("id") final String id) {
|
||||
return new CompanyView(session.getProvider(ExampleService.class).findCompany(id));
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompanyRepresentation getCompany(@PathParam("id") final String id) {
|
||||
return session.getProvider(ExampleService.class).findCompany(id);
|
||||
}
|
||||
|
||||
}
|
|
@ -15,9 +15,8 @@
|
|||
* 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.services.resource.RealmResourceProvider;
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* 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.models.KeycloakSession;
|
|
@ -1,15 +1,22 @@
|
|||
package org.keycloak.examples.domainextension.rest;
|
||||
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.Path;
|
||||
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
|
||||
public class ExampleRestResource {
|
||||
|
||||
private KeycloakSession session;
|
||||
private final KeycloakSession session;
|
||||
private final AuthenticationManager.AuthResult auth;
|
||||
|
||||
public ExampleRestResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
|
||||
}
|
||||
|
||||
@Path("companies")
|
||||
|
@ -17,4 +24,20 @@ public class ExampleRestResource {
|
|||
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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -15,19 +15,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.examples.domainextension.services;
|
||||
package org.keycloak.examples.domainextension.spi;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.examples.domainextension.entities.Company;
|
||||
import org.keycloak.examples.domainextension.CompanyRepresentation;
|
||||
import org.keycloak.provider.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);
|
||||
|
||||
}
|
|
@ -15,9 +15,8 @@
|
|||
* 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;
|
||||
|
||||
public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleService> {
|
|
@ -15,9 +15,8 @@
|
|||
* 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.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
|
@ -15,30 +15,30 @@
|
|||
* 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 javax.persistence.EntityManager;
|
||||
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.examples.domainextension.entities.Company;
|
||||
import org.keycloak.examples.domainextension.services.repository.CompanyRepository;
|
||||
import org.keycloak.examples.domainextension.jpa.Company;
|
||||
import org.keycloak.examples.domainextension.CompanyRepresentation;
|
||||
import org.keycloak.examples.domainextension.spi.ExampleService;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
public class ExampleServiceImpl implements ExampleService {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private CompanyRepository companyRepository;
|
||||
|
||||
public ExampleServiceImpl(KeycloakSession session) {
|
||||
this.session = session;
|
||||
if (getRealm() == null) {
|
||||
throw new IllegalStateException("The service cannot accept a session without a realm in it's context.");
|
||||
}
|
||||
|
||||
companyRepository = new CompanyRepository(getEntityManager());
|
||||
}
|
||||
|
||||
private EntityManager getEntityManager() {
|
||||
|
@ -50,18 +50,35 @@ public class ExampleServiceImpl implements ExampleService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<Company> listCompanies() {
|
||||
return companyRepository.getAll();
|
||||
public List<CompanyRepresentation> listCompanies() {
|
||||
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
|
||||
public Company findCompany(String id) {
|
||||
return companyRepository.findById(id);
|
||||
public CompanyRepresentation findCompany(String id) {
|
||||
Company entity = getEntityManager().find(Company.class, id);
|
||||
return entity==null ? null : new CompanyRepresentation(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCompany(Company company) {
|
||||
companyRepository.persist(company);
|
||||
public CompanyRepresentation addCompany(CompanyRepresentation 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() {
|
|
@ -15,11 +15,11 @@
|
|||
* 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.examples.domainextension.services.ExampleService;
|
||||
import org.keycloak.examples.domainextension.services.ExampleServiceImpl;
|
||||
import org.keycloak.examples.domainextension.spi.ExampleService;
|
||||
import org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class ExampleServiceProviderFactoryImpl implements ExampleServiceProvider
|
|||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "exampleService";
|
||||
return "exampleServiceImpl";
|
||||
}
|
||||
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<addPrimaryKey
|
||||
constraintName="PK_COMPANY"
|
||||
tableName="DOCDATA_COMPANY"
|
||||
tableName="EXAMPLE_COMPANY"
|
||||
columnNames="ID"
|
||||
/>
|
||||
|
||||
|
@ -27,117 +27,7 @@
|
|||
referencedTableName="REALM"
|
||||
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>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.examples.domainextension.DomainExtensionProviderFactory
|
||||
org.keycloak.examples.domainextension.jpa.ExampleJpaEntityProviderFactory
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.examples.domainextension.services.spi.ExampleServiceProviderFactoryImpl
|
||||
org.keycloak.examples.domainextension.spi.impl.ExampleServiceProviderFactoryImpl
|
|
@ -15,4 +15,4 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.examples.domainextension.services.spi.ExampleSpi
|
||||
org.keycloak.examples.domainextension.spi.ExampleSpi
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.examples.domainextension.rest.ExampleResourceProviderFactory
|
||||
org.keycloak.examples.domainextension.rest.ExampleRealmResourceProviderFactory
|
|
@ -102,6 +102,11 @@
|
|||
<artifactId>jackson-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
@ -44,4 +44,10 @@ public interface JpaEntityProvider extends Provider {
|
|||
*/
|
||||
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();
|
||||
|
||||
}
|
||||
|
|
|
@ -23,12 +23,17 @@ import liquibase.changelog.ChangeSet;
|
|||
import liquibase.changelog.RanChangeSet;
|
||||
import liquibase.exception.LiquibaseException;
|
||||
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.liquibase.conn.LiquibaseConnectionProvider;
|
||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -54,25 +59,20 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
ThreadLocalSessionContext.setCurrentSession(session);
|
||||
|
||||
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);
|
||||
if (!changeSets.isEmpty()) {
|
||||
if (changeSets.get(0).getId().equals(FIRST_VERSION)) {
|
||||
logger.info("Initializing database schema");
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
|
||||
logger.debugv("Updating database from {0} to {1}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
|
||||
} else {
|
||||
logger.infov("Updating database");
|
||||
}
|
||||
// Run update for each custom JpaEntityProvider
|
||||
Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
|
||||
for (JpaEntityProvider jpaProvider : jpaProviders) {
|
||||
String customChangelog = jpaProvider.getChangelogLocation();
|
||||
if (customChangelog != null) {
|
||||
String factoryId = jpaProvider.getFactoryId();
|
||||
String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
|
||||
liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
}
|
||||
|
||||
liquibase.update((Contexts) null);
|
||||
logger.debug("Completed database update");
|
||||
} else {
|
||||
logger.debug("Database is up to date");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to update database", e);
|
||||
|
@ -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
|
||||
public void validate(Connection connection, String defaultSchema) {
|
||||
logger.debug("Validating if database is updated");
|
||||
|
||||
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);
|
||||
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",
|
||||
ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
|
||||
throw new RuntimeException(errorMessage);
|
||||
} else {
|
||||
logger.debug("Validation passed. Database is up-to-date");
|
||||
// Validate each custom JpaEntityProvider
|
||||
Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
|
||||
for (JpaEntityProvider jpaProvider : jpaProviders) {
|
||||
String customChangelog = jpaProvider.getChangelogLocation();
|
||||
if (customChangelog != null) {
|
||||
String factoryId = jpaProvider.getFactoryId();
|
||||
String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
|
||||
liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
|
||||
validateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
}
|
||||
}
|
||||
|
||||
} 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);
|
||||
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
|
||||
public void close() {
|
||||
}
|
||||
|
|
|
@ -18,25 +18,18 @@
|
|||
package org.keycloak.connections.jpa.updater.liquibase.conn;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
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.PostgresPlusDatabase;
|
||||
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.DummyLockService;
|
||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
import liquibase.Liquibase;
|
||||
import liquibase.changelog.ChangeLogParameters;
|
||||
import liquibase.changelog.ChangeSet;
|
||||
import liquibase.changelog.DatabaseChangeLog;
|
||||
import liquibase.database.Database;
|
||||
|
@ -46,8 +39,6 @@ import liquibase.database.jvm.JdbcConnection;
|
|||
import liquibase.exception.LiquibaseException;
|
||||
import liquibase.logging.LogFactory;
|
||||
import liquibase.logging.LogLevel;
|
||||
import liquibase.parser.ChangeLogParser;
|
||||
import liquibase.parser.ChangeLogParserFactory;
|
||||
import liquibase.resource.ClassLoaderResourceAccessor;
|
||||
import liquibase.resource.ResourceAccessor;
|
||||
import liquibase.servicelocator.ServiceLocator;
|
||||
|
@ -61,12 +52,9 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
|
|||
private static final Logger logger = Logger.getLogger(DefaultLiquibaseConnectionProvider.class);
|
||||
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
private KeycloakSession keycloakSession;
|
||||
|
||||
@Override
|
||||
public LiquibaseConnectionProvider create(KeycloakSession session) {
|
||||
this.keycloakSession = session;
|
||||
if (!initialized) {
|
||||
synchronized (this) {
|
||||
if (!initialized) {
|
||||
|
@ -143,63 +131,26 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
|
|||
}
|
||||
|
||||
String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG : LiquibaseJpaUpdaterProvider.CHANGELOG;
|
||||
logger.debugf("Using changelog file: %s", changelog);
|
||||
|
||||
ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
|
||||
DatabaseChangeLog databaseChangeLog = generateDynamicChangeLog(changelog, resourceAccessor, database);
|
||||
|
||||
logger.debugf("Using changelog file %s and changelogTableName %s", changelog, database.getDatabaseChangeLogTableName());
|
||||
|
||||
return new Liquibase(databaseChangeLog, resourceAccessor, database);
|
||||
return new Liquibase(changelog, resourceAccessor, database);
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to be able to provide extra changesets as an extension to the Keycloak data model.
|
||||
* But we do not want users to be able to not execute certain parts of the Keycloak internal data model.
|
||||
* Therefore, we generate a dynamic changelog here that always contains the keycloak changelog file
|
||||
* and optionally include the user extension changelog files.
|
||||
*
|
||||
* @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);
|
||||
@Override
|
||||
public Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
|
||||
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
|
||||
if (defaultSchema != null) {
|
||||
database.setDefaultSchemaName(defaultSchema);
|
||||
}
|
||||
|
||||
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;
|
||||
if (locations.isEmpty()) {
|
||||
// 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;
|
||||
ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classloader);
|
||||
database.setDatabaseChangeLogTableName(changelogTableName);
|
||||
|
||||
logger.debugf("Using changelog file %s and changelogTableName %s", changelogLocation, database.getDatabaseChangeLogTableName());
|
||||
|
||||
return new Liquibase(changelogLocation, resourceAccessor, database);
|
||||
}
|
||||
|
||||
private static class LogWrapper extends LogFactory {
|
||||
|
|
|
@ -30,4 +30,6 @@ public interface LiquibaseConnectionProvider extends Provider {
|
|||
|
||||
Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException;
|
||||
|
||||
Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException;
|
||||
|
||||
}
|
||||
|
|
|
@ -82,4 +82,16 @@ public class JpaUtils {
|
|||
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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -138,6 +138,10 @@
|
|||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-adapter-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-authz-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
|
||||
|
|
Loading…
Reference in a new issue