diff --git a/package-firewall/bash/lib/common.sh b/package-firewall/bash/lib/common.sh index 5dab948..75ecdfb 100644 --- a/package-firewall/bash/lib/common.sh +++ b/package-firewall/bash/lib/common.sh @@ -5,6 +5,9 @@ # Functions: # detect_console_user — finds the logged-in user when running as root # resolve_user_home — resolves home via dscl / getent / POSIX +# _endor_mkdir_owned +# — mkdir -p that chowns newly created +# directory segments to the console user # upsert_block # — non-destructive, idempotent sentinel-block writer # delegates to upsert_block_pip when has @@ -33,6 +36,18 @@ ENDOR_BLOCK_END="# ===== END ENDOR PACKAGE FIREWALL =====" ENDOR_XML_BLOCK_START="" ENDOR_XML_BLOCK_END="" +# Per-entry sentinel markers wrapping our and . They live in the +# shared fragment (shared/blocks/mavensettings.txt) and are what let each entry be +# merged into whichever / container already exists — Maven's +# schema forbids a second container. Both the bash and PowerShell generators read +# that same fragment, so these strings MUST match it byte-for-byte. The legacy +# ENDOR_XML_BLOCK_* pair above is still recognised on strip/remove so files written +# by older versions are cleaned up correctly. +ENDOR_XML_SERVER_START="" +ENDOR_XML_SERVER_END="" +ENDOR_XML_MIRROR_START="" +ENDOR_XML_MIRROR_END="" + # ── User attribution helpers ────────────────────────────────────────────────── # Encode @ into the Basic-auth username. The firewall # decodes the label, auths with the real API key, and logs it as "User". @@ -110,6 +125,23 @@ resolve_user_home() { echo "$home" } +# _endor_mkdir_owned +# +# mkdir -p that hands any directories it CREATES to the console user. MDM runs +# as root, so a bare mkdir -p leaves new dirs root-owned (755) — the user can +# read through them but cannot write inside (e.g. Maven failing to create +# ~/.m2/repository, XDG tools failing to create ~/.config/). Only the +# topmost segment that did not previously exist is chown'd (recursively), so +# pre-existing directories and their contents are never touched. No-op when +# the chain already exists. +_endor_mkdir_owned() { + local dir="$1" owner="$2" group="$3" d="$1" topnew="" + while [[ -n "$d" && ! -d "$d" ]]; do topnew="$d"; d=$(dirname "$d"); done + mkdir -p "$dir" + [[ -n "$topnew" ]] && chown -R "$owner:$group" "$topnew" + return 0 +} + # upsert_block # # Non-destructive, idempotent config writer using sentinel blocks. @@ -145,7 +177,7 @@ upsert_block() { return 0 fi - mkdir -p "$(dirname "$file")" + _endor_mkdir_owned "$(dirname "$file")" "$owner" "$group" # Strip any existing Endor block, preserving everything else if [[ -f "$file" ]] && grep -qF "$ENDOR_BLOCK_START" "$file" 2>/dev/null; then @@ -159,6 +191,13 @@ upsert_block() { mv "$tmp" "$file" fi + # A file without a trailing newline would glue our start marker onto its last + # line, and the substring-based strip on re-run/remove would then delete that + # user line. Terminate it first so the marker always gets its own line. + if [[ -s "$file" && -n "$(tail -c 1 "$file")" ]]; then + echo >> "$file" + fi + printf '%s\n%s\n%s\n' \ "$ENDOR_BLOCK_START" \ "$content" \ @@ -216,7 +255,7 @@ upsert_block_pip() { return 0 fi - mkdir -p "$(dirname "$file")" + _endor_mkdir_owned "$(dirname "$file")" "$owner" "$group" if [[ "$merge" == "1" ]]; then key_pattern=$(printf '%s\n' "$content" | awk ' @@ -301,77 +340,195 @@ upsert_block_pip() { chmod 600 "$file" } +# ── Maven XML helpers ───────────────────────────────────────────────────────── +# Support upsert_xml_block / remove_xml_block. Multi-line content is read from a +# temp file with getline rather than `awk -v`, because BSD/macOS awk rejects a +# multi-line -v value ("newline in string"). Awk built-in names (sub, close, index, +# length, split…) must NOT be used as awk variables — the injected block is held in +# `blk` and the closing tag in `closetag`. + +# _endor_xml_extract (fragment on stdin) -> inclusive block on stdout +# Emits the sentinel-delimited entry (the markers included) so it can be merged +# as-is into an existing container and located again on re-run/removal. +_endor_xml_extract() { + awk -v s="$1" -v e="$2" ' + index($0,s){grab=1} + grab{print} + index($0,e){grab=0} + ' +} + +# _endor_xml_strip_managed -> stdout with every managed region removed +# (the per-entry server/mirror sub-blocks AND the legacy combined block). +_endor_xml_strip_managed() { + awk -v ss="$ENDOR_XML_SERVER_START" -v se="$ENDOR_XML_SERVER_END" \ + -v ms="$ENDOR_XML_MIRROR_START" -v me="$ENDOR_XML_MIRROR_END" \ + -v bs="$ENDOR_XML_BLOCK_START" -v be="$ENDOR_XML_BLOCK_END" ' + index($0,ss){skip=1} index($0,ms){skip=1} index($0,bs){skip=1} + !skip{print} + index($0,se){skip=0} index($0,me){skip=0} index($0,be){skip=0} + ' "$1" +} + +# _endor_xml_into_container +# Inserts as the FIRST child of an existing container (so an Endor +# catch-all mirror wins Maven precedence). Prints result; rc 0 = injected, +# rc 1 = container absent (caller must create it). Handles the multi-line form +# (), the single-line empty form (), +# and the self-closed form (). +_endor_xml_into_container() { + local file="$1" open="$2" closetag="$3" selfclose="$4" subfile="$5" + if grep -qF "$open" "$file"; then + awk -v openlit="$open" -v subfile="$subfile" ' + BEGIN{ while((getline l < subfile)>0) blk=(blk==""?l:blk RS l); close(subfile) } + !done && (p=index($0,openlit)) { + print substr($0,1,p+length(openlit)-1); print blk + rest=substr($0,p+length(openlit)); if(rest!="") print rest + done=1; next + } + { print } + ' "$file" + return 0 + elif grep -qF "$selfclose" "$file"; then + awk -v sclit="$selfclose" -v opentag="$open" -v closetag="$closetag" -v subfile="$subfile" ' + BEGIN{ while((getline l < subfile)>0) blk=(blk==""?l:blk RS l); close(subfile) } + !done && (p=index($0,sclit)) { + print substr($0,1,p-1) opentag; print blk + print closetag substr($0,p+length(sclit)) + done=1; next + } + { print } + ' "$file" + return 0 + fi + return 1 +} + +# _endor_xml_insert_ordered ... +# Inserts before the first "later-ordered" sibling (per Maven's schema +# sequence servers→mirrors→profiles→activeProfiles→pluginGroups), else before +# . Keeps a newly-created container in its schema-valid position. +_endor_xml_insert_ordered() { + local file="$1" subfile="$2"; shift 2 + awk -v subfile="$subfile" -v laters="$*" ' + BEGIN{ while((getline l < subfile)>0) blk=(blk==""?l:blk RS l); close(subfile); n=split(laters,arr," ") } + !done { for(i=1;i<=n;i++) if(index($0,arr[i])){ print blk; done=1; break } } + !done && index($0,""){ print blk; done=1 } + { print } + ' "$file" +} + # upsert_xml_block # -# Idempotent writer for an XML settings file (Maven ~/.m2/settings.xml). -# Inserts an XML-comment-delimited immediately BEFORE the closing -# tag, so it always lands inside the root element. -# - File absent → create a minimal settings.xml wrapping the fragment -# - File present, has block → replace only the delimited fragment -# - File present, no block → insert fragment just before -# - DRY_RUN=1 → print intent, write nothing +# Idempotent, MERGE-AWARE writer for Maven ~/.m2/settings.xml. The +# (shared/blocks/mavensettings.txt) carries two sentinel-delimited entries — a +# and a . Maven's schema forbids a second /, so +# each entry is merged into whichever container already exists; a container is +# created only when it is absent. +# - File absent or empty → create a minimal settings.xml with both containers +# - File has no → ERROR (rc 1): corrupt file, cannot merge safely +# - present → insert our as its first child +# - absent → create (in schema order) wrapping our entry +# - present/absent → same treatment for our +# - Re-run → prior managed entries (new or legacy) stripped first +# - DRY_RUN=1 → print intent, write nothing upsert_xml_block() { - local file="$1" fragment="$2" owner="$3" group="$4" tmp + local file="$1" fragment="$2" owner="$3" group="$4" tmp cf sf_server sf_mirror + + # Pull each sentinel-delimited entry out of the fragment. Each keeps its own + # markers so it can be merged into an existing container and found again later. + sf_server=$(mktemp); sf_mirror=$(mktemp) + printf '%s\n' "$fragment" | _endor_xml_extract "$ENDOR_XML_SERVER_START" "$ENDOR_XML_SERVER_END" > "$sf_server" + printf '%s\n' "$fragment" | _endor_xml_extract "$ENDOR_XML_MIRROR_START" "$ENDOR_XML_MIRROR_END" > "$sf_mirror" + + # The merge path needs an anchor line (existing container, later sibling, or + # ) and would otherwise emit nothing — silently leaving the + # firewall unconfigured. So: a file that exists but is empty/whitespace-only + # is treated as absent (create from scratch); a non-empty file with no + # cannot be merged into safely and must fail loudly instead of + # reporting success. + local create=0 + if [[ ! -f "$file" ]] || ! grep -q '[^[:space:]]' "$file" 2>/dev/null; then + create=1 + elif ! grep -qF '' "$file" 2>/dev/null; then + if [[ "${DRY_RUN:-0}" == "1" ]]; then + echo "[dry-run] action : FAIL — malformed settings.xml (no ); fix or delete it, then re-run" + echo "[dry-run] file : $file" + echo "" + rm -f "$sf_server" "$sf_mirror" + return 0 + fi + echo "[endor] ERROR: $file exists but contains no — cannot merge safely." >&2 + echo "[endor] Maven firewall NOT configured. Fix or delete the file, then re-run." >&2 + rm -f "$sf_server" "$sf_mirror" + return 1 + fi if [[ "${DRY_RUN:-0}" == "1" ]]; then - if [[ -f "$file" ]] && grep -qF "$ENDOR_XML_BLOCK_START" "$file" 2>/dev/null; then - echo "[dry-run] action : REPLACE Endor block in settings.xml" - elif [[ -f "$file" ]]; then - echo "[dry-run] action : INSERT Endor block before " + if [[ "$create" == "1" ]]; then + echo "[dry-run] action : CREATE settings.xml with + " else - echo "[dry-run] action : CREATE settings.xml with Endor block" + if grep -qF "" "$file" 2>/dev/null; then + echo "[dry-run] action : MERGE into existing " + else + echo "[dry-run] action : CREATE with Endor " + fi + if grep -qF "" "$file" 2>/dev/null; then + echo "[dry-run] action : MERGE into existing (inserted first)" + else + echo "[dry-run] action : CREATE with Endor " + fi fi echo "[dry-run] file : $file" echo "$fragment" | sed 's/^/[dry-run] /' echo "" + rm -f "$sf_server" "$sf_mirror" return 0 fi - mkdir -p "$(dirname "$file")" + _endor_mkdir_owned "$(dirname "$file")" "$owner" "$group" - # Case 1: file does not exist -> write a complete minimal settings.xml - if [[ ! -f "$file" ]]; then + # Case 1: file absent (or empty/whitespace-only) -> minimal settings.xml + # with both containers, in order. + if [[ "$create" == "1" ]]; then { echo '' echo '' - echo "$fragment" + echo ' '; cat "$sf_server"; echo ' ' + echo ' '; cat "$sf_mirror"; echo ' ' echo '' } > "$file" chown "$owner:$group" "$file"; chmod 600 "$file" + rm -f "$sf_server" "$sf_mirror" return 0 fi - # Case 2: existing Endor block -> strip it first (preserve the rest) - if grep -qF "$ENDOR_XML_BLOCK_START" "$file" 2>/dev/null; then - tmp=$(mktemp) - awk -v s="$ENDOR_XML_BLOCK_START" -v e="$ENDOR_XML_BLOCK_END" ' - index($0, s) { skip=1; next } - index($0, e) { skip=0; next } - !skip { print } - ' "$file" > "$tmp" + # Case 2: strip any prior managed entries so re-runs stay idempotent. + tmp=$(mktemp); _endor_xml_strip_managed "$file" > "$tmp"; mv "$tmp" "$file" + + # Case 3a: merge the entry (create only if absent). + tmp=$(mktemp) + if _endor_xml_into_container "$file" "" "" "" "$sf_server" > "$tmp"; then mv "$tmp" "$file" + else + cf=$(mktemp); { echo ' '; cat "$sf_server"; echo ' '; } > "$cf" + _endor_xml_insert_ordered "$file" "$cf" " "$tmp" + mv "$tmp" "$file"; rm -f "$cf" fi - # Case 3: insert the fresh fragment immediately before the first . - # The fragment is passed via a temp file and read with getline rather than - # `awk -v frag=...`, because BSD/macOS awk rejects a multi-line value in -v - # ("newline in string"). getline-from-file is portable across BSD and GNU awk. - local fragfile; fragfile=$(mktemp) - printf '%s\n' "$fragment" > "$fragfile" + # Case 3b: merge the entry (create only if absent). tmp=$(mktemp) - awk -v fragfile="$fragfile" ' - /<\/settings>/ && !done { - while ((getline line < fragfile) > 0) print line - close(fragfile) - done=1 - } - { print } - ' "$file" > "$tmp" - mv "$tmp" "$file" - rm -f "$fragfile" + if _endor_xml_into_container "$file" "" "" "" "$sf_mirror" > "$tmp"; then + mv "$tmp" "$file" + else + cf=$(mktemp); { echo ' '; cat "$sf_mirror"; echo ' '; } > "$cf" + _endor_xml_insert_ordered "$file" "$cf" " "$tmp" + mv "$tmp" "$file"; rm -f "$cf" + fi + rm -f "$sf_server" "$sf_mirror" chown "$owner:$group" "$file"; chmod 600 "$file" } @@ -444,37 +601,37 @@ remove_block() { } # remove_xml_block -# Strips the Endor XML fragment from settings.xml. If the file is left with an -# empty element (i.e. it was Endor-only), the whole file is deleted. +# Strips the Endor managed entries (new per-entry server/mirror sub-blocks AND the +# legacy combined block) from settings.xml. Everything else — pre-existing +# / entries, any other user settings, and the containers/skeleton +# themselves — is always preserved; the file is never deleted. Content outside our +# sentinels may be the user's, and an emptied / +# left behind is harmless, valid XML. remove_xml_block() { local file="$1" owner="$2" group="$3" tmp [[ -f "$file" ]] || { echo "[endor-remove] skip (not found) : $file"; return 0; } - if ! grep -qF "$ENDOR_XML_BLOCK_START" "$file" 2>/dev/null; then + if ! grep -qF "$ENDOR_XML_SERVER_START" "$file" 2>/dev/null \ + && ! grep -qF "$ENDOR_XML_MIRROR_START" "$file" 2>/dev/null \ + && ! grep -qF "$ENDOR_XML_BLOCK_START" "$file" 2>/dev/null; then echo "[endor-remove] skip (no Endor block): $file"; return 0 fi if [[ "${DRY_RUN:-0}" == "1" ]]; then - echo "[dry-run] action : REMOVE Endor block from settings.xml" + echo "[dry-run] action : REMOVE Endor / from settings.xml" echo "[dry-run] file : $file" return 0 fi tmp=$(mktemp) - awk -v s="$ENDOR_XML_BLOCK_START" -v e="$ENDOR_XML_BLOCK_END" ' - index($0, s) { skip=1; next } - index($0, e) { skip=0; next } - !skip { print } - ' "$file" > "$tmp" - - # If only the empty XML scaffold remains, the file was Endor-only -> delete it - if ! grep -qE '<(server|mirror|profile|proxy|pluginGroup|repository)' "$tmp"; then - rm -f "$file" "$tmp" - echo "[endor-remove] deleted (was empty) : $file" - else - mv "$tmp" "$file"; chown "$owner:$group" "$file"; chmod 600 "$file" - echo "[endor-remove] block removed : $file" - fi + _endor_xml_strip_managed "$file" > "$tmp" + + # Never delete the file: anything outside our sentinels is (or may be) the + # user's own configuration, and guessing "is this only our skeleton?" risks + # destroying it. The emptied containers we may leave behind are valid XML + # and a no-op for Maven. + mv "$tmp" "$file"; chown "$owner:$group" "$file"; chmod 600 "$file" + echo "[endor-remove] block removed : $file" } # warn_if_key_conflict