Skip to content

Commit c2d6015

Browse files
committed
Report policy exceptions back to the caller
Fixes: QubesOS/qubes-issues#10746
1 parent 8ec5286 commit c2d6015

6 files changed

Lines changed: 90 additions & 31 deletions

File tree

qrexec/client.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@
2121
from typing import Optional
2222

2323
from .utils import prepare_subprocess_kwds
24+
# The PolicyAdmin*Exception is used by issubclass(..., PolicyAdminException).
25+
# pylint: disable=unused-import
26+
from .policy.admin import (
27+
PolicyAdminException,
28+
PolicyAdminTokenException,
29+
PolicyAdminFileNotFoundException,
30+
PolicyAdminProtocolException,
31+
PolicyAdminSyntaxException,
32+
PolicyAdminInvalidFileNameException,
33+
PolicyAdminInvalidFilePathException,
34+
)
2435

2536
QREXEC_CLIENT_DOM0 = "/usr/bin/qrexec-client"
2637
QREXEC_CLIENT_VM = "/usr/bin/qrexec-client-vm"
@@ -45,11 +56,22 @@ def call(dest: str, rpcname: str, arg: Optional[str] = None, *, payload=None):
4556
:param str or None arg: argument of the call
4657
:param str or bytes or file or None payload: an payload to the qrexec call
4758
:rtype: str
48-
:raises subprocess.CalledProcessError: on failure
59+
:raises subprocess.CalledProcessError: on unexpected failure, instance of \
60+
PolicyAdminException otherwise
4961
"""
5062
command = make_command(dest=dest, rpcname=rpcname, arg=arg)
5163
kwds = prepare_subprocess_kwds(payload, for_popen=False)
52-
return subprocess.check_output(command, **kwds).decode()
64+
try:
65+
return subprocess.check_output(command, **kwds).decode()
66+
except subprocess.CalledProcessError as exc:
67+
stderr_exc = exc.stderr.decode()
68+
stderr_exc_type = stderr_exc.split(" ")[0]
69+
if stderr_exc_type in globals():
70+
exception = globals()[stderr_exc_type]
71+
if issubclass(exception, PolicyAdminException):
72+
stderr_exc_msg = stderr_exc[len(exception.__name__ + " ") :]
73+
raise exception(stderr_exc_msg) from exc
74+
raise
5375

5476

5577
async def call_async(dest, rpcname, arg=None, *, payload=None):

qrexec/policy/admin.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,44 @@ class PolicyAdminException(Exception):
3838
"""
3939

4040

41-
class PolicyAdminTokenException(Exception):
41+
class PolicyAdminTokenException(PolicyAdminException):
4242
"""
4343
A token check exception, indicating that a file is in unexpected state.
4444
"""
4545

4646

47+
class PolicyAdminFileNotFoundException(PolicyAdminException):
48+
"""
49+
Policy cannot be found.
50+
"""
51+
52+
53+
class PolicyAdminProtocolException(PolicyAdminException):
54+
"""
55+
Client sent an invalid request that it should have known to not send in the
56+
first place.
57+
"""
58+
59+
60+
class PolicyAdminSyntaxException(PolicyAdminProtocolException):
61+
"""
62+
Client sent an invalid policy. That it should have known to not send in the
63+
first place. Clients should validate the policy before sending them.
64+
"""
65+
66+
67+
class PolicyAdminInvalidFileNameException(PolicyAdminProtocolException):
68+
"""
69+
Client sent a policy with invalid name.
70+
"""
71+
72+
73+
class PolicyAdminInvalidFilePathException(PolicyAdminProtocolException):
74+
"""
75+
Client sent a policy using a path out of bounds.
76+
"""
77+
78+
4779
def method(service_name, *, no_arg=False, no_payload=False):
4880
def decorator(func):
4981
func.api_service_name = service_name
@@ -89,29 +121,29 @@ def handle_request(
89121
"""
90122

91123
if not all(char in RPCNAME_ALLOWED_CHARSET for char in arg):
92-
raise PolicyAdminException(
124+
raise PolicyAdminProtocolException(
93125
'Invalid argument: "{}"\n'
94126
"Valid characters are letters, numbers, dot, plus, hyphen and "
95127
"underline".format(arg)
96128
)
97129

98130
func = self._find_method(service_name)
99131
if not func:
100-
raise PolicyAdminException(
132+
raise PolicyAdminProtocolException(
101133
"unrecognized method: {}".format(service_name)
102134
)
103135

104136
args = []
105137

106138
if func.api_no_arg:
107139
if arg != "":
108-
raise PolicyAdminException("Unexpected argument")
140+
raise PolicyAdminProtocolException("Unexpected argument")
109141
else:
110142
args.append(arg)
111143

112144
if func.api_no_payload:
113145
if payload != b"":
114-
raise PolicyAdminException("Unexpected payload")
146+
raise PolicyAdminProtocolException("Unexpected payload")
115147
else:
116148
args.append(payload)
117149

@@ -178,7 +210,7 @@ def policy_include_get(self, arg):
178210

179211
def _common_get(self, path: Path) -> bytes:
180212
if not path.is_file():
181-
raise PolicyAdminException("Not found: {}".format(path))
213+
raise PolicyAdminFileNotFoundException("Not found: {}".format(path))
182214

183215
data = path.read_bytes()
184216
token = compute_token(data)
@@ -198,7 +230,7 @@ def policy_include_replace(self, arg, payload):
198230

199231
def _common_replace(self, path: Path, payload: bytes) -> bytes:
200232
if b"\n" not in payload:
201-
raise PolicyAdminException(
233+
raise PolicyAdminProtocolException(
202234
"Payload needs to include first line with token"
203235
)
204236
token, data = payload.split(b"\n", 1)
@@ -234,7 +266,7 @@ def _common_remove(self, path: Path, payload: str) -> None:
234266
self._check_token(payload, path)
235267

236268
if not path.is_file():
237-
raise PolicyAdminException("Not found: {}".format(path))
269+
raise PolicyAdminFileNotFoundException("Not found: {}".format(path))
238270

239271
self._validate(path, None)
240272

@@ -245,10 +277,10 @@ def _common_remove(self, path: Path, payload: str) -> None:
245277
@method("policy.GetFiles", no_payload=True)
246278
def policy_get_files(self, arg):
247279
if not isinstance(arg, str) or not arg:
248-
raise PolicyAdminException("Service cannot be empty.")
280+
raise PolicyAdminProtocolException("Service cannot be empty.")
249281
invalid_chars = get_invalid_characters(arg, disallowed="+")
250282
if invalid_chars:
251-
raise PolicyAdminException(
283+
raise PolicyAdminProtocolException(
252284
"Service {!r} contains invalid characters: {!r}".format(
253285
arg, invalid_chars
254286
)
@@ -276,15 +308,15 @@ def policy_get_files(self, arg):
276308

277309
def _get_path(self, arg: str, dir_path: str, suffix: str) -> Path:
278310
if not re.compile(r"^[\w-]+$").match(arg):
279-
raise PolicyAdminException(
311+
raise PolicyAdminInvalidFileNameException(
280312
f"Invalid policy file name: {arg}\n"
281313
"Names must contain only alphanumeric characters, "
282314
"underscore and hyphen."
283315
)
284316
path = dir_path / (arg + suffix)
285317
path = path.resolve()
286318
if path.parent != dir_path:
287-
raise PolicyAdminException(
319+
raise PolicyAdminInvalidFilePathException(
288320
"Expecting a path inside {}".format(dir_path)
289321
)
290322

@@ -296,7 +328,7 @@ def _validate(self, path: Path, content: Optional[str]):
296328
policy_path=self.policy_path, overrides={path: content}
297329
)
298330
except PolicySyntaxError as exc:
299-
raise PolicyAdminException(
331+
raise PolicyAdminSyntaxException(
300332
"Policy change validation failed: {}".format(exc)
301333
) from exc
302334

@@ -312,7 +344,7 @@ def _check_token(self, token: bytes, path: Path):
312344
return
313345

314346
if not token.startswith(b"sha256:"):
315-
raise PolicyAdminException("Unrecognized token")
347+
raise PolicyAdminProtocolException("Unrecognized token")
316348

317349
if not path.exists():
318350
raise PolicyAdminTokenException(

qrexec/tools/qubes_policy.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from ..policy.admin_client import PolicyClient
2929
from .. import RPCNAME_ALLOWED_CHARSET
30+
from ..policy.admin import PolicyAdminException
3031

3132
parser = argparse.ArgumentParser(
3233
usage="qubes-policy {[-l]|-g|-r|-d} [include/][RPCNAME[+ARGUMENT]]"
@@ -131,10 +132,13 @@ def main(args=None):
131132
client=client,
132133
)
133134
except subprocess.CalledProcessError as e:
134-
print("Command failed")
135+
print("Command failed", file=sys.stderr)
135136
output = e.output.decode().rstrip()
136137
if output:
137-
print(output)
138+
print(output, file=sys.stderr)
139+
sys.exit(1)
140+
except PolicyAdminException as e:
141+
print(e, file=sys.stderr)
138142
sys.exit(1)
139143

140144

qrexec/tools/qubes_policy_admin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from ..policy.admin import (
3131
PolicyAdmin,
3232
PolicyAdminException,
33-
PolicyAdminTokenException,
3433
)
3534
from .. import POLICYPATH
3635

@@ -56,7 +55,7 @@ def main():
5655
admin = PolicyAdmin(POLICYPATH)
5756
try:
5857
response = admin.handle_request(service_name, argument, payload)
59-
except (PolicyAdminException, PolicyAdminTokenException) as exc:
58+
except PolicyAdminException as exc:
6059
logging.warning(
6160
"%s+%s (%s): error: %s", service_name, argument, source, exc
6261
)

qrexec/tools/qubes_policy_editor.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
import sys
3030
import tempfile
3131
from ..policy.admin_client import PolicyClient
32-
from .. import RPCNAME_ALLOWED_CHARSET, POLICYPATH, INCLUDEPATH
32+
from ..policy.admin import (
33+
PolicyAdminException,
34+
PolicyAdminFileNotFoundException,
35+
)
36+
from .. import RPCNAME_ALLOWED_CHARSET
3337

3438

3539
def validate_name(name):
@@ -68,25 +72,22 @@ def manage_policy(self) -> None:
6872
# not found, ignore, else abort as the request was refused.
6973
file_exists = False
7074
if self.is_include:
71-
wanted_path = str(INCLUDEPATH) + "/" + self.policy + "\n"
7275
suffix = "_include_" + self.policy
7376
else:
74-
wanted_path = str(POLICYPATH) + "/" + self.policy + ".policy\n"
7577
suffix = "_" + self.policy + ".policy"
7678

7779
try:
7880
original_content, token = client.policy_get(
7981
policy=self.policy, is_include=self.is_include
8082
)
8183
file_exists = True
84+
except PolicyAdminFileNotFoundException:
85+
pass
8286
except subprocess.CalledProcessError as exc:
83-
not_found = "Not found: " + wanted_path
84-
if exc.output.decode() != not_found:
85-
print(
86-
f"Failed to get policy {self.policy!r}: {exc}",
87-
file=sys.stderr,
88-
)
89-
sys.exit(1)
87+
print(
88+
f"Failed to get policy {self.policy!r}: {exc}", file=sys.stderr
89+
)
90+
sys.exit(1)
9091

9192
# pylint: disable=consider-using-with
9293
tmpfile = tempfile.NamedTemporaryFile(suffix=suffix)
@@ -112,7 +113,7 @@ def manage_policy(self) -> None:
112113
token=token,
113114
is_include=self.is_include,
114115
)
115-
except subprocess.CalledProcessError as exc:
116+
except PolicyAdminException as exc:
116117
print(
117118
f"Failed to replace policy {self.policy!r} with file {tmpfile.name!r}: "
118119
f"{exc}",

qrexec/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,5 @@ def prepare_subprocess_kwds(
195195
# XXX this breaks on file-like objects that don't have .fileno
196196
kwds["stdin"] = payload
197197
kwds["input"] = None
198+
kwds["stderr"] = subprocess.PIPE
198199
return kwds

0 commit comments

Comments
 (0)