|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Minimal policy helpers for SOLEN |
| 3 | +# Looks for a YAML policy file and answers allow/deny queries. |
| 4 | +# |
| 5 | +# Policy resolution precedence (first found wins): |
| 6 | +# 1) $SOLEN_POLICY (set to /dev/null to disable policy) |
| 7 | +# 2) $HOME/.serverutils/policy.yaml |
| 8 | +# 3) /etc/solen/policy.yaml |
| 9 | +# |
| 10 | +# Behavior: |
| 11 | +# - If no policy file is present, all checks allow by default (return 0). |
| 12 | +# - If a list exists under allow.* for a given check, membership is required. |
| 13 | +# - deny.* lists override and force a deny when matched. |
| 14 | + |
| 15 | +__SOLEN_POLICY_LOADED=0 |
| 16 | +__SOLEN_POLICY_FILE="" |
| 17 | +declare -a _SOLEN_ALLOW_TOKENS |
| 18 | +declare -a _SOLEN_ALLOW_SERV_RESTART |
| 19 | +declare -a _SOLEN_ALLOW_PATHS_PRUNE |
| 20 | +declare -a _SOLEN_DENY_SERV_RESTART |
| 21 | +declare -a _SOLEN_DENY_PATHS_PRUNE |
| 22 | + |
| 23 | +solen__policy_path() { |
| 24 | + local p="" |
| 25 | + if [[ -n "${SOLEN_POLICY:-}" ]]; then |
| 26 | + [[ "${SOLEN_POLICY}" == "/dev/null" ]] && echo "" && return 0 |
| 27 | + [[ -r "${SOLEN_POLICY}" ]] && echo "${SOLEN_POLICY}" && return 0 |
| 28 | + fi |
| 29 | + if [[ -r "${HOME}/.serverutils/policy.yaml" ]]; then |
| 30 | + echo "${HOME}/.serverutils/policy.yaml"; return 0 |
| 31 | + fi |
| 32 | + if [[ -r "/etc/solen/policy.yaml" ]]; then |
| 33 | + echo "/etc/solen/policy.yaml"; return 0 |
| 34 | + fi |
| 35 | + echo "" |
| 36 | +} |
| 37 | + |
| 38 | +solen__arr_contains() { # arrname value |
| 39 | + local __arr_name="$1"; shift |
| 40 | + local needle="$1"; shift || true |
| 41 | + local v |
| 42 | + # shellcheck disable=SC1087,SC2128 |
| 43 | + for v in ${!__arr_name[@]}; do :; done >/dev/null 2>&1 || true |
| 44 | + # iterate using indirect expansion |
| 45 | + local -n __arr_ref="${__arr_name}" |
| 46 | + for v in "${__arr_ref[@]}"; do |
| 47 | + [[ "$v" == "$needle" ]] && return 0 |
| 48 | + done |
| 49 | + return 1 |
| 50 | +} |
| 51 | + |
| 52 | +solen__path_has_prefix() { # haystack_path candidate_prefix |
| 53 | + local path="$1"; local pref="$2" |
| 54 | + [[ -z "$path" || -z "$pref" ]] && return 1 |
| 55 | + case "$path" in |
| 56 | + "$pref"|"$pref"/*) return 0 ;; |
| 57 | + esac |
| 58 | + return 1 |
| 59 | +} |
| 60 | + |
| 61 | +solen__read_inline_list() { # input like: key: ["a", "b", c] |
| 62 | + local line="$1" |
| 63 | + local inside="${line#*[}" |
| 64 | + inside="${inside%]*}" |
| 65 | + # split by comma |
| 66 | + local IFS=, |
| 67 | + read -r -a parts <<< "$inside" |
| 68 | + for it in "${parts[@]}"; do |
| 69 | + it="${it## }"; it="${it%% }" |
| 70 | + it="${it%\r}" |
| 71 | + it="${it%\n}" |
| 72 | + it="${it%\,}" |
| 73 | + it="${it#\"}"; it="${it%\"}" |
| 74 | + it="${it#\'}"; it="${it%\'}" |
| 75 | + [[ -n "$it" ]] && printf '%s\n' "$it" |
| 76 | + done |
| 77 | +} |
| 78 | + |
| 79 | +solen__policy_load_awk() { |
| 80 | + local f="$1" |
| 81 | + local mode="" sub="" |
| 82 | + while IFS= read -r line; do |
| 83 | + # normalize tabs |
| 84 | + case "$line" in $'\t'*) line="${line//$'\t'/ }" ;; esac |
| 85 | + # state transitions |
| 86 | + if [[ "$line" =~ ^[[:space:]]*allow: ]]; then mode="allow"; sub=""; continue; fi |
| 87 | + if [[ "$line" =~ ^[[:space:]]*deny: ]]; then mode="deny"; sub=""; continue; fi |
| 88 | + if [[ "$line" =~ ^[[:space:]]*services: ]]; then sub="services"; continue; fi |
| 89 | + if [[ "$line" =~ ^[[:space:]]*paths: ]]; then sub="paths"; continue; fi |
| 90 | + if [[ "$line" =~ ^[[:space:]]*tokens: ]]; then |
| 91 | + if [[ "$line" =~ \[.*\] ]]; then |
| 92 | + while IFS= read -r item; do _SOLEN_ALLOW_TOKENS+=("$item"); done < <(solen__read_inline_list "$line") |
| 93 | + continue |
| 94 | + fi |
| 95 | + # read multi-line list that follows under allow: tokens: |
| 96 | + while IFS= read -r nxt; do |
| 97 | + [[ ! "$nxt" =~ ^[[:space:]]*-[[:space:]] ]] && { line="$nxt"; break; } |
| 98 | + nxt="${nxt#*- }"; nxt="${nxt## }"; nxt="${nxt%% }"; nxt="${nxt#\"}"; nxt="${nxt%\"}"; nxt="${nxt#\'}"; nxt="${nxt%\'}" |
| 99 | + [[ -n "$nxt" ]] && _SOLEN_ALLOW_TOKENS+=("$nxt") |
| 100 | + done |
| 101 | + continue |
| 102 | + fi |
| 103 | + # services.restart / services.enable |
| 104 | + if [[ "$mode" == "allow" && "$sub" == "services" && "$line" =~ ^[[:space:]]*restart: ]]; then |
| 105 | + if [[ "$line" =~ \[.*\] ]]; then |
| 106 | + while IFS= read -r item; do _SOLEN_ALLOW_SERV_RESTART+=("$item"); done < <(solen__read_inline_list "$line") |
| 107 | + else |
| 108 | + while IFS= read -r nxt; do |
| 109 | + [[ ! "$nxt" =~ ^[[:space:]]*-[[:space:]] ]] && { line="$nxt"; break; } |
| 110 | + nxt="${nxt#*- }"; nxt="${nxt## }"; nxt="${nxt%% }"; nxt="${nxt#\"}"; nxt="${nxt%\"}"; nxt="${nxt#\'}"; nxt="${nxt%\'}" |
| 111 | + [[ -n "$nxt" ]] && _SOLEN_ALLOW_SERV_RESTART+=("$nxt") |
| 112 | + done |
| 113 | + fi |
| 114 | + continue |
| 115 | + fi |
| 116 | + # paths.prune |
| 117 | + if [[ "$sub" == "paths" && "$line" =~ ^[[:space:]]*prune: ]]; then |
| 118 | + local arrname="_SOLEN_ALLOW_PATHS_PRUNE" |
| 119 | + if [[ "$mode" == "deny" ]]; then arrname="_SOLEN_DENY_PATHS_PRUNE"; fi |
| 120 | + if [[ "$line" =~ \[.*\] ]]; then |
| 121 | + while IFS= read -r item; do eval "$arrname+=(\"\$item\")"; done < <(solen__read_inline_list "$line") |
| 122 | + else |
| 123 | + while IFS= read -r nxt; do |
| 124 | + [[ ! "$nxt" =~ ^[[:space:]]*-[[:space:]] ]] && { line="$nxt"; break; } |
| 125 | + nxt="${nxt#*- }"; nxt="${nxt## }"; nxt="${nxt%% }"; nxt="${nxt#\"}"; nxt="${nxt%\"}"; nxt="${nxt#\'}"; nxt="${nxt%\'}" |
| 126 | + [[ -n "$nxt" ]] && eval "$arrname+=(\"\$nxt\")" |
| 127 | + done |
| 128 | + fi |
| 129 | + continue |
| 130 | + fi |
| 131 | + # deny.services.restart |
| 132 | + if [[ "$mode" == "deny" && "$sub" == "services" && "$line" =~ ^[[:space:]]*restart: ]]; then |
| 133 | + if [[ "$line" =~ \[.*\] ]]; then |
| 134 | + while IFS= read -r item; do _SOLEN_DENY_SERV_RESTART+=("$item"); done < <(solen__read_inline_list "$line") |
| 135 | + else |
| 136 | + while IFS= read -r nxt; do |
| 137 | + [[ ! "$nxt" =~ ^[[:space:]]*-[[:space:]] ]] && { line="$nxt"; break; } |
| 138 | + nxt="${nxt#*- }"; nxt="${nxt## }"; nxt="${nxt%% }"; nxt="${nxt#\"}"; nxt="${nxt%\"}"; nxt="${nxt#\'}"; nxt="${nxt%\'}" |
| 139 | + [[ -n "$nxt" ]] && _SOLEN_DENY_SERV_RESTART+=("$nxt") |
| 140 | + done |
| 141 | + fi |
| 142 | + continue |
| 143 | + fi |
| 144 | + done < "$f" |
| 145 | +} |
| 146 | + |
| 147 | +solen__policy_load() { |
| 148 | + [[ $__SOLEN_POLICY_LOADED -eq 1 ]] && return 0 |
| 149 | + __SOLEN_POLICY_FILE="$(solen__policy_path)" |
| 150 | + if [[ -z "$__SOLEN_POLICY_FILE" || ! -r "$__SOLEN_POLICY_FILE" ]]; then |
| 151 | + __SOLEN_POLICY_LOADED=1; return 0 |
| 152 | + fi |
| 153 | + # Clear arrays |
| 154 | + _SOLEN_ALLOW_TOKENS=() |
| 155 | + _SOLEN_ALLOW_SERV_RESTART=() |
| 156 | + _SOLEN_ALLOW_PATHS_PRUNE=() |
| 157 | + _SOLEN_DENY_SERV_RESTART=() |
| 158 | + _SOLEN_DENY_PATHS_PRUNE=() |
| 159 | + if command -v yq >/dev/null 2>&1; then |
| 160 | + # allow tokens |
| 161 | + while IFS= read -r t; do [[ -n "$t" && "$t" != "null" ]] && _SOLEN_ALLOW_TOKENS+=("$t"); done < <(yq -r '.allow.tokens[]? // empty' "$__SOLEN_POLICY_FILE" 2>/dev/null) |
| 162 | + # services.restart allow/deny |
| 163 | + while IFS= read -r s; do [[ -n "$s" && "$s" != "null" ]] && _SOLEN_ALLOW_SERV_RESTART+=("$s"); done < <(yq -r '.allow.services.restart[]? // empty' "$__SOLEN_POLICY_FILE" 2>/dev/null) |
| 164 | + while IFS= read -r s; do [[ -n "$s" && "$s" != "null" ]] && _SOLEN_DENY_SERV_RESTART+=("$s"); done < <(yq -r '.deny.services.restart[]? // empty' "$__SOLEN_POLICY_FILE" 2>/dev/null) |
| 165 | + # paths.prune allow/deny |
| 166 | + while IFS= read -r p; do [[ -n "$p" && "$p" != "null" ]] && _SOLEN_ALLOW_PATHS_PRUNE+=("$p"); done < <(yq -r '.allow.paths.prune[]? // empty' "$__SOLEN_POLICY_FILE" 2>/dev/null) |
| 167 | + while IFS= read -r p; do [[ -n "$p" && "$p" != "null" ]] && _SOLEN_DENY_PATHS_PRUNE+=("$p"); done < <(yq -r '.deny.paths.prune[]? // empty' "$__SOLEN_POLICY_FILE" 2>/dev/null) |
| 168 | + else |
| 169 | + solen__policy_load_awk "$__SOLEN_POLICY_FILE" |
| 170 | + fi |
| 171 | + __SOLEN_POLICY_LOADED=1 |
| 172 | +} |
| 173 | + |
| 174 | +solen_policy_allows_token() { # token |
| 175 | + local token="$1" |
| 176 | + solen__policy_load |
| 177 | + # No policy => allow |
| 178 | + if [[ -z "$__SOLEN_POLICY_FILE" ]]; then return 0; fi |
| 179 | + # If allow list is empty or missing, allow |
| 180 | + local count=${#_SOLEN_ALLOW_TOKENS[@]} |
| 181 | + if (( count == 0 )); then return 0; fi |
| 182 | + solen__arr_contains _SOLEN_ALLOW_TOKENS "$token" |
| 183 | +} |
| 184 | + |
| 185 | +solen_policy_allows_service_restart() { # service |
| 186 | + local svc="$1" |
| 187 | + solen__policy_load |
| 188 | + if [[ -z "$__SOLEN_POLICY_FILE" ]]; then return 0; fi |
| 189 | + # deny overrides |
| 190 | + if solen__arr_contains _SOLEN_DENY_SERV_RESTART "$svc"; then return 1; fi |
| 191 | + local count=${#_SOLEN_ALLOW_SERV_RESTART[@]} |
| 192 | + if (( count == 0 )); then return 0; fi |
| 193 | + solen__arr_contains _SOLEN_ALLOW_SERV_RESTART "$svc" |
| 194 | +} |
| 195 | + |
| 196 | +solen_policy_allows_prune_path() { # path |
| 197 | + local path="$1" |
| 198 | + solen__policy_load |
| 199 | + if [[ -z "$__SOLEN_POLICY_FILE" ]]; then return 0; fi |
| 200 | + # deny overrides |
| 201 | + local p |
| 202 | + for p in "${_SOLEN_DENY_PATHS_PRUNE[@]}"; do |
| 203 | + if solen__path_has_prefix "$path" "$p"; then return 1; fi |
| 204 | + done |
| 205 | + local count=${#_SOLEN_ALLOW_PATHS_PRUNE[@]} |
| 206 | + if (( count == 0 )); then return 0; fi |
| 207 | + for p in "${_SOLEN_ALLOW_PATHS_PRUNE[@]}"; do |
| 208 | + if solen__path_has_prefix "$path" "$p"; then return 0; fi |
| 209 | + done |
| 210 | + return 1 |
| 211 | +} |
| 212 | + |
0 commit comments