はじめに
Windows 上のプリザンターから、別の Windows サーバーにある共有フォルダへ
サーバスクリプトの $ps.file でフォルダを作成できるのかを整理します。
先に結論を書くと、Windows 版の標準的な構成では、$ps.file からネットワーク共有へ
直接作成することはできません。
ネットワークドライブがサービスから見えないだけでなく、UNC パスも
$ps.file のパス検証で明確に拒否されます。そのため、プリザンターからアクセスできる
ローカルフォルダへ依頼を出し、別プロセスで共有先へ反映する構成などが必要です。
Linux では、プリザンターの許可範囲内のディレクトリへ SMB や NFS を OS 側で
マウントする方法があります。ただし、これは $ps.file のネットワーク共有機能ではなく、
OS のファイルシステムとして見せる構成です。
ネットワークドライブが見えない理由
エクスプローラーで割り当てた Z: などのネットワークドライブは、
割り当てを行ったユーザーのログオンセッションに属します。
一方、IIS のアプリケーションプールや Windows サービスとして動くプリザンターは、
通常、対話ログオン中のユーザーとは別のアカウント、別のセッションで動作します。
そのため、管理者がエクスプローラーで見えている Z: を Script.json に設定しても、
プリザンターのプロセスからは見えないことがあります。
この問題はプリザンター固有ではなく、Windows サービスからマップ済みドライブを
参照するときに起きる一般的な問題です。
UNC パスを使っても回避できない
ネットワークドライブが見えない問題だけであれば、一般的な Windows アプリケーションでは
\\fileserver\share 形式の UNC パスへ置き換える方法が候補になります。
しかし、$ps.file ではこの方法も使用できません。
ServerScriptFilePath にも UNC パスは指定できない
$ps.file は Script.json の ServerScriptFilePath をルートとして動作しますが、
この設定へ UNC パスを書けばよいわけではありません。
次のような設定は使用できません。
{
"ServerScriptFileSizeMax": 1,
"ServerScriptFilePath": "\\\\file01\\pleasanter-data"
}
実装では、ServerScriptFilePath をプリザンターのカレントディレクトリと結合して
正規化したうえで、許可されたローカルの範囲内にあるかを検証しています。
UNC パスはこの範囲外になるため、basepath の不正なパスとして拒否されます。
実際の NormalizePath の該当部分を見てみます。
private string NormalizePath(
string section,
string path)
{
var rootPath = Parameters.Script.ServerScriptFilePath;
if (rootPath == null) throw new ArgumentException(Messages.InvalidPath(Context, "basepath").Text);
var rootPath2 = Path.GetFullPath(Path.Combine(Environments.CurrentDirectoryPath, rootPath));
if (!rootPath2.StartsWith(Environments.CurrentDirectoryPath))
{
throw new ArgumentException(Messages.InvalidPath(Context, "basepath").Text);
}
var regexSection = new Regex(@"^[0-9a-zA-Z_\.\-]*$");
if (!regexSection.IsMatch(section)) throw new ArgumentException(Messages.InvalidPath(Context, "section").Text);
var regexPath = new Regex(@"^[0-9a-zA-Z_\.\-/]*$");
if (path != null && !regexPath.IsMatch(path)) throw new ArgumentException(Messages.InvalidPath(Context, "path").Text);
var sectionPath = Path.Combine(rootPath2, section);
var fullPath = Path.GetFullPath(Path.Combine(sectionPath, path?.Replace('/', Path.DirectorySeparatorChar) ?? ""));
if (!fullPath.StartsWith(sectionPath)) throw new ArgumentException(Messages.InvalidPath(Context, "path").Text);
return fullPath;
}
処理を順番に追うと、拒否される理由が分かります。
-
Path.CombineでカレントディレクトリとServerScriptFilePathを結合する -
Path.GetFullPathで絶対パスへ正規化する - 正規化後のパスが
Environments.CurrentDirectoryPathから始まるか確認する - 範囲外なら
basepathの不正なパスとして例外にする
.NET の Path.Combine は、後ろの引数がルートを持つパスなら、それより前のパスを
結合結果に残しません。したがって \\file01\pleasanter-data や Z:\share を指定すると、
rootPath2 はプリザンターのカレントディレクトリ配下にならず、3 番目の検査で拒否されます。
さらに、regexSection と regexPath が入力できる文字を制限しています。
Windows の UNC パスに必要な \ とドライブ指定に必要な : は path で許可されていません。
最後の fullPath.StartsWith(sectionPath) は、正規化後のパスがセクション配下から
出ていないことを確認します。したがって、次の指定はいずれも回避策にはなりません。
-
ServerScriptFilePathに UNC パスを設定する -
ServerScriptFilePathにZ:\shareなどの絶対パスを設定する -
$ps.fileのpath引数に UNC パス、ドライブ文字、..を渡す
UNC パスが拒否されるのはアクセス権不足による失敗ではなく、$ps.file 自体の
パス制限です。共有アクセス権と NTFS アクセス権を追加しても、この制限は解除されません。
Windows の net use でフォルダへマウントできるか
net use では、SMB 共有をドライブ文字へ割り当てるか、ドライブ文字を付けずに
共有への接続を確立できます。しかし、Linux のマウントポイントのように、SMB 共有を
既存のローカルフォルダへマウントする機能はありません。
rem ドライブ文字へ割り当てる
net use Z: \\file01\pleasanter-data
rem ドライブ文字を付けずに接続だけを確立する
net use \\file01\pleasanter-data
前者で得られる Z: は絶対パスなので、ServerScriptFilePath に指定すると
rootPath2.StartsWith(Environments.CurrentDirectoryPath) の検査で拒否されます。
さらに、ドライブの割り当ては実行したユーザーのログオンセッションに属するため、
管理者が作成した Z: を IIS のアプリケーションプールから参照できるとは限りません。
後者は資格情報を含む SMB セッションを確立するだけです。ローカルフォルダへ変換する
ものではなく、アクセス時には引き続き \\file01\pleasanter-data という UNC パスを
使うため、$ps.file のパス検証を通過できません。
Windows のディレクトリシンボリックリンクを使ってローカルフォルダのように見せる方法は
ありますが、後述する理由から採用しません。したがって Windows では、Syncthing などで
両サーバーのローカルフォルダを同期する構成が分かりやすいです。
サードパーティー製品ならフォルダへマウントできる
Windows 標準の net use ではできませんが、ユーザーモードファイルシステムを提供する
WinFsp と、それに対応したツールを使えば、リモートストレージを
ドライブ文字ではなく特定のディレクトリへマウントできます。
SMB 共有を対象にする場合の候補は、WinFsp を利用する
rclone mount です。rclone には
SMB バックエンド があり、SMB 共有を rclone のリモートとして
登録したうえで、プリザンターの許可範囲内にあるディレクトリへマウントできます。
たとえば、rclone で file-server という SMB リモートを設定済みとします。
マウントポイントはプリザンターのカレントディレクトリ配下に置きます。
$mountPoint = 'C:\web\pleasanter\App_Data\ServerScriptFiles\remote'
rclone mount 'file-server:pleasanter-data' $mountPoint `
--vfs-cache-mode writes `
--log-file 'C:\Logs\rclone-pleasanter.log' `
--log-level INFO
Script.json には、カレントディレクトリからの相対パスを設定します。
{
"ServerScriptFilePath": "App_Data/ServerScriptFiles/remote"
}
この構成なら、NormalizePath が検査する文字列は許可範囲内のローカルパスです。
検査を通過した後のファイル操作を WinFsp と rclone が受け取り、SMB 共有へ転送します。
上記は構成例です。rclone・WinFspのバージョン、マウントポイントの要件、使用する
Windowsアカウントでのディレクトリマウント可否を検証環境で確認してください。
rclone マウントを運用するときの注意点
技術的には可能ですが、シンボリックリンクと同様に、$ps.file のコードが
ネットワーク共有を明示的に許可するわけではありません。ローカルに見えるマウントポイントを
OS 層で用意し、字句ベースのパス検査後に別のファイルシステムへ転送する構成です。
本番運用では、少なくとも次を確認します。
- サービス化:対話ユーザーのターミナルではなく、Windows サービスとして起動する
- 起動順序:rclone のマウント完了後にプリザンターを起動する
- 実行アカウント:IIS と rclone の両方からマウントポイントへアクセスできるか確認する
- 認証情報:rclone の設定ファイルを専用サービスアカウントだけが読めるようにする
- 停止時の動作:マウント解除中にローカルの同名フォルダへ書き込まないよう監視する
- キャッシュ:書き込みキャッシュ、反映タイミング、障害時の未転送データを確認する
- ファイル操作:リネーム、上書き、削除、ロック、同時アクセスの挙動を確認する
- 障害復旧:SMB 切断、rclone 停止、Windows 再起動後に自動復旧できるようにする
- 製品更新:プリザンター、rclone、WinFsp の更新後に回帰テストする
特に重要なのは、マウントプロセスが停止した場合です。マウントポイントとして使っていた
ディレクトリが通常のローカルディレクトリとして見えてしまう構成では、$ps.file が
共有へ送ったつもりのデータをローカルへ書き込む可能性があります。マウント状態の監視と、
マウントが無い状態では書き込めない権限・起動制御を用意します。
また、マウントポイント配下は $ps.file の読み取り、書き込み、削除、移動、コピーなどの
操作対象になります。サーバスクリプトを編集できる利用者の範囲を制限し、rclone の
SMB アカウントにも対象共有の必要最小限の権限だけを付与します。
どの方式を選ぶか
| 要件 | 向いている方式 |
|---|---|
| 作成直後から共有上に存在する必要がある | rclone・WinFspによるディレクトリマウント |
| 数秒から数分の反映遅延を許容できる | Syncthing、robocopy、外部ワーカー |
| 処理結果、再実行、承認、監査を管理したい | 社内API、外部ワーカー |
$ps.file のアクセス範囲を広げたくない |
Syncthing、robocopy、外部ワーカー |
即時性が必須なら rclone と WinFsp は選択肢になります。一方、単に作成したファイルを
共有へ届ける目的なら、プリザンターとネットワーク共有を直接結合しない Syncthing や
外部ワーカーの方が、障害と権限の境界を分離しやすい構成です。
Linux の場合
絶対パスや UNC 風の指定は同様に拒否される
Linux でも NormalizePath の考え方は変わりません。たとえば SMB 共有を
/mnt/pleasanter-data にマウントしても、これを ServerScriptFilePath へ絶対パスで
指定すると、プリザンターのカレントディレクトリ配下ではないため拒否されます。
また、Windows の UNC パスをそのまま Linux 版へ渡す方法も使用できません。
$ps.file がネットワークプロトコルを扱うのではなく、OS から見えるファイルシステム上の
パスを検証して操作するためです。
カレントディレクトリ配下をマウントポイントにする
Linux では、SMB や NFS のファイルシステムを任意のディレクトリへマウントできます。
プリザンターのカレントディレクトリ配下へ管理者がマウントポイントを用意した場合、
パス文字列は許可範囲内のままなので、現在の NormalizePath の検査を通過します。
たとえば、プリザンターのカレントディレクトリを /opt/pleasanter とすると、
次のような構成です。
/opt/pleasanter/
└── App_Data/
└── ServerScriptFiles/
└── remote/ ← このディレクトリへ SMB または NFS をマウント
SMB 共有をマウントする例は次のとおりです。
sudo mkdir -p /opt/pleasanter/App_Data/ServerScriptFiles/remote
sudo mount -t cifs //file01/pleasanter-data \
/opt/pleasanter/App_Data/ServerScriptFiles/remote \
-o credentials=/root/.smb-pleasanter,uid=pleasanter,gid=pleasanter,dir_mode=0750,file_mode=0640
Script.json には、カレントディレクトリからの相対パスを設定します。
{
"ServerScriptFilePath": "App_Data/ServerScriptFiles/remote"
}
この場合、Path.GetFullPath が返す文字列は
/opt/pleasanter/App_Data/ServerScriptFiles/remote のままです。マウント先の
実体が別サーバーであっても、パス文字列はカレントディレクトリから始まるため、
現在の StartsWith 検査を通過します。ファイルアクセス時には Linux カーネルが
マウント済みの SMB または NFS へ処理を渡します。
コンテナ環境でも考え方は同じです。ホスト側でマウントした共有を、コンテナ内の
プリザンターのカレントディレクトリ配下へ bind mount します。コンテナ内から見える
ServerScriptFilePath は、あくまで許可範囲内のローカルパスにします。
これは $ps.file が SMB や NFS を正式にサポートするという意味ではありません。
OS のマウント機能によってネットワークファイルシステムをローカルのディレクトリツリーへ
組み込む構成です。プリザンターの更新でリパースポイントやマウントポイントを考慮した
検査が追加された場合は、動作が変わる可能性があります。
Linux で採用する場合は、次の点も設計します。
- 認証情報をコマンドラインや
Script.jsonに書かず、root のみが読める資格情報ファイルに置く -
_netdevや systemd の依存関係を使い、ネットワークとマウントの完了後にプリザンターを起動する - マウント解除時に、同名のローカルディレクトリへ誤って書き込まないよう監視する
-
uid、gid、file_mode、dir_modeを使い、プリザンターの実行ユーザーへ最小権限を与える - 通信断、再接続、タイムアウト、ファイルロック、同名ファイルの競合を検証する
- Web アプリケーションからファイルサーバーへ直接到達できる範囲が広がるリスクを評価する
Linux では技術的に実現できますが、可用性とセキュリティの責務がプリザンターサーバーへ
増えます。単純な一方向転送でよければ、Windows と同様に Syncthing や外部ワーカーへ
分離する方が運用しやすい場合があります。
実行アカウントの変更だけでも解決しない
通常の Windows アプリケーションが共有へ書き込む場合は、実行アカウントに
次の両方のアクセス権が必要です。
| 種類 | フォルダ作成に必要な権限の例 |
|---|---|
| 共有アクセス権 | 変更 |
| NTFS アクセス権 | 変更、または必要な操作だけに絞った権限 |
実効権限は両者の組み合わせで決まります。一方だけを設定しても書き込めません。
しかし、これは OS が UNC パスへのアクセスを試みた後の話です。
$ps.file はその前段のパス検証で拒否するため、実行アカウントを専用の
ドメインサービスアカウントや gMSA へ変更するだけでは解決しません。
$ps.file でできる範囲
ServerScriptFilePath には、プリザンターから許可されているローカルのパスを設定します。
サーバスクリプトでは、そのルートを基準とするセクション名と相対パスを指定します。
var section = "exports";
var path = "2026/09";
var created = $ps.file.createDirectory(section, path);
if (!created) {
context.Log("フォルダを作成できませんでした。実行アカウントとアクセス権を確認してください。");
}
この処理で作成できるのは、設定したローカルルートの
exports\2026\09 です。..、ドライブ文字、UNC パスを path へ渡して
共有先に切り替えることはできません。
利用中のプリザンターのバージョンによって、使用できる $ps.file のメソッドを
事前に確認してください。また、同じ名前のフォルダが既にある場合の戻り値も含め、
検証環境で動作を確認してから本番へ適用してください。
推奨する代替構成
$ps.file の制限を迂回するのではなく、ネットワーク共有を操作する責務を
別プロセスへ分離します。
社内 API を用意する
共有先へアクセスできるサーバーに HTTPS API を用意し、サーバスクリプトの
httpClient からフォルダ作成を依頼します。API 側では入力値を検証し、操作可能な
共有とフォルダを固定します。
Windows サービスやタスクスケジューラで処理する
プリザンターには「フォルダ作成依頼」のレコードだけを登録し、別サーバーの
Windows サービスや定期タスクが API で未処理の依頼を取得して作成します。
処理結果をレコードへ書き戻せば、再実行や監査もしやすくなります。
ローカルフォルダを監視する
$ps.file で許可されたローカルフォルダに依頼ファイルを出力し、専用サービスが
それを監視して共有先へ反映します。ただし、依頼ファイルの改ざん対策、二重処理防止、
失敗時の再実行を設計する必要があります。
Syncthing でサーバー間同期する
Syncthing のことでした。Syncthing を使う方法は現実的です。
両方の Windows サーバーに Syncthing を導入し、プリザンターサーバーのローカルフォルダと、
ファイルサーバーのローカルフォルダを同期します。
この構成では $ps.file は最後までローカルフォルダだけを操作します。Syncthing が
サーバー間の転送を担当するため、$ps.file に UNC パスを認識させる必要がありません。
プリザンター側の共有フォルダタイプは 送信専用、ファイルサーバー側は
受信専用にすると、運用意図を明確にできます。ただし、これはバックアップ機能ではなく、
送信元での変更や削除は同期先にも反映されます。ファイルサーバー側でファイルバージョン管理を
有効にし、Syncthing とは別のバックアップも用意します。
Windows では、ログオンユーザーのスタートアップとしてではなく、専用アカウントの
Windows サービスとして Syncthing を実行する構成が安定します。次の権限を分離します。
| 実行主体 | 必要なアクセス先 |
|---|---|
| プリザンターのアプリケーションプール | プリザンター側のローカル出力フォルダ |
| プリザンター側の Syncthing | 同じローカル出力フォルダ |
| ファイルサーバー側の Syncthing | 同期先のローカルフォルダ |
| 共有フォルダの利用者 | ファイルサーバー側で公開した共有 |
これにより、プリザンターのアプリケーションプールへファイルサーバーのSMB資格情報を
持たせずに済みます。また、ファイルサーバー側では同期先がローカルパスなので、
ネットワークドライブのログオンセッション問題も発生しません。
Syncthing の管理画面を無認証のまま社内ネットワークへ公開しないでください。
管理画面の認証とアクセス元制限、デバイスIDによる接続相手の確認、通信を許可する
ポートの限定を行います。
robocopy で転送する
Windows 標準機能だけで構成するなら、タスクスケジューラから robocopy を定期実行する
方法もあります。この方式では、robocopy の実行アカウントにだけ共有フォルダへの
アクセス権を付与します。
$source = 'C:\web\pleasanter\App_Data\ServerScriptFiles\exports'
$destination = '\\file01\pleasanter-data\exports'
robocopy $source $destination /E /Z /R:3 /W:5 /COPY:DAT /DCOPY:DAT /LOG+:C:\Logs\PleasanterSync.log
if ($LASTEXITCODE -ge 8) {
throw "robocopy failed. ExitCode=$LASTEXITCODE"
}
robocopy は終了コード 0 だけが成功ではなく、0 から 7 はコピー済み、差分あり、
追加コピーありなどの正常系を含みます。この例では 8 以上を失敗として扱います。
同期方式では、次の点を決めておきます。
- 同期方向:プリザンターから共有への一方向同期にするか、双方向同期にするか
- 削除の扱い:ローカルで消えたファイルを共有先でも削除するか
- 書き込み途中の扱い:一時ファイルへ出力してからリネームし、未完成ファイルの転送を防ぐ
- 競合時の扱い:同名ファイルがある場合に上書き、世代保存、エラーのどれにするか
- 実行アカウント:同期処理専用アカウントへ必要最小限のローカル・共有権限を付与する
- 再実行と監視:終了コード、ログ、失敗通知を記録し、ネットワーク断から復旧できるようにする
- マルウェア対策:暗号化や誤削除が同期先へ即時反映されるリスクに備えてバックアップを分離する
特に、ミラーオプションは削除も同期します。たとえば robocopy /MIR はコピー元での
誤削除を共有先にも反映するため、要件が明確でない状態では使用せず、まず /E などの
削除を伴わない設定から検証します。
これらは構成要素が増える一方で、SMB の資格情報を Web アプリケーション側へ
持たせずに済み、再実行、監査、エラー通知を実装しやすい利点があります。
ローカルフォルダ配下にネットワーク共有へのシンボリックリンクなどを作り、
字句上のパス検証をすり抜ける構成は推奨しません。
シンボリックリンクを推奨しない理由
Path.GetFullPath は . や .. を整理した絶対パスを返しますが、途中にある
シンボリックリンクやジャンクションなどのリパースポイントのリンク先までは解決しません。
そのため、許可されたローカルルートの中に共有フォルダへのリンクを置くと、
StartsWith の検査はローカルパスとして通過し、その後のファイル操作時に Windows が
リンク先の共有へ到達する可能性があります。
一見すると使えそうですが、これは次の理由から採用すべきではありません。
-
意図されたアクセス境界を迂回する
NormalizePathは、操作対象をカレントディレクトリとセクション配下へ限定するために
二段階のStartsWith検査を行っています。シンボリックリンクの利用は、この制限を
実質的に無効化します。 -
フォルダ作成以外の操作にも影響する
createDirectoryだけでなく、読み取り、書き込み、削除、移動、コピーなどの
$ps.fileの各処理が同じパス正規化を利用します。リンクを公開すると、想定していた
フォルダ作成だけでなく、共有先のファイルに対する別の操作まで可能になる範囲が
広がります。 -
リンク先の差し替えによる危険がある
パスの検査と実際のファイル操作は同時ではありません。リンクを変更できるユーザーや
プロセスが存在すると、検査後にリンク先を差し替え、プリザンターの実行権限で
意図しない場所を操作させる余地が生まれます。 -
Windows と SMB の実行条件に依存する
リンクの作成権限、IIS の実行アカウント、共有アクセス権、NTFS アクセス権、SMB の
認証状態に依存します。開発環境で動作しても、サービス再起動やアカウント変更後に
動かなくなる可能性があり、保守しにくい構成です。 -
将来の更新で動かなくなる可能性がある
今後、プリザンター側でリパースポイントの検出やリンク先を解決した検証が追加されれば、
この構成は動作しなくなります。公式の入出力経路として保証された利用方法でもありません。
シンボリックリンクは「UNC パスが正式に利用可能になる方法」ではなく、現在の
字句ベースの検査と OS のリンク解決の差を利用した迂回です。セキュリティ境界として設けられた
検査を迂回せず、共有操作は権限と入力値を限定した API や専用サービスへ分離します。
まとめ
- Windows 版の標準的な構成では、
$ps.fileから別サーバーの共有を直接操作できない - マップ済みネットワークドライブはサービスのセッションから見えない
-
net useでは SMB 共有を既存のローカルフォルダへマウントできない - WindowsでもrcloneとWinFspを使えば、許可範囲内のディレクトリへSMB共有をマウントできる
- UNC パスも
$ps.fileのパス検証で明確に拒否される -
ServerScriptFilePathやpath引数へ UNC パスを指定しても回避できない - 実行アカウントへの権限付与だけでは
$ps.fileのパス制限を解除できない - Linux では許可範囲内のディレクトリへ SMB や NFS をマウントすれば技術的に操作できる
- 社内 API、外部ワーカー、監視サービス、同期ソフトへ共有操作を分離する
ネットワークドライブが見えないというイメージは正しいですが、それを UNC パスへ
置き換えても $ps.file では解決しません。Windows 側の認証設定を調整し続けるのではなく、
共有を操作する処理を専用の API やサービスへ切り出すのが現実的です。