Android Studio で ネットワークドライブ上にプロジェクトを作成しようとするとエラーになる原因詳細と当面の回避方法

Android Studio 利用中に直面した問題についてメッチャ深追いしたのでその結果を載せておく。Samba 周りの話はその手の人には常識なのかもしれない。

# 問題

Windows 版 Android Studio (ver. 3.1) で Samba をマウントしたディレクトリ (Z:\AndroidStudioProjects 等のドライブレターを割り当てたパス) にプロジェクトを作成しようとすると、 Everyone に Writable 権限がない場合に失敗する

- そもそも ネットワークドライブにプロジェクトを作成するのは非推奨（だが仕方がない時もある!）
- メッセージ： Could not ensure the target project location exists and is accessible: (パス名) Please try to specify another path.

# 原因

- Android Studio はプロジェクトフォルダ作成直後に書き込み権限をチェックするが、Windows のネットワークフォルダの書き込み権限は正しくチェックできないことがままあり、書き込み権限無しと誤判定される
  - これは、リモートのユーザーID (SID) とローカルのユーザーID (SID) を単純比較しているために起こるように見える
  - ドメインユーザーでマウントしていたり、仮想マシンを複製した場合などで、ローカルとリモートの SID が一致する場合には正しく判定できる
  - Samba は素のままでは SID が idmap_tdb という方法で生成されるためうまくいかない。
    -  特に ``security = ads`` の場合は、 winbind をインストールし ``idmap_ad`` を有効にすれば、AD と一致した SID を返すようになる (後述)


# 修正方法: Samba ドライブのファイル所有者の SID を正しく設定する

Samba が報告する SID が AD の SID と一致しないために起こっている場合、Samba 側に winbind を入れて ``idmap_ad`` を有効にすればいけた。

winbind を入れたあとは ``smbd``, ``nmbd`` を再起動し、 ``net cache flush`` してキャッシュをフラッシュすること。

以下は自分の環境でうまくいったもの。

- AD の DC (Windows Server 2016) をセットアップ
  - 「ユーザーとコンピューター」「表示」「拡張機能」を有効に 
  - ユーザーのプロパティの「属性エディター」タブでユーザーの LDAP 属性を編集できるようになるので、 uidNumber など UNIX 属性を追加しておく
    - ``uidNumber, loginShell, unixHomeDirectory, primaryGroupID``
  - 参考: [Missing Unix Attributes tab in ADUC on Windows 10 and Windows Server 2016](https://wiki.samba.org/index.php/Installing_RSAT#Missing_Unix_Attributes_tab_in_ADUC_on_Windows_10_and_Windows_Server_2016)
- Linux サーバー側 (ubuntu) [参考 Setting up Samba as a Domain Member](https://wiki.samba.org/index.php/Setting_up_Samba_as_a_Domain_Member)
  - Kerberos クライアントの設定 (後述 krb5.conf)
  - sssd の設定
  - smb.conf を書く
    - DC に ubuntu を登録 ``net ads join -U administrator``
    - smbd, nmbd, winbind を起動


## /etc/krb5.conf
- 参考: [Kerberos Client Configuration (Ubuntu Server guide)](https://help.ubuntu.com/community/Kerberos#Client_Configuration)

```conf
[logging]
    default = FILE:/var/log/krb5.log

[libdefaults]
    default_realm = EARL.TEA.LOCALDOMAIN
    kdc_timesync = 1
    ccache_type = 4
    forwardable = true
    proxiable = true

[realms]
    EARL.TEA.LOCALDOMAIN = {
        kdc = win-n74eqau7up2.earl.tea.localdomain
        admin_server = win-n74eqau7up2.earl.tea.localdomain
        default_domain = EARL.TEA.LOCALDOMAIN
    }

[domain_realm]
    .earl.tea.localdomain = EARL.TEA.LOCALDOMAIN
    earl.tea.localdomain = EARL.TEA.LOCALDOMAIN
```

## /etc/sssd/sssd.conf

- ``ldap_id_mapping = false`` にすれば LDAP 側の uidNumber が ローカルの uid にマップされるようになる。
- 設定を変えた場合は ``sss_cache -E`` でキャッシュをクリアする
- 参考: [Integrating with a Windows server using the AD provider (公式)](https://docs.pagure.org/SSSD.sssd/users/ad_provider.html)
- 参考: [SSSD and Active Directory (Ubuntu Server guide)](https://help.ubuntu.com/lts/serverguide/sssd-ad.html)

```conf
[sssd]
services = nss, pam
config_file_version = 2
domains = EARL.TEA.LOCALDOMAIN

[domain/EARL.TEA.LOCALDOMAIN]

id_provider = ad
access_provider = ad

ldap_id_mapping = false
```

## /etc/nsswitch.conf

sssd を入れたら勝手に書き換わっていた。

```
passwd:         compat sss
group:          compat sss
shadow:         compat sss
gshadow:        files

hosts:          files dns
networks:       files

protocols:      db files
services:       db files sss
ethers:         db files
rpc:            db files

netgroup:       nis sss
sudoers:        files sss
```

## /etc/samba/smb.conf

- 再起動: ``sudo systemctl restart smbd.service nmbd.service winbind.service``
- 参考: [smb.conf manpage](https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html)
- 参考: [Idmap config ad (Samba wiki)](https://wiki.samba.org/index.php/Idmap_config_ad)
- 参考: [Setting up Samba as a Domain Member](https://wiki.samba.org/index.php/Setting_up_Samba_as_a_Domain_Member)

```conf
[global]

workgroup = EARL
client signing = yes
client use spnego = yes
kerberos method = secrets and keytab
realm = EARL.TEA.LOCALDOMAIN
security = ads

dos charset = cp932
unix charset = utf-8

log file = /var/log/samba/log.%m
ldap ssl = no

idmap config EARL:backend = ad
idmap config EARL:schema_mode = rfc2307
idmap config EARL:range = 1001-20000

[homes]
comment = Home Directories
browseable = Yes
read only = No
```



# 回避方法: パーミッションを world writeble に

事前に空のフォルダを作成し、 Everyone に対して書き込み権限を与える。
1. Samba ネットワークドライブを公開している Unix 側 では ``chmod 777 パス`` で良い
2. マウントしている Windows 側 ではフォルダのプロパティで変更するか、次のようにする
- ``icacls パス /grant everyone:(F)`` (フォルダに対するフルコントロール)
- ``icacls パス /grant everyone:(IO)(OI)(CI)(F)`` (フルコントロールを子孫フォルダーと子孫ファイルでも継承させる)
  - これを半自動化するスクリプトを後に示す

## スクリプト化

エンドユーザーにコマンドラインを叩かせるのは運営上厄介なので次のようなスクリプトを作った。

```powershell

# Z:\AndroidStudioProjects\ 以下に プロジェクトフォルダを作成するスクリプト (2018/4)
# Android Studio が Z:\ にプロジェクトを作成するには Everyone に書き込み権限がないとうまくいかないため.
# （プロジェクト作成ウィザードの Finish ボタンをクリックした時にエラー）

$WORKSPACE_DIR = "Z:\AndroidStudioProjects"

function Read-InputBoxDialog([string]$Message, [string]$WindowTitle, [string]$DefaultText)
{
    Add-Type -AssemblyName Microsoft.VisualBasic
    return [Microsoft.VisualBasic.Interaction]::InputBox($Message, $WindowTitle, $DefaultText)
}

function MakeDirAndGrantEveryone([string]$path)
{
  Z:
  New-Item $path -itemType Directory -Force
  icacls $path /grant "everyone:(IO)(OI)(CI)(F)"
  icacls $path /grant "everyone:(F)"
}

function msg([string] $text)
{
  Write-Host $text -ForegroundColor Cyan
}

Z:

New-Item $WORKSPACE_DIR -itemType Directory -Force


$newProjectName = Read-InputBoxDialog -Message "新しいプロジェクトの名前を入力してください (スペース不可)。"
if( $newProjectName -ne "" ) { 
  $path = Join-Path $WORKSPACE_DIR $newProjectName
  MakeDirAndGrantEveryone( $path )

  echo ""
  echo "******************************************************************************"
  echo "*"
  echo "*   Android Studio を起動し、"
  echo "*"
  msg  "*      $path"
  echo "*"
  echo "*  にプロジェクトを作成してください。"
  echo "*"
  echo "******************************************************************************"
  echo ""
  pause
}
```


# ソースを追った、より細かい話

1. Android Studio はプロジェクトの作成直後に Java の [Files.isWritable](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html) を呼んでフォルダへの書き込み権限をチェックする。  [NewProjectModel.java:225](https://android.googlesource.com/platform/tools/adt/idea/+/633bb97e754d2ff4138501a977586a070cd89e0e/android/src/com/android/tools/idea/npw/project/NewProjectModel.java#225), [FileSystemFileOp.java:174](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/repository/src/main/java/com/android/repository/io/impl/FileSystemFileOp.java)
2. Samba のネットワークドライブに対して Files.isWritable は 書き込み権限があっても false を返す [参考 OpenJDK issue](https://bugs.openjdk.java.net/browse/JDK-8154915)
3. Java の Files.isWritable は Windows API の [AccessCheck](https://msdn.microsoft.com/en-us/library/windows/desktop/aa374815(v=vs.85).aspx) 関数を呼ぶ ([sun.nio.fs.WindowsFileSystemProvider.java](http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/af0e709d28f9/src/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java#l344), [sun.nio.fs.WindowsSecurity.java](http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/af0e709d28f9/src/windows/classes/sun/nio/fs/WindowsSecurity.java#l141)) が、これも false を返すようだ
  - Android Studio 同梱の Open JDK (JRE) 8 と、最新版の Oracle Java で動作確認

## 各ソフトウェア提供元などの議論

- Oracle, OpenJDK: 
  - OpenJDK にバグ [JDK-8154915](https://bugs.openjdk.java.net/browse/JDK-8154915) がポストされているが no fix でクローズされそうな見込み 
  - Stackoverflow に [Files.isWritable の議論とその回答](https://stackoverflow.com/questions/34661211/is-java-fully-compatible-when-checking-permissions-on-a-samba-mount) がある
- Samba: 2003 年に [それらしい投稿](https://lists.samba.org/archive/samba-technical/2003-August/031368.html) を発見したがリプライなし
- Google: それらしい議論なし
- Microsoft: AuthzAccessCheck を使うサンプルコードが [GetEffectiveRightsFromAcl function](https://msdn.microsoft.com/en-us/library/windows/desktop/aa446637(v=vs.85).aspx) のページに掲載されている。 AuthzAccessCheck は サーバーから取得したファイルの security descriptor の各 ACE (権限リスト) を、 現在のユーザー (を表す AUTHZ_CLIENT_CONTEXT_HANDLE) でチェックするだけ。ドメインにログオンしている状態でこれがうまくいっていないということは、 Samba が渡してくる ACE の SID が現在のユーザーの SID と合致していない可能性がある？ → そうだった。
  - NON-solution 1: [How to check Access Rights - Ruminations](http://blog.aaronballman.com/2011/08/how-to-check-access-rights/)
  - NON-solution 2: [The Windows Access Control Model: Part 2](https://www.codeproject.com/Articles/10200/The-Windows-Access-Control-Model-Part)