]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | #!/usr/bin/python |
2 | ||
3 | import json | |
4 | import subprocess | |
5 | import shlex | |
6 | from StringIO import StringIO | |
7 | import errno | |
8 | import sys | |
9 | import os | |
10 | import io | |
11 | import re | |
12 | ||
13 | ||
14 | import rados | |
15 | from ceph_argparse import * | |
16 | ||
17 | keyring_base = '/tmp/cephtest-caps.keyring' | |
18 | ||
19 | class UnexpectedReturn(Exception): | |
20 | def __init__(self, cmd, ret, expected, msg): | |
21 | if isinstance(cmd, list): | |
22 | self.cmd = ' '.join(cmd) | |
23 | else: | |
24 | assert isinstance(cmd, str) or isinstance(cmd, unicode), \ | |
25 | 'cmd needs to be either a list or a str' | |
26 | self.cmd = cmd | |
27 | self.cmd = str(self.cmd) | |
28 | self.ret = int(ret) | |
29 | self.expected = int(expected) | |
30 | self.msg = str(msg) | |
31 | ||
32 | def __str__(self): | |
33 | return repr('{c}: expected return {e}, got {r} ({o})'.format( | |
34 | c=self.cmd, e=self.expected, r=self.ret, o=self.msg)) | |
35 | ||
36 | def call(cmd): | |
37 | if isinstance(cmd, list): | |
38 | args = cmd | |
39 | elif isinstance(cmd, str) or isinstance(cmd, unicode): | |
40 | args = shlex.split(cmd) | |
41 | else: | |
42 | assert False, 'cmd is not a string/unicode nor a list!' | |
43 | ||
44 | print 'call: {0}'.format(args) | |
45 | proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
46 | ret = proc.wait() | |
47 | ||
48 | return (ret, proc) | |
49 | ||
50 | def expect(cmd, expected_ret): | |
51 | ||
52 | try: | |
53 | (r, p) = call(cmd) | |
54 | except ValueError as e: | |
55 | print >> sys.stderr, \ | |
56 | 'unable to run {c}: {err}'.format(c=repr(cmd), err=e.message) | |
57 | return errno.EINVAL | |
58 | ||
59 | assert r == p.returncode, \ | |
60 | 'wth? r was supposed to match returncode!' | |
61 | ||
62 | if r != expected_ret: | |
63 | raise UnexpectedReturn(repr(cmd), r, expected_ret, str(p.stderr.read())) | |
64 | ||
65 | return p | |
66 | ||
67 | def expect_to_file(cmd, expected_ret, out_file, mode='a'): | |
68 | ||
69 | # Let the exception be propagated to the caller | |
70 | p = expect(cmd, expected_ret) | |
71 | assert p.returncode == expected_ret, \ | |
72 | 'expected result doesn\'t match and no exception was thrown!' | |
73 | ||
74 | with io.open(out_file, mode) as file: | |
75 | file.write(unicode(p.stdout.read())) | |
76 | ||
77 | return p | |
78 | ||
79 | class Command: | |
80 | def __init__(self, cid, j): | |
81 | self.cid = cid[3:] | |
82 | self.perms = j['perm'] | |
83 | self.module = j['module'] | |
84 | ||
85 | self.sig = '' | |
86 | self.args = [] | |
87 | for s in j['sig']: | |
88 | if not isinstance(s, dict): | |
89 | assert isinstance(s, str) or isinstance(s,unicode), \ | |
90 | 'malformatted signature cid {0}: {1}\n{2}'.format(cid,s,j) | |
91 | if len(self.sig) > 0: | |
92 | self.sig += ' ' | |
93 | self.sig += s | |
94 | else: | |
95 | self.args.append(s) | |
96 | ||
97 | def __str__(self): | |
98 | return repr('command {0}: {1} (requires \'{2}\')'.format(self.cid,\ | |
99 | self.sig, self.perms)) | |
100 | ||
101 | ||
102 | def destroy_keyring(path): | |
103 | if not os.path.exists(path): | |
104 | raise Exception('oops! cannot remove inexistent keyring {0}'.format(path)) | |
105 | ||
106 | # grab all client entities from the keyring | |
107 | entities = [m.group(1) for m in [re.match(r'\[client\.(.*)\]', l) | |
108 | for l in [str(line.strip()) | |
109 | for line in io.open(path,'r')]] if m is not None] | |
110 | ||
111 | # clean up and make sure each entity is gone | |
112 | for e in entities: | |
113 | expect('ceph auth del client.{0}'.format(e), 0) | |
114 | expect('ceph auth get client.{0}'.format(e), errno.ENOENT) | |
115 | ||
116 | # remove keyring | |
117 | os.unlink(path) | |
118 | ||
119 | return True | |
120 | ||
121 | def test_basic_auth(): | |
122 | # make sure we can successfully add/del entities, change their caps | |
123 | # and import/export keyrings. | |
124 | ||
125 | expect('ceph auth add client.basicauth', 0) | |
126 | expect('ceph auth caps client.basicauth mon \'allow *\'', 0) | |
127 | # entity exists and caps do not match | |
128 | expect('ceph auth add client.basicauth', errno.EINVAL) | |
129 | # this command attempts to change an existing state and will fail | |
130 | expect('ceph auth add client.basicauth mon \'allow w\'', errno.EINVAL) | |
131 | expect('ceph auth get-or-create client.basicauth', 0) | |
132 | expect('ceph auth get-key client.basicauth', 0) | |
133 | expect('ceph auth get-or-create client.basicauth2', 0) | |
134 | # cleanup | |
135 | expect('ceph auth del client.basicauth', 0) | |
136 | expect('ceph auth del client.basicauth2', 0) | |
137 | ||
138 | return True | |
139 | ||
140 | def gen_module_keyring(module): | |
141 | module_caps = [ | |
142 | ('all', '{t} \'allow service {s} rwx\'', 0), | |
143 | ('none', '', errno.EACCES), | |
144 | ('wrong', '{t} \'allow service foobar rwx\'', errno.EACCES), | |
145 | ('right', '{t} \'allow service {s} {p}\'', 0), | |
146 | ('no-execute', '{t} \'allow service {s} x\'', errno.EACCES) | |
147 | ] | |
148 | ||
149 | keyring = '{0}.service-{1}'.format(keyring_base,module) | |
150 | for perms in 'r rw x'.split(): | |
151 | for (n,p,r) in module_caps: | |
152 | c = p.format(t='mon', s=module, p=perms) | |
153 | expect_to_file( | |
154 | 'ceph auth get-or-create client.{cn}-{cp} {caps}'.format( | |
155 | cn=n,cp=perms,caps=c), 0, keyring) | |
156 | ||
157 | return keyring | |
158 | ||
159 | ||
160 | def test_all(): | |
161 | ||
162 | ||
163 | perms = { | |
164 | 'good': { | |
165 | 'broad':[ | |
166 | ('rwx', 'allow *'), | |
167 | ('r', 'allow r'), | |
168 | ('rw', 'allow rw'), | |
169 | ('x', 'allow x'), | |
170 | ], | |
171 | 'service':[ | |
172 | ('rwx', 'allow service {s} rwx'), | |
173 | ('r', 'allow service {s} r'), | |
174 | ('rw', 'allow service {s} rw'), | |
175 | ('x', 'allow service {s} x'), | |
176 | ], | |
177 | 'command':[ | |
178 | ('rwx', 'allow command "{c}"'), | |
179 | ], | |
180 | 'command-with':[ | |
181 | ('rwx', 'allow command "{c}" with {kv}') | |
182 | ], | |
183 | 'command-with-prefix':[ | |
184 | ('rwx', 'allow command "{c}" with {key} prefix {val}') | |
185 | ] | |
186 | }, | |
187 | 'bad': { | |
188 | 'broad':[ | |
189 | ('none', ''), | |
190 | ], | |
191 | 'service':[ | |
192 | ('none1', 'allow service foo rwx'), | |
193 | ('none2', 'allow service foo r'), | |
194 | ('none3', 'allow service foo rw'), | |
195 | ('none4', 'allow service foo x'), | |
196 | ], | |
197 | 'command':[ | |
198 | ('none', 'allow command foo'), | |
199 | ], | |
200 | 'command-with':[ | |
201 | ('none', 'allow command "{c}" with foo=bar'), | |
202 | ], | |
203 | 'command-with-prefix':[ | |
204 | ('none', 'allow command "{c}" with foo prefix bar'), | |
205 | ], | |
206 | } | |
207 | } | |
208 | ||
209 | cmds = { | |
210 | '':[ | |
211 | { | |
212 | 'cmd':('status', '', 'r') | |
213 | }, | |
214 | { | |
215 | 'pre':'heap start_profiler', | |
216 | 'cmd':('heap', 'heapcmd=stats', 'rw'), | |
217 | 'post':'heap stop_profiler' | |
218 | } | |
219 | ], | |
220 | 'auth':[ | |
221 | { | |
222 | 'pre':'', | |
c07f9fc5 | 223 | 'cmd':('auth ls', '', 'r'), |
7c673cae FG |
224 | 'post':'' |
225 | }, | |
226 | { | |
227 | 'pre':'auth get-or-create client.foo mon \'allow *\'', | |
228 | 'cmd':('auth caps', 'entity="client.foo"', 'rw'), | |
229 | 'post':'auth del client.foo' | |
230 | } | |
231 | ], | |
232 | 'pg':[ | |
233 | { | |
234 | 'cmd':('pg getmap', '', 'r'), | |
235 | }, | |
236 | ], | |
237 | 'mds':[ | |
238 | { | |
239 | 'cmd':('mds getmap', '', 'r'), | |
240 | }, | |
241 | { | |
242 | 'cmd':('mds cluster_down', '', 'rw'), | |
243 | 'post':'mds cluster_up' | |
244 | }, | |
245 | ], | |
246 | 'mon':[ | |
247 | { | |
248 | 'cmd':('mon getmap', '', 'r') | |
249 | }, | |
250 | { | |
251 | 'cmd':('mon remove', 'name=a', 'rw') | |
252 | } | |
253 | ], | |
254 | 'osd':[ | |
255 | { | |
256 | 'cmd':('osd getmap', '', 'r'), | |
257 | }, | |
258 | { | |
259 | 'cmd':('osd pause', '', 'rw'), | |
260 | 'post':'osd unpause' | |
261 | }, | |
262 | { | |
263 | 'cmd':('osd crush dump', '', 'r') | |
264 | }, | |
265 | ], | |
266 | 'config-key':[ | |
267 | { | |
c07f9fc5 | 268 | 'pre':'config-key set foo bar', |
7c673cae FG |
269 | 'cmd':('config-key get', 'key=foo', 'r') |
270 | }, | |
271 | { | |
c07f9fc5 | 272 | 'pre':'config-key set foo bar', |
7c673cae FG |
273 | 'cmd':('config-key del', 'key=foo', 'rw') |
274 | } | |
275 | ] | |
276 | } | |
277 | ||
278 | for (module,cmd_lst) in cmds.iteritems(): | |
279 | k = keyring_base + '.' + module | |
280 | for cmd in cmd_lst: | |
281 | ||
282 | (cmd_cmd, cmd_args, cmd_perm) = cmd['cmd'] | |
283 | cmd_args_key = '' | |
284 | cmd_args_val = '' | |
285 | if len(cmd_args) > 0: | |
286 | (cmd_args_key, cmd_args_val) = cmd_args.split('=') | |
287 | ||
288 | print 'generating keyring for {m}/{c}'.format(m=module,c=cmd_cmd) | |
289 | # gen keyring | |
290 | for (good_or_bad,kind_map) in perms.iteritems(): | |
291 | for (kind,lst) in kind_map.iteritems(): | |
292 | for (perm, cap) in lst: | |
293 | cap_formatted = cap.format( | |
294 | s=module, | |
295 | c=cmd_cmd, | |
296 | kv=cmd_args, | |
297 | key=cmd_args_key, | |
298 | val=cmd_args_val) | |
299 | ||
300 | if len(cap_formatted) == 0: | |
301 | run_cap = '' | |
302 | else: | |
303 | run_cap = 'mon \'{fc}\''.format(fc=cap_formatted) | |
304 | ||
305 | cname = 'client.{gb}-{kind}-{p}'.format( | |
306 | gb=good_or_bad,kind=kind,p=perm) | |
307 | expect_to_file( | |
308 | 'ceph auth get-or-create {n} {c}'.format( | |
309 | n=cname,c=run_cap), 0, k) | |
310 | # keyring generated | |
311 | print 'testing {m}/{c}'.format(m=module,c=cmd_cmd) | |
312 | ||
313 | # test | |
314 | for good_bad in perms.iterkeys(): | |
315 | for (kind,lst) in perms[good_bad].iteritems(): | |
316 | for (perm,_) in lst: | |
317 | cname = 'client.{gb}-{k}-{p}'.format(gb=good_bad,k=kind,p=perm) | |
318 | ||
319 | if good_bad == 'good': | |
320 | expect_ret = 0 | |
321 | else: | |
322 | expect_ret = errno.EACCES | |
323 | ||
324 | if ( cmd_perm not in perm ): | |
325 | expect_ret = errno.EACCES | |
326 | if 'with' in kind and len(cmd_args) == 0: | |
327 | expect_ret = errno.EACCES | |
328 | if 'service' in kind and len(module) == 0: | |
329 | expect_ret = errno.EACCES | |
330 | ||
331 | if 'pre' in cmd and len(cmd['pre']) > 0: | |
332 | expect('ceph {0}'.format(cmd['pre']), 0) | |
333 | expect('ceph -n {cn} -k {k} {c} {arg_val}'.format( | |
334 | cn=cname,k=k,c=cmd_cmd,arg_val=cmd_args_val), expect_ret) | |
335 | if 'post' in cmd and len(cmd['post']) > 0: | |
336 | expect('ceph {0}'.format(cmd['post']), 0) | |
337 | # finish testing | |
338 | destroy_keyring(k) | |
339 | ||
340 | ||
341 | return True | |
342 | ||
343 | ||
344 | def test_misc(): | |
345 | ||
346 | k = keyring_base + '.misc' | |
347 | expect_to_file( | |
348 | 'ceph auth get-or-create client.caps mon \'allow command "auth caps"' \ | |
349 | ' with entity="client.caps"\'', 0, k) | |
350 | expect('ceph -n client.caps -k {kf} mon_status'.format(kf=k), errno.EACCES) | |
351 | expect('ceph -n client.caps -k {kf} auth caps client.caps mon \'allow *\''.format(kf=k), 0) | |
352 | expect('ceph -n client.caps -k {kf} mon_status'.format(kf=k), 0) | |
353 | destroy_keyring(k) | |
354 | ||
355 | def main(): | |
356 | ||
357 | test_basic_auth() | |
358 | test_all() | |
359 | test_misc() | |
360 | ||
361 | print 'OK' | |
362 | ||
363 | return 0 | |
364 | ||
365 | if __name__ == '__main__': | |
366 | main() |