カテゴリー: テクノロジー

Solr 8.4.0 で追加された”Untrusted Configsets”の制限について

Solr 8.4.0 の Changes には以下の項目があります。

SOLR-14071: Untrusted configsets (ones that are uploaded via unsecured configset API) cannot use <lib> directive. Consider enabling authentication/authorization so that the uploaded configsets are trusted. Note: If you already have a collection using untrusted configset that uses directive, it will not load after upgrading to 8.4. You can re-upload your configset using “bin/solr zk -upconfig ..” or place your libraries in the classpath and restart Solr.

「Untrusted configsets は lib ディレクティブを使えません」とあります。
configsets が trusted として扱われるためには、認証有りでアップロードしなければならないとのことです。Solr 8.4.0 では _default の solrconfig.xml から lib を使う設定が無くなったのもこの変更に合わせてのようです。

Untrusted configsets では lib ディレクティブが使えないというのが具体的にどういう意味なのかを確認してみました。

まず Solr 8.4.0 が起動した状態で sample_techproducts_configs (このサンプル設定には 8.4.0 でも lib ディレクティブが含まれます)を configsets_test という名前でアップロードします。

$ (cd solr-8.4.0/server/solr/configsets/sample_techproducts_configs/conf && zip -r - *) > configsets_test.zip
$ curl -X POST --header "Content-Type:application/octet-stream" --data-binary @configsets_test.zip "http://localhost:8983/solr/admin/configs?action=UPLOAD&name=configsets_test"

アップロードした configsets_test を使ってコレクションを作成します。

curl -s "http://localhost:8983/solr/admin/collections?action=CREATE&name=configsets_test&numShards=1&replicationFactor=1&collection.configName=configsets_test&omitHeader=true"
{
  "failure":{
    "127.0.1.1:8983_solr":"org.apache.solr.client.solrj.impl.HttpSolrClient$RemoteSolrException:Error from server at http://127.0.1.1:8983/solr: Error CREATEing SolrCore 'configsets_test_shard1_replica_n1': Unable to create core [configsets_test_shard1_replica_n1] Caused by: The configset for this collection was uploaded without any authentication in place, and use of  is not available for collections with untrusted configsets. To use this component, re-upload the configset after enabling authentication and authorization."},
  "Operation create caused exception:":"org.apache.solr.common.SolrException:org.apache.solr.common.SolrException: Underlying core creation failed while creating collection: configsets_test",
  "exception":{
    "msg":"Underlying core creation failed while creating collection: configsets_test",
    "rspCode":400},
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"Underlying core creation failed while creating collection: configsets_test",
    "code":400}}

アップロードはできましたが、その configsets を使ってコレクションを作成することろでエラーになりました。

次に BASIC 認証を設定してからコレクション作成してみます。

$ ./solr-8.4.0/server/scripts/cloud-scripts/zkcli.sh -zkhost localhost:9983 -cmd put /security.json '{
"authentication":{
   "blockUnknown": true,
   "class":"solr.BasicAuthPlugin",
   "credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="}
 }
}'

ここで Solr を再起動して BASIC 認証が有効になった状態で再度コレクション作成。

$ curl --user solr:SolrRocks -s "http://localhost:8983/solr/admin/collections?action=CREATE&name=configsets_test&numShards=1&replicationFactor=1&collection.configName=configsets_test&omitHeader=true"
{
  "failure":{
    "127.0.1.1:8983_solr":"org.apache.solr.client.solrj.impl.HttpSolrClient$RemoteSolrException:Error from server at http://127.0.1.1:8983/solr: Error CREATEing SolrCore 'configsets_test_shard1_replica_n1': Unable to create core [configsets_test_shard1_replica_n1] Caused by: The configset for this collection was uploaded without any authentication in place, and use of  is not available for collections with untrusted configsets. To use this component, re-upload the configset after enabling authentication and authorization."},
  "Operation create caused exception:":"org.apache.solr.common.SolrException:org.apache.solr.common.SolrException: Underlying core creation failed while creating collection: configsets_test",
  "exception":{
    "msg":"Underlying core creation failed while creating collection: configsets_test",
    "rspCode":400},
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"Underlying core creation failed while creating collection: configsets_test",
    "code":400}}

やはりエラーになります。

confitsets を trusted として扱ってもらうために、認証有りの状態で再度アップロードします。UPLOAD コマンドでは上書きできないので、DELETE してから UPLOAD します。

$ curl --user solr:SolrRocks -X POST --header "Content-Type:application/octet-stream" --data-binary @configsets_test.zip "http://localhost:8983/solr/admin/configs?action=DELETE&name=configsets_test"
{
  "responseHeader":{
    "status":0,
    "QTime":220}}
$ curl --user solr:SolrRocks -X POST --header "Content-Type:application/octet-stream" --data-binary @configsets_test.zip "http://localhost:8983/solr/admin/configs?action=UPLOAD&name=configsets_test"
{
  "responseHeader":{
    "status":0,
    "QTime":292}}

認証有りでアップロードした configsets を使ってコレクションを作成してみます。

$ curl --user solr:SolrRocks -s "http://localhost:8983/solr/admin/collections?action=CREATE&name=configsets_test&numShards=1&replicationFactor=1&collection.configName=configsets_test&omitHeader=true"
{
  "success":{
    "127.0.1.1:8983_solr":{
      "responseHeader":{
        "status":0,
        "QTime":2126},
      "core":"configsets_test_shard1_replica_n1"}}}

今度はうまくいきました。

結論としては、「Untrusted configsets では lib ディレクティブが使えない」とは、libディレクティブを含む Untrusted configsets ではコレクションを生成できないという意味でした。

ところで、configsets アップロードする方法としては、Solr の Configsets API を使う方法とは別に zookeeper に直接アップロードする方法もあります。

$ ./solr-8.4.0/server/scripts/cloud-scripts/zkcli.sh -zkhost localhost:9983 -cmd upconfig -confdir solr-8.4.0/server/solr/configsets/sample_techproducts_configs/conf -confname configsets_test2
INFO  - 2020-02-23 22:52:50.804; org.apache.solr.common.cloud.ConnectionManager; Waiting for client to connect to ZooKeeper
INFO  - 2020-02-23 22:52:50.821; org.apache.solr.common.cloud.ConnectionManager; zkClient has connected
INFO  - 2020-02-23 22:52:50.821; org.apache.solr.common.cloud.ConnectionManager; Client is connected to ZooKeeper
$ curl --user solr:SolrRocks -s "http://localhost:8983/solr/admin/collections?action=CREATE&name=configsets_test2&numShards=1&replicationFactor=1&collection.configName=configsets_test2&omitHeader=true"
{
  "success":{
    "127.0.1.1:8983_solr":{
      "responseHeader":{
        "status":0,
        "QTime":1361},
      "core":"configsets_test2_shard1_replica_n1"}}}

なんとコレクション生成できてしまいました。

現時点では Untrusted Configsets の制限はまだ不十分なようで、今後の進展に注意する必要がありそうです。

SolrにBasic認証を設定する

Solrの管理画面やAPIへのアクセスに認証を掛けることができます。この記事ではBASIC認証を設定してみます。

SolrCloudの環境では Zookeeper の /security.json に設定ファイルを置きます。 Solrリファレンス にはユーザ:solr パスワード:SolrRocks で設定する例が載っています。

$ ./zkcli.sh -zkhost localhost:9983 -cmd put /security.json '{
"authentication":{
   "blockUnknown": true,
   "class":"solr.BasicAuthPlugin",
   "credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="}
 }
}'

この後Solrの再起動でBASIC認証が有効になり、管理画面にブラウザでアクセスすると、ユーザ名とパスワードの入力を促されます。ここで solr と SolrRocks で確かにログインできます。

このままリファレンスに載っているユーザとパスワードを使い続ける訳にはいかないので、別のユーザを追加することを考えます。初めは security.json に追加の設定を記載するのかと思いましたが、パスワードをSolrが期待する形式でハッシュ化する方法が分かりませんでした。結局のところ、リファレンスに書いてある通り、APIで設定変更するしかなさそうです。

ここで、APIへのHTTPアクセスにも認証が必要となります。

新しいユーザの追加

curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'Content-type:application/json' -d '{"set-user": {"solradmin":"solrpass"}}'

最初に設定した solr ユーザの削除

curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'Content-type:application/json' -d  '{"delete-user": ["solr"]}'

追加と削除の後の security.json の内容を確認

$ server/scripts/cloud-scripts/zkcli.sh -zkhost localhost:9983 -cmd get /security.json
{"authentication":{
    "blockUnknown":true,
    "class":"solr.BasicAuthPlugin",
    "credentials":{"solradmin":"AkZTG0uVSAgfzB/OC+kI1tqw9aeR8pP05tpNdcnnfN8= wRKTRwEcTX5eiCgEmCfIRXdRf9zIq2/Aef/LP6ZzdIQ="},
    "":{"v":13}}}

確かに変わっています。パスワードもハッシュ化されています。

管理画面だけではなく、コマンドラインツールも認証が必要になります。

bin/post -c mycollection data.json
POSTing file data4.json (application/json) to [base]/json/docs
SimplePostTool: WARNING: Solr returned an error #401 (require authentication) for url: http://localhost:8983/solr/mycollection/update/json/docs
SimplePostTool: FATAL: Looks like Solr is secured and would not let us in. Try with another user in '-u' parameter

オプションで指定するだけなので簡単。

bin/post -u solradmin:solrpass -c mycollection data.json

昔は認証が必要な場合は Jetty 側でなんとかしていたようですが、今は Solr 側で設定できるようになっています。Solr 8.4 からは認証が設定されていない場合にはAPI経由での設定ファイルのアップロード(というかアップロードする設定の内容)に制限が付くこともあるので、この辺の設定もきちんと押さえておきたいところです。

Apacheの前にNginxを置いた際にApache側で行う設定例

サーバーは一度設定するとなかなか次に設定することがなく、設定方法を忘れてしまいます。
今回はApacheが走っているサーバーの前段としてNginxを用意した場合にするであろう設定例です。
※ファイルの場所はUbuntuの例です。他のディストリビューションの場合はパスが変わってくるので適宜読み替えてください。

Apacheの設定

ポートを80から8080へ変更します。※例ではついでにIPv4のみにしています。
/etc/apache2/ports.conf

Listen 80
Listen 127.0.0.1:8080

Apacheのアクセスログに残すIPがリモートのIPになるようremoteipモジュールを設定します。
※何も設定しないと全てNginxの走っているサーバーのIPになってしまいます。

このモジュールの設定ファイルを作成します。
/etc/apache2/mods-available/remoteip.conf

RemoteIPInternalProxy 127.0.0.1
RemoteIPHeader        X-Forwarded-For
RemoteIPProxiesHeader X-Forwarded-By

モジュールと設定を有効化します。

$ sudo a2enmod remoteip

一般的にログフォーマットの設定では%hのほうがデフォルトで使われているのでこれを%aへ変更します。log_config

LogFormat "%v:%p %a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined

ここまでの設定を有効化します。

$ sudo apache2ctl restart

これでApache側でリモートのIPが取得できるようになります。

お疲れさまでした。

Laravelのエラーログに情報を追加する方法

ログの拡張

開発中であればエラーはそのまま表示されるので解決は比較的容易ですが、運用中に発生した場合はエラーログから調査するので少し時間がかかります。

エラーログにはデフォルトで発生時ログインしているuserIdが一緒に出力されエラーが発生したファイル名や行番号も出力されるので基本的にはそちらからエラーの調査は行えますが、特定のレコード時だけ発生したなどになるとそれだけでは時間がかかる時があります。

なので出力されるログの内容を拡張できないかフレームワークのソースを調べたところ

Illuminate/Foundation/Exceptions/Handler.php
にcontextと目的の関数があり

contextの関数をオーバーライドすればいい様子

以下実装例

app/Exceptions/Handler.php
/**
 * ログに記載する追加情報
 */
protected function context()
{
$context = parent::context(); try { return array_merge($context, [ // 一緒に記録したい情報 'uri' => env('REQUEST_URI'), ]); } catch (Throwable $e) { return $context; } }

以上!

SolrのUAX29URLEmailTokenizerを使う

Solrにはさまざまなトークナイザーが用意されており、その中に UAX29URLEmailTokenizer があります。
このトークナイザーは基本的には StandardTokenizer と同じ挙動なのですが、以下の要素をトークンとして切り出してくれるという特徴があります。

  • ドメイン名
  • URL(http(s)://, file://, ftp://)
  • Eメールアドレス
  • IPアドレス(IPv4とIPv6)

UAX29URLEmailTokenizer を使うフィールドタイプとフィールドを定義して動作を確認してみます。

<fieldType name="text_url" class="solr.TextField" positionIncrementGap="100">
  <analyzer>
    <tokenizer class="solr.UAX29URLEmailTokenizerFactory"/>
  </analyzer>
</fieldType>                                                                                                            
<field name="url_test" type="text_url" stored="true"/>

この設定で「http://www.example.com/example/です。」をトークナイズした結果は以下の通りです。

日本語の文書を扱う場合、通常の文章の部分が StandardTokenizer 相当でトークナイズされるといろいろ使い勝手が悪いので、たとえば形態素解析のトークナイザーと組み合わせることを考えます。

<fieldType name="url" class="solr.TextField" positionIncrementGap="100">
  <analyzer type="index">
    <tokenizer class="solr.UAX29URLEmailTokenizerFactory"/>
    <filter class="solr.TypeTokenFilterFactory" types="url_whitelist.txt" useWhitelist="true"/>
  </analyzer>
  <analyzer type="query">
    <tokenizer class="solr.KeywordTokenizerFactory"/>
  </analyzer>
</fieldType>

<field name="body" type="text_ja" stored="true"/>
<field name="url" type="url" stored="true"/>
<copyField source="body" dest="url"/>

UAX29URLEmailTokenizer の後ろに TypeTokenFilter を追加することで、必要なトークンタイプだけを残すことができます。TypeTokenFilter のホワイトリストとして url_whitelist.txt に以下の1行だけを記述して、URLだけを残すようにします。

<URL>

クエリ用のアナライザを別に定義したのは、URLのフィールドの検索では余計なトークナイズをさせずに文字列マッチ相当の動作にしたいからです。
urlフィールドをurlフィールドタイプとして定義し、text_jaフィールドタイプであるbodyフィールドのコピーフィールドにしています。

以下の文書を投入します。

[
    {"id":"1",
     "body":"昨日 http://blog.splout.co.jp/ をみた。"
    },
    {"id":"2",
     "body":"明後日は http://splout.co.jp/ をみる。"
    }
]

各フィールドにどういうトークンが納められたのかを確認します。

$ curl -s 'http://localhost:8983/solr/urlTokenizer/terms?terms.fl=url&terms.limit=-1&omitHeader=true&wt=json'
{
  "terms":{
    "url":[
      "http://blog.splout.co.jp/",1,
      "http://splout.co.jp/",1]}}

urlフィールドにはURLだけが納められています。

$ curl -s 'http://localhost:8983/solr/urlTokenizer/terms?terms.fl=body&terms.limit=-1&omitHeader=true&wt=json'
{
  "terms":{
    "body":[
      "co",2,
      "http",2,
      "jp",2,
      "splout",2,
      "みる",2,
      "blog",1,
      "明後日",1,
      "昨日",1]}}

bodyフィールドには通常の形態素解析結果が納められています。

この状態で検索してみます。まずは body フィールドを “http://splout.co.jp/” で。

$ curl -s 'http://localhost:8983/solr/urlTokenizer/select?q=body%3Ahttp%5C%3A%5C%2F%5C%2Fsplout.co.jp%5C%2F'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"body:http\\:\\/\\/splout.co.jp\\/"}},
  "response":{"numFound":2,"start":0,"docs":[
      {
        "id":"2",
        "body":"明後日は http://splout.co.jp/ をみる。",
        "url":"明後日は http://splout.co.jp/ をみる。",
        "_version_":1656715432355168256},
      {
        "id":"1",
        "body":"昨日 http://blog.splout.co.jp/ をみた。",
        "url":"昨日 http://blog.splout.co.jp/ をみた。",
        "_version_":1656715432353071104}]
  }}

“http”, “splout”, “co”, “jp” がバラバラにトークンとして扱われるので、”http://blog.splout.co.jp/” も “http://splout.co.jp/” も両方がヒットしています。
次に url フィールドを同じく “http://splout.co.jp/” で。

$ curl -s 'http://localhost:8983/solr/urlTokenizer/select?q=url%3Ahttp%5C%3A%5C%2F%5C%2Fsplout.co.jp%5C%2F'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"url:http\\:\\/\\/splout.co.jp\\/"}},
  "response":{"numFound":1,"start":0,"docs":[
      {
        "id":"2",
        "body":"明後日は http://splout.co.jp/ をみる。",
        "url":"明後日は http://splout.co.jp/ をみる。",
        "_version_":1656715432355168256}]
  }}

“http://splout.co.jp/” の方だけがヒットします。

urlフィールドの検索ではワイルドカードを使うこともできます。”http://*.splout.co.jp/” を指定して splout.co.jp のサブドメインだけを検索してみます。

$ curl -s 'http://localhost:8983/solr/urlTokenizer/select?q=url%3Ahttp%5C%3A%5C%2F%5C%2F*.splout.co.jp%5C%2F'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":1,
    "params":{
      "q":"url:http\\:\\/\\/*.splout.co.jp\\/"}},
  "response":{"numFound":1,"start":0,"docs":[
      {
        "id":"1",
        "body":"昨日 http://blog.splout.co.jp/ をみた。",
        "url":"昨日 http://blog.splout.co.jp/ をみた。",
        "_version_":1656715432353071104}]
  }}

文章に含まれるURLやメールアドレスの扱いは案外面倒なものです。
UAX29URLEmailTokenizer をうまく使えば、自前でURLやメールアドレスを抽出してstringフィールドに格納するよりもバグも少なく取り扱うことができるでしょう。