Tuesday May 05, 2009

Open Source Identity Connector for Google Apps

google apps

Identity Manager 8.1 (or later) contains support for a very cool open source connector framework. The project name isn't too exciting (Identity Connectors ?  definitely needs some marketing love ) but the end result is fantastic.

 I had the experience of porting my IdM legacy adapter for Google Apps to this new framework. The API is much simpler (no depedencies on IdM or any other product), and the concepts are clean and well thought out. Just what one would expect from a team that a has a few decades of experience building connectors. 

 You can download the google apps connector, along with many other bundles here.

 Feedback is most welcome

An OpenSSO Lab using VirtualBox

opensso logo

As part of the Reboot conference in Victoria, I developed a hands on OpenSSO lab using VirtualBox.  The virtual image is running OpenSolaris 2009.06 (dev), GlassFish v3, and OpenSSO (express build 7).  

The goal of the lab is to provide a short (it should take you 4-5 hours to complete) introduction to OpenSSO that covers basic installation, SAML federation using Fedlets, and installing Policy Agents.  

The intended audience for the lab are developers or those that want to understand the functional aspects of OpenSSO. David Goldsmith has developed a much more comprehensive OpenSSO lab that focuses on complex, load balanced configurations. If you are more interested in the infrastructure side of things, check out his blog posting on the lab.  David is a training pro - and his stuff is always top notch. 

This lab is meant to be facilitated, but if you want to follow along at home,  read on:

  • Make sure you have a computer with a MINIMUM of 2GB of RAM, 10GB free disk. Dual core CPU is strongly recommended. 
  • Download and install VirtualBox 2.2.2. or later (earlier versions are not compatible with this lab)
  • You should have an ssh client (e.g. putty.exe on Windows) installed.

The lab documents (zipped pdf) can be downloaded here.

The Virtual Image is split into four zip fragments:


These fragments needs to be pasted together, and then unzipped.

On Unix-ish systems:  

cat xa[abcd].zip > bigfile.zip

On Windows use can use the copy command:

copy /b xaa.zip+xab.zip+xac.zip+xad.zip   bigfile.zip

Unzip the file, and then import the appliance into VirtualBox.

The image is a \*completed\* version of the lab (everything is installed). The idea is to give you a known starting point. If you want to experiment with the lab, start with the uninstall procedures first. You can take a VirtualBox snapshot to save the lab state, or use ZFS snapshots from within the OpenSolaris image.  

 To save some memory, the OpenSolaris GUI has been disabled. You can enable the GUI with the following command:

pfexec svcadm enable gdm  

Note:  When importing the appliance you may encounter a bug where the import "hangs".  After letting the disk fully import, you will need to kill VirtualBox and start it again. You will find that the imported disk image is now in the Media Manager - and can be added to the new Virtual machine. You should be able to boot the image after assigning the disk. 

Important Update: 

Running a Solaris 64 bit kernel under VirtualBox can occasionally trigger very high CPU consumption- making the startup of OpenSSO take a long time (10+ minutes). See this bugid for details.  To get around this problem boot the image in 32 bit mode. The easiest way to do this is to change your VirtualBox settings to select "OpenSolaris" as the OS Type instead of "OpenSolaris 64 bit". 

Thursday Feb 26, 2009

Spring comes early - an updated OpenSSO Spring Security provider

Following on the pioneering work of Robert Dale  and Miguel Alonso I have updated the OpenSSO Spring provider with additional support for authorization. You can now use Spring security JSP tags, method security annotations and Spring method security point cuts.

Where to get it!

You can download the provider and a sample Spring application from the OpenSSO Extensions project page.

The package-info header is reproduced below:

Package com.sun.identity.provider.springsecurity Description

A Spring 2 Security provider for OpenSSO.

Provides authentication and authorization plugins for the Spring 2 Security framework. For an example of how to configure this module refer to the OpenSSO / Spring example


The provider delegates authentication to the OpenSSO instance configured in the applications AMConfig.properties. When a user tries to access an application web page, the spring provider will check for a valid SSOToken. If the user is not authenticated they will be redirected to OpenSSO. Once authentication is complete, OpenSSO will redirect the user back to the application.

Upon authentication, a Spring UserDetails object is created for the user and placed in the session. This can be used by the application to query for the user principal and other information. The spring security authentication tags can be used within a JSP, as shown in the following example:

  The Logged on Principal is <security:authentication property="principal.username"/>

Authorization - Web URL Policy

The provider delegates URL policy decisions to OpenSSO. This is different than most Spring 2 providers where the URL policy is configured in the application using annotations or spring XML configuration.

OpenSSO is queried for URL policy decisions, and will return ALLOW, DENY or null. A null return means that OpenSSO does not have a policy for the requested URL. The provider will return an ABSTAIN vote if the OpenSSO policy decision is null. If you wish to implement a policy of "Deny that which is not explicity permitted" you will want to use Springs AffirmativeBased voter in your security configuration. This ensures that at least one voter must "ALLOW" the request.

Authorization - Roles

Spring Security uses the concept of GrantedAuthorities which are analagous to roles in OpenSSO. This provider converts OpenSSO group (role) membership into Spring GrantedAuthorities. The current implementation converts an OpenSSO group membership (for example "staff") into a GrantedAuthority by concatenating the prefix "ROLE_" with the upper cased group name. For example, if a user belongs to the OpenSSO groups "staff" and "admins", they will be granted "ROLE_STAFF" and "ROLE_ADMINS" authorizations.

Authorizations can be used in JSPs using the Spring security tags. For example, the following JSP snippet will output different results depending on whether the user belongs to the staff group or not:

<security:authorize ifAllGranted="ROLE_STAFF">
    <div align="left"><h2>Congrats!! You have the Staff role</h2></div>

<security:authorize ifNotGranted="ROLE_STAFF">
    <div align="left"><h2>TOO BAD SO SAD - You do NOT have the Staff role</h2></div>

Authorizations can also be used to protect methods using Spring pointcuts or annotations. The example below demonstrates using JSR security annotations:

@RolesAllowed("ROLE_ADMIN") public String sayHello() { return "Hello" }

The above method will throw a Spring Security AccessException if the user is not in the admin group.

Monday Dec 29, 2008

A Python script to encode iMovie .dv files to MP4 using HandBrake

I recently captured some old 8mm video tape of the family to iMovie.  My strategy for archiving is to copy the material to a new medium every few years. We will see how that works out.

In any event, I want to throw all the videos on a hard drive and store it offsite in our safety deposit box. However, the raw .DV footage is too large and won't fit (and I am too cheap to buy more hard drives or a larger safety deposit box). 

Necessity being the mother of invention, I needed a script to encode and compress all my .DV footage. MP4 seems to be a good choice for a format that is widely supported and offers good compression quality.  I wanted to preserve the folder event structure in iMovie - so a simple find command was not going to cut it.

I started off with bash - but quickly became frustrated with the quirky syntax. One of the problems with shell scripts is that file and path names are strings - not objects. You often run into escaping issues when presented with long file names and special characters.  You can work around these issues - but it is much more painful than it ought to be.

I elected to give python a go and see if it was any easier. This is my first python script - and I don't claim this is a particullarily good example. However - if you are facing the same issue - please feel free to use this as a starting point.

You will need a copy of the Handbrake CLI  to perform the mp4 encoding. Handbrake uses the x264 encoder.

Without further ado, here is the script:

 1 #!/usr/bin/env python

 2 # Script to encode DV files to MP4 using HandBrakeCLI
 3 # Preserves the folder structure of the iMove events
 4 __author__="warrenstrange"
 5 __date__ ="$Dec 28, 2008 6:28:09 PM$"

 7 import os
 8 import subprocess
 9 import os.path

11 # Source for .dv files
12 root = "/Volumes/Velocity1/iMovie Events.localized"
13 # Destination for encoded MP4 files
14 dest = "/Volumes/WSTCD/mp4.bak"

15 #dest = "/var/tmp/xx"
17 def encode(path, dest):
18     for root, dirs, files in os.walk(path, topdown=False):

19         for name in files:
20             # Get the extension
21             (base, ext)=os.path.splitext(name)
22             # If it is a .dv file

23             if ext.lower() == ".dv":
24                 # Get the directory folder where the .dv file is stored
25                 (h,tail) = os.path.split(root)
26                 # Form the destination directory folder name

27                 destdir = os.path.join(dest,tail)
28                 # Create the destination directory if required
29                 if not os.path.exists(destdir):
30                     os.mkdir(destdir)

31                 # Form the output file name (.MP4 extenstion)
32                 newfile = os.path.join(destdir, base + ".MP4")
33                 infile = os.path.join(root,name)
34                 # If it already exists don't overwrite it - skip it

35                 if os.path.exists(newfile):
36                     print "skipping " + infile
37                 else:
38                     subprocess.call( ["HandBrakeCLI", "--preset=Universal", "-i" ,infile, "-o", newfile])

41 if __name__ == "__main__":
42     encode(root, dest)

Friday Nov 14, 2008

DimDim Web Conferencing

I just tried DimDim web conferencing. This is a free (for up to 20 people) Web conferencing solution.

Participants only need a web browser (the usually suspects, Firefox, IE, Safari). If you are hosting and want to share your desktop you need to install a browser plugin  (works on Mac, Window and Linux).  Desktop sharing is a tad laggy but was still very usable.

Check it out at:


Monday Mar 10, 2008

Identity Manager IDE released as open source!

The Identity Manager IDE plugin for Netbeans has now been released as open source under the CDDL license.

Get it while it's hot @ http://identitymanageride.dev.java.net/

Highlights include support for Netbeans 6.0, and a new "Object Builder" to manage IdM Objects.

Friday Apr 20, 2007

Fun with Spring, Ldap and Java Annotations

I thought it would be neat to have a way to persist Java objects to and from an Ldap directory. Kinda like "Hibernate lite" for directories. I saw several inquiries on the Hibernate forums discussing a custom mapper for Ldap - but nothing has ever been implemented. The concensus seems to be that it ought to be possible - but perhaps the relational model that Hibernate is based on is not a great fit for Ldap.

In any event, I created a very simple package which I have Dubbed "Slapper". The readme is presented below. If you think this has value, drop me a note and I may extend the implementation...


What does it stand for? How about “Simple Ldap Mapper”. OK, yes it is kinda lame

In a nutshell, Slapper is a utility package that uses annotations to persist Java POJOs to and from an Ldap directory. This is a very simplistic mapper - It currently does not handle any relationship navigation. This package uses Spring Ldap.

To use Slapper, you first create a POJO (well almost a POJO, as it must implement the DirObject interface) that has the appropriate annotations. Here is an example:

@ObjectClass({"inetorgperson", "organizationalperson", "person", "top"})
public class UserAccount implements DirObject {
private Name name;
private String commonName;
private String lastName;
private byte[] userPassword;

@DirectoryAttribute("userPassword" )
public byte[] getUserPassword() {
return userPassword;

public void setUserPassword(byte[] userPassword) {
this.userPassword = userPassword;

public String getCommonName() {
return commonName;

\* @param commonName
public void setCommonName(String commonName) {
this.commonName = commonName;


The next step is to configure your Spring beans.xml to create a DAO object that can read and write to this object type. Here is an

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"

<bean id="contextSource" class="org.springframework.ldap.support.LdapContextSource">
<property name="url" value="ldap://localhost:389" />
<property name="base" value="dc=example,dc=com" />
<property name="userName" value="cn=Directory Manager" />
<property name="password" value="passw0rd" />

<bean id="ldapTemplate" class="org.springframework.ldap.LdapTemplate">
<constructor-arg ref="contextSource" />

<bean name="userMarshaller" class="com.my2do.slapper.Marshaller">
<constructor-arg value="com.my2do.slapper.example.UserAccount"/>

<bean id="userDAO" class="com.my2do.slapper.LdapDAOImpl">
<property name="ldapTemplate" ref="ldapTemplate" />
<property name="marshaller" ref="userMarshaller"/>


The ldapTemplate is standard Spring Ldap code (see the Spring docs for the details). The only tricky bit of the above is the Marshaller. This is a class that knows how to marshal annotated POJOs
to and from directory attributes. The marshaller processes @DirectoryAttribute annotations and maps them to the appropriate Ldap attribute. In the above example, the userDAO bean is configured to read and write beans of type UserAccount.

The DAO interface is fairly simple at this point:

public interface LdapDAO {

\* Retrieve an ldap object from the directory
\* @param dn String Dn of the object
\* @return Returns an object populated with directory attributes. The
\* type of the object will be that created by the underlying Marshaller
public Object getObject(String dn);
\* Retrieve an ldap object from the directory
\* @param name (ldap dn) of the object to fetch
\* @return Returns an object populated with directory attributes. The
\* type of the object will be that created by the underlying Marshaller
public Object getObject(Name name);

\* Create the object in the directory. The Dn of the object
\* must be set properly (getName() must return a valid Dn). The objects
\* attributes will be extracted with the Marsheller instance and sent
\* to the directory.
\* @param obj object to create.
public void create(DirObject obj) ;

\* Delete the object from the directory. The objects getName() must
\* return a valid Dn.
\* @param obj Object to delete
public void delete(DirObject obj) ;

\* Updated the given object.
\* The object will first be read back from the directory, and only
\* changed attributes will be modified.
\* @param obj Object to update
public void update(DirObject obj) ;

\* Setter for marhsaller. This will normally be injected by Spring,
\* but is exposed as part of the interface in case you want
\* to control the Marshalling strategy
\* @param m Marshaller instance that knows how to marshal directory
\* attributes to/from an object
public void setMarshaller(Marshaller m);

\* Very simple search function. Will search at the base dn and subtrees for
\* objects which meet the filter criteria. Objects will be marshalled into the
\* list.
public List search(Name base, String filter );


Putting it all together, here is a sample test that shows how the API
is used (some code has been elided for brevity...)


public class SlapperTest {
BeanFactory factory;
LdapDAO userDAO;

public SlapperTest() {
Resource r = new FileSystemResource("test/beans.xml");
factory = new XmlBeanFactory(r);
userDAO = (LdapDAO) factory.getBean("userDAO");

public void crudTest() throws InvalidNameException {
UserAccount ua = new UserAccount();
Name name= new LdapName("uid=test2, ou=People");

ua.setName(name );
ua.setCommonName("Fred Flinstone");

// try to create the user twice - should get an error
try {
fail("Expected to get an exception");
} catch(DataIntegrityViolationException ex) {
System.out.println("OK - Got expected exception" + ex);

UserAccount ua2 = (UserAccount)userDAO.getObject(name);

System.out.println("Got user back " + ua2 + " is equal " + ua2.equals(ua) );


// Dn compare is broken due to bug in LdapName.equals() .....
//assertEquals( ua, ua2);
// so compare some other attribute
assertEquals( ua.getCommonName(), ua2.getCommonName());

// update the user. Only the changes will get sent to the directory
// read it back and make sure the mobile got set
ua2 = (UserAccount)userDAO.getObject(name);
assertEquals( ua.getMobile(), ua2.getMobile());

// delete the user
System.out.println("deleted user " + ua);
// deleting twice seems to be silentluy ignored. OK?

public void simpleSearchTest() throws InvalidNameException {

LdapName base = new LdapName("ou=People");

//String filter = "& (objectclass=inetorgperson) (uid=user\*)";

// build a filter that matches all inetorgperson objects whose
// uid starts with user\*
// These are the sample users created by the OpenDS installer
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "inetorgperson"));
filter.and(new WhitespaceWildcardsFilter("uid", "user "));
System.out.println("Using filter " + filter.encode() );

// List l will be populated with UserAccount objects
// Assumes we have some sample users in the directory
List l = userDAO.search(base, filter.encode());
assertEquals( l.get(0).getClass(), UserAccount.class );

System.out.println("Got search result =" + l);



Powered by ScribeFire.

LdapName.equals() not so equal?

Is it just me, or is LdapName.equals() brain dead? My expectation is that two LdapName's are equal if they represent the same underlying Ldap Dn. Instead, the comparison seems to be case and white space sensitive. OK, I see I'm not the only one who has this problem.

Powered by ScribeFire.

Tuesday Mar 27, 2007

SPML 2.0 presentation

Curious about SPML 2.0 ? Here is an overview presentation that nicely explains the core concepts.

Powered by ScribeFire.

Thursday Mar 01, 2007

Using OpenSSO authentication with JBoss SEAM

The new security model in SEAM makes it easy to plug in different authentication mechanisms. After a couple of hours of hacking (OK, it was more like 6 hours...),  I had the sample SEAM booking demo using OpenSSO for authentication. 

The user enters their OpenSSO username and password on the Booking demo log on page, and the application uses the OpenSSO APIs to authenticate to an OpenSSO server. [If you are trying this out at home, remember that the user must be registered in both OpenSSO AND the SEAM application for this to work. If the user authenticates to OpenSSO, but is not registered in the Booking demo, they will get an error message to this effect].

This code has a long way to go to be truly useful. For example, it does not handle any of the following:

  • Single Sign On with OpenSSO. The current example is authentication only. It does not check for an existing SSO token
  • Redirection to the OpenSSO login page
  • SEAM or JEE Role support
  • Session timeouts, enforcement of URL policy, single logout, etc.

Policy Agent vs. Agentless Deployment

The easiest way to obtain the above missing features is to install an OpenSSO policy agent in the hosting container (GlassFish, in my case).   That being said, I would really love to be able to get this working without requiring the installation of a policy agent.

It would be nice to hand someone a .war file, and have it "just work" without any modification of the container.   This may turn out to be a rather difficult exercise. We will see how far I get...

Deploying the Example

If you are interested in playing with the example, you can download the NetBeans project from http://mediacast.sun.com/share/warren/seamBookingWithOpenSSO.zip

You will also need to download and install OpenSSO, and I suggest you also get the OpenSSO examples as well.

The deployed .war file must contain the amsdk.jar file. In addition, you will need to include an AMConfig.properties file that matches your environment. This configuration file is used by the client code to find and authenticate against the OpenSSO server.

The easiest way to generate a valid AMConfig file is to run the "setup" script in the OpenSSO examples, and test it using the sample "login" script.

Most of the action (no pun intended) is in the AuthenticatorAction.java code. Pasted here for your viewing pleasure:

package org.jboss.seam.example.booking;

import com.sun.identity.authentication.AuthContext;
import com.sun.identity.authentication.spi.AuthLoginException;
import java.io.IOException;
import java.util.List;
import static org.jboss.seam.ScopeType.EVENT;
import static org.jboss.seam.ScopeType.SESSION;
import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.TextOutputCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.core.FacesMessages;
import org.jboss.seam.log.Log;
import org.jboss.seam.security.Identity;

 \* @author warren
public class AuthenticatorAction implements Authenticator {
    @In Identity identity;
    @PersistenceContext EntityManager em;
    @Out(required=false, scope = SESSION)
    private User user;
    @Logger private Log log;
    private static final String orgName = "opensso";
    private static final String moduleName = "DataStore";
     \* @return
     \* @throws com.sun.identity.authentication.spi.AuthLoginException
    protected AuthContext getAuthContext() throws AuthLoginException {
        AuthContext lc = new AuthContext(orgName);
        AuthContext.IndexType indexType = AuthContext.IndexType.MODULE_INSTANCE;
        lc.login(indexType, moduleName);
        return lc;
     \* @param lc
     \* @return
     \* @throws UnsupportedCallbackException
    protected boolean login(AuthContext lc) throws UnsupportedCallbackException {
        boolean succeed = false;
        Callback[] callbacks = null;
        // get information requested from module
        while (lc.hasMoreRequirements()) {
            callbacks = lc.getRequirements();
            if (callbacks != null) {
        if (lc.getStatus() == AuthContext.Status.SUCCESS) {
            log.info("Login succeeded.");
            succeed = true;
        } else if (lc.getStatus() == AuthContext.Status.FAILED) {
            log.error("Login failed.");
        } else {
            log.error("Unknown status: " + lc.getStatus());

        return succeed;
    private void addLoginCallbackMessage(Callback[] callbacks)
            throws UnsupportedCallbackException {
        int i = 0;
        try {
            for (i = 0; i < callbacks.length; i++) {
                if (callbacks[i] instanceof TextOutputCallback) {
                } else if (callbacks[i] instanceof NameCallback) {
                } else if (callbacks[i] instanceof PasswordCallback) {
                } else
                    throw new UnsupportedCallbackException(callbacks[i]);
        } catch (IOException e) {
            log.error("Login Failed", e);
            throw new UnsupportedCallbackException(callbacks[i],e.getMessage());
    private void handleNameCallback(NameCallback nc) throws IOException {
    private void handlePasswordCallback(PasswordCallback pc) throws IOException {
        String passwd = identity.getPassword();
     \* @return true if the user was succesfully authenticated, false otherwise
    public boolean authenticate() {
        log.trace("Username=" + identity.getUsername() +
                " password=" + identity.getPassword());
        try     {
            User user = null;
            AuthContext lc = getAuthContext();
            log.debug("Got Authcontext=" + lc);
            if (login(lc)) {
                log.info("OpenSSO login for user #0 succeeded", identity.getUsername());
                if( ! fetchDBUser(identity.getUsername())) {
                    String msg = "OpenSSO login succeded, but there is no user with this name in the datbase";
                    return false;
                return true;
            return false;
        } catch (AuthLoginException ex) {
            log.error("AuthLogin problem", ex);
            return false;
        } catch(UnsupportedCallbackException ex2) {
            log.error("Callback problem", ex2);
            return false;
    private boolean fetchDBUser(String username) {
        List results = em.createQuery(
                "select u from User u where u.username=:username")
                .setParameter("username", username)
        if ( results.size()==0 ) {
            return false;
        } else {
            user = (User) results.get(0);
            return true;
    @Remove @Destroy
    public void destroy() {}

powered by performancing firefox

Monday Feb 12, 2007

Buildix - project in a box

Those crafty ThoughtWorkers stole my idea! 

Buildix  provides a turnkey vmware image with all the goodies to kick start a development project, including subversion, a project wiki, a bug tracker,  and Cruise Control. 

In my humble experience, many organizations lack a basic project support infrastructure - and you often spend a couple of weeks pulling this stuff together. Buildix is going to be a huge time saver.   This is exactly the sweet spot for virtualization.

[PS: I'd love to see this running as a Xen image on OpenSolaris!]

powered by performancing firefox

Monday Jul 17, 2006

Flash demo of JCAPS

I created a couple of flash demos of JCAPS 5.1 for a customer presentation. I thought I would share these demos here in case anyone is interested. Note these are not comprehensive (there are many features not covered), nor do I claim that they are action packed or thrilling (unless you like this sort of thing :-) )

The first flash recording is a demo of eDesigner using a fictional application called Duke's bank. It is approx 16 minutes in length.

The second is a demo of eManager and shows how to monitor the application (approx. 6 minutes).

Friday Jun 30, 2006

Testing Google Analytics

Just testing...

Friday Mar 31, 2006

Getting Groovy with SPML

Getting Groovy with SPML

For those of you interested in Groovy and Identity Manager (thousands of people, right?), here is an example of using Groovy with the OpenSPML toolkit to delete/create a batch of users (for example, you might use this to create a bunch of accounts for students in a lab).

// Sample Groovy script to delete/add a batch of users using the OpenSPML toolkit
// client handle to the spml connection is passed in to us from the caller

import org.openspml.message.\*;

numusers = 1 // number of test users to create 

println "Deleting Old Users..." 
for(i in 1..numusers) { deleteUser(i,false) }

println "Adding Users.." 
for(i in 1..numusers) { addUser(i) }

// Add a bulk user 
def addUser(i) {
    req = new AddRequest();
    req.setAttributes( ["password.password":"xyzzy", 
            "global.firstname": "Spml", 
            "waveset.roles":["Basic Employee"],
           "waveset.organization":"Top:Employees" ] );

// delete a user .
def deleteUser(i,throwErrors) {
    req = new DeleteRequest();
    response = client.request(req)
        client.throwErrors( res)

There is a small piece of Java scaffolding code (not shown) that creates the SPML connection, and then invokes the groovy script. If any one is interested in the code please feel free to get in touch.

Friday Jan 27, 2006

Methodology: Agile is for kids. Waterfall is back baby!

A friend sent me this link:

Waterfall 2006 international conference

Pretty funny stuff. I particularly like these seminars:

Pair Managing: Two Managers per Programmer by Jim Highsmith

User Interaction: It Was Hard to Build, It Should Be Hard to Use by Jeff Patton

FIT Testing In When You Can; Otherwise Skip It by Ward Cunningham

Oh, and for the Ruby crowd, we have:

Ruby On Snails: Slow Down Development With This New Framework by Dave Thomas and Mike Clark


Things that amuse me


« February 2017

No bookmarks in folder