master moved from 358946777afb to 8ef5143b7e4d 4 new revisions: Revision: 8a6c6b5f5e68 Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Tue Sep 9 11:49:40 2014 UTCLog: initial support for read / write synchronization from / to LDAP for us...
https://code.google.com/p/haiku-depot-web-app/source/detail?r=8a6c6b5f5e68 Revision: 52c9f839987d Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Fri Sep 12 08:53:03 2014 UTCLog: check for source and indicate when source is available when viewing a ...
https://code.google.com/p/haiku-depot-web-app/source/detail?r=52c9f839987d Revision: 8ab5cf122712 Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Mon Sep 15 10:09:53 2014 UTC Log: preparations for next version https://code.google.com/p/haiku-depot-web-app/source/detail?r=8ab5cf122712 Revision: 8ef5143b7e4d Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Mon Sep 15 10:51:49 2014 UTC Log: resolve minor issue in viewing pkg version in jsp pages https://code.google.com/p/haiku-depot-web-app/source/detail?r=8ef5143b7e4d ============================================================================== Revision: 8a6c6b5f5e68 Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Tue Sep 9 11:49:40 2014 UTCLog: initial support for read / write synchronization from / to LDAP for users
https://code.google.com/p/haiku-depot-web-app/source/detail?r=8a6c6b5f5e68 Added:/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/SynchronizeUsersRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/SynchronizeUsersResult.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/LdapUserUpdateListener.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/ldap/LdapConnectionPoolFactory.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/ldap/LdapConnectionPoolHolder.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/LdapException.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/LdapSynchronizeUsersService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/LocalLdapSynchronizeUsersService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/model/LdapPerson.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/model/LdapSynchronizeUsersJob.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/security/AuthenticationServiceIT.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/user/NoopLdapSynchronizeUsersService.java
Modified:/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java
/haikudepotserver-parent/pom.xml /haikudepotserver-rpm/src/main/etc/config__config.properties /haikudepotserver-webapp/pom.xml/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/User.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthenticationService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthorizationService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/model/Permission.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/UserRatingDerivationTriggerListener.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/UserOrchestrationService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/userrating/LocalUserRatingDerivationService.java
/haikudepotserver-webapp/src/main/resources/spring/application-context.xml /haikudepotserver-webapp/src/main/resources/spring/persistence.xml/haikudepotserver-webapp/src/main/webapp/js/app/controller/rootoperations.html /haikudepotserver-webapp/src/main/webapp/js/app/controller/rootoperationscontroller.js
/haikudepotserver-webapp/src/test/resources/spring/test.xml ======================================= --- /dev/null+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/SynchronizeUsersRequest.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,9 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class SynchronizeUsersRequest { +} ======================================= --- /dev/null+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/SynchronizeUsersResult.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,9 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class SynchronizeUsersResult { +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/LdapUserUpdateListener.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,86 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.cayenne; + +import com.google.common.base.Preconditions; +import org.apache.cayenne.LifecycleListener; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.apache.cayenne.reflect.LifecycleCallbackRegistry; +import org.haikuos.haikudepotserver.dataobjects.User; +import org.haikuos.haikudepotserver.user.LdapException; +import org.haikuos.haikudepotserver.user.UserOrchestrationService; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; + +/**+ * <p>This will attempt to listen to changes to a user and then after those changes have been
+ * persisted, to look them up and to relay those into an LDAP server.</p> + */ + +public class LdapUserUpdateListener implements LifecycleListener { + + @Resource + ServerRuntime serverRuntime; + + @Resource + UserOrchestrationService userOrchestrationService; + + @PostConstruct + public void init() {+ LifecycleCallbackRegistry callbackRegistry = serverRuntime.getDataDomain().getEntityResolver().getCallbackRegistry();
+ callbackRegistry.addListener(User.class, this); + } + + private void triggerUpdateUser(Object entity) { + Preconditions.checkNotNull(entity); + ObjectContext context = serverRuntime.getContext();+ User user = User.getByObjectId(context, ((User) entity).getObjectId());
+ + try { + userOrchestrationService.ldapUpdateUser(context, user); + } + catch(LdapException le) {+ throw new RuntimeException("unable to ldap update user; " + user.toString(), le);
+ } + } + + @Override + public void postAdd(Object entity) { + } + + @Override + public void prePersist(Object entity) { + } + + @Override + public void postPersist(Object entity) { + triggerUpdateUser(entity); + } + + @Override + public void preRemove(Object entity) { + } + + @Override + public void postRemove(Object entity) { + } + + @Override + public void preUpdate(Object entity) { + } + + @Override + public void postUpdate(Object entity) { + triggerUpdateUser(entity); + } + + @Override + public void postLoad(Object entity) { + } + +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/ldap/LdapConnectionPoolFactory.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,78 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.ldap; + +import com.google.common.base.Strings; +import org.apache.directory.ldap.client.api.LdapConnectionConfig; +import org.apache.directory.ldap.client.api.LdapConnectionPool; +import org.apache.directory.ldap.client.api.PoolableLdapConnectionFactory; +import org.springframework.beans.factory.FactoryBean; + +/**+ * <p>This factory will create a connection pool for LDAP connections. It will produce a holder object that + * either returns an {@link org.apache.directory.ldap.client.api.LdapConnectionPool} or will return null
+ * depending on the configuration of the LDAP server.</p> + */ ++public class LdapConnectionPoolFactory implements FactoryBean<LdapConnectionPoolHolder> {
+ + private String host; + private Integer port; + private String userDn; + private String password; + + public void setHost(String host) { + this.host = host; + } + + public void setPort(Integer port) { + this.port = port; + } + + public void setUserDn(String userDn) { + this.userDn = userDn; + } + + public void setPassword(String password) { + this.password = password; + } + + private boolean isConfigured() { + return !Strings.isNullOrEmpty(host); + } + + @Override + public LdapConnectionPoolHolder getObject() throws Exception { + + if(isConfigured()) { + + LdapConnectionConfig config = new LdapConnectionConfig(); + config.setLdapHost(host); + config.setLdapPort(null == port ? 398 : port.intValue()); + config.setName(userDn); + config.setCredentials(password); ++ PoolableLdapConnectionFactory factory = new PoolableLdapConnectionFactory(config);
+ LdapConnectionPool pool = new LdapConnectionPool( factory ); + pool.setTestOnBorrow( true ); + + return new LdapConnectionPoolHolder(pool); + } + + return new LdapConnectionPoolHolder(); + } + + @Override + public Class<?> getObjectType() { + return LdapConnectionPoolHolder.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/ldap/LdapConnectionPoolHolder.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,30 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.ldap; + +import org.apache.directory.ldap.client.api.LdapConnectionPool; + +/**+ * <p>This 'holder' can either hold an {@link org.apache.directory.ldap.client.api.LdapConnectionPool} or it
+ * can hold null.</p> + */ + +public class LdapConnectionPoolHolder { + + private LdapConnectionPool connectionPool; + + public LdapConnectionPoolHolder() { + } + + public LdapConnectionPoolHolder(LdapConnectionPool connectionPool) { + this.connectionPool = connectionPool; + } + + public LdapConnectionPool get() { + return connectionPool; + } + +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/LdapException.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,13 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.user; + +public class LdapException extends Exception { + + public LdapException(String message, Throwable cause) { + super(message, cause); + } +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/LdapSynchronizeUsersService.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,18 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.user; + +import org.haikuos.haikudepotserver.user.model.LdapSynchronizeUsersJob; + +/**+ * <p>Implementations of this service are able to synchronize user data into an LDAP directory.</p>
+ */ + +public interface LdapSynchronizeUsersService { + + public void submit(final LdapSynchronizeUsersJob job); + +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/LocalLdapSynchronizeUsersService.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,80 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.user; + +import com.google.common.base.Preconditions; +import org.apache.cayenne.configuration.server.ServerRuntime;+import org.haikuos.haikudepotserver.support.AbstractLocalBackgroundProcessingService;
+import org.haikuos.haikudepotserver.user.model.LdapSynchronizeUsersJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Resource; + +/**+ * <p>This background service will asynchronously go through the users and will relay the data for the users
+ * into an LDAP server.</p> + */ + +public class LocalLdapSynchronizeUsersService + extends AbstractLocalBackgroundProcessingService + implements LdapSynchronizeUsersService { ++ protected static Logger LOGGER = LoggerFactory.getLogger(LocalLdapSynchronizeUsersService.class);
+ + @Resource + ServerRuntime serverRuntime; + + @Resource + UserOrchestrationService userOrchestrationService; + + @Override + public void submit(LdapSynchronizeUsersJob job) { + Preconditions.checkNotNull(job);+ Preconditions.checkState(null != executor, "the service is not running, but a job is being submitted");
+ executor.submit(new LdapUpdateUsersJobRunnable(this, job)); + LOGGER.info("did submit job to update users in ldap directory"); + } + + protected void run(LdapSynchronizeUsersJob job) { + Preconditions.checkNotNull(job); + + try {+ userOrchestrationService.ldapSynchronizeAllUsers(serverRuntime.getContext()); + userOrchestrationService.ldapRemoveNonExistentUsers(serverRuntime.getContext());
+ } + catch(LdapException le) { + LOGGER.error("unable to ldap synchronize users", le); + } + } + + public static class LdapUpdateUsersJobRunnable implements Runnable { + + private LdapSynchronizeUsersJob job; + + private LocalLdapSynchronizeUsersService service; + + public LdapUpdateUsersJobRunnable( + LocalLdapSynchronizeUsersService service, + LdapSynchronizeUsersJob job) { + Preconditions.checkNotNull(service); + Preconditions.checkNotNull(job); + this.service = service; + this.job = job; + } + + public LdapSynchronizeUsersJob getJob() { + return job; + } + + @Override + public void run() { + service.run(job); + } + + } + +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/model/LdapPerson.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,81 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.user.model; + +import org.apache.directory.api.ldap.model.name.Dn; + +/**+ * <p>Provides an easily used model object to represent a person as stored in the LDAP directory.</p>
+ */ + +public class LdapPerson { + + /**+ * <p>This is the distinguished name that uniquely identifies the user.</p>
+ */ + + private Dn dn; + + private String cn; + private String mail; + private String sn; + private String uid; + private String userPassword; + + public Dn getDn() { + return dn; + } + + public void setDn(Dn dn) { + this.dn = dn; + } + + public String getCn() { + return cn; + } + + public void setCn(String cn) { + this.cn = cn; + } + + public String getMail() { + return mail; + } + + public void setMail(String mail) { + this.mail = mail; + } + + public String getSn() { + return sn; + } + + public void setSn(String sn) { + this.sn = sn; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getUserPassword() { + return userPassword; + } + + public void setUserPassword(String userPassword) { + this.userPassword = userPassword; + } + + @Override + public String toString() { + return "ldap-person; " + (null!=getDn() ? getDn() : "???"); + } + +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/model/LdapSynchronizeUsersJob.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,13 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.user.model; + +/**+ * <p>This object models a job that can be submitted to synchronize the user data into an LDAP directory.</p>
+ */ + +public class LdapSynchronizeUsersJob { +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/security/AuthenticationServiceIT.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,36 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.security; + +import org.fest.assertions.Assertions; +import org.haikuos.haikudepotserver.AbstractIntegrationTest; +import org.haikuos.haikudepotserver.dataobjects.User; +import org.junit.Test; + +import javax.annotation.Resource; + +public class AuthenticationServiceIT extends AbstractIntegrationTest { + + @Resource + AuthenticationService authenticationService; + + @Test + public void testHashPassword() { + User user = new User(); + user.setPasswordSalt("cad3422ea02761f8");+ String passwordHash = authenticationService.hashPassword(user,"p4mphl3t"); + Assertions.assertThat(passwordHash).isEqualTo("b9c4717bc5c6d16f2be9e967ab0c752f8ac2084f95781989f39cf8736e2edeef");
+ } + + @Test + public void testHashPassword_2() { + User user = new User(); + user.setPasswordSalt("66a9b264bf730ac2");+ String passwordHash = authenticationService.hashPassword(user,"Pa55word0"); + Assertions.assertThat(passwordHash).isEqualTo("d439da8f2ec8c7aa3d0c9c2a1dd7cd6dcbf8b4435f9e288cc1a6f7b77d47361e");
+ } + +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/user/NoopLdapSynchronizeUsersService.java Tue Sep 9 11:49:40 2014 UTC
@@ -0,0 +1,20 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.user; + +import org.haikuos.haikudepotserver.user.model.LdapSynchronizeUsersJob; + +/** + * <P>No-operation implementation of this service for testing purposes.</P> + */ ++public class NoopLdapSynchronizeUsersService implements LdapSynchronizeUsersService {
+ + @Override + public void submit(LdapSynchronizeUsersJob job) { + } + +} =======================================--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java Tue Jul 8 09:00:54 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java Tue Sep 9 11:49:40 2014 UTC
@@ -18,6 +18,12 @@ @JsonRpcService("/api/v1/user") public interface UserApi { + /**+ * <P>This method will synchronize user data with external systems; such as LDAP servers.</P>
+ */ ++ SynchronizeUsersResult synchronizeUsers(SynchronizeUsersRequest synchronizeUsersRequest);
+ /*** <p>This method will update the user based on the data in the request. Only the data which is included
* in the filter will be updated.</p> ======================================= --- /haikudepotserver-parent/pom.xml Thu Aug 14 11:03:33 2014 UTC +++ /haikudepotserver-parent/pom.xml Tue Sep 9 11:49:40 2014 UTC @@ -139,7 +139,7 @@ <version>1.0</version> </dependency> - <!-- DATABASE / PERSISTENCE --> + <!-- DATABASE / PERSISTENCE / DIRECTORY --> <dependency> <groupId>com.googlecode.flyway</groupId> <artifactId>flyway-core</artifactId> @@ -150,6 +150,11 @@ <artifactId>cayenne-server</artifactId> <version>3.1RC1</version> </dependency> + <dependency> + <groupId>org.apache.directory.api</groupId> + <artifactId>api-all</artifactId> + <version>1.0.0-M24</version> + </dependency> <!-- APPLICATION SERVER --> <dependency> =======================================--- /haikudepotserver-rpm/src/main/etc/config__config.properties Thu Aug 14 11:03:33 2014 UTC +++ /haikudepotserver-rpm/src/main/etc/config__config.properties Tue Sep 9 11:49:40 2014 UTC
@@ -86,3 +86,19 @@ #smtp.auth=false #smtp.starttls=false email.from=noreply@xxxxxxxxxxxx + +# ------------------------------------------- +# LDAP related +# If the LDAP setting are commented-out then the system will operate as if +# there were no LDAP server and will not attempt to synchronize user data +# to the LDAP directory server. + +#ldap.host=localhost +#ldap.port=10389 +#ldap.user.dn=cn=haikudepotserver,ou=services,dc=haiku-os,dc=org +#ldap.password=Pa55word2 + +# This setting provides the location in the LDAP directory where the users +# of the system are stored. + +#ldap.people.dn=ou=people,dc=haiku-os,dc=org ======================================= --- /haikudepotserver-webapp/pom.xml Thu Aug 14 11:03:33 2014 UTC +++ /haikudepotserver-webapp/pom.xml Tue Sep 9 11:49:40 2014 UTC @@ -93,7 +93,7 @@ <artifactId>rome</artifactId> </dependency> - <!-- DATABASE / PERSISTENCE --> + <!-- DATABASE / PERSISTENCE / DIRECTORY --> <dependency> <groupId>com.googlecode.flyway</groupId> <artifactId>flyway-core</artifactId> @@ -102,6 +102,10 @@ <groupId>org.apache.cayenne</groupId> <artifactId>cayenne-server</artifactId> </dependency> + <dependency> + <groupId>org.apache.directory.api</groupId> + <artifactId>api-all</artifactId> + </dependency> <!-- APPLICATION SERVER --> <dependency> =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java Tue Sep 9 11:49:40 2014 UTC
@@ -25,7 +25,9 @@ import org.haikuos.haikudepotserver.security.AuthenticationService; import org.haikuos.haikudepotserver.security.AuthorizationService; import org.haikuos.haikudepotserver.security.model.Permission; +import org.haikuos.haikudepotserver.user.LdapSynchronizeUsersService; import org.haikuos.haikudepotserver.user.UserOrchestrationService; +import org.haikuos.haikudepotserver.user.model.LdapSynchronizeUsersJob; import org.haikuos.haikudepotserver.user.model.UserSearchSpecification; import org.haikuos.haikudepotserver.userrating.UserRatingDerivationService;import org.haikuos.haikudepotserver.userrating.UserRatingOrchestrationService;
@@ -66,6 +68,28 @@ @Resource PasswordResetOrchestrationService passwordResetOrchestrationService; + + @Resource + LdapSynchronizeUsersService ldapUpdateUsersService; + + @Override+ public SynchronizeUsersResult synchronizeUsers(SynchronizeUsersRequest synchronizeUsersRequest) {
+ Preconditions.checkNotNull(synchronizeUsersRequest); + + final ObjectContext context = serverRuntime.getContext(); + + if(!authorizationService.check( + context, + tryObtainAuthenticatedUser(context).orNull(), + null, + Permission.USER_SYNCHRONIZE)) { + throw new AuthorizationFailureException(); + } + + ldapUpdateUsersService.submit(new LdapSynchronizeUsersJob()); + + return new SynchronizeUsersResult(); + } @Overridepublic UpdateUserResult updateUser(UpdateUserRequest updateUserRequest) throws ObjectNotFoundException {
=======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/User.java Sat Jul 26 04:02:29 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/User.java Tue Sep 9 11:49:40 2014 UTC
@@ -12,6 +12,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.ObjectId; import org.apache.cayenne.exp.ExpressionFactory; @@ -31,9 +32,11 @@ public class User extends _User implements CreateAndModifyTimestamped { + public final static String NICKNAME_ROOT = "root"; +public final static Pattern NICKNAME_PATTERN = Pattern.compile("^[a-z0-9]{4,16}$"); public final static Pattern PASSWORDHASH_PATTERN = Pattern.compile("^[a-f0-9]{64}$"); - public final static Pattern PASSWORDSALT_PATTERN = Pattern.compile("^[a-f0-9]{64}$"); + public final static Pattern PASSWORDSALT_PATTERN = Pattern.compile("^[a-f0-9]{10,32}$");
public static List<User> findByEmail(ObjectContext context, String email) {
Preconditions.checkNotNull(context); @@ -157,13 +160,46 @@ } )); } + + /**+ * <p>The LDAP entry for a person can have a 'userPassword' attribute. This has the format + * {SSHA-256}<base64data>. The base 64 data, decoded to bytes contains 20 bytes of
+ * hash and the rest is salt.</p> + *+ * <p>This method will convert the information stored in this user over into a format suitable
+ * for storage in this format for LDAP.</p> + * + * <p>SSHA stands for salted SHA hash.</p> + * + * <p>RFC-2307</p> + */ + + public String toLdapUserPasswordAttributeValue() { + StringBuilder builder = new StringBuilder(); + builder.append("{SSHA-256}"); ++ byte[] hash = BaseEncoding.base16().decode(getPasswordHash().toUpperCase());
+ + if(32 != hash.length) {+ throw new IllegalStateException("the password hash should be 20 bytes long");
+ } ++ byte[] salt = BaseEncoding.base16().decode(getPasswordSalt().toUpperCase());
+ + byte[] hashAndSalt = new byte[hash.length + salt.length]; + System.arraycopy(hash, 0, hashAndSalt, 0, hash.length); + System.arraycopy(salt, 0, hashAndSalt, hash.length, salt.length); + + return "{SSHA-256}" + BaseEncoding.base64().encode(hashAndSalt); + } /** * <p>This method will configure a random salt value.</p> */ public void setPasswordSalt() {- setPasswordSalt(Hashing.sha256().hashUnencodedChars(UUID.randomUUID().toString()).toString()); + String randomHash = Hashing.sha256().hashUnencodedChars(UUID.randomUUID().toString()).toString(); + setPasswordSalt(randomHash.substring(0,16)); // LDAP server doesn't seem to like very long salts
} public Boolean getDerivedCanManageUsers() { =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthenticationService.java Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthenticationService.java Tue Sep 9 11:49:40 2014 UTC
@@ -11,7 +11,7 @@ import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.MACVerifier; @@ -31,6 +31,7 @@ import javax.annotation.PostConstruct; import javax.annotation.Resource; +import java.security.MessageDigest; import java.text.ParseException; import java.util.List; import java.util.UUID; @@ -188,7 +189,20 @@ */ public String hashPassword(User user, String passwordClear) {- return Hashing.sha256().hashUnencodedChars(user.getPasswordSalt() + passwordClear).toString(); + byte[] saltBytes = BaseEncoding.base16().decode(user.getPasswordSalt().toUpperCase());
+ + MessageDigest sha; + try { + sha = MessageDigest.getInstance("SHA-256"); + sha.update(passwordClear.getBytes(Charsets.UTF_8)); + } + catch (java.security.NoSuchAlgorithmException e) {+ throw new IllegalStateException("no SHA-256 crypt algorithm available",e);
+ } + + sha.update(saltBytes); + + return BaseEncoding.base16().encode(sha.digest()).toLowerCase(); } private int countMatches(String s, CharToBooleanFunction fn) { =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthorizationService.java Tue Aug 19 11:01:57 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthorizationService.java Tue Sep 9 11:49:40 2014 UTC
@@ -192,6 +192,7 @@ null!=authenticatedUser&& (authenticatedUser.getIsRoot() || authenticatedUser.equals(target));
+ case USER_SYNCHRONIZE: case USER_LIST:return null!=authenticatedUser && authenticatedUser.getIsRoot();
=======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/model/Permission.java Tue Aug 19 11:01:57 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/model/Permission.java Tue Sep 9 11:49:40 2014 UTC
@@ -18,6 +18,7 @@ USER_EDIT(TargetType.USER), USER_CHANGEPASSWORD(TargetType.USER), USER_LIST(null), + USER_SYNCHRONIZE(null), AUTHORIZATION_CONFIGURE(null), =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/UserRatingDerivationTriggerListener.java Mon Jun 23 10:01:05 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/UserRatingDerivationTriggerListener.java Tue Sep 9 11:49:40 2014 UTC
@@ -76,4 +76,5 @@ @Override public void postLoad(Object entity) { } + } =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/UserOrchestrationService.java Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/user/UserOrchestrationService.java Tue Sep 9 11:49:40 2014 UTC
@@ -5,19 +5,31 @@ package org.haikuos.haikudepotserver.user; -import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; +import com.google.common.base.*; import com.google.common.collect.Lists; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.query.EJBQLQuery; +import org.apache.cayenne.query.SelectQuery; +import org.apache.directory.api.ldap.model.cursor.CursorException; +import org.apache.directory.api.ldap.model.cursor.EntryCursor; +import org.apache.directory.api.ldap.model.entry.*; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.api.ldap.model.message.ModifyRequest; +import org.apache.directory.api.ldap.model.message.ModifyRequestImpl; +import org.apache.directory.api.ldap.model.message.SearchScope; +import org.apache.directory.ldap.client.api.LdapConnection; import org.haikuos.haikudepotserver.dataobjects.User; import org.haikuos.haikudepotserver.support.cayenne.LikeHelper; +import org.haikuos.haikudepotserver.support.ldap.LdapConnectionPoolHolder; +import org.haikuos.haikudepotserver.user.model.LdapPerson; import org.haikuos.haikudepotserver.user.model.UserSearchSpecification; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; +import javax.annotation.Resource; import java.util.List; /** @@ -28,9 +40,337 @@ public class UserOrchestrationService {protected static Logger LOGGER = LoggerFactory.getLogger(UserOrchestrationService.class);
+ + /**+ * <p>Users are processed in batches to avoid excessive memory or IO consumption at once. This constant
+ * determines the size of the batches.</p> + */ + + private final static int BATCH_SIZE_USER = 20; ++ private final static String LDAP_ATTRIBUTE_KEY_CN = "cn"; // common name
+ private final static String LDAP_ATTRIBUTE_KEY_SN = "sn"; // surname + private final static String LDAP_ATTRIBUTE_KEY_MAIL = "mail"; // mail + private final static String LDAP_ATTRIBUTE_KEY_UID = "uid"; // uid+ private final static String LDAP_ATTRIBUTE_KEY_USERPASSWORD = "userPassword"; // contains a hash + private final static String LDAP_ATTRIBUTE_KEY_OBJECTCLASS = "ObjectClass";
++ private final static String LDAP_ATTRIBUTE_VALUE_OBJECTCLASS_INETORGPERSON = "inetOrgPerson";
+ + @Resource + LdapConnectionPoolHolder ldapConnectionPoolHolder; + + @Value("${ldap.people.dn:}") + String ldapPeopleDn; + + @PostConstruct + public void init() { + if(null!=ldapConnectionPoolHolder.get()) { + if(Strings.isNullOrEmpty(ldapPeopleDn)) {+ throw new IllegalStateException("the ldap people DN must be configured if LDAP is to be used to back users; data");
+ } + } + } // ------------------------------ - // SEARCH + // LDAP + + /**+ * <p>LDAP may or may not be configured. This method will return true if the system is
+ * configured for interaction with LDAP.</p> + */ + + public boolean isLdapConfigured() { + return null!=ldapConnectionPoolHolder.get(); + } + + private LdapPerson createPerson(Entry entry) throws LdapException { + + Attribute mailA = entry.get(LDAP_ATTRIBUTE_KEY_MAIL); + Attribute uidA = entry.get(LDAP_ATTRIBUTE_KEY_UID);+ Attribute userPasswordA = entry.get(LDAP_ATTRIBUTE_KEY_USERPASSWORD);
+ + LdapPerson ldapPerson = new LdapPerson(); + ldapPerson.setDn(entry.getDn()); + ldapPerson.setMail(null == mailA ? null : mailA.getString()); + ldapPerson.setCn(entry.get(LDAP_ATTRIBUTE_KEY_CN).getString()); + ldapPerson.setSn(entry.get(LDAP_ATTRIBUTE_KEY_SN).getString()); + ldapPerson.setUid(null == uidA ? null : uidA.getString());+ ldapPerson.setUserPassword(null == userPasswordA ? null : new String(userPasswordA.getBytes(), Charsets.US_ASCII));
+ + return ldapPerson; + } ++ private Optional<LdapPerson> ldapFindPerson(LdapConnection ldapConnection, final String nickname) throws org.haikuos.haikudepotserver.user.LdapException {
+ Preconditions.checkNotNull(ldapConnection); + Preconditions.checkNotNull(nickname);+ Preconditions.checkState(User.NICKNAME_PATTERN.matcher(nickname).matches(),"the nickname is illegal");
+ + LdapPerson ldapPerson = null; + + try { + EntryCursor cursor = ldapConnection.search(+ String.format("%s=%s, %s", LDAP_ATTRIBUTE_KEY_CN, nickname, ldapPeopleDn), + String.format("(%s=%s)", LDAP_ATTRIBUTE_KEY_OBJECTCLASS, LDAP_ATTRIBUTE_VALUE_OBJECTCLASS_INETORGPERSON),
+ SearchScope.OBJECT); + + if (cursor.next()) { + ldapPerson = createPerson(cursor.get()); + } + + if (cursor.next()) {+ throw new IllegalStateException("found two matches for the user; " + nickname);
+ } + } + catch(LdapException le) {+ throw new org.haikuos.haikudepotserver.user.LdapException("an error arose finding the user in the ldap system; " + nickname, le);
+ } + catch(CursorException ce) {+ throw new org.haikuos.haikudepotserver.user.LdapException("an error arose finding the user in the ldap system; " + nickname, ce);
+ } + + return Optional.fromNullable(ldapPerson); + } + + private void accumulateModification( + List<Modification> modifications, + String attributeName, + String existingValue, + String newValue) {+ Modification modification = createModification(attributeName,existingValue,newValue);
+ + if(null!=modification) { + modifications.add(modification); + } + } + + private Modification createModification( + String attributeName, + String existingValue, + String newValue) { + + Preconditions.checkState(!Strings.isNullOrEmpty(attributeName)); + + boolean existingN = !Strings.isNullOrEmpty(existingValue); + boolean newN = !Strings.isNullOrEmpty(newValue); + + if(existingN != newN) { + + if(existingN) {+ return new DefaultModification(ModificationOperation.REMOVE_ATTRIBUTE, attributeName, existingValue);
+ } + else {+ return new DefaultModification(ModificationOperation.ADD_ATTRIBUTE, attributeName, newValue);
+ } + + } + else { + if(newN && !existingValue.equals(newValue)) {+ return new DefaultModification(ModificationOperation.REPLACE_ATTRIBUTE, attributeName, newValue);
+ } + else {+ LOGGER.trace("new and old attribute values are the same; will not need to replace them");
+ } + } + + return null; + } + + /**+ * <p>This method will update the supplied user in the supplied object context with the LDAP server.</p>
+ */ ++ public void ldapUpdateUser(ObjectContext context, User user) throws org.haikuos.haikudepotserver.user.LdapException {
+ Preconditions.checkNotNull(context); + Preconditions.checkNotNull(user); + + if(user.getNickname().equals(User.NICKNAME_ROOT)) { + LOGGER.info("will not update the root user to ldap"); + } + else { + if (isLdapConfigured()) { + + LdapConnection ldapConnection = null; + + try {+ ldapConnection = ldapConnectionPoolHolder.get().getConnection(); + Optional<LdapPerson> ldapPersonOptional = ldapFindPerson(ldapConnection, user.getNickname());
+ + if (!user.getActive()) { + + if (ldapPersonOptional.isPresent()) {+ ldapConnection.delete(ldapPersonOptional.get().getDn()); + LOGGER.info("did delete ldap directory data for user; " + user.toString());
+ } else {+ LOGGER.debug("no need take any action for inactive user {}; there is no data present in the ldap directory", user.toString());
+ } + + } else { + + if (ldapPersonOptional.isPresent()) { ++ ModifyRequest modifyRequest = new ModifyRequestImpl(); + modifyRequest.setName(ldapPersonOptional.get().getDn());
++ List<Modification> modifications = Lists.newArrayList();
++ accumulateModification(modifications, LDAP_ATTRIBUTE_KEY_CN, ldapPersonOptional.get().getCn(), user.getNickname()); + accumulateModification(modifications, LDAP_ATTRIBUTE_KEY_SN, ldapPersonOptional.get().getSn(), user.getNickname()); + accumulateModification(modifications, LDAP_ATTRIBUTE_KEY_UID, ldapPersonOptional.get().getUid(), user.getNickname()); + accumulateModification(modifications, LDAP_ATTRIBUTE_KEY_USERPASSWORD, ldapPersonOptional.get().getUserPassword(), user.toLdapUserPasswordAttributeValue()); + accumulateModification(modifications, LDAP_ATTRIBUTE_KEY_MAIL, ldapPersonOptional.get().getMail(), user.getEmail());
+ + if (!modifications.isEmpty()) { + ldapConnection.modify( + ldapPersonOptional.get().getDn(),+ modifications.toArray(new Modification[modifications.size()])
+ );+ LOGGER.info("did update ldap directory entry for; {}", user.toString());
+ } + + } else { ++ DefaultEntry defaultEntry = new DefaultEntry(String.format("%s=%s,%s", LDAP_ATTRIBUTE_KEY_CN, user.getNickname(), ldapPeopleDn));
++ defaultEntry.add(new DefaultAttribute(LDAP_ATTRIBUTE_KEY_OBJECTCLASS, LDAP_ATTRIBUTE_VALUE_OBJECTCLASS_INETORGPERSON));
++ defaultEntry.add(new DefaultAttribute(LDAP_ATTRIBUTE_KEY_CN, user.getNickname())); + defaultEntry.add(new DefaultAttribute(LDAP_ATTRIBUTE_KEY_SN, user.getNickname())); + defaultEntry.add(new DefaultAttribute(LDAP_ATTRIBUTE_KEY_UID, user.getNickname())); + defaultEntry.add(new DefaultAttribute(LDAP_ATTRIBUTE_KEY_USERPASSWORD, user.toLdapUserPasswordAttributeValue()));
+ + if (!Strings.isNullOrEmpty(user.getEmail())) {+ defaultEntry.add(new DefaultAttribute(LDAP_ATTRIBUTE_KEY_MAIL, user.getEmail()));
+ } + + ldapConnection.add(defaultEntry);+ LOGGER.info("did create ldap directory entry for; {}", user.toString());
+ } + + } + + } catch (Exception le) {+ throw new org.haikuos.haikudepotserver.user.LdapException("unable to update user to ldap directory; " + user.getNickname(), le); // TODO; better exception handling
+ } finally { + if (null != ldapConnection) { + try {+ ldapConnectionPoolHolder.get().releaseConnection(ldapConnection);
+ } catch (Exception e) { + // ignore + } + } + } + + } else {+ LOGGER.debug("ldap is not configured --> did not update ldap for user {}", user.toString());
+ } + } + + } + + /**+ * <p>This method will go through all of the users in the database and will synchronize them all with the + * LDAP directory; adding, modifying and deleting entries as necessary.</p>
+ */ ++ public void ldapSynchronizeAllUsers(ObjectContext context) throws org.haikuos.haikudepotserver.user.LdapException {
+ Preconditions.checkNotNull(context); + + if(isLdapConfigured()) { + + LOGGER.info("will update all users to ldap"); + + SelectQuery query = new SelectQuery(User.class); + query.setFetchLimit(BATCH_SIZE_USER); + query.setFetchOffset(0); + Integer lastCount = null; + int count = 0; + + while (null == lastCount || lastCount >= BATCH_SIZE_USER) { + + if (null != lastCount) {+ query.setFetchOffset(query.getFetchOffset() + BATCH_SIZE_USER);
+ } ++ @SuppressWarnings("unchecked") List<User> userBatch = context.performQuery(query);
+ lastCount = userBatch.size(); + + for (User user : userBatch) { + ldapUpdateUser(context, user); + } + + count += lastCount; + } + + LOGGER.info("did update {} users to ldap", count); + + } + else {+ LOGGER.info("ldap is not configured --> will not update all users to ldap");
+ } + } + + /**+ * <p>This method will search the LDAP directory at the root for people and will find all of the users. If there + * exists some users who do not exist in the database then those users will be deleted from the LDAP server.</p>
+ */ ++ // it is not clear if this will be necessary, but is probably a good idea to avoid people adding spurious data
+ // into the HDS LDAP directory. ++ public void ldapRemoveNonExistentUsers(ObjectContext context) throws org.haikuos.haikudepotserver.user.LdapException {
+ + Preconditions.checkNotNull(context); + + if(isLdapConfigured()) { + + LdapConnection ldapConnection = null; + + try {+ ldapConnection = ldapConnectionPoolHolder.get().getConnection();
+ + EntryCursor cursor = ldapConnection.search( + ldapPeopleDn,+ String.format("(%s=%s)", LDAP_ATTRIBUTE_KEY_OBJECTCLASS, LDAP_ATTRIBUTE_VALUE_OBJECTCLASS_INETORGPERSON),
+ SearchScope.ONELEVEL); ++ // could be more efficient, but probably does not matter at this stage.
+ + while(cursor.next()) { + LdapPerson person = createPerson(cursor.get());+ Optional<User> userOptional = User.getByNickname(context, person.getCn());
++ if(!userOptional.isPresent() | | !userOptional.get().getActive()) { + LOGGER.info("will delete ldap directory entry as no active user can be found for; {}", person.getCn());
+ ldapConnection.delete(person.getDn());+ LOGGER.info("did delete ldap directory entry for; {}", person.getCn());
+ } + else {+ LOGGER.trace("have found active user for person {}; will not remove", person.getCn());
+ } + + } + + } catch (Exception le) {+ throw new org.haikuos.haikudepotserver.user.LdapException("unable to remove non-existent users from the ldap directory", le); // TODO; better exception handling
+ } finally { + if (null != ldapConnection) { + try {+ ldapConnectionPoolHolder.get().releaseConnection(ldapConnection);
+ } catch (Exception e) { + // ignore + } + } + } + } + else {+ LOGGER.info("ldap is not configured --> will not remove non-existent users from ldap directory");
+ } + + } + + // ------------------------------ + // DATABASE SEARCH private String prepareWhereClause( List<Object> parameterAccumulator, @@ -71,6 +411,7 @@ * <p>Undertakes a search for users.</p> */ + @SuppressWarnings("unchecked") public List<User> search( ObjectContext context, UserSearchSpecification searchSpecification) { @@ -97,6 +438,7 @@ query.setFetchOffset(searchSpecification.getOffset()); query.setFetchLimit(searchSpecification.getLimit()); + //noinspection unchecked return (List<User>) context.performQuery(query); } @@ -126,12 +468,12 @@ query.setParameter(i+1, parameters.get(i)); } - List<Long> result = (List<Long>) context.performQuery(query);+ @SuppressWarnings("unchecked") List<Long> result = (List<Long>) context.performQuery(query);
switch(result.size()) { case 1: - return result.get(0).longValue(); + return result.get(0); default:throw new IllegalStateException("the result should have contained a single long result");
=======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/userrating/LocalUserRatingDerivationService.java Tue Aug 19 11:01:57 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/userrating/LocalUserRatingDerivationService.java Tue Sep 9 11:49:40 2014 UTC
@@ -1,3 +1,8 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + package org.haikuos.haikudepotserver.userrating; import com.google.common.base.Preconditions; @@ -26,7 +31,7 @@ Preconditions.checkNotNull(job);Preconditions.checkState(null!=executor, "the service is not running, but a job is being submitted");
executor.submit(new UserRatingDerivationJobRunnable(this, job));- LOGGER.info("have submitted job to derive user rating; {}", job.toString()); + LOGGER.info("did submit job to derive user rating; {}", job.toString());
} protected void run(UserRatingDerivationJob job) { =======================================--- /haikudepotserver-webapp/src/main/resources/spring/application-context.xml Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/spring/application-context.xml Tue Sep 9 11:49:40 2014 UTC
@@ -25,6 +25,25 @@<bean class="org.haikuos.haikudepotserver.support.cayenne.UserRatingDerivationTriggerListener"/>
+ <!-- LDAP --> + + <!-- + Processes requests to synchronize the HDS database into LDAP + --> + + <bean+ class="org.haikuos.haikudepotserver.user.LocalLdapSynchronizeUsersService"
+ init-method="startAsyncAndAwaitRunning" + destroy-method="stopAsyncAndAwaitTerminated"> + </bean> + + <!--+ Picks up on Cayenne ORM events around user changes so that LDAP can (optionally) be
+ updated with changes. + --> ++ <bean class="org.haikuos.haikudepotserver.support.cayenne.LdapUserUpdateListener"/>
+ <!-- EMAIL --><bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
=======================================--- /haikudepotserver-webapp/src/main/resources/spring/persistence.xml Sat Jun 7 06:02:54 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/spring/persistence.xml Tue Sep 9 11:49:40 2014 UTC
@@ -48,4 +48,16 @@ </constructor-arg> </bean> + <!-- + LDAP + --> ++ <bean class="org.haikuos.haikudepotserver.support.ldap.LdapConnectionPoolFactory">
+ <property name="host" value="${ldap.host:}"/> + <property name="port" value="${ldap.port:}"/> + <property name="userDn" value="${ldap.user.dn:}"/> + <property name="password" value="${ldap.password:}"/> + </bean> + + </beans> =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/rootoperations.html Tue Aug 19 11:01:57 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/rootoperations.html Tue Sep 9 11:49:40 2014 UTC
@@ -16,6 +16,14 @@ <em>— did initiate</em> </span> </li> + <li> + <a href="" ng-click="goSynchronizeUsers()"> + Synchronize users (with LDAP) + </a> + <span ng-show="didSynchronizeUsersTimeout"> + <em>— did initiate</em> + </span> + </li><li><a href="" ng-click="goRuntimeInformation()">Runtime information</a></li> <li><a href="" ng-click="goPaginationControlPlayground()">Pagination playground</a></li> <li><a href="" ng-click="goRaiseExceptionInLocalRuntime()">Raise test exception in javascript environment</a></li>
=======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/rootoperationscontroller.js Tue Aug 19 11:01:57 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/rootoperationscontroller.js Tue Sep 9 11:49:40 2014 UTC
@@ -22,6 +22,9 @@ $scope.didDeriveAndStoreUserRatingsForAllPkgs = false; $scope.deriveAndStoreUserRatingsForAllPkgsTimeout = undefined; + $scope.didSynchronizeUsers = false; + $scope.didSynchronizeUsersTimeout = undefined; + $scope.goDeriveAndStoreUserRatingsForAllPkgs = function() { jsonRpc.call( constants.ENDPOINT_API_V1_USERRATING, @@ -48,6 +51,32 @@ ); }; + $scope.goSynchronizeUsers = function() { + jsonRpc.call( + constants.ENDPOINT_API_V1_USER, + "synchronizeUsers", + [{}] + ).then( + function() { + $log.info('requested synchronize users'); + $scope.didSynchronizeUsers = true; + + if($scope.didSynchronizeUsersTimeout) {+ $timeout.cancel($scope.didSynchronizeUsersTimeout);
+ } ++ $scope.didSynchronizeUsersTimeout = $timeout(function() {
+ $scope.didSynchronizeUsers = false; + $scope.didSynchronizeUsersTimeout = undefined; + }, 3000); + }, + function(err) { + $log.error('unable to synchronize users'); + errorHandling.handleJsonRpcError(err); + } + ); + }; + /*** <p>This is outside of the normal breadcrumb navigation system because it is a
* developers' feature.</p> =======================================--- /haikudepotserver-webapp/src/test/resources/spring/test.xml Tue Jul 8 09:00:54 2014 UTC +++ /haikudepotserver-webapp/src/test/resources/spring/test.xml Tue Sep 9 11:49:40 2014 UTC
@@ -8,6 +8,7 @@<bean class="org.haikuos.haikudepotserver.IntegrationTestSupportService"/> <bean class="org.haikuos.haikudepotserver.userrating.NoopUserRatingDerviationService"/> + <bean class="org.haikuos.haikudepotserver.user.NoopLdapSynchronizeUsersService"/> <bean id="mailSender" class="org.haikuos.haikudepotserver.CapturingMailSender"/>
============================================================================== Revision: 52c9f839987d Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Fri Sep 12 08:53:03 2014 UTCLog: check for source and indicate when source is available when viewing a package version.
https://code.google.com/p/haiku-depot-web-app/source/detail?r=52c9f839987d Added:/haikudepotserver-webapp/src/main/webapp/js/app/directive/booleanindicatordirective.js
Modified:/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java
/haikudepotserver-webapp/src/main/resources/messages.properties /haikudepotserver-webapp/src/main/resources/messages_de.properties /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js
======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/booleanindicatordirective.js Fri Sep 12 08:53:03 2014 UTC
@@ -0,0 +1,45 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + * <p>This directive renders a small piece of text to indicate if the state + * bound is truthy or falsey</p> + */ + +angular.module('haikudepotserver').directive('booleanIndicator',[ + 'messageSource','userState', + function(messageSource,userState) { + return { + restrict: 'E', + link: function ($scope, element, attributes) { + + var stateExpression = attributes['state']; + + if (!stateExpression || !stateExpression.length) {+ throw Error('a value for the binding \'state\' was expected');
+ } + + var containerE = angular.element('<span></span>'); + element.replaceWith(containerE); + + $scope.$watch(stateExpression, function (newValue) { + + messageSource.get( + userState.naturalLanguageCode(), + 'gen.' + (!!newValue ? 'yes' : 'no') + ).then( + function (str) { + containerE.text(str); + }, + function () { + containerE.text('???'); + } + ); + + }); + } + }; + } +]); =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Wed Sep 3 09:55:22 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Fri Sep 12 08:53:03 2014 UTC
@@ -384,7 +384,7 @@public GetPkgResult getPkg(GetPkgRequest request) throws ObjectNotFoundException {
Preconditions.checkNotNull(request); - Preconditions.checkState(!Strings.isNullOrEmpty(request.name));+ Preconditions.checkState(!Strings.isNullOrEmpty(request.name), "request pkg name is required");
Preconditions.checkNotNull(request.versionType);Preconditions.checkState(!Strings.isNullOrEmpty(request.naturalLanguageCode));
=======================================--- /haikudepotserver-webapp/src/main/resources/messages.properties Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/messages.properties Fri Sep 12 08:53:03 2014 UTC
@@ -13,6 +13,8 @@ gen.pkg.title.plural=Packages gen.created.title=Created gen.modified.title=Modified +gen.yes=Yes +gen.no=No breadcrumb.home.title=Home breadcrumb.editRepository.title=Edit {0} @@ -197,6 +199,7 @@ listUsers.showInactiveAction.title=Show inactive users listUsers.createUserAction.title=Create user +viewPkg.sourceAvailable.title=Source available viewPkg.prominence.title=Prominence viewPkg.categories.title=Categories viewPkg.categories.none=None =======================================--- /haikudepotserver-webapp/src/main/resources/messages_de.properties Sun Aug 31 10:18:17 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/messages_de.properties Fri Sep 12 08:53:03 2014 UTC
@@ -11,6 +11,8 @@ gen.pkg.title.plural=Pakete gen.created.title=Erstellt gen.modified.title=Geändert +gen.yes=Ja +gen.no=Nein breadcrumb.home.title=Home breadcrumb.editRepository.title={0} umbenennen @@ -193,6 +195,7 @@ listUsers.showInactiveAction.title=Zeige inaktive Benutzer listUsers.createUserAction.title=Benutzer anlegen +viewPkg.sourceAvailable.title=Quellcode verfügbar viewPkg.prominence.title=Empfehlungsstufen viewPkg.categories.title=Kategorien viewPkg.categories.none=keine =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Tue Aug 19 11:01:57 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Fri Sep 12 08:53:03 2014 UTC
@@ -62,6 +62,10 @@ <div id="pkg-metadata-container"> <dl>+ <dt><message key="viewPkg.sourceAvailable.title"></message></dt>
+ <dd>+ <boolean-indicator state="!!pkg.versions[0].sourcePkg"></boolean-indicator>
+ </dd> <dt><message key="viewPkg.categories.title"></message></dt> <dd> <span ng-show="!pkgCategories">...</span> =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Tue Aug 19 11:01:57 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Fri Sep 12 08:53:03 2014 UTC
@@ -97,6 +97,7 @@ refetchPkgCategories(); refetchUserRatings(); refetchProminence(); + refetchIsSourcePkgAvailable(); }, function() { errorHandling.navigateToError(); // already logged @@ -136,6 +137,71 @@ ); } + + /**+ * <p>If there is a source package for this viewed package then this function
+ * will go off and get it.</p> + */ + + function refetchIsSourcePkgAvailable() { + + if(!$scope.pkg) {+ throw Error('there is not pkg available from which it would be possible to ascertain if a source pkg exists.');
+ } + + var pv = $scope.pkg.versions[0]; + + jsonRpc.call( + constants.ENDPOINT_API_V1_PKG, + 'getPkg', + [{ + name : $scope.pkg.name + "_source", + versionType : 'SPECIFIC', + incrementViewCounter : false, + architectureCode : 'source',+ naturalLanguageCode: constants.NATURALLANGUAGECODE_ENGLISH,
+ major: pv.major, + minor : pv.minor, + micro : pv.micro, + preRelease : pv.preRelease, + revision : pv.revision + }] + ).then( + function(result) { + $log.info('source exists for pkg'); + pv.sourcePkg = result; + }, + function(err) { + + switch(err.code) { + + case jsonRpc.errorCodes.OBJECTNOTFOUND: + + switch(err.data.entityName) { + + case 'Pkg': + case 'PkgVersion':+ $log.info('there is no source package found');
+ pv.sourcePkg = null; + break; + + default:+ $log.error('unable to ascertain if source is available for the package'); + errorHandling.handleJsonRpcError(err);
+ break; + } + break; + + default:+ $log.error('unable to ascertain if source is available for the package');
+ errorHandling.handleJsonRpcError(err); + break; + + } + } + ); + + } function refetchPkgIconMetaData() { ============================================================================== Revision: 8ab5cf122712 Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Mon Sep 15 10:09:53 2014 UTC Log: preparations for next version https://code.google.com/p/haiku-depot-web-app/source/detail?r=8ab5cf122712 Added:/haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.18__Password_hash_changes.sql
Modified: /haikudepotserver-docs/src/main/latex/docs/part-config.tex /haikudepotserver-rpm/src/main/etc/config__config.properties /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js
======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.18__Password_hash_changes.sql Mon Sep 15 10:09:53 2014 UTC
@@ -0,0 +1,56 @@ +-- ------------------------------------------------------ +-- PASSWORD HASHES ARE CHANGING +-- ------------------------------------------------------ +-- It is necessary to reset the root user's password back +-- to the original value and it is also necessary to +-- remove those users who don't have email addresses as +-- it will not be possible for them to reset their +-- passwords. +-- ------------------------------------------------------ + +UPDATE haikudepot.user SET+ password_hash='b9c4717bc5c6d16f2be9e967ab0c752f8ac2084f95781989f39cf8736e2edeef',
+ password_salt='cad3422ea02761f8' +WHERE + nickname='root'; + +-- the following query will get a list of effected parties; +-- SELECT +-- nickname,+-- (SELECT true FROM haikudepot.user_password_reset_token WHERE user_id=u.id LIMIT 1) AS prt, +-- (SELECT true FROM haikudepot.permission_user_pkg WHERE user_id=u.id LIMIT 1) AS pup, +-- (SELECT true FROM haikudepot.user_rating WHERE user_id=u.id LIMIT 1) AS ur
+-- FROM +-- haikudepot.user u +-- WHERE +-- u.email IS NULL +-- AND u.nickname <> 'root'; + +DELETE FROM + haikudepot.permission_user_pkg +WHERE + user_id IN ( + SELECT id FROM haikudepot.user u + WHERE u.email IS NULL AND u.nickname <> 'root' + ); + +DELETE FROM + haikudepot.user_password_reset_token +WHERE + user_id IN ( + SELECT id FROM haikudepot.user u + WHERE u.email IS NULL AND u.nickname <> 'root' + ); + +DELETE FROM + haikudepot.user_rating +WHERE + user_id IN ( + SELECT id FROM haikudepot.user u + WHERE u.email IS NULL AND u.nickname <> 'root' + ); + +DELETE FROM haikudepot.user +WHERE email IS NULL AND nickname <> 'root' + + =======================================--- /haikudepotserver-docs/src/main/latex/docs/part-config.tex Thu Aug 14 11:03:33 2014 UTC +++ /haikudepotserver-docs/src/main/latex/docs/part-config.tex Mon Sep 15 10:09:53 2014 UTC
@@ -127,3 +127,13 @@ \subsubsection{\tt email.from}This is the email address from which emails outbound from the system will be sent. Typically this might be a ``no-reply'' email address such as ``noreply@xxxxxxxxxxxx''.
+ +\subsection{LDAP} ++\fcolorbox{red}{white}{\parbox{\textwidth}{\color{red}The LDAP integration is not intended to be used at this point in time.}}
+ +\subsubsection{\tt ldap.host} +\subsubsection{\tt ldap.port} +\subsubsection{\tt ldap.user.dn} +\subsubsection{\tt ldap.password} +\subsubsection{\tt ldap.people.dn} =======================================--- /haikudepotserver-rpm/src/main/etc/config__config.properties Tue Sep 9 11:49:40 2014 UTC +++ /haikudepotserver-rpm/src/main/etc/config__config.properties Mon Sep 15 10:09:53 2014 UTC
@@ -88,6 +88,7 @@ email.from=noreply@xxxxxxxxxxxx # ------------------------------------------- +# == NOT READY TO BE USED == # LDAP related # If the LDAP setting are commented-out then the system will operate as if # there were no LDAP server and will not attempt to synchronize user data =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Fri Sep 12 08:53:03 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Mon Sep 15 10:09:53 2014 UTC
@@ -62,8 +62,8 @@ <div id="pkg-metadata-container"> <dl>- <dt><message key="viewPkg.sourceAvailable.title"></message></dt>
- <dd>+ <dt ng-show="undefined!=viewPkg.sourceAvailable"><message key="viewPkg.sourceAvailable.title"></message></dt>
+ <dd ng-show="undefined!=viewPkg.sourceAvailable"><boolean-indicator state="!!pkg.versions[0].sourcePkg"></boolean-indicator>
</dd> <dt><message key="viewPkg.categories.title"></message></dt> =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Fri Sep 12 08:53:03 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Mon Sep 15 10:09:53 2014 UTC
@@ -145,61 +145,67 @@ function refetchIsSourcePkgAvailable() { - if(!$scope.pkg) {- throw Error('there is not pkg available from which it would be possible to ascertain if a source pkg exists.');
- } +// [apl 15.sep.2014]+// disabling this for now because it creates exception logs on the server-side if +// there is no source package (often). Maybe this can be dealt with later on, but
+// I don't want to delay release because of this. - var pv = $scope.pkg.versions[0]; - jsonRpc.call( - constants.ENDPOINT_API_V1_PKG, - 'getPkg', - [{ - name : $scope.pkg.name + "_source", - versionType : 'SPECIFIC', - incrementViewCounter : false, - architectureCode : 'source',- naturalLanguageCode: constants.NATURALLANGUAGECODE_ENGLISH,
- major: pv.major, - minor : pv.minor, - micro : pv.micro, - preRelease : pv.preRelease, - revision : pv.revision - }] - ).then( - function(result) { - $log.info('source exists for pkg'); - pv.sourcePkg = result; - }, - function(err) { - - switch(err.code) { - - case jsonRpc.errorCodes.OBJECTNOTFOUND: - - switch(err.data.entityName) { - - case 'Pkg': - case 'PkgVersion':- $log.info('there is no source package found');
- pv.sourcePkg = null; - break; - - default:- $log.error('unable to ascertain if source is available for the package'); - errorHandling.handleJsonRpcError(err);
- break; - } - break; - - default:- $log.error('unable to ascertain if source is available for the package');
- errorHandling.handleJsonRpcError(err); - break; - - } - } - ); +// if(!$scope.pkg) {+// throw Error('there is not pkg available from which it would be possible to ascertain if a source pkg exists.');
+// } +// +// var pv = $scope.pkg.versions[0]; +// +// jsonRpc.call( +// constants.ENDPOINT_API_V1_PKG, +// 'getPkg', +// [{ +// name : $scope.pkg.name + "_source", +// versionType : 'SPECIFIC', +// incrementViewCounter : false, +// architectureCode : 'source',+// naturalLanguageCode: constants.NATURALLANGUAGECODE_ENGLISH,
+// major: pv.major, +// minor : pv.minor, +// micro : pv.micro, +// preRelease : pv.preRelease, +// revision : pv.revision +// }] +// ).then( +// function(result) { +// $log.info('source exists for pkg'); +// pv.sourcePkg = result; +// }, +// function(err) { +// +// switch(err.code) { +// +// case jsonRpc.errorCodes.OBJECTNOTFOUND: +// +// switch(err.data.entityName) { +// +// case 'Pkg': +// case 'PkgVersion':+// $log.info('there is no source package found');
+// pv.sourcePkg = null; +// break; +// +// default:+// $log.error('unable to ascertain if source is available for the package'); +// errorHandling.handleJsonRpcError(err);
+// break; +// } +// break; +// +// default:+// $log.error('unable to ascertain if source is available for the package');
+// errorHandling.handleJsonRpcError(err); +// break; +// +// } +// } +// ); } ============================================================================== Revision: 8ef5143b7e4d Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Mon Sep 15 10:51:49 2014 UTC Log: resolve minor issue in viewing pkg version in jsp pages https://code.google.com/p/haiku-depot-web-app/source/detail?r=8ef5143b7e4d Modified:/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp
=======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Fri Sep 12 08:53:03 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Mon Sep 15 10:51:49 2014 UTC
@@ -1109,7 +1109,7 @@ } ); - LOGGER.info( + LOGGER.debug("did search and find {} pkg versions for get bulk pkg; fetch in {}ms, marshall in {}ms",
pkgVersions.size(), postFetchMs - preFetchMs, =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java Sun Aug 31 12:05:02 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java Mon Sep 15 10:51:49 2014 UTC
@@ -146,7 +146,7 @@ }public PkgVersionLocalization getPkgVersionLocalizationOrFallback(final NaturalLanguage naturalLanguage) { - return getPkgVersionLocalizationOrFallback(naturalLanguage.getCode()); + return getPkgVersionLocalizationOrFallbackByCode(naturalLanguage.getCode());
} /** @@ -173,7 +173,8 @@ * for english if the desired language is not available.</p> */- public PkgVersionLocalization getPkgVersionLocalizationOrFallback(final String naturalLanguageCode) {
+ // have to add the 'byCode' for JSP to be able to work with it.+ public PkgVersionLocalization getPkgVersionLocalizationOrFallbackByCode(final String naturalLanguageCode) { Optional<PkgVersionLocalization> pkgVersionLocalizationOptional = Optional.absent();
if(!Strings.isNullOrEmpty(naturalLanguageCode)) { =======================================--- /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp Sun Aug 31 10:18:17 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp Mon Sep 15 10:51:49 2014 UTC
@@ -39,7 +39,7 @@<multipage:pkgIcon pkgVersion="${data.pkgVersion}" size="32"/>
</div> <div id="pkg-title-text">- <h1><c:out value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage.code).summary}"/></h1> + <h1><c:out value="${data.pkgVersion.getPkgVersionLocalizationOrFallbackByCode(data.currentNaturalLanguage.code).summary}"/></h1>
<div class="muted"> <small> <c:out value="${data.pkgVersion.pkg.name}"></c:out> @@ -68,7 +68,7 @@ <div id="pkg-description-container"> <p>- <multipage:plainTextContent value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage.code).description}"></multipage:plainTextContent> + <multipage:plainTextContent value="${data.pkgVersion.getPkgVersionLocalizationOrFallbackByCode(data.currentNaturalLanguage.code).description}"></multipage:plainTextContent>
</p> </div>