契約・NDA管理設計
1. 概要
業務委託メンバーの契約管理、NDA(秘密保持契約)の電子署名、契約期間とアクセス権限の連動、退場時の手続きを体系的に管理するための設計書。
2. 初回ログイン時のNDA同意フロー
2.1 フロー概要
初回ログイン → NDA同意画面表示 → NDA内容確認 → 電子署名 → 同意記録保存 → アクセス権有効化2.2 詳細フロー
┌─────────────────────────────────────────────┐
│ 1. 委託メンバーがGitLabに初回ログイン │
│ (SSOまたはGitLabアカウント) │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 2. NDA同意状態チェック (API Middleware) │
│ - DBで同意済みかを確認 │
│ - 未同意の場合、NDA同意ページへリダイレクト │
└──────────────┬──────────────────────────────┘
│ 未同意
▼
┌─────────────────────────────────────────────┐
│ 3. NDA同意画面表示 │
│ - NDA全文表示 │
│ - スクロール完了必須 │
│ - 同意チェックボックス │
│ - 電子署名入力欄 │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 4. 電子署名の実行 │
│ - 署名者氏名入力 │
│ - タイムスタンプ記録 │
│ - IPアドレス記録 │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 5. 同意記録の保存 │
│ - DBに同意情報を保存 │
│ - 署名付きPDFを生成・保存 │
│ - 確認メール送信(委託メンバー+管理者) │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 6. アクセス権の有効化 │
│ - GitLab Developer権限の付与 │
│ - Issue単位のアクセス付与開始 │
└─────────────────────────────────────────────┘2.3 NDA同意画面設計
画面レイアウト
┌──────────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 秘密保持契約(NDA)同意 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 以下のNDA内容をご確認の上、同意してください。 │
│ ※全文をスクロールしてお読みください。 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 第1条(目的) │ │
│ │ 本契約は、業務委託に伴い開示される秘密情報の... │ │
│ │ │ │
│ │ 第2条(秘密情報の定義) │ │
│ │ 本契約における秘密情報とは... │ │
│ │ │ │
│ │ 第3条(秘密保持義務) │ │
│ │ ... │ │
│ │ │ │
│ │ [スクロール可能エリア - 全文表示] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 署名者情報 │ │
│ │ │ │
│ │ 氏名: [________________________] │ │
│ │ メール: outsource@example.com (自動入力) │ │
│ │ 日付: 2026-03-28 (自動入力) │ │
│ │ │ │
│ │ ☐ 上記NDA内容を全て確認し、同意します。 │ │
│ │ ☐ 秘密情報の取り扱いについて理解しました。 │ │
│ │ ☐ 契約終了時にローカルデータを削除することに │ │
│ │ 同意します。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [同意して署名する] [後で確認する] │
│ │
│ ※「後で確認する」を選択した場合、リポジトリへの │
│ アクセスは制限されたままとなります。 │
│ │
└──────────────────────────────────────────────────────────┘画面仕様
| 項目 | 仕様 |
|---|---|
| NDA全文表示エリア | スクロール可能、高さ400px |
| スクロール完了検知 | 最下部到達で同意チェックボックスを有効化 |
| 同意チェックボックス | 3項目全てチェック必須 |
| 署名者氏名 | 手動入力(アカウント名と一致確認) |
| 同意ボタン | 全チェック完了 + 氏名入力で有効化 |
| 「後で確認する」 | アクセスは制限されたまま、次回ログイン時に再表示 |
2.4 NDA同意のAPI実装
python
# NDA同意チェックミドルウェア
class NDACheckMiddleware:
EXCLUDED_PATHS = ["/api/nda/", "/api/auth/", "/static/"]
async def __call__(self, request, call_next):
if any(request.url.path.startswith(p) for p in self.EXCLUDED_PATHS):
return await call_next(request)
user = get_current_user(request)
if user and user.user_type == "outsource":
nda_status = await get_nda_status(user.id)
if not nda_status or not nda_status.is_agreed:
return RedirectResponse("/nda/agree")
return await call_next(request)
# NDA同意API
@router.post("/api/nda/agree")
async def agree_nda(request: NDAAgreeRequest, current_user: User):
# バリデーション
if not request.all_checkboxes_checked:
raise HTTPException(400, "全ての同意項目にチェックしてください")
if not request.signer_name:
raise HTTPException(400, "署名者氏名を入力してください")
# 同意記録の保存
nda_record = NDARecord(
user_id=current_user.id,
signer_name=request.signer_name,
agreed_at=datetime.utcnow(),
ip_address=request.client.host,
user_agent=request.headers.get("user-agent"),
nda_version=CURRENT_NDA_VERSION,
)
await save_nda_record(nda_record)
# 署名付きPDF生成
pdf_path = await generate_signed_nda_pdf(nda_record)
# 確認メール送信
await send_nda_confirmation_email(current_user, pdf_path)
# GitLabアクセス権の有効化
await activate_gitlab_access(current_user.gitlab_user_id)
# 監査ログ記録
await log_audit("NDA_AGREED", current_user.id, detail={
"nda_version": CURRENT_NDA_VERSION,
"signer_name": request.signer_name,
})
return {"status": "success", "message": "NDA同意が完了しました"}3. 電子署名の管理方法
3.1 署名データモデル
sql
CREATE TABLE nda_agreements (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
gitlab_user_id INTEGER NOT NULL,
signer_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
nda_version VARCHAR(20) NOT NULL,
agreed_at TIMESTAMP WITH TIME ZONE NOT NULL,
ip_address INET NOT NULL,
user_agent TEXT,
pdf_storage_key VARCHAR(500),
signature_hash VARCHAR(64) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
revoked_at TIMESTAMP WITH TIME ZONE,
revoked_reason VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_nda_user_id ON nda_agreements(user_id);
CREATE INDEX idx_nda_gitlab_user_id ON nda_agreements(gitlab_user_id);
CREATE INDEX idx_nda_active ON nda_agreements(is_active);3.2 署名ハッシュの生成
署名の真正性を確認するため、以下の情報からSHA-256ハッシュを生成する:
python
import hashlib
import json
def generate_signature_hash(record: NDARecord) -> str:
payload = json.dumps({
"user_id": record.user_id,
"signer_name": record.signer_name,
"email": record.email,
"nda_version": record.nda_version,
"agreed_at": record.agreed_at.isoformat(),
"ip_address": str(record.ip_address),
}, sort_keys=True)
return hashlib.sha256(payload.encode()).hexdigest()3.3 署名付きPDFの生成・保存
| 項目 | 仕様 |
|---|---|
| PDF内容 | NDA全文 + 署名情報(氏名、日時、IP) |
| 保存先 | S3互換ストレージ(暗号化有効) |
| 暗号化 | AES-256 サーバーサイド暗号化 |
| アクセス権 | 管理者のみダウンロード可 |
| 保存パス | nda-documents/{year}/{user_id}/{timestamp}.pdf |
| 保存期間 | 契約終了後5年 |
3.4 NDAバージョン管理
NDA内容が更新された場合、未同意の新バージョンに対して再同意を求める。
python
CURRENT_NDA_VERSION = "1.0.0"
async def check_nda_version(user_id: int) -> bool:
"""現行バージョンのNDAに同意済みかチェック"""
latest = await get_latest_nda_agreement(user_id)
if not latest:
return False
return latest.nda_version == CURRENT_NDA_VERSION and latest.is_active4. 契約期間とアクセス権限の連動
4.1 契約データモデル
sql
CREATE TABLE outsource_contracts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
gitlab_user_id INTEGER NOT NULL,
contract_start DATE NOT NULL,
contract_end DATE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
-- status: 'pending', 'active', 'expiring_soon', 'expired', 'terminated'
auto_revoke BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_contract_status ON outsource_contracts(status);
CREATE INDEX idx_contract_end ON outsource_contracts(contract_end);4.2 契約状態遷移
pending ──(NDA同意)──> active ──(期限30日前)──> expiring_soon ──(期限到来)──> expired
│
└──(契約解除)──> terminated4.3 自動アクセス剥奪の実装
日次バッチ処理
python
# 日次実行: 契約期限チェック & アクセス剥奪
async def daily_contract_check():
today = date.today()
# 1. 期限切れ契約の処理
expired_contracts = await get_contracts_by_status_and_date(
status="active",
contract_end_before=today
)
for contract in expired_contracts:
await revoke_gitlab_access(contract.gitlab_user_id)
await update_contract_status(contract.id, "expired")
await log_audit("CONTRACT_EXPIRED", contract.user_id)
await send_notification(
to=["admin", contract.user_id],
subject="契約期間終了に伴うアクセス権剥奪",
body=f"契約期間が終了したため、GitLabへのアクセス権を剥奪しました。"
)
# 2. 期限間近の通知(30日前)
expiring_contracts = await get_contracts_expiring_within(days=30)
for contract in expiring_contracts:
if contract.status == "active":
await update_contract_status(contract.id, "expiring_soon")
await send_notification(
to=["admin"],
subject=f"契約期限30日前通知: {contract.user_id}",
body=f"契約終了日: {contract.contract_end}"
)
# 3. 期限間近の最終通知(7日前)
final_warning = await get_contracts_expiring_within(days=7)
for contract in final_warning:
await send_notification(
to=["admin", contract.user_id],
subject="契約終了7日前の最終通知",
body=f"契約終了日: {contract.contract_end}。終了後はアクセスが自動的に無効化されます。"
)4.4 通知スケジュール
| タイミング | 通知先 | 内容 |
|---|---|---|
| 契約終了30日前 | 管理者 | 契約更新検討の依頼 |
| 契約終了14日前 | 管理者 + 委託メンバー | 契約更新有無の確認 |
| 契約終了7日前 | 管理者 + 委託メンバー | 最終警告、退場準備依頼 |
| 契約終了3日前 | 管理者 + 委託メンバー | 退場チェックリスト送付 |
| 契約終了当日 | 管理者 | アクセス自動剥奪完了通知 |
4.5 契約更新フロー
更新判断 → 新規契約レコード作成 → expires_at更新 → NDA再確認(バージョン変更時のみ)5. 退場時のチェックリスト
5.1 退場チェックリストデータモデル
sql
CREATE TABLE offboarding_checklists (
id SERIAL PRIMARY KEY,
contract_id INTEGER NOT NULL REFERENCES outsource_contracts(id),
user_id INTEGER NOT NULL REFERENCES users(id),
status VARCHAR(20) DEFAULT 'pending',
-- status: 'pending', 'in_progress', 'completed'
initiated_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
completed_by INTEGER REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE offboarding_checklist_items (
id SERIAL PRIMARY KEY,
checklist_id INTEGER NOT NULL REFERENCES offboarding_checklists(id),
item_key VARCHAR(100) NOT NULL,
item_label TEXT NOT NULL,
category VARCHAR(50) NOT NULL,
is_completed BOOLEAN DEFAULT FALSE,
completed_at TIMESTAMP WITH TIME ZONE,
completed_by VARCHAR(50),
-- completed_by: 'system' (自動) or ユーザー名 (手動確認)
evidence TEXT,
notes TEXT
);5.2 チェックリスト項目
カテゴリ: アクセス権削除(自動実行)
| # | 項目 | 実行者 | 自動/手動 | 確認方法 |
|---|---|---|---|---|
| 1 | GitLabプロジェクトメンバーから削除 | System | 自動 | API応答確認 |
| 2 | GitLab Personal Access Token無効化 | System | 自動 | API応答確認 |
| 3 | GitLab SSH鍵削除 | System | 自動 | API応答確認 |
| 4 | CI/CD関連の一時トークン無効化 | System | 自動 | API応答確認 |
| 5 | Webhook連携の無効化 | System | 自動 | API応答確認 |
カテゴリ: データ削除確認(手動確認)
| # | 項目 | 確認者 | 自動/手動 | 確認方法 |
|---|---|---|---|---|
| 6 | ローカルリポジトリの削除 | 委託メンバー | 手動 | 自己申告 + スクリーンショット |
| 7 | ローカル環境の設定ファイル削除 | 委託メンバー | 手動 | 自己申告 |
| 8 | 開発用ダミーデータの削除 | 委託メンバー | 手動 | 自己申告 |
| 9 | ブラウザ保存パスワード・Cookieの削除 | 委託メンバー | 手動 | 自己申告 |
| 10 | メール・チャットの業務関連データ削除 | 委託メンバー | 手動 | 自己申告 |
カテゴリ: 管理者確認
| # | 項目 | 確認者 | 自動/手動 | 確認方法 |
|---|---|---|---|---|
| 11 | 未完了Issue/MRの引継ぎ確認 | 管理者 | 手動 | GitLab上で確認 |
| 12 | 最終監査ログレビュー | 管理者 | 手動 | 監査ダッシュボード |
| 13 | NDA継続義務の通知 | 管理者 | 手動 | メール送信確認 |
| 14 | 退場完了報告 | 管理者 | 手動 | チェックリスト完了 |
5.3 自動退場処理の実装
python
async def execute_offboarding(contract_id: int):
contract = await get_contract(contract_id)
checklist = await create_offboarding_checklist(contract)
# 自動実行項目の処理
auto_items = {
"remove_project_member": remove_gitlab_member,
"revoke_pat": revoke_personal_access_tokens,
"remove_ssh_keys": remove_ssh_keys,
"revoke_ci_tokens": revoke_ci_cd_tokens,
"disable_webhooks": disable_user_webhooks,
}
for item_key, handler in auto_items.items():
try:
await handler(contract.gitlab_user_id)
await mark_checklist_item_completed(
checklist.id, item_key, completed_by="system"
)
await log_audit(f"OFFBOARDING_{item_key.upper()}", contract.user_id)
except Exception as e:
await log_audit(
f"OFFBOARDING_{item_key.upper()}_FAILED",
contract.user_id,
detail={"error": str(e)}
)
await send_alert(
severity="HIGH",
message=f"退場処理の自動実行に失敗: {item_key} - {e}"
)
# 手動確認項目のリマインド送信
await send_offboarding_reminder(contract.user_id, checklist.id)
return checklist5.4 退場時の通知テンプレート
委託メンバー向け
件名: 【重要】業務委託終了に伴う退場手続きのお願い
{signer_name} 様
契約期間の終了({contract_end})に伴い、退場手続きをお願いいたします。
以下の作業を実施し、完了をご報告ください:
1. ローカルリポジトリの削除
- クローンした全リポジトリを完全削除してください
- ゴミ箱も空にしてください
2. 設定ファイル・データの削除
- .env ファイル等の設定ファイル
- 開発用のダミーデータ
- 業務関連のメモ・ドキュメント
3. ブラウザデータの削除
- GitLab関連の保存パスワード
- Cookie・セッション情報
4. 未完了タスクの引継ぎ
- 未完了のIssue/MRがある場合は管理者にご連絡ください
確認用URL: {checklist_url}
※NDAに基づく秘密保持義務は契約終了後も継続します。
ご不明点がございましたらお問い合わせください。5.5 退場完了の確認フロー
退場処理開始
│
├─ 自動処理(即時実行)
│ ├─ GitLabアクセス権削除
│ ├─ トークン無効化
│ └─ SSH鍵削除
│
├─ 委託メンバー確認(3日以内)
│ ├─ ローカルデータ削除の自己申告
│ └─ チェックリストの各項目に確認チェック
│
├─ 管理者確認(5日以内)
│ ├─ 未完了タスクの引継ぎ確認
│ ├─ 最終監査ログレビュー
│ └─ NDA継続義務通知の送信
│
└─ 退場完了
├─ 全項目完了の確認
├─ 退場完了記録の保存
└─ 契約ステータスを 'expired' に更新6. 全体タイムライン
[契約開始]
│
├─ アカウント作成
├─ 初回ログイン → NDA同意
├─ アクセス権有効化
│
│ ... 業務遂行期間 ...
│
├─ 契約終了30日前: 管理者通知
├─ 契約終了14日前: 双方通知
├─ 契約終了7日前: 最終警告
├─ 契約終了3日前: 退場チェックリスト送付
│
[契約終了]
│
├─ 自動アクセス剥奪
├─ 退場チェックリスト実施
├─ 管理者最終確認
│
[退場完了]
│
└─ NDA秘密保持義務継続(契約終了後も有効)