「subversionのリポジトリブラウザとBTSとWikiが一緒になった」というtracを使ってみる。というか以前から使っていたのだが環境の問題かあちこちいじらないと動かなかったのでメモ。

当初はmod_pythonを用いようとしたのだが、Segmentation FaultまたはAbortでapacheが死んでしまった。以下の要求を満たす形で構築したい、ので試行錯誤した。

  • ブラウザだけ用意できればアクセスできるようにしたい。httpsでアクセスしたい。
  • 外から見た場合に、https://xxxx.exmple.com:2222/のようなポート番号指定のURLではなくhttps://xxxx.example.com/project/のように表現したい。→穴を開けるポートを増やしたくない。
    • ウチではサーバにlinuxを使っている。外部公開しているポイントの穴を増やすことも避けたいが、iptablesによる許可ポートもできるだけ増やしたくない。
  • 認証はapacheの機能で行いたい。
    • これは「セキュリティはインフラサイドで確保するもの」という自身のポリシーもあるけど、Active DirectoryでSubversionのリポジトリとtracの認証を共通化したかった+apacheの認証モジュールの多さと設定の柔軟さを活かしたいから、という理由
  • 外部からはhttpsで接続したい
    • プライベートなプロジェクトを管理したいから。
  • CGI型はかなりレスポンスが遅くなるので採用しない。
    • なのでmod_pythonを選択したいところだが、apacheが死んでしまう問題を解決できなかったのでスタンドアロン型を選んだ。
    • また、スタンドアロン型といっても管理するアクセスログが増えるのはイヤで、apacheのログとひっくるめて管理できるようにしたい。
で、
  • フロントエンドはapacheで、認証もここで行う。ついでにSSLも可に。
  • 内部でtracdを動かし、apache→tracd間はmod_proxyでリバースプロクシでつなぐ
という方向に決めた。
tracenv

導入

  • trac本家はここ(http://www.edgewall.com/trac/)だが、日本語化したものをインタアクト株式会社が公開しているのでありがたく使わせてもらう。
  • ClearSilver、SQLite、パイソンバインディングなどもろもろ用意せねばならんのだが、このあたりは既に詳しいページが多数あるので割愛。

設定

  • SSLの設定はあちこちに情報があるので割愛。
  • tracの設定もあちこちに情報があるので割愛。ただ、今回想定している環境は「外向きにはhttps」なので、デフォルトのままだとtracdが返す情報(特にLocationヘッダ)でhttpスキームを使われてしまうのでbase_urlを指定した。
    (projectenv)/conf/trac.ini

    [trac]
    base_url = https://外向け/trac
  • リバースプロクシは以下のような感じ。(Location内にリバースプロクシ設定を書いているので、パラメータが略されている点に注意)
    <Location /trac>
    ProxyPass http://tracdのIP:tracdのポート/trac
    ProxyPassReverse http://tracdのIP:tracdのポート/trac
    </Location>
  • 認証の設定もあちこちに情報があるので割愛。ふつーにtracのURL範囲を認証要に設定すればよい。BasicでもKerberosでも任意選択できる。
  • ここまででhttps://外向け/trac/へのアクセスがapacheにより認証されたうえで、内部で動作しているtracdにプロクシされるはず。ウチの環境の場合、内部は内部ネットワーク向けのDNSを動かしていて、外向けのFQDNと内部ホストのFQDNを同じにしているので(IPは異なる)、これが異なる場合はcookie関係の設定が必要かも。

tracユーザの識別問題

  • trac上のユーザ識別も必要だ。なぜならチケットの発行やマイルストーンの編集などの権限判断は「trac上のユーザ」で行われるから。したがってapacheで認証したユーザをなんとかしてtracdにも認識させたい。tracdで使う認証がBasicまたはDigestであれば、apacheとtracdの間で認証データベースを共有することで解決できる。しかし今回のように「BasicでもDigestでもなくKerberosが使いたい」「mod_proxyを介している」という条件が加わると、tracdの認証オプションだけは解決できないようだ。
  • で、「apacheで認証したリクエストだけがtracdに到達する」という状況から「tracdへのアクセスは認証済み」と考える。Authorizationフィールドからユーザ名を抜き出してtracユーザとして使用することにする。
  • 下のようなパッチをあてて、tracdのコマンドラインオプションと「認証しない認証クラス」を捏造する。
    --- trac-0.9.5-ja-1/scripts/tracd   2005-12-02 10:10:17.000000000 +0900
    +++ trac-0.9.5-ja-1kai/scripts/tracd    2006-06-04 20:38:59.000000000 +0900
    @@ -16,6 +16,7 @@
     # Author: Jonas Borgstr 

     from trac.web.standalone import BasicAuth, DigestAuth, TracHTTPServer
    +from trac.web.TrustedAuth import *

     import getopt
     import locale
    @@ -56,6 +57,7 @@
         try:
             opts, args = getopt.getopt(sys.argv[1:], "a:p:b:de:",
                                        ["auth=", "port=", "hostname=","daemonize",
    +                                   "trusted-auth=",
                                         "env-parent-dir=", "basic-auth="])
         except getopt.GetoptError, e:
             print e
    @@ -66,6 +68,8 @@
                 add_auth(auths, a, DigestAuth)
             if o == '--basic-auth':
                 add_auth(auths, a, BasicAuth)
    +        if o == '--trusted-auth':
    +            add_auth(auths, a, TrustedAuth)
             if o in ("-p", "--port"):
                 port = int(a)
             elif o in ("-b", "--hostname"):
    url: archives/2006/06/trac.html --- trac-0.9.5-ja-1/trac/web/TrustedAuth.py 1970-01-01 09:00:00.000000000 +0900
    +++ trac-0.9.5-ja-1kai/trac/web/TrustedAuth.py  2006-06-04 20:40:22.000000000 +0900
    @@ -0,0 +1,67 @@
    +# -*- coding: utf-8 -*-
    +#
    +# This is adhoc auth?-module under tracd.
    +#
    +# If you are using tracd and external proxy(ex. mod_proxy) with some auth
    +#   method, the access for tracd is already authorized.
    +#
    +# TrustedAuth regard the user as already trusted.
    +# The user extraced from Authorization field.
    +#
    +# Author: tckz<tckz@nifty.com>
    +#
    +
    +from trac import util, __version__
    +from trac.web.api import Request
    +from trac.web.cgi_frontend import TracFieldStorage
    +
    +import urllib2
    +
    +try:
    +    from base64 import b64decode
    +except ImportError:
    +    from base64 import decodestring as b64decode
    +
    +
    +class TrustedAuth:
    +    def __init__(self, dummy, realm):
    +        self.realm = realm
    +
    +    def send_auth_request(self, req):
    +        req.send_response(401)
    +        req.end_headers()
    +
    +    def parse_auth_header(self, authorization):
    +        values = {}
    +        for value in urllib2.parse_http_list(authorization):
    +            n, v = value.split('=', 1)
    +            if v[0] == '"' and v[-1] == '"':
    +                values[n] = v[1:-1]
    +            else:
    +                values[n] = v
    +        return values
    +
    +    def do_auth(self, req):
    +        if not 'Authorization' in req.headers:
    +            self.send_auth_request(req)
    +            return None
    +
    +        user = ""
    +        if req.headers['Authorization'].startswith('Basic'):
    +            auth = req.headers['Authorization'][len('Basic')+1:]
    +            auth = b64decode(auth).split(':')
    +            if len(auth) == 2:
    +                user, password = auth
    +        elif req.headers['Authorization'].startswith('Digest'):
    +            auth = self.parse_auth_header(req.headers['Authorization'][7:])
    +            if auth.has_key('username'):
    +                user = auth['username']
    +        else:
    +            self.send_auth_request(req)
    +
    +        if user == "":
    +            self.send_auth_request(req)
    +            return None
    +
    +        return user
    +
  • tracd起動時のコマンドラインは↓のような感じで。--trusted-authの引数にカンマ区切りの3要素を指定しなければならないのは、元々tracdのadd_authメソッドに手を入れていないから。「*」部分はtracプロジェクト名。「*」にすると全部のプロジェクトが対象となる(元々の仕様)
    /usr/bin/tracd --trusted-auth '*,,' -p ポート -b localhostとか -d -e parent-dir-of-envs
ちなみにCGI型の場合、apacheで認証したユーザすなわちREMOTE_USERがtracユーザとして扱われるようで特別なことはなにも必要ない。(だけど遅い、と)

python触るのは初めてなのでもっといい書き方を教えて欲しいところだが、あんましpython好きくないかも。「元々のソースに存在するブロックに対応して追加したelseブロックがautoindentでHTが使われたために対応するブロックとして認識されずsyntax error」というのは好きになれそうもない。でもgeekには人気があるっていうよね。