Merge pull request #347 from stianst/master
Ensure Realm names are unique
This commit is contained in:
commit
dabf101d96
11 changed files with 236 additions and 22 deletions
|
@ -0,0 +1,27 @@
|
||||||
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class ModelDuplicateException extends ModelException {
|
||||||
|
|
||||||
|
public ModelDuplicateException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelDuplicateException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelDuplicateException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelDuplicateException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelDuplicateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class ModelException extends RuntimeException {
|
||||||
|
|
||||||
|
public ModelException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.TypedQuery;
|
import javax.persistence.TypedQuery;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -18,7 +19,7 @@ public class JpaKeycloakSession implements KeycloakSession {
|
||||||
protected EntityManager em;
|
protected EntityManager em;
|
||||||
|
|
||||||
public JpaKeycloakSession(EntityManager em) {
|
public JpaKeycloakSession(EntityManager em) {
|
||||||
this.em = em;
|
this.em = PersistenceExceptionConverter.create(em);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.keycloak.models.jpa;
|
||||||
|
|
||||||
|
import org.hibernate.exception.ConstraintViolationException;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class PersistenceExceptionConverter implements InvocationHandler {
|
||||||
|
|
||||||
|
private EntityManager em;
|
||||||
|
|
||||||
|
public static EntityManager create(EntityManager em) {
|
||||||
|
return (EntityManager) Proxy.newProxyInstance(EntityManager.class.getClassLoader(), new Class[]{EntityManager.class}, new PersistenceExceptionConverter(em));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PersistenceExceptionConverter(EntityManager em) {
|
||||||
|
this.em = em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||||
|
try {
|
||||||
|
return method.invoke(em, args);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
Throwable c = e.getCause();
|
||||||
|
if (c.getCause() != null && c.getCause() instanceof ConstraintViolationException) {
|
||||||
|
throw new ModelDuplicateException(c);
|
||||||
|
} else {
|
||||||
|
throw new ModelException(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
package org.keycloak.models.jpa.entities;
|
package org.keycloak.models.jpa.entities;
|
||||||
|
|
||||||
|
|
||||||
import org.keycloak.models.ApplicationModel;
|
|
||||||
|
|
||||||
import javax.persistence.CascadeType;
|
import javax.persistence.CascadeType;
|
||||||
import javax.persistence.CollectionTable;
|
import javax.persistence.CollectionTable;
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
|
@ -37,7 +35,9 @@ public class RealmEntity {
|
||||||
@Id
|
@Id
|
||||||
protected String id;
|
protected String id;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
protected String name;
|
protected String name;
|
||||||
|
|
||||||
protected boolean enabled;
|
protected boolean enabled;
|
||||||
protected boolean sslNotRequired;
|
protected boolean sslNotRequired;
|
||||||
protected boolean registrationAllowed;
|
protected boolean registrationAllowed;
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.keycloak.models.mongo.api;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.TYPE;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
@Target({TYPE})
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@Inherited
|
||||||
|
public @interface MongoIndex {
|
||||||
|
|
||||||
|
String[] fields();
|
||||||
|
|
||||||
|
String name() default "";
|
||||||
|
|
||||||
|
boolean unique() default false;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.keycloak.models.mongo.api;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.TYPE;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
@Target({TYPE})
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@Inherited
|
||||||
|
public @interface MongoIndexes {
|
||||||
|
|
||||||
|
MongoIndex[] value();
|
||||||
|
|
||||||
|
}
|
|
@ -6,11 +6,16 @@ import com.mongodb.DB;
|
||||||
import com.mongodb.DBCollection;
|
import com.mongodb.DBCollection;
|
||||||
import com.mongodb.DBCursor;
|
import com.mongodb.DBCursor;
|
||||||
import com.mongodb.DBObject;
|
import com.mongodb.DBObject;
|
||||||
|
import com.mongodb.MongoException;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
import org.keycloak.models.mongo.api.MongoCollection;
|
import org.keycloak.models.mongo.api.MongoCollection;
|
||||||
import org.keycloak.models.mongo.api.MongoEntity;
|
import org.keycloak.models.mongo.api.MongoEntity;
|
||||||
import org.keycloak.models.mongo.api.MongoField;
|
import org.keycloak.models.mongo.api.MongoField;
|
||||||
import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
|
import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
|
||||||
|
import org.keycloak.models.mongo.api.MongoIndex;
|
||||||
|
import org.keycloak.models.mongo.api.MongoIndexes;
|
||||||
import org.keycloak.models.mongo.api.MongoStore;
|
import org.keycloak.models.mongo.api.MongoStore;
|
||||||
import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
|
import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
|
||||||
import org.keycloak.models.mongo.api.context.MongoTask;
|
import org.keycloak.models.mongo.api.context.MongoTask;
|
||||||
|
@ -88,6 +93,8 @@ public class MongoStoreImpl implements MongoStore {
|
||||||
// dropDatabase();
|
// dropDatabase();
|
||||||
clearManagedCollections(managedEntityTypes);
|
clearManagedCollections(managedEntityTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initManagedCollections(managedEntityTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void dropDatabase() {
|
protected void dropDatabase() {
|
||||||
|
@ -106,6 +113,46 @@ public class MongoStoreImpl implements MongoStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void initManagedCollections(Class<? extends MongoEntity>[] managedEntityTypes) {
|
||||||
|
for (Class<? extends MongoEntity> clazz : managedEntityTypes) {
|
||||||
|
EntityInfo entityInfo = getEntityInfo(clazz);
|
||||||
|
String dbCollectionName = entityInfo.getDbCollectionName();
|
||||||
|
if (dbCollectionName != null && !database.collectionExists(dbCollectionName)) {
|
||||||
|
DBCollection dbCollection = database.getCollection(dbCollectionName);
|
||||||
|
|
||||||
|
logger.debug("Created collection " + dbCollection.getName() + " in " + this.database.getName());
|
||||||
|
|
||||||
|
MongoIndex index = clazz.getAnnotation(MongoIndex.class);
|
||||||
|
if (index != null) {
|
||||||
|
createIndex(dbCollection, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
MongoIndexes indexes = clazz.getAnnotation(MongoIndexes.class);
|
||||||
|
if (indexes != null) {
|
||||||
|
for (MongoIndex i : indexes.value()) {
|
||||||
|
createIndex(dbCollection, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createIndex(DBCollection dbCollection, MongoIndex index) {
|
||||||
|
BasicDBObject fields = new BasicDBObject();
|
||||||
|
for (String f : index.fields()) {
|
||||||
|
fields.put(f, 1);
|
||||||
|
}
|
||||||
|
String name = index.name();
|
||||||
|
if (name.length() == 0) {
|
||||||
|
name = null;
|
||||||
|
}
|
||||||
|
boolean unique = index.unique();
|
||||||
|
|
||||||
|
dbCollection.ensureIndex(fields, name, unique);
|
||||||
|
|
||||||
|
logger.debug("Created index " + fields + (unique ? " (unique)" : "") + " on " + dbCollection.getName() + " in " + this.database.getName());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
public void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
||||||
Class<? extends MongoEntity> clazz = entity.getClass();
|
Class<? extends MongoEntity> clazz = entity.getClass();
|
||||||
|
@ -129,7 +176,15 @@ public class MongoStoreImpl implements MongoStore {
|
||||||
// Adding "_id"
|
// Adding "_id"
|
||||||
dbObject.put("_id", currentId);
|
dbObject.put("_id", currentId);
|
||||||
|
|
||||||
dbCollection.insert(dbObject);
|
try {
|
||||||
|
dbCollection.insert(dbObject);
|
||||||
|
} catch (MongoException e) {
|
||||||
|
if (e instanceof MongoException.DuplicateKey) {
|
||||||
|
throw new ModelDuplicateException(e);
|
||||||
|
} else {
|
||||||
|
throw new ModelException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Treat object as created in this transaction (It is already submited to transaction)
|
// Treat object as created in this transaction (It is already submited to transaction)
|
||||||
context.addCreatedEntity(entity);
|
context.addCreatedEntity(entity);
|
||||||
|
|
|
@ -1242,12 +1242,13 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ApplicationModel getAdminApp() {
|
public ApplicationModel getAdminApp() {
|
||||||
return new ApplicationAdapter(this, realm.getAdminApp(), invocationContext);
|
ApplicationEntity appData = getMongoStore().loadEntity(ApplicationEntity.class, realm.getAdminAppId(), invocationContext);
|
||||||
|
return new ApplicationAdapter(this, appData, invocationContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAdminApp(ApplicationModel app) {
|
public void setAdminApp(ApplicationModel app) {
|
||||||
realm.setAdminApp(((ApplicationAdapter) app).getMongoEntity());
|
realm.setAdminAppId(app.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,6 +7,8 @@ import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity;
|
||||||
import org.keycloak.models.mongo.api.MongoCollection;
|
import org.keycloak.models.mongo.api.MongoCollection;
|
||||||
import org.keycloak.models.mongo.api.MongoEntity;
|
import org.keycloak.models.mongo.api.MongoEntity;
|
||||||
import org.keycloak.models.mongo.api.MongoField;
|
import org.keycloak.models.mongo.api.MongoField;
|
||||||
|
import org.keycloak.models.mongo.api.MongoIndex;
|
||||||
|
import org.keycloak.models.mongo.api.MongoIndexes;
|
||||||
import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
|
import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -21,6 +23,7 @@ import java.util.Set;
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
@MongoCollection(collectionName = "realms")
|
@MongoCollection(collectionName = "realms")
|
||||||
|
@MongoIndex(fields = { "name" }, unique = true)
|
||||||
public class RealmEntity extends AbstractMongoIdentifiableEntity implements MongoEntity {
|
public class RealmEntity extends AbstractMongoIdentifiableEntity implements MongoEntity {
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
@ -70,7 +73,7 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
|
||||||
private long auditExpiration;
|
private long auditExpiration;
|
||||||
private List<String> auditListeners = new ArrayList<String>();
|
private List<String> auditListeners = new ArrayList<String>();
|
||||||
|
|
||||||
private ApplicationEntity adminApp;
|
private String adminAppId;
|
||||||
|
|
||||||
@MongoField
|
@MongoField
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
@ -397,12 +400,12 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
|
||||||
}
|
}
|
||||||
|
|
||||||
@MongoField
|
@MongoField
|
||||||
public ApplicationEntity getAdminApp() {
|
public String getAdminAppId() {
|
||||||
return adminApp;
|
return adminAppId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAdminApp(ApplicationEntity adminApp) {
|
public void setAdminAppId(String adminAppId) {
|
||||||
this.adminApp = adminApp;
|
this.adminAppId = adminAppId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.jboss.resteasy.util.GenericType;
|
||||||
import org.keycloak.models.AdminRoles;
|
import org.keycloak.models.AdminRoles;
|
||||||
import org.keycloak.models.ApplicationModel;
|
import org.keycloak.models.ApplicationModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -49,6 +50,7 @@ public class RealmsAdminResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final CacheControl noCache = new CacheControl();
|
public static final CacheControl noCache = new CacheControl();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
noCache.setNoCache(true);
|
noCache.setNoCache(true);
|
||||||
}
|
}
|
||||||
|
@ -97,21 +99,22 @@ public class RealmsAdminResource {
|
||||||
|
|
||||||
logger.debug("importRealm: {0}", rep.getRealm());
|
logger.debug("importRealm: {0}", rep.getRealm());
|
||||||
RealmManager realmManager = new RealmManager(session);
|
RealmManager realmManager = new RealmManager(session);
|
||||||
if (realmManager.getRealmByName(rep.getRealm()) != null) {
|
|
||||||
|
try {
|
||||||
|
RealmModel realm = realmManager.importRealm(rep);
|
||||||
|
grantPermissionsToRealmCreator(realm);
|
||||||
|
|
||||||
|
URI location = realmUrl(uriInfo).build(realm.getName());
|
||||||
|
logger.debug("imported realm success, sending back: {0}", location.toString());
|
||||||
|
return Response.created(location).build();
|
||||||
|
} catch (ModelDuplicateException e) {
|
||||||
return Flows.errors().exists("Realm " + rep.getRealm() + " already exists");
|
return Flows.errors().exists("Realm " + rep.getRealm() + " already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
RealmModel realm = realmManager.importRealm(rep);
|
|
||||||
grantPermissionsToRealmCreator(realm);
|
|
||||||
|
|
||||||
URI location = realmUrl(uriInfo).build(realm.getName());
|
|
||||||
logger.debug("imported realm success, sending back: {0}", location.toString());
|
|
||||||
return Response.created(location).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
public Response uploadRealm(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
|
public Response uploadRealm(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
|
||||||
if (!auth.hasRealmRole(AdminRoles.CREATE_REALM)) {
|
if (!auth.hasRealmRole(AdminRoles.CREATE_REALM)) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
@ -122,9 +125,16 @@ public class RealmsAdminResource {
|
||||||
RealmManager realmManager = new RealmManager(session);
|
RealmManager realmManager = new RealmManager(session);
|
||||||
for (InputPart inputPart : inputParts) {
|
for (InputPart inputPart : inputParts) {
|
||||||
inputPart.setMediaType(MediaType.APPLICATION_JSON_TYPE);
|
inputPart.setMediaType(MediaType.APPLICATION_JSON_TYPE);
|
||||||
RealmRepresentation rep = inputPart.getBody(new GenericType<RealmRepresentation>(){});
|
RealmRepresentation rep = inputPart.getBody(new GenericType<RealmRepresentation>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
RealmModel realm;
|
||||||
|
try {
|
||||||
|
realm = realmManager.importRealm(rep);
|
||||||
|
} catch (ModelDuplicateException e) {
|
||||||
|
return Flows.errors().exists("Realm " + rep.getRealm() + " already exists");
|
||||||
|
}
|
||||||
|
|
||||||
RealmModel realm = realmManager.importRealm(rep);
|
|
||||||
grantPermissionsToRealmCreator(realm);
|
grantPermissionsToRealmCreator(realm);
|
||||||
|
|
||||||
if (inputParts.size() == 1) {
|
if (inputParts.size() == 1) {
|
||||||
|
|
Loading…
Reference in a new issue