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