__init__.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
  6. # Copyright (C) 2017 Marek Marczykowski-Górecki
  7. # <marmarek@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, see <http://www.gnu.org/licenses/>.
  21. import asyncio
  22. import functools
  23. class ProtocolError(AssertionError):
  24. '''Raised when something is wrong with data received'''
  25. pass
  26. class PermissionDenied(Exception):
  27. '''Raised deliberately by handlers when we decide not to cooperate'''
  28. pass
  29. def method(name, *, no_payload=False, endpoints=None):
  30. '''Decorator factory for methods intended to appear in API.
  31. The decorated method can be called from public API using a child of
  32. :py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
  33. called using remote management interface.
  34. :param str name: qrexec rpc method name
  35. :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
  36. also will not pass payload at all to the method
  37. The expected function method should have one argument (other than usual
  38. *self*), ``untrusted_payload``, which will contain the payload.
  39. .. warning::
  40. This argument has to be named such, to remind the programmer that the
  41. content of this variable is indeed untrusted.
  42. If *no_payload* is true, then the method is called with no arguments.
  43. '''
  44. def decorator(func):
  45. if no_payload:
  46. # the following assignment is needed for how closures work in Python
  47. _func = func
  48. @functools.wraps(_func)
  49. def wrapper(self, untrusted_payload, **kwargs):
  50. if untrusted_payload != b'':
  51. raise ProtocolError('unexpected payload')
  52. return _func(self, **kwargs)
  53. func = wrapper
  54. # pylint: disable=protected-access
  55. if endpoints is None:
  56. func._rpcname = ((name, None),)
  57. else:
  58. func._rpcname = tuple(
  59. (name.format(endpoint=endpoint), endpoint)
  60. for endpoint in endpoints)
  61. return func
  62. return decorator
  63. def apply_filters(iterable, filters):
  64. '''Apply filters returned by mgmt-permission:... event'''
  65. for selector in filters:
  66. iterable = filter(selector, iterable)
  67. return iterable
  68. class AbstractQubesAPI(object):
  69. '''Common code for Qubes Management Protocol handling
  70. Different interfaces can expose different API call sets, however they share
  71. common protocol and common implementation framework. This class is the
  72. latter.
  73. To implement a new interface, inherit from this class and write at least one
  74. method and decorate it with :py:func:`api` decorator. It will have access to
  75. pre-defined attributes: :py:attr:`app`, :py:attr:`src`, :py:attr:`dest`,
  76. :py:attr:`arg` and :py:attr:`method`.
  77. There are also two helper functions for firing events associated with API
  78. calls.
  79. '''
  80. def __init__(self, app, src, method_name, dest, arg, send_event=None):
  81. #: :py:class:`qubes.Qubes` object
  82. self.app = app
  83. #: source qube
  84. self.src = self.app.domains[src.decode('ascii')]
  85. #: destination qube
  86. self.dest = self.app.domains[dest.decode('ascii')]
  87. #: argument
  88. self.arg = arg.decode('ascii')
  89. #: name of the method
  90. self.method = method_name.decode('ascii')
  91. #: callback for sending events if applicable
  92. self.send_event = send_event
  93. #: is this operation cancellable?
  94. self.cancellable = False
  95. untrusted_candidates = []
  96. for attr in dir(self):
  97. func = getattr(self, attr)
  98. if not callable(func):
  99. continue
  100. try:
  101. # pylint: disable=protected-access
  102. for mname, endpoint in func._rpcname:
  103. if mname != self.method:
  104. continue
  105. untrusted_candidates.append((func, endpoint))
  106. except AttributeError:
  107. continue
  108. if not untrusted_candidates:
  109. raise ProtocolError('no such method: {!r}'.format(self.method))
  110. assert len(untrusted_candidates) == 1, \
  111. 'multiple candidates for method {!r}'.format(self.method)
  112. #: the method to execute
  113. self._handler = untrusted_candidates[0]
  114. self._running_handler = None
  115. del untrusted_candidates
  116. def execute(self, *, untrusted_payload):
  117. '''Execute management operation.
  118. This method is a coroutine.
  119. '''
  120. handler, endpoint = self._handler
  121. kwargs = {}
  122. if endpoint is not None:
  123. kwargs['endpoint'] = endpoint
  124. self._running_handler = asyncio.ensure_future(handler(
  125. untrusted_payload=untrusted_payload, **kwargs))
  126. return self._running_handler
  127. def cancel(self):
  128. '''If operation is cancellable, interrupt it'''
  129. if self.cancellable and self._running_handler is not None:
  130. self._running_handler.cancel()
  131. def fire_event_for_permission(self, **kwargs):
  132. '''Fire an event on the source qube to check for permission'''
  133. return self.src.fire_event_pre('mgmt-permission:' + self.method,
  134. dest=self.dest, arg=self.arg, **kwargs)
  135. def fire_event_for_filter(self, iterable, **kwargs):
  136. '''Fire an event on the source qube to filter for permission'''
  137. return apply_filters(iterable,
  138. self.fire_event_for_permission(**kwargs))