diff --git a/examples/providers/domain-extension/.gitignore b/examples/providers/domain-extension/.gitignore deleted file mode 100644 index b83d22266a..0000000000 --- a/examples/providers/domain-extension/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/examples/providers/domain-extension/README.md b/examples/providers/domain-extension/README.md index c8163cd12f..e1aa2cdb62 100644 --- a/examples/providers/domain-extension/README.md +++ b/examples/providers/domain-extension/README.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/examples/providers/domain-extension/invoke-authenticated.sh b/examples/providers/domain-extension/invoke-authenticated.sh new file mode 100755 index 0000000000..19b56b1de1 --- /dev/null +++ b/examples/providers/domain-extension/invoke-authenticated.sh @@ -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"; + diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java new file mode 100644 index 0000000000..2b76d576aa --- /dev/null +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java @@ -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; + } +} diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/Company.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/Company.java deleted file mode 100644 index 6e61d33fe7..0000000000 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/Company.java +++ /dev/null @@ -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 regions = new HashSet<>(); - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "company", orphanRemoval = true) - private final Set 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 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); - } - -} diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccount.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccount.java deleted file mode 100644 index 7015d4295b..0000000000 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccount.java +++ /dev/null @@ -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; - } - -} diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccountRegionRole.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccountRegionRole.java deleted file mode 100644 index 17635dcbdb..0000000000 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccountRegionRole.java +++ /dev/null @@ -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; - } - -} diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/Region.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java similarity index 63% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/Region.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java index c0d9fc2525..bca8e1d78a 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/Region.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java @@ -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; + } } diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/entity/ExampleJpaEntityProvider.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java similarity index 69% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/entity/ExampleJpaEntityProvider.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java index e18056dbe2..b6529fd707 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/entity/ExampleJpaEntityProvider.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java @@ -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 Erik Mulder @@ -35,16 +31,20 @@ public class ExampleJpaEntityProvider implements JpaEntityProvider { @Override public List> getEntities() { - return Arrays.asList(Company.class, Region.class, UserAccount.class, UserAccountRegionRole.class); + return Collections.>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; + } } diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/entity/ExampleJpaEntityProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java similarity index 92% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/entity/ExampleJpaEntityProviderFactory.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java index 181559c2ed..2c919f4c2e 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/entity/ExampleJpaEntityProviderFactory.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java @@ -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) { diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java index b3dab2be27..8236264119 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java @@ -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 getMasterAccounts() { - List companies = session.getProvider(ExampleService.class).listCompanies(); - Set companyViews = new HashSet<>(); - for (Company company : companies) { - companyViews.add(new CompanyView(company)); - } - return companyViews; + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public List 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); } } \ No newline at end of file diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/rest/ExampleRealmResourceProvider.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java similarity index 89% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/rest/ExampleRealmResourceProvider.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java index e2aa72c491..0882d22044 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/rest/ExampleRealmResourceProvider.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java @@ -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; diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/rest/ExampleRealmResourceProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java similarity index 95% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/rest/ExampleRealmResourceProviderFactory.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java index a3eef600fc..33c2ca2720 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/providers/rest/ExampleRealmResourceProviderFactory.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java @@ -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; diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java index 43098103ea..db774cf382 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java @@ -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"); + } + } + } diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/model/CompanyView.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/model/CompanyView.java deleted file mode 100644 index ae6d166a9b..0000000000 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/model/CompanyView.java +++ /dev/null @@ -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; - } - -} diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/repository/AbstractRepository.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/repository/AbstractRepository.java deleted file mode 100644 index 5aeac0ce45..0000000000 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/repository/AbstractRepository.java +++ /dev/null @@ -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 { - - private final EntityManager entityManager; - private final Class clazz; - - @SuppressWarnings("unchecked") - public AbstractRepository(EntityManager entityManager) { - this.entityManager = entityManager; - - clazz = (Class) ((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); - } -} diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/repository/CompanyRepository.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/repository/CompanyRepository.java deleted file mode 100644 index d1c0516c6b..0000000000 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/repository/CompanyRepository.java +++ /dev/null @@ -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 { - - public CompanyRepository(EntityManager entityManager) { - super(entityManager); - } - - public List getAll() { - return getEntityManager().createNamedQuery("findAllCompanies", Company.class).getResultList(); - } - - public List getAll(String realmId) { - return getEntityManager().createNamedQuery("findByRealm", Company.class).setParameter("realmId", realmId) - .getResultList(); - } - -} diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/ExampleService.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java similarity index 73% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/ExampleService.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java index 0b5e7ddb12..7f41327102 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/ExampleService.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java @@ -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 listCompanies(); + List listCompanies(); - Company findCompany(String id); + CompanyRepresentation findCompany(String id); - void addCompany(Company company); + CompanyRepresentation addCompany(CompanyRepresentation company); } diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleServiceProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java similarity index 86% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleServiceProviderFactory.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java index f1b72a79d7..2c6a122a73 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleServiceProviderFactory.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java @@ -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 { diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleSpi.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java similarity index 90% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleSpi.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java index 481a5e009e..811ec92fb6 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleSpi.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java @@ -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; diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/ExampleServiceImpl.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java similarity index 53% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/ExampleServiceImpl.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java index ecadcc14ee..49cc228103 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/ExampleServiceImpl.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java @@ -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 listCompanies() { - return companyRepository.getAll(); + public List listCompanies() { + List companyEntities = getEntityManager().createNamedQuery("findByRealm", Company.class) + .setParameter("realmId", getRealm().getId()) + .getResultList(); + + List 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() { diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleServiceProviderFactoryImpl.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java similarity index 84% rename from examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleServiceProviderFactoryImpl.java rename to examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java index 6fb110bfae..e4e2ddf092 100644 --- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/spi/ExampleServiceProviderFactoryImpl.java +++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java @@ -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"; } } diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml index 3d96bdbe02..ec4e5a1511 100644 --- a/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml +++ b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml @@ -16,7 +16,7 @@ @@ -27,117 +27,7 @@ referencedTableName="REALM" referencedColumnNames="ID" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory index 19058b3b77..c7b88db9a7 100644 --- a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory +++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.examples.domainextension.DomainExtensionProviderFactory +org.keycloak.examples.domainextension.jpa.ExampleJpaEntityProviderFactory diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.services.spi.ExampleServiceProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory similarity index 88% rename from examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.services.spi.ExampleServiceProviderFactory rename to examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory index 0b048653e5..57f9f89dd5 100644 --- a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.services.spi.ExampleServiceProviderFactory +++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.examples.domainextension.services.spi.ExampleServiceProviderFactoryImpl +org.keycloak.examples.domainextension.spi.impl.ExampleServiceProviderFactoryImpl diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 47b695ed25..e013bbdce5 100644 --- a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.examples.domainextension.services.spi.ExampleSpi +org.keycloak.examples.domainextension.spi.ExampleSpi diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory index f46b8a69f5..ea81617385 100644 --- a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.examples.domainextension.rest.ExampleResourceProviderFactory \ No newline at end of file +org.keycloak.examples.domainextension.rest.ExampleRealmResourceProviderFactory \ No newline at end of file diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml index 47035c58aa..e1c8742af4 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -102,6 +102,11 @@ jackson-core provided + + junit + junit + test + diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java index c64c96520d..567080e8d9 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java @@ -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(); + } diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java index 8e9d253a6d..2610174ee5 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java @@ -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 Stian Thorgersen @@ -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 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 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 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 changeSets = liquibase.listUnrunChangeSets((Contexts) null); + if (!changeSets.isEmpty()) { + List 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 changeSets = liquibase.listUnrunChangeSets((Contexts) null); - if (!changeSets.isEmpty()) { - List 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 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 changeSets = liquibase.listUnrunChangeSets((Contexts) null); + if (!changeSets.isEmpty()) { + List 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() { } diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java index ca6d722ebd..5a5870212c 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java @@ -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 locations = new ArrayList<>(); - Set 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 { diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java index 5aa81cc84e..215bd1d07a 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java @@ -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; + } diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java index d93c02ba8b..5ac7d2f6a9 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java @@ -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())); + } + } diff --git a/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java b/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java new file mode 100644 index 0000000000..838f1c05e7 --- /dev/null +++ b/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java @@ -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 Marek Posolda + */ +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")); + } +} diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index d394e2a383..c1d0412011 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -138,6 +138,10 @@ org.keycloak keycloak-saml-adapter-core + + org.keycloak + keycloak-authz-client + org.keycloak keycloak-saml-servlet-filter-adapter