Wednesday, 15 December 2010

Subversion and Gnome Keyring

The problem:

You want to run an svn command in a cron task as a user that is already logged in and authenticated against a running gnome-keyring-daemon and the svn repository in question, but DBus prevents a user acquiring the privilege to access his own daemon without an associated x-session-manager.

The solution:

Attach to the Gnome session artificially, in order to be granted access to the gnome-keyring-daemon through DBus.

Details:

So how do you do that?

Well, there are three things required here.  Firstly, the DBus session bus address.  This will be something along the lines of:

unix:abstract=/tmp/dbus-abcdefghijk,guid=1234567890abcdef09878654321

It essentially enables applications that use DBus, to actually use it.  It's the flag that notifies the applications that DBus is available.  However, this alone will not solve the problem, since this will just allow the authentication request to take place but not actually be authenticated.  So, svn will see the environment variable and request authentication through DBus to the authentication agent (gnome-keyring), but there is nothing to tell DBus what the authentication agent is and where it is.  Next step...

Get the authentication agent.

This will be the gnome-keyring daemon pretending to be an ssh-agent, since it assumes the responsibility of the SSH agent when users use the gnome-keyring-manager, authentication for SSH keys is done through the gnome-keyring-daemon SSH authentication socket.  So how do you attach to this?

The Auth socket lives in the tmp directory, but it's no use hunting for it, since there could be lots of dead instances or instances owned by other users.  The easiest way is to hijack your own x-session-manager's environment and politely steel the socket and PID from the environment.  Let's see how we do that...

$ export pid=$(ps -C x-session-manager -o pid --no-heading)
$ cat /proc/${pid//[^0-9]/}/environ | sed 's/\x00/\n/g' | grep SSH

This will give you the path to the socket and the PID of the SSH agent in use by the x-session-manager; the one you want to pretend launched your shell.  The best way to be doing this however, is from one of the getty terminals that aren't running within your x-session, or by locally ssh'ing onto your machine to detach yourself from your x-session.  This way, you can be sure it is all working.

So is that it?  Not quite, keep reading...

So, you have the DBus address, the agent socket and PID, what more could you possibly need?  Well, anything X related must be authenticated against the X server, otherwise all authentication through DBus and essentially the gnome-keyring-daemon, will fail due to X authentication issues.  So finally, we must hijack our own X session, by associating ourselves with our own X authentication cookie.  This is in the form of some UUID.  This simplest way to obtain it is exactly the same as obtaining the SSH agent information.  You politely ask the kernel for it:


$ export pid=$(ps -C x-session-manager -o pid --no-heading)
$ cat /proc/${pid//[^0-9]/}/environ | sed 's/\x00/\n/g' | grep XDG_SESSION_COOKIE

So with this arsenal of environment variables, you can effectively mimic a process created by the x-session-manager, and start having friendly conversations with the x-session-manager and gnome-keyring-daemon.  However, it's all a bit dirty at the moment, so we can clean it up quite easily.  Create a file to include in your .bashrc file.  This will ensure that any processes created by "you" will attempt to associate themselves with an x-session.  I always opt for something like .bash_functions:

#!/bin/bash

################################################################################
#
# Attaches the current BASH session to a GNOME keyring daemon
#
# Returns 0 on success 1 on failure.
#
function gnome-keyring-attach() {
    local -a vars=( \
        DBUS_SESSION_BUS_ADDRESS \
        SSH_AUTH_SOCK \
        SSH_AGENT_PID \
        XDG_SESSION_COOKIE \
    )
    local pid=$(ps -C x-session-manager -o pid --no-heading)
    eval "unset ${vars[@]}; $(printf "export %s;" $(sed 's/\x00/\n/g' /proc/${pid//[^0-9]/}/environ | grep $(printf -- "-e ^%s= " "${vars[@]}")) )"
}



The reason it is a function, is because calling a script would run as a child process, so setting anything up in the environment there will have no affect on the calling environment.  Instead, calling a bash function allows the function to modify the calling environment.  You could of course write a function that prints the shell environment settings to the screen, where they can be imported into the current environment, but I find this is tidier.  Alternative method:

eval "$(gnome-keyring-attach)"

All you need to do now, is invoke this function when required.  Either in your cron task or in every session if you so wish to grant yourself access to your X session remotely, for example.

3 comments:

  1. Hi,

    I'm wanting to do something similar to what you've done and allow offlineimap to access the gnome-keyring from a cron-job. This is by far the closest thing I've found to accomplishing this.

    I've created the .bash_functions file and sourced it into my ~/.bashrc file. I can successfully use the function from the command line as well.

    The trouble I'm having is figuring out how to incorporate this into cron, however. Would you mind showing how you use the eval "$(gnome-keyring-attach)" script either directly via cron or in the script you call from cron? Also, have I added that function correctly?

    Thanks.

    ReplyDelete
  2. @Jason: cron probably doesn't load ~/.bashrc, would be my guess. I have a similar script that I put into my ~/bin directory, and I use it from cron with eval "$HOME/bin/re-ssh-agent" && rsync.... You could convert the above function to a plain script and use it that way.

    ReplyDelete
  3. @Jason: as @sfink suggested, .bashrc is not imported by cron, purely because cron does not execute its commands as login shells. The easiest way to ensure your scripts acquire access to the user's login environment is to dot in the functions file. $HOME is usually set, but if not, you may find specifying the path more reliable:

    Your cron script:

    #!/bin/bash

    . /home/Jason/.bash_functions

    gnome-keyring-attach || {
    echo >&2 Failed to attach to gnome-keyring daemon
    exit 1
    }

    # The rest of your script.

    That is pretty much the format of all my cron scripts.

    ReplyDelete