diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/MongoDBSessionFactory.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/MongoDBSessionFactory.java new file mode 100644 index 0000000000..1c82f49f65 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/MongoDBSessionFactory.java @@ -0,0 +1,43 @@ +package org.keycloak.services.models.nosql.adapters; + +import java.net.UnknownHostException; + +import com.mongodb.DB; +import com.mongodb.MongoClient; +import org.keycloak.services.models.KeycloakSession; +import org.keycloak.services.models.KeycloakSessionFactory; +import org.keycloak.services.models.nosql.api.NoSQL; +import org.keycloak.services.models.nosql.impl.MongoDBImpl; + +/** + * NoSQL implementation based on MongoDB + * + * @author Marek Posolda + */ +public class MongoDBSessionFactory implements KeycloakSessionFactory { + + private final MongoClient mongoClient; + private final NoSQL mongoDB; + + public MongoDBSessionFactory(String host, int port, String dbName) { + try { + // TODO: authentication support + mongoClient = new MongoClient(host, port); + + DB db = mongoClient.getDB(dbName); + mongoDB = new MongoDBImpl(db); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + @Override + public KeycloakSession createSession() { + return new NoSQLSession(mongoDB); + } + + @Override + public void close() { + mongoClient.close(); + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLRealm.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLRealm.java deleted file mode 100644 index c061f8b8c3..0000000000 --- a/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLRealm.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.keycloak.services.models.nosql.adapters; - -import org.keycloak.services.models.nosql.api.NoSQLCollection; -import org.keycloak.services.models.nosql.api.NoSQLField; -import org.keycloak.services.models.nosql.api.NoSQLId; -import org.keycloak.services.models.nosql.api.NoSQLObject; - -/** - * @author Marek Posolda - */ -@NoSQLCollection(collectionName = "realms") -public class NoSQLRealm implements NoSQLObject { - - private String oid; - private String prop1; - private Integer prop2; - - @NoSQLId - public String getOid() { - return oid; - } - - public void setOid(String oid) { - this.oid = oid; - } - - @NoSQLField(fieldName = "property1") - public String getProp1() { - return prop1; - } - - public void setProp1(String prop1) { - this.prop1 = prop1; - } - - @NoSQLField(fieldName = "property2") - public Integer getProp2() { - return prop2; - } - - public void setProp2(Integer prop2) { - this.prop2 = prop2; - } - - @Override - public String toString() { - return "NoSQLRealm [ oid=" + oid + ", prop1=" + prop1 + ", prop2=" + prop2 + "]"; - } -} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLSession.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLSession.java new file mode 100644 index 0000000000..10e5413a2e --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLSession.java @@ -0,0 +1,67 @@ +package org.keycloak.services.models.nosql.adapters; + +import java.util.List; + +import org.jboss.resteasy.spi.NotImplementedYetException; +import org.keycloak.services.models.KeycloakSession; +import org.keycloak.services.models.KeycloakTransaction; +import org.keycloak.services.models.RealmModel; +import org.keycloak.services.models.UserModel; +import org.keycloak.services.models.nosql.data.RealmData; +import org.keycloak.services.models.nosql.api.NoSQL; + +/** + * @author Marek Posolda + */ +public class NoSQLSession implements KeycloakSession { + + private static final NoSQLTransaction PLACEHOLDER = new NoSQLTransaction(); + private final NoSQL noSQL; + + public NoSQLSession(NoSQL noSQL) { + this.noSQL = noSQL; + } + + @Override + public KeycloakTransaction getTransaction() { + return PLACEHOLDER; + } + + @Override + public void close() { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public RealmModel createRealm(String name) { + RealmData newRealm = new RealmData(); + newRealm.setName(name); + + noSQL.saveObject(newRealm); + + RealmAdapter realm = new RealmAdapter(newRealm, noSQL); + return realm; + } + + @Override + public RealmModel createRealm(String id, String name) { + // Ignore ID for now. It seems that it exists just for workaround picketlink + return createRealm(name); + } + + @Override + public RealmModel getRealm(String id) { + RealmData realmData = noSQL.loadObject(RealmData.class, id); + return new RealmAdapter(realmData, noSQL); + } + + @Override + public List getRealms(UserModel admin) { + throw new NotImplementedYetException(); + } + + @Override + public void deleteRealm(RealmModel realm) { + noSQL.removeObject(RealmData.class, realm.getId()); + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLTransaction.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLTransaction.java new file mode 100644 index 0000000000..60a1338970 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLTransaction.java @@ -0,0 +1,39 @@ +package org.keycloak.services.models.nosql.adapters; + +import org.keycloak.services.models.KeycloakTransaction; + +/** + * @author Marek Posolda + */ +public class NoSQLTransaction implements KeycloakTransaction { + + @Override + public void begin() { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void commit() { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void rollback() { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void setRollbackOnly() { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public boolean getRollbackOnly() { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public boolean isActive() { + return true; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/RealmAdapter.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/RealmAdapter.java new file mode 100644 index 0000000000..58f73ef1ca --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/RealmAdapter.java @@ -0,0 +1,492 @@ +package org.keycloak.services.models.nosql.adapters; + +import java.io.IOException; +import java.io.StringWriter; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bouncycastle.openssl.PEMWriter; +import org.jboss.resteasy.security.PemUtils; +import org.keycloak.services.models.ApplicationModel; +import org.keycloak.services.models.RealmModel; +import org.keycloak.services.models.RequiredCredentialModel; +import org.keycloak.services.models.RoleModel; +import org.keycloak.services.models.SocialLinkModel; +import org.keycloak.services.models.UserCredentialModel; +import org.keycloak.services.models.UserModel; +import org.keycloak.services.models.nosql.api.NoSQL; +import org.keycloak.services.models.nosql.api.NoSQLQuery; +import org.keycloak.services.models.nosql.data.RealmData; +import org.keycloak.services.models.nosql.data.RoleData; +import org.keycloak.services.models.nosql.data.UserData; + +/** + * @author Marek Posolda + */ +public class RealmAdapter implements RealmModel { + + private final RealmData realm; + private final NoSQL noSQL; + + protected volatile transient PublicKey publicKey; + protected volatile transient PrivateKey privateKey; + + public RealmAdapter(RealmData realmData, NoSQL noSQL) { + this.realm = realmData; + this.noSQL = noSQL; + } + + @Override + public String getId() { + return realm.getId(); + } + + @Override + public String getName() { + return realm.getName(); + } + + @Override + public void setName(String name) { + realm.setName(name); + updateRealm(); + } + + @Override + public boolean isEnabled() { + return realm.isEnabled(); + } + + @Override + public void setEnabled(boolean enabled) { + realm.setEnabled(enabled); + updateRealm(); + } + + @Override + public boolean isSocial() { + return realm.isSocial(); + } + + @Override + public void setSocial(boolean social) { + realm.setSocial(social); + updateRealm(); + } + + @Override + public boolean isAutomaticRegistrationAfterSocialLogin() { + return realm.isAutomaticRegistrationAfterSocialLogin(); + } + + @Override + public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) { + realm.setAutomaticRegistrationAfterSocialLogin(automaticRegistrationAfterSocialLogin); + updateRealm(); + } + + @Override + public boolean isSslNotRequired() { + return realm.isSslNotRequired(); + } + + @Override + public void setSslNotRequired(boolean sslNotRequired) { + realm.setSslNotRequired(sslNotRequired); + updateRealm(); + } + + @Override + public boolean isCookieLoginAllowed() { + return realm.isCookieLoginAllowed(); + } + + @Override + public void setCookieLoginAllowed(boolean cookieLoginAllowed) { + realm.setCookieLoginAllowed(cookieLoginAllowed); + updateRealm(); + } + + @Override + public boolean isRegistrationAllowed() { + return realm.isRegistrationAllowed(); + } + + @Override + public void setRegistrationAllowed(boolean registrationAllowed) { + realm.setRegistrationAllowed(registrationAllowed); + updateRealm(); + } + + @Override + public int getTokenLifespan() { + return realm.getTokenLifespan(); + } + + @Override + public void setTokenLifespan(int tokenLifespan) { + realm.setTokenLifespan(tokenLifespan); + updateRealm(); + } + + @Override + public int getAccessCodeLifespan() { + return realm.getAccessCodeLifespan(); + } + + @Override + public void setAccessCodeLifespan(int accessCodeLifespan) { + realm.setAccessCodeLifespan(accessCodeLifespan); + updateRealm(); + } + + @Override + public String getPublicKeyPem() { + return realm.getPublicKeyPem(); + } + + @Override + public void setPublicKeyPem(String publicKeyPem) { + realm.setPublicKeyPem(publicKeyPem); + this.publicKey = null; + updateRealm(); + } + + @Override + public String getPrivateKeyPem() { + return realm.getPrivateKeyPem(); + } + + @Override + public void setPrivateKeyPem(String privateKeyPem) { + realm.setPrivateKeyPem(privateKeyPem); + this.privateKey = null; + updateRealm(); + } + + @Override + public PublicKey getPublicKey() { + if (publicKey != null) return publicKey; + String pem = getPublicKeyPem(); + if (pem != null) { + try { + publicKey = PemUtils.decodePublicKey(pem); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return publicKey; + } + + @Override + public void setPublicKey(PublicKey publicKey) { + this.publicKey = publicKey; + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try { + pemWriter.writeObject(publicKey); + pemWriter.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + String s = writer.toString(); + setPublicKeyPem(PemUtils.removeBeginEnd(s)); + } + + @Override + public PrivateKey getPrivateKey() { + if (privateKey != null) return privateKey; + String pem = getPrivateKeyPem(); + if (pem != null) { + try { + privateKey = PemUtils.decodePrivateKey(pem); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return privateKey; + } + + @Override + public void setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try { + pemWriter.writeObject(privateKey); + pemWriter.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + String s = writer.toString(); + setPrivateKeyPem(PemUtils.removeBeginEnd(s)); + } + + @Override + public List getRequiredCredentials() { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void addRequiredCredential(String cred) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public boolean validatePassword(UserModel user, String password) { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public boolean validateTOTP(UserModel user, String password, String token) { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void updateCredential(UserModel user, UserCredentialModel cred) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public UserModel getUser(String name) { + NoSQLQuery query = NoSQLQuery.create().put("loginName", name).put("realmId", getId()); + UserData user = noSQL.loadSingleObject(UserData.class, query); + + if (user == null) { + return null; + } else { + return new UserAdapter(user, noSQL); + } + } + + @Override + public UserModel addUser(String username) { + if (getUser(username) != null) { + throw new IllegalArgumentException("User " + username + " already exists"); + } + + UserData userData = new UserData(); + userData.setLoginName(username); + userData.setEnabled(true); + userData.setRealmId(getId()); + + noSQL.saveObject(userData); + return new UserAdapter(userData, noSQL); + } + + @Override + public RoleAdapter getRole(String name) { + NoSQLQuery query = NoSQLQuery.create().put("name", name).put("realmId", getId()); + RoleData role = noSQL.loadSingleObject(RoleData.class, query); + if (role == null) { + return null; + } else { + return new RoleAdapter(role, noSQL); + } + } + + @Override + public RoleModel addRole(String name) { + if (getRole(name) != null) { + throw new IllegalArgumentException("Role " + name + " already exists"); + } + + RoleData roleData = new RoleData(); + roleData.setName(name); + roleData.setRealmId(getId()); + + noSQL.saveObject(roleData); + return new RoleAdapter(roleData, noSQL); + } + + @Override + public List getRoles() { + NoSQLQuery query = NoSQLQuery.create().put("realmId", getId()); + List roles = noSQL.loadObjects(RoleData.class, query); + + List result = new ArrayList(); + for (RoleData role : roles) { + result.add(new RoleAdapter(role, noSQL)); + } + + return result; + } + + @Override + public List getDefaultRoles() { + List defaultRoleModels = new ArrayList(); + if (realm.getDefaultRoles() != null) { + for (String name : realm.getDefaultRoles()) { + RoleAdapter role = getRole(name); + if (role != null) { + defaultRoleModels.add(role); + } + } + } + return defaultRoleModels; + } + + @Override + public void addDefaultRole(String name) { + if (getRole(name) == null) { + addRole(name); + } + + String[] defaultRoles = realm.getDefaultRoles(); + if (defaultRoles == null) { + defaultRoles = new String[1]; + } else { + defaultRoles = Arrays.copyOf(defaultRoles, defaultRoles.length + 1); + } + defaultRoles[defaultRoles.length - 1] = name; + + realm.setDefaultRoles(defaultRoles); + updateRealm(); + } + + @Override + public void updateDefaultRoles(String[] defaultRoles) { + for (String name : defaultRoles) { + if (getRole(name) == null) { + addRole(name); + } + } + + realm.setDefaultRoles(defaultRoles); + updateRealm(); + } + + @Override + public Map getResourceNameMap() { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public List getApplications() { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public ApplicationModel addApplication(String name) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public boolean hasRole(UserModel user, RoleModel role) { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void grantRole(UserModel user, RoleModel role) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public Set getRoleMappings(UserModel user) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void addScope(UserModel agent, String roleName) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public Set getScope(UserModel agent) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public boolean isRealmAdmin(UserModel agent) { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void addRealmAdmin(UserModel agent) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public RoleModel getRoleById(String id) { + RoleData role = noSQL.loadObject(RoleData.class, id); + if (role == null) { + return null; + } else { + return new RoleAdapter(role, noSQL); + } + } + + @Override + public List getRequiredApplicationCredentials() { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public List getRequiredOAuthClientCredentials() { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public boolean hasRole(UserModel user, String role) { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public ApplicationModel getApplicationById(String id) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void addRequiredOAuthClientCredential(String type) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void addRequiredResourceCredential(String type) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void updateRequiredCredentials(Set creds) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void updateRequiredOAuthClientCredentials(Set creds) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void updateRequiredApplicationCredentials(Set creds) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public UserModel getUserBySocialLink(SocialLinkModel socialLink) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public Set getSocialLinks(UserModel user) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void addSocialLink(UserModel user, SocialLinkModel socialLink) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void removeSocialLink(UserModel user, SocialLinkModel socialLink) { + //To change body of implemented methods use File | Settings | File Templates. + } + + protected void updateRealm() { + noSQL.saveObject(realm); + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/RoleAdapter.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/RoleAdapter.java new file mode 100644 index 0000000000..edde2e40bd --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/RoleAdapter.java @@ -0,0 +1,49 @@ +package org.keycloak.services.models.nosql.adapters; + +import org.keycloak.services.models.RoleModel; +import org.keycloak.services.models.nosql.api.NoSQL; +import org.keycloak.services.models.nosql.data.RoleData; +import org.keycloak.services.models.nosql.data.UserData; + +/** + * Wrapper around RoleData object, which will persist wrapped object after each set operation (compatibility with picketlink based impl) + * + * @author Marek Posolda + */ +public class RoleAdapter implements RoleModel { + + private final RoleData role; + private final NoSQL noSQL; + + public RoleAdapter(RoleData roleData, NoSQL noSQL) { + this.role = roleData; + this.noSQL = noSQL; + } + + @Override + public String getName() { + return role.getName(); + } + + @Override + public String getDescription() { + return role.getDescription(); + } + + @Override + public void setDescription(String description) { + role.setDescription(description); + noSQL.saveObject(role); + } + + @Override + public String getId() { + return role.getId(); + } + + @Override + public void setName(String name) { + role.setName(name); + noSQL.saveObject(role); + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/UserAdapter.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/UserAdapter.java new file mode 100644 index 0000000000..781ba266c8 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/UserAdapter.java @@ -0,0 +1,93 @@ +package org.keycloak.services.models.nosql.adapters; + +import java.util.Map; + +import org.keycloak.services.models.UserModel; +import org.keycloak.services.models.nosql.api.NoSQL; +import org.keycloak.services.models.nosql.data.UserData; + +/** + * Wrapper around UserData object, which will persist wrapped object after each set operation (compatibility with picketlink based impl) + * + * @author Marek Posolda + */ +public class UserAdapter implements UserModel { + + private final UserData user; + private final NoSQL noSQL; + + public UserAdapter(UserData userData, NoSQL noSQL) { + this.user = userData; + this.noSQL = noSQL; + } + + @Override + public String getLoginName() { + return user.getLoginName(); + } + + @Override + public boolean isEnabled() { + return user.isEnabled(); + } + + @Override + public void setEnabled(boolean enabled) { + user.setEnabled(enabled); + noSQL.saveObject(user); + } + + @Override + public String getFirstName() { + return user.getFirstName(); + } + + @Override + public void setFirstName(String firstName) { + user.setFirstName(firstName); + noSQL.saveObject(user); + } + + @Override + public String getLastName() { + return user.getLastName(); + } + + @Override + public void setLastName(String lastName) { + user.setLastName(lastName); + noSQL.saveObject(user); + } + + @Override + public String getEmail() { + return user.getEmail(); + } + + @Override + public void setEmail(String email) { + user.setEmail(email); + noSQL.saveObject(user); + } + + @Override + public void setAttribute(String name, String value) { + user.setAttribute(name, value); + } + + @Override + public void removeAttribute(String name) { + user.removeAttribute(name); + noSQL.saveObject(user); + } + + @Override + public String getAttribute(String name) { + return user.getAttribute(name); + } + + @Override + public Map getAttributes() { + return user.getAttributes(); + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/AbstractAttributedNoSQLObject.java b/services/src/main/java/org/keycloak/services/models/nosql/api/AbstractAttributedNoSQLObject.java new file mode 100644 index 0000000000..efdddee67c --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/AbstractAttributedNoSQLObject.java @@ -0,0 +1,37 @@ +package org.keycloak.services.models.nosql.api; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Marek Posolda + */ +public abstract class AbstractAttributedNoSQLObject implements AttributedNoSQLObject { + + // Simple hashMap for now (no thread-safe) + private Map attributes = new HashMap(); + + @Override + public void setAttribute(String name, String value) { + attributes.put(name, value); + } + + @Override + public void removeAttribute(String name) { + // attributes.remove(name); + + // ensure that particular attribute has null value, so it will be deleted in DB. TODO: needs to be improved + attributes.put(name, null); + } + + @Override + public String getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Map getAttributes() { + return Collections.unmodifiableMap(attributes); + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java index dc16bb6ec1..9da8166286 100644 --- a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java @@ -15,12 +15,14 @@ public interface NoSQL { T loadObject(Class type, String oid); - List loadObjects(Class type, Map queryAttributes); + T loadSingleObject(Class type, NoSQLQuery query); + + List loadObjects(Class type, NoSQLQuery query); // Object must have filled oid void removeObject(NoSQLObject object); void removeObject(Class type, String oid); - void removeObjects(Class type, Map queryAttributes); + void removeObjects(Class type, NoSQLQuery query); } diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLQuery.java b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLQuery.java new file mode 100644 index 0000000000..d26064400c --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLQuery.java @@ -0,0 +1,34 @@ +package org.keycloak.services.models.nosql.api; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Marek Posolda + */ +public class NoSQLQuery { + + private Map queryAttributes = new HashMap(); + + private NoSQLQuery() {}; + + public static NoSQLQuery create() { + return new NoSQLQuery(); + } + + public NoSQLQuery put(String name, Object value) { + queryAttributes.put(name, value); + return this; + } + + public Map getQueryAttributes() { + return Collections.unmodifiableMap(queryAttributes); + } + + @Override + public String toString() { + return "NoSQLQuery [" + queryAttributes + "]"; + } + +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/types/Converter.java b/services/src/main/java/org/keycloak/services/models/nosql/api/types/Converter.java new file mode 100644 index 0000000000..221db2b7a9 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/types/Converter.java @@ -0,0 +1,18 @@ +package org.keycloak.services.models.nosql.api.types; + +/** + * SPI object to convert object from application type to database type and vice versa. Shouldn't be directly used by application. + * Various converters should be registered in TypeConverter, which is main entry point to be used by application + * + * @author Marek Posolda + */ +public interface Converter { + + T convertDBObjectToApplicationObject(S dbObject); + + S convertApplicationObjectToDBObject(T applicationObject); + + Class getApplicationObjectType(); + + Class getDBObjectType(); +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/types/ConverterKey.java b/services/src/main/java/org/keycloak/services/models/nosql/api/types/ConverterKey.java new file mode 100644 index 0000000000..f4b6fe0cdf --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/types/ConverterKey.java @@ -0,0 +1,30 @@ +package org.keycloak.services.models.nosql.api.types; + +/** + * @author Marek Posolda + */ +class ConverterKey { + + private final Class applicationObjectType; + private final Class dbObjectType; + + public ConverterKey(Class applicationObjectType, Class dbObjectType) { + this.applicationObjectType = applicationObjectType; + this.dbObjectType = dbObjectType; + } + + @Override + public int hashCode() { + return applicationObjectType.hashCode() * 13 + dbObjectType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !obj.getClass().equals(this.getClass())) { + return false; + } + + ConverterKey tc = (ConverterKey)obj; + return tc.applicationObjectType.equals(this.applicationObjectType) && tc.dbObjectType.equals(this.dbObjectType); + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/types/TypeConverter.java b/services/src/main/java/org/keycloak/services/models/nosql/api/types/TypeConverter.java new file mode 100644 index 0000000000..7bdb384625 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/types/TypeConverter.java @@ -0,0 +1,43 @@ +package org.keycloak.services.models.nosql.api.types; + +import java.util.HashMap; +import java.util.Map; + +/** + * Registry of converters, which allow to convert application object to database objects. TypeConverter is main entry point to be used by application. + * Application can create instance of TypeConverter and then register required Converter objects. + * + * @author Marek Posolda + */ +public class TypeConverter { + + private Map> converterRegistry = new HashMap>(); + + public void addConverter(Converter converter) { + ConverterKey converterKey = new ConverterKey(converter.getApplicationObjectType(), converter.getDBObjectType()); + converterRegistry.put(converterKey, converter); + } + + public T convertDBObjectToApplicationObject(S dbObject, Class expectedApplicationObjectType, Class expectedDBObjectType) { + Converter converter = getConverter(expectedApplicationObjectType, expectedDBObjectType); + return converter.convertDBObjectToApplicationObject(dbObject); + } + + public S convertApplicationObjectToDBObject(T applicationobject, Class expectedApplicationObjectType, Class expectedDBObjectType) { + Converter converter = getConverter(expectedApplicationObjectType, expectedDBObjectType); + return converter.convertApplicationObjectToDBObject(applicationobject); + } + + private Converter getConverter( Class expectedApplicationObjectType, Class expectedDBObjectType) { + ConverterKey key = new ConverterKey(expectedApplicationObjectType, expectedDBObjectType); + Converter converter = (Converter)converterRegistry.get(key); + + if (converter == null) { + throw new IllegalStateException("Can't found converter for expectedApplicationObject=" + expectedApplicationObjectType + ", expectedDBObjectType=" + expectedDBObjectType); + } + + return converter; + } + + +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/data/RealmData.java b/services/src/main/java/org/keycloak/services/models/nosql/data/RealmData.java new file mode 100644 index 0000000000..8e6a663630 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/data/RealmData.java @@ -0,0 +1,144 @@ +package org.keycloak.services.models.nosql.data; + +import org.keycloak.services.models.nosql.api.NoSQLCollection; +import org.keycloak.services.models.nosql.api.NoSQLField; +import org.keycloak.services.models.nosql.api.NoSQLId; +import org.keycloak.services.models.nosql.api.NoSQLObject; + +/** + * @author Marek Posolda + */ +@NoSQLCollection(collectionName = "realms") +public class RealmData implements NoSQLObject { + + private String id; + private String name; + private boolean enabled; + private boolean sslNotRequired; + private boolean cookieLoginAllowed; + private boolean registrationAllowed; + private boolean social; + private boolean automaticRegistrationAfterSocialLogin; + private int tokenLifespan; + private int accessCodeLifespan; + private String publicKeyPem; + private String privateKeyPem; + private String[] defaultRoles; + + @NoSQLId + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @NoSQLField + public String getName() { + return name; + } + + public void setName(String realmName) { + this.name = realmName; + } + + @NoSQLField + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @NoSQLField + public boolean isSslNotRequired() { + return sslNotRequired; + } + + public void setSslNotRequired(boolean sslNotRequired) { + this.sslNotRequired = sslNotRequired; + } + + @NoSQLField + public boolean isCookieLoginAllowed() { + return cookieLoginAllowed; + } + + public void setCookieLoginAllowed(boolean cookieLoginAllowed) { + this.cookieLoginAllowed = cookieLoginAllowed; + } + + @NoSQLField + public boolean isRegistrationAllowed() { + return registrationAllowed; + } + + public void setRegistrationAllowed(boolean registrationAllowed) { + this.registrationAllowed = registrationAllowed; + } + + @NoSQLField + public boolean isSocial() { + return social; + } + + public void setSocial(boolean social) { + this.social = social; + } + + @NoSQLField + public boolean isAutomaticRegistrationAfterSocialLogin() { + return automaticRegistrationAfterSocialLogin; + } + + public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) { + this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin; + } + + @NoSQLField + public int getTokenLifespan() { + return tokenLifespan; + } + + public void setTokenLifespan(int tokenLifespan) { + this.tokenLifespan = tokenLifespan; + } + + @NoSQLField + public int getAccessCodeLifespan() { + return accessCodeLifespan; + } + + public void setAccessCodeLifespan(int accessCodeLifespan) { + this.accessCodeLifespan = accessCodeLifespan; + } + + @NoSQLField + public String getPublicKeyPem() { + return publicKeyPem; + } + + public void setPublicKeyPem(String publicKeyPem) { + this.publicKeyPem = publicKeyPem; + } + + @NoSQLField + public String getPrivateKeyPem() { + return privateKeyPem; + } + + public void setPrivateKeyPem(String privateKeyPem) { + this.privateKeyPem = privateKeyPem; + } + + @NoSQLField + public String[] getDefaultRoles() { + return defaultRoles; + } + + public void setDefaultRoles(String[] defaultRoles) { + this.defaultRoles = defaultRoles; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/data/RoleData.java b/services/src/main/java/org/keycloak/services/models/nosql/data/RoleData.java new file mode 100644 index 0000000000..7d4ba4b3c2 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/data/RoleData.java @@ -0,0 +1,65 @@ +package org.keycloak.services.models.nosql.data; + +import org.keycloak.services.models.nosql.api.NoSQLCollection; +import org.keycloak.services.models.nosql.api.NoSQLField; +import org.keycloak.services.models.nosql.api.NoSQLId; +import org.keycloak.services.models.nosql.api.NoSQLObject; + +/** + * @author Marek Posolda + */ +@NoSQLCollection(collectionName = "roles") +public class RoleData implements NoSQLObject { + + private String id; + private String name; + private String description; + + private String realmId; + private String applicationId; + + @NoSQLId + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @NoSQLField + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @NoSQLField + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @NoSQLField + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + @NoSQLField + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/data/UserData.java b/services/src/main/java/org/keycloak/services/models/nosql/data/UserData.java new file mode 100644 index 0000000000..70aab2a06e --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/data/UserData.java @@ -0,0 +1,85 @@ +package org.keycloak.services.models.nosql.data; + +import org.keycloak.services.models.nosql.api.AbstractAttributedNoSQLObject; +import org.keycloak.services.models.nosql.api.NoSQLCollection; +import org.keycloak.services.models.nosql.api.NoSQLField; +import org.keycloak.services.models.nosql.api.NoSQLId; + +/** + * @author Marek Posolda + */ +@NoSQLCollection(collectionName = "users") +public class UserData extends AbstractAttributedNoSQLObject { + + private String id; + private String loginName; + private String firstName; + private String lastName; + private String email; + private boolean enabled; + + private String realmId; + + @NoSQLId + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @NoSQLField + public String getLoginName() { + return loginName; + } + + public void setLoginName(String loginName) { + this.loginName = loginName; + } + + @NoSQLField + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + @NoSQLField + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @NoSQLField + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @NoSQLField + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @NoSQLField + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java b/services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java index bbdd84685c..7d61b85f7d 100644 --- a/services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java +++ b/services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java @@ -13,6 +13,7 @@ import com.mongodb.DBCursor; import com.mongodb.DBObject; import org.bson.types.ObjectId; import org.jboss.resteasy.logging.Logger; +import org.jboss.resteasy.spi.NotImplementedYetException; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.models.nosql.api.AttributedNoSQLObject; import org.keycloak.services.models.nosql.api.NoSQL; @@ -20,9 +21,14 @@ import org.keycloak.services.models.nosql.api.NoSQLCollection; import org.keycloak.services.models.nosql.api.NoSQLField; import org.keycloak.services.models.nosql.api.NoSQLId; import org.keycloak.services.models.nosql.api.NoSQLObject; +import org.keycloak.services.models.nosql.api.NoSQLQuery; +import org.keycloak.services.models.nosql.api.types.Converter; +import org.keycloak.services.models.nosql.api.types.TypeConverter; +import org.keycloak.services.models.nosql.impl.types.BasicDBListToStringArrayConverter; import org.picketlink.common.properties.Property; import org.picketlink.common.properties.query.AnnotatedPropertyCriteria; import org.picketlink.common.properties.query.PropertyQueries; +import org.picketlink.common.reflection.Types; /** * @author Marek Posolda @@ -32,14 +38,20 @@ public class MongoDBImpl implements NoSQL { private final DB database; // private static final Logger logger = Logger.getLogger(MongoDBImpl.class); + private final TypeConverter typeConverter; + public MongoDBImpl(DB database) { this.database = database; + + typeConverter = new TypeConverter(); + typeConverter.addConverter(new BasicDBListToStringArrayConverter()); } private ConcurrentMap, ObjectInfo> objectInfoCache = new ConcurrentHashMap, ObjectInfo>(); + @Override public void saveObject(NoSQLObject object) { Class clazz = object.getClass(); @@ -96,32 +108,62 @@ public class MongoDBImpl implements NoSQL { } @Override - public List loadObjects(Class type, Map queryAttributes) { + public T loadSingleObject(Class type, NoSQLQuery query) { + List result = loadObjects(type, query); + if (result.size() > 1) { + throw new IllegalStateException("There are " + result.size() + " results for type=" + type + ", query=" + query + ". We expect just one"); + } else if (result.size() == 1) { + return result.get(0); + } else { + // 0 results + return null; + } + } + + @Override + public List loadObjects(Class type, NoSQLQuery query) { + Map queryAttributes = query.getQueryAttributes(); + ObjectInfo objectInfo = getObjectInfo(type); DBCollection dbCollection = database.getCollection(objectInfo.getDbCollectionName()); - BasicDBObject query = new BasicDBObject(); + BasicDBObject dbQuery = new BasicDBObject(); for (Map.Entry queryAttr : queryAttributes.entrySet()) { - query.append(queryAttr.getKey(), queryAttr.getValue()); + dbQuery.append(queryAttr.getKey(), queryAttr.getValue()); } - DBCursor cursor = dbCollection.find(query); + DBCursor cursor = dbCollection.find(dbQuery); return convertCursor(type, cursor); } @Override public void removeObject(NoSQLObject object) { - //To change body of implemented methods use File | Settings | File Templates. + Class type = object.getClass(); + ObjectInfo objectInfo = getObjectInfo(type); + + Property idProperty = objectInfo.getOidProperty(); + String oid = idProperty.getValue(object); + + removeObject(type, oid); } @Override public void removeObject(Class type, String oid) { - //To change body of implemented methods use File | Settings | File Templates. + ObjectInfo objectInfo = getObjectInfo(type); + DBCollection dbCollection = database.getCollection(objectInfo.getDbCollectionName()); + + BasicDBObject query = new BasicDBObject("_id", new ObjectId(oid)); + dbCollection.remove(query); } @Override - public void removeObjects(Class type, Map queryAttributes) { - //To change body of implemented methods use File | Settings | File Templates. + public void removeObjects(Class type, NoSQLQuery query) { + throw new NotImplementedYetException(); + } + + // Possibility to add converters + public void addConverter(Converter converter) { + typeConverter.addConverter(converter); } private ObjectInfo getObjectInfo(Class objectClass) { @@ -173,7 +215,20 @@ public class MongoDBImpl implements NoSQL { } else if ((property = objectInfo.getPropertyByName(key)) != null) { // It's declared property with @DBField annotation - property.setValue(object, value); + Class expectedType = property.getJavaClass(); + Class actualType = value != null ? value.getClass() : expectedType; + + // handle primitives + expectedType = Types.boxedClass(expectedType); + actualType = Types.boxedClass(actualType); + + if (actualType.isAssignableFrom(expectedType)) { + property.setValue(object, value); + } else { + // we need to convert + Object convertedValue = typeConverter.convertDBObjectToApplicationObject(value, expectedType, actualType); + property.setValue(object, convertedValue); + } } else if (object instanceof AttributedNoSQLObject) { // It's attributed object and property is not declared, so we will call setAttribute diff --git a/services/src/main/java/org/keycloak/services/models/nosql/impl/Test.java b/services/src/main/java/org/keycloak/services/models/nosql/impl/Test.java deleted file mode 100644 index 375418ab41..0000000000 --- a/services/src/main/java/org/keycloak/services/models/nosql/impl/Test.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.keycloak.services.models.nosql.impl; - -import java.net.UnknownHostException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.mongodb.DB; -import com.mongodb.MongoClient; -import org.keycloak.services.models.nosql.adapters.NoSQLRealm; - -/** - * TODO: delete - * - * @author Marek Posolda - */ -public class Test { - - public static void main(String[] args) throws UnknownHostException { - MongoClient mongoClient = new MongoClient( "localhost" , 27017 ); - DB javaDB = mongoClient.getDB("java"); - - MongoDBImpl test = new MongoDBImpl(javaDB); - NoSQLRealm realm = new NoSQLRealm(); - realm.setOid("522085fc31dab908ec31c0cb"); - realm.setProp1("something1"); - realm.setProp2(12); - test.saveObject(realm); - System.out.println(realm.getOid()); - - realm = test.loadObject(NoSQLRealm.class, "522085fc31dab908ec31c0cb"); - System.out.println("Loaded realm: " + realm); - - Map query = new HashMap(); - query.put("prop1", "sm"); - List queryResults = test.loadObjects(NoSQLRealm.class, query); - System.out.println("results1: " + queryResults); - - query.put("prop1", "something2"); - queryResults = test.loadObjects(NoSQLRealm.class, query); - System.out.println("results2: " + queryResults); - - query.put("prop2", 12); - queryResults = test.loadObjects(NoSQLRealm.class, query); - System.out.println("results3: " + queryResults); - - query.put("prop1", "something1"); - queryResults = test.loadObjects(NoSQLRealm.class, query); - System.out.println("results4: " + queryResults); - - mongoClient.close(); - } -} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/impl/types/BasicDBListToStringArrayConverter.java b/services/src/main/java/org/keycloak/services/models/nosql/impl/types/BasicDBListToStringArrayConverter.java new file mode 100644 index 0000000000..0f9d5d5917 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/impl/types/BasicDBListToStringArrayConverter.java @@ -0,0 +1,41 @@ +package org.keycloak.services.models.nosql.impl.types; + +import com.mongodb.BasicDBList; +import org.keycloak.services.models.nosql.api.types.Converter; + +/** + * Convert BasicDBList to String[] and viceversa (T needs to be declared as Object as Array is not possible here :/ ) + * + * @author Marek Posolda + */ +public class BasicDBListToStringArrayConverter implements Converter { + + private static final String[] PLACEHOLDER = new String[] {}; + + @Override + public Object convertDBObjectToApplicationObject(BasicDBList dbObject) { + return dbObject.toArray(PLACEHOLDER); + } + + @Override + public BasicDBList convertApplicationObjectToDBObject(Object applicationObject) { + BasicDBList list = new BasicDBList(); + + String[] array = (String[])applicationObject; + for (String key : array) { + list.add(key); + } + + return list; + } + + @Override + public Class getApplicationObjectType() { + return PLACEHOLDER.getClass(); + } + + @Override + public Class getDBObjectType() { + return BasicDBList.class; + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 49855b9af7..089ab3998f 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -7,6 +7,11 @@ import org.keycloak.models.picketlink.PicketlinkKeycloakSession; import org.keycloak.models.picketlink.PicketlinkKeycloakSessionFactory; import org.keycloak.models.picketlink.mappings.ApplicationEntity; import org.keycloak.models.picketlink.mappings.RealmEntity; +import org.keycloak.services.models.KeycloakSessionFactory; +import org.keycloak.services.models.nosql.adapters.MongoDBSessionFactory; +import org.keycloak.services.models.picketlink.PicketlinkKeycloakSession; +import org.keycloak.services.models.picketlink.mappings.ApplicationEntity; +import org.keycloak.services.models.picketlink.mappings.RealmEntity; import org.keycloak.social.SocialRequestManager; import org.picketlink.idm.PartitionManager; import org.picketlink.idm.config.IdentityConfigurationBuilder; @@ -54,8 +59,9 @@ public class KeycloakApplication extends Application { } public static KeycloakSessionFactory buildSessionFactory() { - EntityManagerFactory emf = Persistence.createEntityManagerFactory("keycloak-identity-store"); - return new PicketlinkKeycloakSessionFactory(emf, buildPartitionManager()); + // EntityManagerFactory emf = Persistence.createEntityManagerFactory("keycloak-identity-store"); + // return new PicketlinkKeycloakSessionFactory(emf, buildPartitionManager()); + return new MongoDBSessionFactory("localhost", 27017, "keycloak"); } public KeycloakSessionFactory getFactory() {