ContentProvider で特定のアプリにのみ利用を許可する

ContentProvider の利用を制限したい事ってありますよね。あると思います。その方法について調べてみました。

Permission

この方法だと、同一署名からの許可のみしか行なえない。もっと柔軟に制限したい。

grantUriPermission

grantUriPermission を使えば、パッケージ名単位で Permission を切り替えることが出来る。

ただ、 grantUriPermission には大きな問題があって、 URI が完全一致しない場合にしか効力を発揮しない。
たとえば、 ContentProvider でアクセス出来うる URI が大量にあったり、 URI 内に id を持っていると、全ての URI に対して grantUriPermission を呼び出す必要がある。
また、許可を取り消したいときに使う revokeUriPermission ではパッケージ名を指定しないので、複数パッケージからの許可をしていた場合に、再登録する必要がある。
その辺を考慮した使い方が出来ればとても有用。

使い方

Manifest.xml の Provider タグで、 grantUriPermission 使うよ!と宣言します。

<provider android:name=".MyProvider" 
          android:authorities="com.example.sampleprovider.myprovider"
          android:grantUriPermissions="true" />

アプリ起動後、どこかのタイミングで特定のパッケージからのアクセスを許可する。
フラグは query なら READ 、 update/delete/insert なら READ と WRITE を指定します。

grantUriPermission("com.example.sampleresolver", 
                   Uri.parse("content://com.example.sampleprovider/myprovider/"), 
                   Intent.FLAG_GRANT_READ_URI_PERMISSION);

// 以後、 com.example.sampleresolver が content://com.example.sampleprovider/myprovider/ に
// アクセスした場合は読み込み (query) のみ許可される。

// 許可をやめたい時は revokeUriPermission を使う。
revokeUriPermission(Uri.parse("content://com.example.sampleprovider/myprovider/"), 
                    Intent.FLAG_GRANT_READ_URI_PERMISSION);

Binder.getCallingUri

ようは ContentProvider 呼び出し時に、パッケージ名を調べて許可されていないなら SecurityException を投げれば良いだけなんです。
Permission とか、 grantUriPermission とか、深く考えすぎていました。

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    enforcePermission(Binder.getCallingPid());
    // do something ...
}

private String getPackageNameFromPid(int pid) {
    ActivityManager am = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
    for (RunningAppProcessInfo info : am.getRunningAppProcesses()) {
        if (info.pid == pid) {
            return info.processName;
        }
    }
    
    return null;
}

private void enforcePermission(int pid) {
    String packageName = getPackageNameFromPid(pid);
    if (!packageName.equals("com.example.sampleresolver")) {
        throw new SecurityException();
    }
} 

これで OK

IPC の時は Binder.getCallingUid() とかを使って柔軟に接続を制限しよう。 Android の Permission なんて時代遅れだよ!!