Миграция пользователей Atlassian Jira на LDAP

Date December 26th, 2013 Author Vitaly Agapov

Это будет самая замечательная миграция! Я покажу вам свои любимые забегаловки… А знаете, я поменяю цвет, когда подсохнет грибок на моей шкурке!..

м/ф «Ледниковый период»

migrationМиграция пользователей, хранящихся в локальной директории в Jira на LDAP-сервер (AD либо OpenLDAP) не вызовет никаких трудностей, если логины этих пользователей совпадают в Jira и на LDAP-сервере. В этом случае надо просто настроить новую User Directory в Jira и поменять ей приоритет на самый высокий, чтобы аутентификация сперва проходила через неё. Если же есть пользователи, у которых в Jira один логин, а в том же Active Directory – другой, то тут вылезет ряд проблем. В основном эти проблемы будут связаны с тем, что LDAP-аккаунт Джирой будет восприниматься как совершенно отдельный пользователь, а все задачи, комментарии и история останутся привязанными к старому пользователю. Посмотрим, как решить эту проблему.

Подготовка маппинга логинов пользователей

Для начала нужно получить соответсвие всех старых логинов в Jira всем новых логинам в LDAP. Если пользователей два, пять или даже десять, то это можно сделать и вручную. Если же их гораздо больше, то поможет набросанный мною Perl-скрипт create-user-map.pl, который выбирает из БД Джиры всех пользователей и по их электронной почте производит поиск по LDAP (в моём случае – в AD) соответствующего пользователя. Потом всё это он выплёвывает в виде хэша, который пригодится непосредственно для миграции, хэша, который пригодится для оповещений по почте, списка пользователей, у которых логин не меняется и списка пользователей Jira, для которых в AD не найдено соответствие.

#!/usr/bin/perl

use Net::LDAP;
use DBI;
use Data::Dumper;

glob $dbName = "jiraDB";
glob $dbHost = "127.0.0.1";
glob $dbUser = "jiraUser";
glob $dbPass = "jiraPass";

glob $ADUrl = "x.x.x.x";
glob $ADBaseDN = "OU=Users,DC=example,DC=com";
glob $ADDNattr = "sAMAccountName";
glob $ADBindDN = "CN=address book,OU=Users,DC=example,DC=com";
glob $ADBindPWD = "password";

my %map;
my %BCmails;

glob $dbh = DBI->connect("DBI:mysql:database=$dbName;host=$dbHost",$dbUser,$dbPass,{'RaiseError' => 1});
        $dbh->do("SET NAMES utf8");
        $dbh->do("SET CHARACTER SET utf8");
        $dbh->do("SET character_set_connection=utf8");

my $sql = "SELECT user_name, CONCAT(first_name, ' ', last_name) AS name, email_address FROM cwd_user WHERE directory_id=1 AND active=1";
my $sth = $dbh->prepare( $sql );
$sth->execute;
while (my $ref = $sth->fetchrow_hashref()) {
	my $mail = $ref->{email_address};
	$map{$mail}->{jiraLogin} = $ref->{user_name};	
	$map{$mail}->{jiraName} = $ref->{name};
	my $entry=searchADbyMail($mail);	
	if ($entry) {
		$map{$mail}->{ADDN}=$entry->dn;
		$map{$mail}->{ADLogin}=$entry->get_value($ADDNattr);
		$map{$mail}->{ADName}=$entry->get_value('name');
		#$entry->dump;
	}
}
$sth->finish;
$dbh->disconnect();

print "Mapped accounts with different logins\n";
print "glob %map = (\n";
foreach my $mail ( sort keys %map ) {
	if ( $map{$mail}->{ADLogin} && $map{$mail}->{jiraLogin} ne $map{$mail}->{ADLogin} ) {
		print "    '".$map{$mail}->{jiraLogin}."' => '".$map{$mail}->{ADLogin}."',\n";
		$BCmails{$mail} = $map{$mail}->{ADLogin};
	}
}
print ");\n";

print "\nMapped accounts with same logins\n";
foreach my $mail ( sort keys %map ) {
        if ( $map{$mail}->{jiraLogin} eq $map{$mail}->{ADLogin} ) {
                print $map{$mail}->{jiraLogin}.' '.$mail."\n";
		$BCmails{$mail} = $map{$mail}->{ADLogin};
        }
}

print "\nNot mapped accounts\n";
foreach my $mail ( sort keys %map ) {
        unless ( $map{$mail}->{ADLogin} ) {
                print $map{$mail}->{jiraName}.' '.$mail."\n";
        }
}

print "\nMails for broadcast\n";
print "glob %mails = (\n";
foreach my $mail ( sort keys %BCmails ) {
	print "    '".$mail."' => '".$BCmails{$mail}."',\n";
}
print ");\n";

exit(0);

sub searchADbyMail {
        my $mail = shift;
        my $ldap = Net::LDAP->new( $ADUrl );
        unless ($ldap) { print "$@. Could not connect to AD"; return; }
        my $mesg = $ldap->bind($ADBindDN, password => $ADBindPWD, version => 3 );
        $mesg->{resultCode} && return undef;
        $mesg = $ldap->search(
            base   => $ADBaseDN,
            attrs => [$ADDNattr, "name"],
            filter => "mail=".$mail,
        );
        $mesg->code && return undef;
        my $entry = $mesg->shift_entry;
        $ldap->unbind;
	return $entry;
}

Само собой в скрипте надо вставить свои параметры БД и LDAP.

Рассылка оповещений

Пользователям Jira, которых коснётся миграция, было бы неплохо послать письма с информацией о смене способа аутентификации. И ещё лучше при этом сообщить, какой же новый логин будет использоваться дальше.

Для этого надо хэш %mails из результата работы предыдущего скрипта вида:

glob %mails = (
    'user1@example.com' => 'user1',
    # .......
    'user2@example.com' => 'user2',
);

Поместить в файл config.pl и запустить такой вот Perl-скрипт send-broadcast.pl:

#!/usr/bin/perl
use MIME::Lite;

require "config.pl";

$text = qq ~
Добрый день,
<br/><br/>
С утра xxxx.xx.xx изменится принцип авторизации в Jira. Теперь для входа необходимо будет использовать логин и пароль из доменной учётной записи. 
<br/><br/>
Ваш доменный логин - %USERLOGIN%
<br/><br/>
Все задачи, статьи, комментарии, дашборды и прочие элементы останутся доступны и будут привязаны к новым учётным записям. Старые аккаунты будут удалены.
<br/>
---<br/>
С уважением,<br/>
Подпись
~;

foreach my $mail ( sort keys %mails) {
	print $mail."\n";
	my $data = $text;
	$data =~ s/%USERLOGIN%/$mails{$mail}/;
	my $msg = MIME::Lite->new(
		From    => 'noreply@example.com',
		To      => $mail,
		Subject => 'Новая схема авторизации в Jira',
		Data    => $data,
        );
        #$msg->attr('content-type.charset' => 'UTF-8');
	$msg->attr("Content-type" => "text/html; charset=UTF-8");
        $msg->send('smtp', 'x.x.x.x');
}

Миграция

Допустим, новая user directory в Jira уже настроена и работает. Тогда останавливаем Джиру, делаем бэкап её базы данных и запускаем следующий Perl-скрипт. Только перед началом в config.pl добавляем ещё и хэш %map, полученный от запуска первого скрипта или просто заполненный вручную. В общем выглядеть config.pl должен примерно так:

glob $dbName = "jiraDB";
glob $dbHost = "127.0.0.1";
glob $dbUser = "jiraUser";
glob $dbPass = "jiraPass";
 
glob %map = (
    'jirauser1' => 'ldapuser1',
    # .......
    'jirauser2' => 'ldapuser2',
);

А вот самый главный скрипт migrate-jira-to-ad.pl, непосредственно производящий миграцию. Он проглядывает все таблицы (кроме тех, которые начинаются на cwd_, то есть содержат учётки и группы) и во всех полях varchar или text меняет старые логины на новые – в Jira привязка элементов к пользователю сделана не по id, а по юзернейму.

#!/usr/bin/perl
 
use DBI;
use Data::Dumper;
 
require "config.pl";
 
glob $dbh = DBI->connect("DBI:mysql:database=$dbName;host=$dbHost",$dbUser,$dbPass,{'RaiseError' => 1});
        $dbh->do("SET NAMES utf8");
        $dbh->do("SET CHARACTER SET utf8");
        $dbh->do("SET character_set_connection=utf8");
 
my $tables = $dbh->selectall_arrayref("SHOW TABLES");
 
foreach ( @{$tables} ) {
    my $table = $_->[0];
    next if ($table =~ /^cwd_/);
 
    my $sth = $dbh->prepare( "DESCRIBE $table" );
    $sth->execute;
    while (my $ref = $sth->fetchrow_hashref()) {
        my $col = $ref->{Field};
        my $type = $ref->{Type};
        next unless ($type =~ /^varchar/ || $type =~ /text/);
        foreach my $username ( keys %map ) {
            my $sql = "SELECT COUNT(*) FROM $table WHERE `$col` = '$username'";
            my $sth2 = $dbh->prepare($sql);
            $sth2->execute;
            my $count = $sth2->fetchrow();
            if ($count) {
                print "Table ".$table."\n";
                print " $col - $type   :$count\n";
                $sql = "UPDATE $table SET `$col` = '$map{$username}' WHERE `$col` = '$username'";
                print $sql."\n\n";
                eval { $dbh->do($sql); }; warn $@ if $@;
            }
            $sth2->finish;
        }
    }
    $sth->finish;
}
$dbh->disconnect();

Запускаем Jira и производим реиндексацию из админки. Останется только назначить новым пользователям старые группы, а старых пользователей деактивировать или удалить. Если есть пользователи, у которых логины в разных User Directories совпадают, то, как уже говорилось, надо поставить повысить LDAP’у приоритет. Только сначала нужно запомнить, в каких группах состояли старые аккаунты, так как при назначении групп новым аккаунтам будет не с чем сверяться.

Другие продукты Atlassian

Для других продуктов, независимо от того, используют они для аутентификации Джиру или будут ходить напрямую в LDAP-сервер, тоже придётся провернуть кое какие действия.

Для миграции статей и комментариев в Atlassian Confluence можно воспользоваться тем же самым скриптом migrate-jira-to-ad.pl. Нужно будет только в config.pl поменять параметры подключения к базе на базу Confluence. Работает, проверено.

В Atlassian Stash всё чуть сложнее. Там привязка прав на репозитории завязана на id аккаунтов, а не юзернеймы. Поэтому мой скрипт не подойдёт. Можно написать другой скрипт, а можно поменять все права вручную. Также надо у старых аккаунтов удалить SSH-ключи, иначе эти же ключи нельзя будет привязать к новым аккаунтам. Ещё можно в базе передать личные репозитории новым аккаунтам. Но это простая задача, на ней можно не останавливаться.

В Bamboo всё наоборот проще. Там надо обеспечить только нужные права новым аккаунтам. Если они рулятся группами в Jira, то можно сказать, что и делать ничего не надо – всё уже сделано.

 

Tags: , ,
Category: Perl | 2 Comments »

Comments

2 комментариев на “Миграция пользователей Atlassian Jira на LDAP”

  1. Михаил

    А ты пробовал сам запускать его?

  2. Vitaly Agapov

    А как я по-твоему у нас миграцию проводил?
     

Leave a comment

 Comment Form