Revision 5e12acfe

b/NEWS
29 29
  ``gnt-node add`` now invokes a new tool on the destination node, named
30 30
  ``prepare-node-join``, to configure the SSH daemon. Paramiko is no
31 31
  longer necessary to configure nodes' SSH daemons via ``gnt-node add``.
32
- A new user option, :pyeval:`rapi.RAPI_ACCESS_READ`, has been added
33
  for RAPI users. It allows granting permissions to query for
34
  information to a specific user without giving
35
  :pyeval:`rapi.RAPI_ACCESS_WRITE` permissions.
32 36

  
33 37

  
34 38
Version 2.6.1
b/doc/rapi.rst
24 24
``/var/lib/ganeti/rapi/users``) on startup. Changes to the file will be
25 25
read automatically.
26 26

  
27
Each line consists of two or three fields separated by whitespace. The
28
first two fields are for username and password. The third field is
29
optional and can be used to specify per-user options. Currently,
30
``write`` is the only option supported and enables the user to execute
31
operations modifying the cluster. Lines starting with the hash sign
32
(``#``) are treated as comments.
27
Lines starting with the hash sign (``#``) are treated as comments. Each
28
line consists of two or three fields separated by whitespace. The first
29
two fields are for username and password. The third field is optional
30
and can be used to specify per-user options (separated by comma without
31
spaces). Available options:
32

  
33
.. pyassert::
34

  
35
  rapi.RAPI_ACCESS_ALL == set([
36
    rapi.RAPI_ACCESS_WRITE,
37
    rapi.RAPI_ACCESS_READ,
38
    ])
39

  
40
:pyeval:`rapi.RAPI_ACCESS_WRITE`
41
  Enables the user to execute operations modifying the cluster. Implies
42
  :pyeval:`rapi.RAPI_ACCESS_READ` access.
43
:pyeval:`rapi.RAPI_ACCESS_READ`
44
  Allow access to operations querying for information.
33 45

  
34 46
Passwords can either be written in clear text or as a hash. Clear text
35 47
passwords may not start with an opening brace (``{``) or they must be
......
51 63
  # Hashed password for Jessica
52 64
  jessica {HA1}7046452df2cbb530877058712cf17bd4 write
53 65

  
66
  # Monitoring can query for values
67
  monitoring {HA1}ec018ffe72b8e75bb4d508ed5b6d079c query
68

  
69
  # A user who can query and write
70
  superuser {HA1}ec018ffe72b8e75bb4d508ed5b6d079c query,write
71

  
54 72

  
55 73
.. [#pwhash] Using the MD5 hash of username, realm and password is
56 74
   described in :rfc:`2617` ("HTTP Authentication"), sections 3.2.2.2
......
1121 1139

  
1122 1140
Request information for connecting to instance's console.
1123 1141

  
1124
Supports the following commands: ``GET``.
1142
.. pyassert::
1143

  
1144
  not (hasattr(rlib2.R_2_instances_name_console, "PUT") or
1145
       hasattr(rlib2.R_2_instances_name_console, "POST") or
1146
       hasattr(rlib2.R_2_instances_name_console, "DELETE"))
1147

  
1148
Supports the following commands: ``GET``. Requires authentication with
1149
one of the following options:
1150
:pyeval:`utils.CommaJoin(rlib2.R_2_instances_name_console.GET_ACCESS)`.
1125 1151

  
1126 1152
``GET``
1127 1153
~~~~~~~
......
1640 1666
:pyeval:`utils.CommaJoin(constants.QR_VIA_RAPI)`. See the :doc:`query2
1641 1667
design document <design-query2>` for more details.
1642 1668

  
1643
Supports the following commands: ``GET``, ``PUT``.
1669
.. pyassert::
1670

  
1671
  (rlib2.R_2_query.GET_ACCESS == rlib2.R_2_query.PUT_ACCESS and
1672
   not (hasattr(rlib2.R_2_query, "POST") or
1673
        hasattr(rlib2.R_2_query, "DELETE")))
1674

  
1675
Supports the following commands: ``GET``, ``PUT``. Requires
1676
authentication with one of the following options:
1677
:pyeval:`utils.CommaJoin(rlib2.R_2_query.GET_ACCESS)`.
1644 1678

  
1645 1679
``GET``
1646 1680
~~~~~~~
b/lib/rapi/__init__.py
21 21
"""Ganeti RAPI module"""
22 22

  
23 23
RAPI_ACCESS_WRITE = "write"
24
RAPI_ACCESS_READ = "read"
25

  
26
RAPI_ACCESS_ALL = frozenset([
27
  RAPI_ACCESS_WRITE,
28
  RAPI_ACCESS_READ,
29
  ])
b/lib/rapi/rlib2.py
1226 1226
  """/2/instances/[instance_name]/console resource.
1227 1227

  
1228 1228
  """
1229
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1229
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1230 1230
  GET_OPCODE = opcodes.OpInstanceConsole
1231 1231

  
1232 1232
  def GET(self):
......
1278 1278

  
1279 1279
  """
1280 1280
  # Results might contain sensitive information
1281
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1281
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1282
  PUT_ACCESS = GET_ACCESS
1282 1283
  GET_OPCODE = opcodes.OpQuery
1283 1284
  PUT_OPCODE = opcodes.OpQuery
1284 1285

  
b/test/ganeti.server.rapi_unittest.py
168 168
      else:
169 169
        return None
170 170

  
171
    def _LookupUserWithWrite(name):
172
      if name == username:
173
        return http.auth.PasswordFileUser(name, password, [
174
          rapi.RAPI_ACCESS_WRITE,
175
          ])
176
      else:
177
        return None
178

  
179
    for qr in constants.QR_VIA_RAPI:
180
      # The /2/query resource has somewhat special rules for authentication as
181
      # it can be used to retrieve critical information
182
      path = "/2/query/%s" % qr
183

  
184
      for method in rapi.baserlib._SUPPORTED_METHODS:
185
        # No authorization
186
        (code, _, _) = self._Test(method, path, "", "")
187

  
188
        if method in (http.HTTP_DELETE, http.HTTP_POST):
189
          self.assertEqual(code, http.HttpNotImplemented.code)
190
          continue
191

  
192
        self.assertEqual(code, http.HttpUnauthorized.code)
193

  
194
        # Incorrect user
195
        (code, _, _) = self._Test(method, path, header_fn(True), "",
196
                                  user_fn=self._LookupWrongUser)
197
        self.assertEqual(code, http.HttpUnauthorized.code)
198

  
199
        # User has no write access, but the password is correct
200
        (code, _, _) = self._Test(method, path, header_fn(True), "",
201
                                  user_fn=_LookupUserNoWrite)
202
        self.assertEqual(code, http.HttpForbidden.code)
203

  
204
        # Wrong password and no write access
205
        (code, _, _) = self._Test(method, path, header_fn(False), "",
206
                                  user_fn=_LookupUserNoWrite)
207
        self.assertEqual(code, http.HttpUnauthorized.code)
208

  
209
        # Wrong password with write access
210
        (code, _, _) = self._Test(method, path, header_fn(False), "",
211
                                  user_fn=_LookupUserWithWrite)
212
        self.assertEqual(code, http.HttpUnauthorized.code)
213

  
214
        # Prepare request information
215
        if method == http.HTTP_PUT:
216
          reqpath = path
217
          body = serializer.DumpJson({
218
            "fields": ["name"],
219
            })
220
        elif method == http.HTTP_GET:
221
          reqpath = "%s?fields=name" % path
222
          body = ""
171
    for access in [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]:
172
      def _LookupUserWithWrite(name):
173
        if name == username:
174
          return http.auth.PasswordFileUser(name, password, [
175
            access,
176
            ])
223 177
        else:
224
          self.fail("Unknown method '%s'" % method)
225

  
226
        # User has write access, password is correct
227
        (code, _, data) = self._Test(method, reqpath, header_fn(True), body,
228
                                     user_fn=_LookupUserWithWrite,
229
                                     luxi_client=_FakeLuxiClientForQuery)
230
        self.assertEqual(code, http.HTTP_OK)
231
        self.assertTrue(objects.QueryResponse.FromDict(data))
178
          return None
179

  
180
      for qr in constants.QR_VIA_RAPI:
181
        # The /2/query resource has somewhat special rules for authentication as
182
        # it can be used to retrieve critical information
183
        path = "/2/query/%s" % qr
184

  
185
        for method in rapi.baserlib._SUPPORTED_METHODS:
186
          # No authorization
187
          (code, _, _) = self._Test(method, path, "", "")
188

  
189
          if method in (http.HTTP_DELETE, http.HTTP_POST):
190
            self.assertEqual(code, http.HttpNotImplemented.code)
191
            continue
192

  
193
          self.assertEqual(code, http.HttpUnauthorized.code)
194

  
195
          # Incorrect user
196
          (code, _, _) = self._Test(method, path, header_fn(True), "",
197
                                    user_fn=self._LookupWrongUser)
198
          self.assertEqual(code, http.HttpUnauthorized.code)
199

  
200
          # User has no write access, but the password is correct
201
          (code, _, _) = self._Test(method, path, header_fn(True), "",
202
                                    user_fn=_LookupUserNoWrite)
203
          self.assertEqual(code, http.HttpForbidden.code)
204

  
205
          # Wrong password and no write access
206
          (code, _, _) = self._Test(method, path, header_fn(False), "",
207
                                    user_fn=_LookupUserNoWrite)
208
          self.assertEqual(code, http.HttpUnauthorized.code)
209

  
210
          # Wrong password with write access
211
          (code, _, _) = self._Test(method, path, header_fn(False), "",
212
                                    user_fn=_LookupUserWithWrite)
213
          self.assertEqual(code, http.HttpUnauthorized.code)
214

  
215
          # Prepare request information
216
          if method == http.HTTP_PUT:
217
            reqpath = path
218
            body = serializer.DumpJson({
219
              "fields": ["name"],
220
              })
221
          elif method == http.HTTP_GET:
222
            reqpath = "%s?fields=name" % path
223
            body = ""
224
          else:
225
            self.fail("Unknown method '%s'" % method)
226

  
227
          # User has write access, password is correct
228
          (code, _, data) = self._Test(method, reqpath, header_fn(True), body,
229
                                       user_fn=_LookupUserWithWrite,
230
                                       luxi_client=_FakeLuxiClientForQuery)
231
          self.assertEqual(code, http.HTTP_OK)
232
          self.assertTrue(objects.QueryResponse.FromDict(data))
233

  
234
  def testConsole(self):
235
    path = "/2/instances/inst1.example.com/console"
236

  
237
    for method in rapi.baserlib._SUPPORTED_METHODS:
238
      # No authorization
239
      (code, _, _) = self._Test(method, path, "", "")
240

  
241
      if method == http.HTTP_GET:
242
        self.assertEqual(code, http.HttpUnauthorized.code)
243
      else:
244
        self.assertEqual(code, http.HttpNotImplemented.code)
232 245

  
233 246

  
234 247
class _FakeLuxiClientForQuery:

Also available in: Unified diff