Ariweb Suite 0.1.1 기반: 공통 라이브러리(db·http·세션·작업 엔진·할당/예약·알림·감사·보관소), 포털 뼈대, Webmaker/AriMail 도입 소스와 manifest
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
9ca4cee341
commit
c7aea8d419
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# 웹메이커(webmaker) 계정 연동을 위한 읽기 도우미 설치(1.0.8, 요구사항 1) — root 로 실행:
|
||||
# sudo bash <APP_DIR>/repo/deploy/webmaker-access.sh [<웹메이커 DATA_ROOT>] [--site <site.env 경로>] [--node <node 실행 파일>] [--sudo]
|
||||
#
|
||||
# 왜 필요한가
|
||||
# webmaker 인증 모드는 웹메이커의 사용자 파일(DATA_ROOT/users.json, sites/, modules/, domains.json)을 읽기 전용으로 참조합니다. 그런데 웹메이커는 그 파일을
|
||||
# 자기 서비스 계정(예: master)만 읽을 수 있게(0600/0700) 쓰므로 AriMail 실행 계정(예: arihakdang)이 직접 읽을 수 없습니다. 웹메이커가 파일을 다시 쓸 때마다
|
||||
# 권한이 0600 으로 돌아가므로 ACL 로도 풀리지 않습니다. 그래서 "그 계정으로 스냅샷 하나를 읽어 JSON 으로 돌려주는" 도우미를 둡니다.
|
||||
#
|
||||
# 하는 일
|
||||
# 1) server/webmaker-snapshot.mjs 의 사본을 root 소유로 /usr/local/lib/arimail/ 에 둡니다(실행 계정이 바꿀 수 없어야 다른 계정으로 도는 코드가 안전).
|
||||
# 실행용 node 도 root 소유여야 하므로 /usr/bin/node(22 이상)를 먼저 찾고, 없으면 --node 로 지정한 root 소유 파일을 씁니다.
|
||||
# 2) systemd 소켓 도우미(기본): /run/arimail-webmaker/<슬러그>.sock 에 접속하면 systemd 가 웹메이커 소유 계정으로 도우미를 띄워 JSON 을 소켓에 씁니다(Accept=yes).
|
||||
# AriMail 서비스 유닛은 NoNewPrivileges=yes·CapabilityBoundingSet 로 잠겨 있어 sudo 가 동작하지 않으므로 이 방식이 필요합니다. 소켓은 AriMail 실행 계정의 그룹만 접속(0660).
|
||||
# 3) --sudo: systemd 가 없는 서버용 — 감싸는 스크립트 /usr/local/lib/arimail/webmaker-snapshot 와 sudoers 규칙 한 줄(그 디렉터리 인수 하나만 허용)을 설치합니다.
|
||||
# 4) 실행 계정으로 한 번 읽어 보고(계정 수 출력), 전체 설정 화면에서 적을 값(디렉터리·읽을 계정)을 안내합니다.
|
||||
# 게이트웨이 설정·환경 파일·데이터와 웹메이커의 파일은 건드리지 않습니다. 다시 실행해도 안전합니다(같은 내용으로 덮어씀).
|
||||
set -euo pipefail
|
||||
[ "$(id -u)" = 0 ] || { echo 'root 로 실행하세요' >&2; exit 1; }
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
DATA_ROOT=""; SITE_FILE=""; NODE_OVERRIDE=""; MODE=socket
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--site) SITE_FILE="$2"; shift 2 ;;
|
||||
--node) NODE_OVERRIDE="$2"; shift 2 ;;
|
||||
--sudo) MODE=sudo; shift ;;
|
||||
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) [ -z "$DATA_ROOT" ] || { echo "알 수 없는 인수: $1" >&2; exit 1; }; DATA_ROOT="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
if [ -z "$SITE_FILE" ]; then APP_GUESS="$(cd "$DEPLOY_DIR/../.." && pwd)"; if [ -f "$APP_GUESS/site.env" ]; then SITE_FILE="$APP_GUESS/site.env"; else SITE_FILE="$DEPLOY_DIR/sites/${SITE:-arihakdang}.env"; fi; fi
|
||||
load_site "$SITE_FILE"
|
||||
# 데이터 디렉터리: 인수 → webmaker 서비스의 환경 변수(DATA_ROOT) → /data/webmaker
|
||||
if [ -z "$DATA_ROOT" ]; then
|
||||
DATA_ROOT="$(systemctl show -p Environment webmaker 2>/dev/null | tr ' ' '\n' | sed -n 's/^DATA_ROOT=//p' | head -1 || true)"
|
||||
[ -n "$DATA_ROOT" ] || DATA_ROOT=/data/webmaker
|
||||
fi
|
||||
DATA_ROOT="${DATA_ROOT%/}"
|
||||
# sudoers·유닛 파일에 그대로 적히므로 경로에 허용하는 글자를 제한합니다(공백·와일드카드·쉼표가 들어가면 규칙이 깨지거나 넓어짐).
|
||||
printf '%s' "$DATA_ROOT" | grep -Eq '^/[A-Za-z0-9._/-]+$' || { echo "데이터 디렉터리는 영문·숫자·._- 로 된 절대 경로여야 합니다: $DATA_ROOT" >&2; exit 1; }
|
||||
[ -f "$DATA_ROOT/users.json" ] || { echo "웹메이커 사용자 파일이 없습니다: $DATA_ROOT/users.json (DATA_ROOT 를 확인하세요)" >&2; exit 1; }
|
||||
OWNER="$(stat -c %U "$DATA_ROOT/users.json")"
|
||||
id "$APP_USER" >/dev/null 2>&1 || { echo "AriMail 실행 계정이 없습니다: $APP_USER (사이트 설정 $SITE_FILE)" >&2; exit 1; }
|
||||
APP_GROUP="$(id -gn "$APP_USER")"
|
||||
# 실행용 node: root 소유 + 22 이상. /usr/bin/node → --node 지정 → 사이트의 NODE_BIN(루트 소유일 때만).
|
||||
pick_node() {
|
||||
local cand; for cand in "$NODE_OVERRIDE" /usr/bin/node "$NODE_BIN"; do
|
||||
[ -n "$cand" ] && [ -x "$cand" ] || continue
|
||||
[ "$(stat -c %U "$cand")" = root ] || { echo " (건너뜀) root 소유가 아닌 node: $cand" >&2; continue; }
|
||||
local major; major="$("$cand" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
|
||||
[ "$major" -ge 22 ] 2>/dev/null || { echo " (건너뜀) node 22 미만: $cand" >&2; continue; }
|
||||
printf '%s' "$cand"; return 0
|
||||
done; return 1
|
||||
}
|
||||
NODE_EXEC="$(pick_node)" || { echo "root 소유의 node 22 이상을 찾지 못했습니다. apt 로 설치하거나 --node <경로> 로 지정하세요(실행 계정이 바꿀 수 있는 파일은 쓰지 않습니다)." >&2; exit 1; }
|
||||
LIB=/usr/local/lib/arimail
|
||||
# 슬러그·소켓 경로: server/webmaker.mjs 의 webmakerSocketPath 와 같은 규칙(/data/webmaker → data-webmaker).
|
||||
SLUG="$(printf '%s' "$DATA_ROOT" | sed 's#^/*##; s#[^A-Za-z0-9]#-#g')"; SOCK_DIR=/run/arimail-webmaker; SOCK="$SOCK_DIR/$SLUG.sock"
|
||||
echo "▶ 웹메이커 읽기 도우미 설치($MODE): 데이터 $DATA_ROOT (소유 $OWNER) · AriMail 계정 $APP_USER($APP_GROUP) · node $NODE_EXEC"
|
||||
install -d -o root -g root -m 755 "$LIB"
|
||||
install -o root -g root -m 644 "$DEPLOY_DIR/../server/webmaker-snapshot.mjs" "$LIB/webmaker-snapshot.mjs"
|
||||
[ "$MODE" = socket ] && ! command -v systemctl >/dev/null 2>&1 && { echo " systemd 가 없어 sudo 방식으로 설치합니다"; MODE=sudo; }
|
||||
if [ "$MODE" = socket ]; then
|
||||
UNIT="arimail-webmaker-$SLUG"
|
||||
install -d -o root -g root -m 755 "$SOCK_DIR"
|
||||
printf 'd %s 0755 root root -\n' "$SOCK_DIR" > /etc/tmpfiles.d/arimail-webmaker.conf
|
||||
cat > "/etc/systemd/system/$UNIT.socket" <<UNIT1
|
||||
[Unit]
|
||||
Description=AriMail webmaker account snapshot socket ($DATA_ROOT)
|
||||
# deploy/webmaker-access.sh 가 설치. AriMail(LOGIN_MODE=webmaker, 읽을 계정 $OWNER)이 접속하면 아래 서비스가 $OWNER 로 스냅샷 JSON 을 써 줍니다.
|
||||
|
||||
[Socket]
|
||||
ListenStream=$SOCK
|
||||
SocketUser=$OWNER
|
||||
SocketGroup=$APP_GROUP
|
||||
SocketMode=0660
|
||||
Accept=yes
|
||||
MaxConnections=16
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
UNIT1
|
||||
cat > "/etc/systemd/system/$UNIT@.service" <<UNIT2
|
||||
[Unit]
|
||||
Description=AriMail webmaker account snapshot ($DATA_ROOT, per connection)
|
||||
CollectMode=inactive-or-failed
|
||||
|
||||
[Service]
|
||||
User=$OWNER
|
||||
ExecStart=$NODE_EXEC $LIB/webmaker-snapshot.mjs $DATA_ROOT
|
||||
StandardInput=socket
|
||||
StandardOutput=socket
|
||||
StandardError=journal
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
PrivateTmp=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
UNIT2
|
||||
chmod 644 "/etc/systemd/system/$UNIT.socket" "/etc/systemd/system/$UNIT@.service"
|
||||
systemctl daemon-reload; systemctl enable --now "$UNIT.socket" >/dev/null 2>&1 || systemctl restart "$UNIT.socket"
|
||||
systemctl restart "$UNIT.socket"
|
||||
echo " 소켓: $SOCK ($UNIT.socket, 서비스 $UNIT@.service → $OWNER)"
|
||||
# 실행 계정으로 실제 접속(계정 수만 출력)
|
||||
READ_CMD="sudo -u $APP_USER $NODE_EXEC -e"
|
||||
JS='const s=require("net").connect(process.argv[1]);let b="";s.setEncoding("utf8");s.on("data",c=>b+=c);s.on("end",()=>{const d=JSON.parse(b);console.log(`회원 ${Object.keys(d.users).length}명 · 사이트 ${Object.keys(d.sites).length}개 · 도메인 연결 ${Object.keys(d.domainSites).length}개`);});s.on("error",e=>{console.error(e.message);process.exit(1);});'
|
||||
if OUT="$($READ_CMD "$JS" "$SOCK" 2>&1)"; then echo "✔ 확인: $APP_USER 계정이 소켓으로 스냅샷을 읽었습니다 — $OUT"
|
||||
else echo "✖ 실행 계정으로 소켓 읽기에 실패했습니다: $OUT (journalctl -u '$UNIT@*' 확인)" >&2; exit 1; fi
|
||||
else
|
||||
cat > "$LIB/webmaker-snapshot" <<WRAP
|
||||
#!/bin/sh
|
||||
# AriMail 웹메이커 계정 스냅샷 도우미(deploy/webmaker-access.sh 가 설치). 첫 인수(데이터 디렉터리)만 넘기고 나머지는 무시합니다.
|
||||
exec "$NODE_EXEC" "$LIB/webmaker-snapshot.mjs" "\$1"
|
||||
WRAP
|
||||
chmod 755 "$LIB/webmaker-snapshot"; chown root:root "$LIB/webmaker-snapshot"
|
||||
# sudoers.d 는 이름에 점이 든 파일을 무시하므로 점을 빼고, 임시 파일을 visudo 로 검사한 뒤에 설치합니다(틀린 파일이 들어가면 서버의 sudo 전체가 막힘).
|
||||
RULE="/etc/sudoers.d/$(printf '%s' "$APP_NAME" | tr -c 'A-Za-z0-9_-' '-')-webmaker"
|
||||
TMP_RULE="$(mktemp)"; printf '%s ALL=(%s) NOPASSWD: %s %s\n' "$APP_USER" "$OWNER" "$LIB/webmaker-snapshot" "$DATA_ROOT" > "$TMP_RULE"
|
||||
visudo -cf "$TMP_RULE" >/dev/null || { rm -f "$TMP_RULE"; echo "✖ sudoers 규칙 검사 실패(계정 이름·경로 확인)" >&2; exit 1; }
|
||||
install -o root -g root -m 440 "$TMP_RULE" "$RULE"; rm -f "$TMP_RULE"
|
||||
echo " sudoers: $RULE (주의: AriMail 서비스 유닛에 NoNewPrivileges=yes 가 있으면 sudo 가 동작하지 않습니다 — systemd 서버에서는 소켓 방식을 쓰세요)"
|
||||
if OUT="$(sudo -u "$APP_USER" sudo -n -u "$OWNER" "$LIB/webmaker-snapshot" "$DATA_ROOT" 2>&1)"; then
|
||||
N="$(printf '%s' "$OUT" | "$NODE_EXEC" -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{const d=JSON.parse(s);console.log(`회원 ${Object.keys(d.users).length}명 · 사이트 ${Object.keys(d.sites).length}개 · 도메인 연결 ${Object.keys(d.domainSites).length}개`);});')"
|
||||
echo "✔ 확인: $APP_USER 계정이 $OWNER 로 스냅샷을 읽었습니다 — $N"
|
||||
else echo "✖ 실행 계정으로 읽기에 실패했습니다: $(printf '%s' "$OUT" | tail -1)" >&2; exit 1; fi
|
||||
fi
|
||||
cat <<EOF2
|
||||
다음 단계: 전체 설정 화면(https://$MAIL_HOST:8791/?token=…)의 도메인 카드에서 도메인을 추가·수정하며
|
||||
로그인 방식 = 웹메이커(webmaker) 계정과 연동, 웹메이커 데이터 디렉터리 = $DATA_ROOT, 파일을 읽을 계정 = $OWNER
|
||||
로 저장하세요(기본 도메인이면 환경 파일 $ETC_DIR/gateway.env 에 LOGIN_MODE=webmaker, WEBMAKER_DIR=$DATA_ROOT, WEBMAKER_USER=$OWNER 를 적고 재시작).
|
||||
EOF2
|
||||
Reference in New Issue
Block a user