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