aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/home/.local/bin/doasedit
blob: a3bdb2d80cfc8d58d8e7c819614b3489a899edb8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/bin/sh -e

help() {
  cat - >&2 <<EOF
doasedit - edit non-user-editable files with an unprivileged editor

usage: doasedit -h | -V
usage: doasedit file ...

Options:
  -h, --help     display help message and exit
  -V, --version  display version information and exit
  --             stop processing command line arguments

Environment Variables:
  DOAS_EDITOR    program used to edit files
  EDITOR         program used to edit files if DOAS_EDITOR is unset

To work properly doasedit needs to always start a new editor instance. Some
editors, graphical ones in particular, open files in previously running
instances. If so, append a command line argument to your (DOAS_)EDITOR variable
such that the editor will always start a new instance (e. g.: 'kate -n').

How it works:
Every File to be edited is duplicated to a user owned file in /tmp. The editor
is then run in user context. After closing the editor the user file replaces
the original file while preserving file attributes. All this is done using doas
as little as possible. Files are edited one after another, not all at once.
EOF
}

# Checks for syntax errors in doas' config
#
# check_doas_conf <target> <tmp_target>
#
check_doas_conf() {
  if printf '%s' "${1}" | grep -q '^/etc/doas\(\.d/.*\)\?\.conf$'; then
    while ! doas -C "${2}"; do
      printf "doasedit: Replacing '%s' would " "$file"
      printf 'introduce the above error and break doas.\n'
      printf '(E)dit again, (O)verwrite anyway, (A)bort: [E/o/a]? '
      read -r choice
      case "$choice" in
      o | O)
        return 0
        ;;
      a | A)
        return 1
        ;;
      e | E | *)
        "$editor_cmd" "$tmpfile"
        ;;
      esac
    done
  fi
  return 0
}

error() {
  printf 'doasedit: %s\n' "${@}" 1>&2
}

_exit() {
  rm -rf "$tmpdir"
  trap - EXIT HUP QUIT TERM INT ABRT
  exit "${1:-0}"
}

# no argument passed
[ "${#}" -eq 0 ] && help && exit 1

while [ "${#}" -ne 0 ]; do
  case "${1}" in
  --)
    shift
    break
    ;;
  --help | -h)
    help
    exit 0
    ;;
  --version | -V)
    printf 'doasedit version 1.0.7\n'
    exit 0
    ;;
  -*)
    printf "doasedit: invalid option: '%s'\n" "${1}"
    help
    exit 1
    ;;
  *)
    break
    ;;
  esac
done

[ "$DOAS_EDITOR" != "" ] && editor_cmd="$DOAS_EDITOR" || editor_cmd="$EDITOR"
# shellcheck disable=SC2086
if [ "$editor_cmd" = "" ]; then
  if command -v vi >/dev/null 2>&1; then
    editor_cmd='vi'
  else
    error 'no editor specified'
    exit 1
  fi
elif ! command -v "$editor_cmd" >/dev/null 2>&1; then
  error "invalid editor command: '${editor_cmd}'"
  exit 1
fi

exit_code=1
trap '_exit "${exit_code}"' EXIT
trap '_exit 130' HUP QUIT TERM INT ABRT
tmpdir="$(mktemp -dt 'doasedit-XXXXXX')"

for file; do
  unset exists readable writable
  dir="$(dirname -- "$file")"
  tmpfile="${tmpdir}/${file##*/}"
  tmpfile_copy="${tmpdir}/copy-of-${file##*/}"
  printf '' | tee "$tmpfile" >"$tmpfile_copy"
  chmod 0600 "$tmpfile" "$tmpfile_copy"

  if [ -e "$file" ]; then
    if ! [ -f "$file" ]; then
      error "${file}: not a regular file"
      continue
    fi
    # -O is not POSIX, but implemented at least in GNU, *BSD and macOS test
    if [ -O "$file" ]; then
      error "${file}: editing your own files is not permitted"
      continue
    fi
    exists=1
  elif doas [ -e "$file" ]; then
    if ! doas [ -f "$file" ]; then
      error "${file}: not a regular file"
      continue
    fi
    exists=0
  else
    # New file?
    if [ -O "$dir" ]; then
      error "${file}: creating files in your own directory is not permitted"
      continue
    elif [ -x "$dir" ] && [ -w "$dir" ]; then
      error "${file}: creating files in a user-writable directory is not permitted"
      continue
    elif ! doas [ -e "$dir" ]; then
      error "${file}: no such directory"
      continue
    # else: root-writable directory
    fi
  fi
  # If this test is true, it's an existent regular file
  if [ "$exists" != "" ]; then
    if [ -w "$file" ]; then
      writable=1
    # Check in advance to make sure that it won't fail after editing.
    elif ! doas dd status=none count=0 of=/dev/null; then
      error "unable to run 'doas dd'"
      continue
    fi
    if [ -r "$file" ]; then
      if [ "$writable" != "" ]; then
        error "${file}: editing user-readable and -writable files is not permitted"
        continue
      fi
      # Read file
      cat -- "$file" >"$tmpfile"
    # Better not suppress stderr here as there might be something of importance.
    elif ! doas cat -- "$file" >"$tmpfile"; then
      error "you are not permitted to call 'doas cat'"
      continue
    fi
    cat "$tmpfile" >"$tmpfile_copy"
  fi

  "$editor_cmd" "$tmpfile"

  check_doas_conf "$file" "$tmpfile" || continue
  if cmp -s "$tmpfile" "$tmpfile_copy"; then
    printf 'doasedit: %s: unchanged\n' "$file"
  else
    if [ "$writable" != "" ]; then
      dd status=none if="$tmpfile" of="$file"
    else
      for de_tries in 2 1 0; do
        if doas dd status=none if="$tmpfile" of="$file"; then
          break
        elif [ "$de_tries" -eq 0 ]; then
          error '3 incorrect password attempts'
          exit 1
        fi
      done
    fi
  fi

  exit_code=0
done

# vim: shiftwidth=2 tabstop=2 noexpandtab