mardi 21 décembre 2010

Premiers pas sous Git



Je me décide enfin à succomber au buzz et tente ma chance sous Git.
Pas tant pour le cote décentralisé que pour les merge simplifiés et les gros move lors d'important refactoring.

Pour tester la bête je vais d'abord migrer des projets existants de Subversion à Git en m'inspirant de cet excellent billet de Jon Maddox :
http://www.jonmaddox.com/2008/03/05/cleanly-migrate-your-subversion-repository-to-a-git-repository

Assembla propose un hébergement Git gratuit jusqu'à 2Go par repository.
J'utilise msysgit pour Git sous Windows.

Une fois Git installé, on identifie l'utilisateur qui fera les commit:

git config --global user.name "Cedric Thiebault"
git config --global user.email "cedric.thiebault@gmail.com"

Sans cette configuration, Git utilisera directement les informations contenues dans votre profil utilisateur système.

Ensuite, il faut créer un fichier texte qui va faire le mapping entre les users SVN et les users Git (svn-git-users.txt):

cthiebault = Cedric Thiebault <cedric.thiebault@gmail.com>

Nous allons maintenant récupérer le projets et son historique depuis SVN dans un répertoire temporaire (sous Git). Finalement, nous clonerons se projet temporaire vers un vrai repository final que nous "pousserons" vers le repository Assembla dans la branche principale (master).

mkdir project-tmp
cd project-tmp
git svn init https://svn-repository/project/trunk --no-metadata
git config svn.authorsfile ../svn-git-users.txt
git svn fetch
cd ..
git clone project-tmp project
cd project
git remote add project git@git.assembla.com:project.git
git push project master

Quelques sites bien pratique pour débuter avec Git :
Git dans la pratique (1/2)
Git dans la pratique (2/2)
Git Ready
#gitfr

lundi 19 avril 2010

GWT et Spring Security (ex ACEGI)



Ce billet présentera comment rapidement sécuriser une application GWT (SmartGWT en fait) en utilisant un formulaire d'authentification géré par GWT et non une page web avec un formulaire HTML classique.

L'application est developpée avec les outils suivants :

Le projet GWT-ent propose une intégration avec Spring Security mais seulement avec la version 2.5 et la qualité du code laisse à désirer...
De plus, avec les dernières versions de Spring Security la configuration a été grandement simplifiée!

Configuration du projet avec Maven

-- POM.xml --

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>3.0.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-core</artifactId>
  <version>3.0.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>3.0.2.RELEASE</version>
</dependency>

Configuration de l'application web

-- web.xml --

<web-app id="gwtSecurity">

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      classpath:applicationContext.xml
      classpath:applicationContext-security.xml
    </param-value>
  </context-param>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <listener>
    <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
  </listener>

  <servlet>
    <servlet-name>rpc-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>rpc-dispatcher</servlet-name>
    <url-pattern>*.rpc</url-pattern>
  </servlet-mapping>

  [...]

</web-app>

Configuration de Spring Security

Ici, nous indiquons à Spring d'utiliser notre UserService pour accéder aux utilisateurs ainsi que le SHA pour crypter les mots de passe.
Nous ne protégeons aucune URL puisque notre application est hébergée sur une unique page.
Nous avons été obligé de redéfinir le RememberMeServices pour forcer le alwaysRemember à vrai. par défaut Spring s'attend à recevoir un paramètre dans l'URL qui lui demanderait de se souvenir de cet utilisateur lors du prochain accès. Comme nous utilisons des appels RPC, nous ne pouvons pas passer ce paramètre dans le request.

-- applicationContext-security.xml --

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider user-service-ref="userService">
      <security:password-encoder hash="sha" />
    </security:authentication-provider>
  </security:authentication-manager>

  <security:http auto-config="true">
    <security:intercept-url pattern="/*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <security:intercept-url pattern="/css/**" filters="none" />
    <security:intercept-url pattern="/images/**" filters="none" />
    <security:logout logout-url="/logout" logout-success-url="/gwt.html" />
    <security:remember-me key="gwtSecurity" />
  </security:http>

  <bean id="rememberMeServices" class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
    <property name="alwaysRemember" value="true" />
    <property name="userDetailsService" ref="userService" />
    <property name="key" value="gwtSecurity" />
  </bean>

</beans>

Configuration de la servlet pour les appels RPC

On utilise ici Gilead et GWT-SL.
-- rpc-dispatcher-servlet.xml --

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
      <map>
        <entry key="/user.rpc" value-ref="userRemoteService" />
        <entry key="/security.rpc" value-ref="securityRemoteService" />
        [...]
      </map>
    </property>
  </bean>

  <bean id="abstractGileadRPCServiceExporter" class="org.gwtwidgets.server.spring.gilead.GileadRPCServiceExporter" abstract="true">
    <property name="beanManager" ref="persistentBeanManager" />
  </bean>

  <bean id="userRemoteService" parent="abstractGileadRPCServiceExporter">
    <property name="service" ref="userService" />
    <property name="serviceInterfaces" value="service.user.UserRemoteService" />
  </bean>

  <bean id="securityRemoteService" parent="abstractGileadRPCServiceExporter">
    <property name="service" ref="securityService" />
    <property name="serviceInterfaces" value="service.security.SecurityRemoteService" />
  </bean>

  [...]

</beans>

Implementation du service d'authentification

Le Authentication Manager de Spring doit pouvoir récupérer un utilisateur par son nom d'usager. Ici c'est la class UserService qui implémentera l'interface org.springframework.security.core.userdetails.UserDetailsService pour pouvoir être utilisé par le Authentication Manager de Spring.

@Service("userService")
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService, UserDetailsService {

  [...]

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
    if (StringUtils.isBlank(username)) throw new UsernameNotFoundException(username);

    User user = findByLogin(username);
    if (user == null) throw new UsernameNotFoundException(username);

    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(user.getPermissions().size());
    for (String perm : user.getPermissions()) {
      authorities.add(new GrantedAuthorityImpl(perm));
    }

    boolean enabled = user.isActive();
    boolean accountNonExpired = true;
    boolean accountNonLocked = true;
    boolean credentialsNonExpired = true;
    return new org.springframework.security.core.userdetails.User(username, user.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
  }

}

Dans notre cas, on ne vas pas d'authentifier par un formulaire HTML qui posterait vers une adresse géré par Spring Security car tout est géré en Javascript et on ne veut pas quitter la page courante.
Il va donc falloir définir un service que l'on pourra appeler via RPC pour ouvrir une session. Ce service s'occupera de gérer aussi le "Remember Me".

@Service("securityService")
public class SecurityServiceImpl implements SecurityService {

  @Resource
  private AuthenticationManager authenticationManager;

  @Resource
  private RememberMeServices rememberMeServices;

  @Override
  public boolean login(String username, String password, boolean rememberMe) {
    try {
      Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
      SecurityContextHolder.getContext().setAuthentication(authentication);
      if (rememberMe) {
        rememberMeServices.loginSuccess(ServletUtils.getRequest(), ServletUtils.getResponse(), authentication);
      }      
      return authentication.isAuthenticated();
    } catch (AuthenticationException e) {
      if (rememberMe) {
        rememberMeServices.loginFail(ServletUtils.getRequest(), ServletUtils.getResponse());
      }
      return false;
    }
  }

  [...]

}

Finalement, il en reste plus qu'à notre interface (ici avec SmartGWT) à appeler notre service d'authentification :

public class LoginForm extends DynamicForm {

  private SecurityRemoteServiceAsync securityService = GWT.create(SecurityRemoteService.class);

  TextItem login;
  PasswordItem password;
  CheckboxItem rememberMe;

  [...]

  void login() {
    securityService.login(username, password, remeberMe,
        new AsyncCallback<Boolean>() {
          public void onFailure(Throwable caught) {
            loginFailed();
          }

          public void onSuccess(Boolean success) {
            if (success) {
              // user successfully logged in
            } else {
              loginFailed();
            }
          }
        });
  }

}