Feature 10941: User Management Enhancements
[osm/osmclient.git] / osmclient / cli_commands / rbac.py
1 # Copyright ETSI Contributors and Others.
2 # All Rights Reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
14 # under the License.
15
16 import click
17 from osmclient.common.exceptions import ClientException
18 from osmclient.cli_commands import utils
19 from prettytable import PrettyTable
20 import json
21 import logging
22 import time
23
24 logger = logging.getLogger("osmclient")
25
26
27 ##############################
28 # Role Management Operations #
29 ##############################
30
31
32 @click.command(name="role-create", short_help="creates a new role")
33 @click.argument("name")
34 @click.option("--permissions", default=None, help="role permissions using a dictionary")
35 @click.pass_context
36 def role_create(ctx, name, permissions):
37 """
38 Creates a new role.
39
40 \b
41 NAME: Name or ID of the role.
42 DEFINITION: Definition of grant/denial of access to resources.
43 """
44 logger.debug("")
45 utils.check_client_version(ctx.obj, ctx.command.name)
46 ctx.obj.role.create(name, permissions)
47
48
49 @click.command(name="role-update", short_help="updates a role")
50 @click.argument("name")
51 @click.option("--set-name", default=None, help="change name of rle")
52 @click.option(
53 "--add",
54 default=None,
55 help="yaml format dictionary with permission: True/False to access grant/denial",
56 )
57 @click.option("--remove", default=None, help="yaml format list to remove a permission")
58 @click.pass_context
59 def role_update(ctx, name, set_name, add, remove):
60 """
61 Updates a role.
62
63 \b
64 NAME: Name or ID of the role.
65 DEFINITION: Definition overwrites the old definition.
66 ADD: Grant/denial of access to resource to add.
67 REMOVE: Grant/denial of access to resource to remove.
68 """
69 logger.debug("")
70 utils.check_client_version(ctx.obj, ctx.command.name)
71 ctx.obj.role.update(name, set_name, None, add, remove)
72
73
74 @click.command(name="role-delete", short_help="deletes a role")
75 @click.argument("name")
76 @click.pass_context
77 def role_delete(ctx, name):
78 """
79 Deletes a role.
80
81 \b
82 NAME: Name or ID of the role.
83 """
84 logger.debug("")
85 utils.check_client_version(ctx.obj, ctx.command.name)
86 ctx.obj.role.delete(name)
87
88
89 @click.command(name="role-list", short_help="list all roles")
90 @click.option(
91 "--filter",
92 default=None,
93 multiple=True,
94 help="restricts the list to the projects matching the filter",
95 )
96 @click.pass_context
97 def role_list(ctx, filter):
98 """
99 List all roles.
100 """
101 logger.debug("")
102 utils.check_client_version(ctx.obj, ctx.command.name)
103 if filter:
104 filter = "&".join(filter)
105 resp = ctx.obj.role.list(filter)
106 table = PrettyTable(["name", "id"])
107 for role in resp:
108 table.add_row([role["name"], role["_id"]])
109 table.align = "l"
110 print(table)
111
112
113 @click.command(name="role-show", short_help="show specific role")
114 @click.argument("name")
115 @click.pass_context
116 def role_show(ctx, name):
117 """
118 Shows the details of a role.
119
120 \b
121 NAME: Name or ID of the role.
122 """
123 logger.debug("")
124 utils.check_client_version(ctx.obj, ctx.command.name)
125 resp = ctx.obj.role.get(name)
126
127 table = PrettyTable(["key", "attribute"])
128 for k, v in resp.items():
129 table.add_row([k, json.dumps(v, indent=2)])
130 table.align = "l"
131 print(table)
132
133
134 ####################
135 # Project mgmt operations
136 ####################
137
138
139 @click.command(name="project-create", short_help="creates a new project")
140 @click.argument("name")
141 # @click.option('--description',
142 # default='no description',
143 # help='human readable description')
144 @click.option("--domain-name", "domain_name", default=None, help="assign to a domain")
145 @click.option(
146 "--quotas",
147 "quotas",
148 multiple=True,
149 default=None,
150 help="provide quotas. Can be used several times: 'quota1=number[,quota2=number,...]'. Quotas can be one "
151 "of vnfds, nsds, nsts, pdus, nsrs, nsis, vim_accounts, wim_accounts, sdns, k8sclusters, k8srepos",
152 )
153 @click.pass_context
154 def project_create(ctx, name, domain_name, quotas):
155 """Creates a new project
156
157 NAME: name of the project
158 DOMAIN_NAME: optional domain name for the project when keystone authentication is used
159 QUOTAS: set quotas for the project
160 """
161 logger.debug("")
162 project = {"name": name}
163 if domain_name:
164 project["domain_name"] = domain_name
165 quotas_dict = _process_project_quotas(quotas)
166 if quotas_dict:
167 project["quotas"] = quotas_dict
168
169 utils.check_client_version(ctx.obj, ctx.command.name)
170 ctx.obj.project.create(name, project)
171
172
173 def _process_project_quotas(quota_list):
174 quotas_dict = {}
175 if not quota_list:
176 return quotas_dict
177 try:
178 for quota in quota_list:
179 for single_quota in quota.split(","):
180 k, v = single_quota.split("=")
181 quotas_dict[k] = None if v in ("None", "null", "") else int(v)
182 except (ValueError, TypeError):
183 raise ClientException(
184 "invalid format for 'quotas'. Use 'k1=v1,v1=v2'. v must be a integer or null"
185 )
186 return quotas_dict
187
188
189 @click.command(name="project-delete", short_help="deletes a project")
190 @click.argument("name")
191 @click.pass_context
192 def project_delete(ctx, name):
193 """deletes a project
194
195 NAME: name or ID of the project to be deleted
196 """
197 logger.debug("")
198 utils.check_client_version(ctx.obj, ctx.command.name)
199 ctx.obj.project.delete(name)
200
201
202 @click.command(name="project-list", short_help="list all projects")
203 @click.option(
204 "--filter",
205 default=None,
206 multiple=True,
207 help="restricts the list to the projects matching the filter",
208 )
209 @click.pass_context
210 def project_list(ctx, filter):
211 """list all projects"""
212 logger.debug("")
213 utils.check_client_version(ctx.obj, ctx.command.name)
214 if filter:
215 filter = "&".join(filter)
216 resp = ctx.obj.project.list(filter)
217 table = PrettyTable(["name", "id"])
218 for proj in resp:
219 table.add_row([proj["name"], proj["_id"]])
220 table.align = "l"
221 print(table)
222
223
224 @click.command(name="project-show", short_help="shows the details of a project")
225 @click.argument("name")
226 @click.pass_context
227 def project_show(ctx, name):
228 """shows the details of a project
229
230 NAME: name or ID of the project
231 """
232 logger.debug("")
233 utils.check_client_version(ctx.obj, ctx.command.name)
234 resp = ctx.obj.project.get(name)
235
236 table = PrettyTable(["key", "attribute"])
237 for k, v in resp.items():
238 table.add_row([k, json.dumps(v, indent=2)])
239 table.align = "l"
240 print(table)
241
242
243 @click.command(
244 name="project-update", short_help="updates a project (only the name can be updated)"
245 )
246 @click.argument("project")
247 @click.option("--name", default=None, help="new name for the project")
248 @click.option(
249 "--quotas",
250 "quotas",
251 multiple=True,
252 default=None,
253 help="change quotas. Can be used several times: 'quota1=number|empty[,quota2=...]' "
254 "(use empty to reset quota to default",
255 )
256 @click.pass_context
257 def project_update(ctx, project, name, quotas):
258 """
259 Update a project name
260
261 :param ctx:
262 :param project: id or name of the project to modify
263 :param name: new name for the project
264 :param quotas: change quotas of the project
265 :return:
266 """
267 logger.debug("")
268 project_changes = {}
269 if name:
270 project_changes["name"] = name
271 quotas_dict = _process_project_quotas(quotas)
272 if quotas_dict:
273 project_changes["quotas"] = quotas_dict
274
275 utils.check_client_version(ctx.obj, ctx.command.name)
276 ctx.obj.project.update(project, project_changes)
277
278
279 ####################
280 # User mgmt operations
281 ####################
282
283
284 @click.command(name="user-create", short_help="creates a new user")
285 @click.argument("username")
286 @click.option(
287 "--password",
288 prompt=True,
289 hide_input=True,
290 confirmation_prompt=True,
291 help="user password",
292 )
293 @click.option(
294 "--projects",
295 # prompt="Comma separate list of projects",
296 multiple=True,
297 callback=lambda ctx, param, value: "".join(value).split(",")
298 if all(len(x) == 1 for x in value)
299 else value,
300 help="list of project ids that the user belongs to",
301 )
302 @click.option(
303 "--project-role-mappings",
304 "project_role_mappings",
305 default=None,
306 multiple=True,
307 help="assign role(s) in a project. Can be used several times: 'project,role1[,role2,...]'",
308 )
309 @click.option("--domain-name", "domain_name", default=None, help="assign to a domain")
310 @click.pass_context
311 def user_create(ctx, username, password, projects, project_role_mappings, domain_name):
312 """Creates a new user
313
314 \b
315 USERNAME: name of the user
316 PASSWORD: password of the user
317 PROJECTS: projects assigned to user (internal only)
318 PROJECT_ROLE_MAPPING: roles in projects assigned to user (keystone)
319 DOMAIN_NAME: optional domain name for the user when keystone authentication is used
320 """
321 logger.debug("")
322 user = {}
323 user["username"] = username
324 user["password"] = password
325 user["projects"] = projects
326 user["project_role_mappings"] = project_role_mappings
327 if domain_name:
328 user["domain_name"] = domain_name
329
330 utils.check_client_version(ctx.obj, ctx.command.name)
331 ctx.obj.user.create(username, user)
332
333
334 @click.command(name="user-update", short_help="updates user information")
335 @click.argument("username")
336 @click.option(
337 "--password",
338 # prompt=True,
339 # hide_input=True,
340 # confirmation_prompt=True,
341 help="user password",
342 )
343 @click.option("--set-username", "set_username", default=None, help="change username")
344 @click.option(
345 "--set-project",
346 "set_project",
347 default=None,
348 multiple=True,
349 help="create/replace the roles for this project: 'project,role1[,role2,...]'",
350 )
351 @click.option(
352 "--remove-project",
353 "remove_project",
354 default=None,
355 multiple=True,
356 help="removes project from user: 'project'",
357 )
358 @click.option(
359 "--add-project-role",
360 "add_project_role",
361 default=None,
362 multiple=True,
363 help="assign role(s) in a project. Can be used several times: 'project,role1[,role2,...]'",
364 )
365 @click.option(
366 "--remove-project-role",
367 "remove_project_role",
368 default=None,
369 multiple=True,
370 help="remove role(s) in a project. Can be used several times: 'project,role1[,role2,...]'",
371 )
372 @click.option("--change_password", "change_password", help="user's current password")
373 @click.option(
374 "--new_password",
375 "new_password",
376 help="user's new password to update in expiry condition",
377 )
378 @click.option(
379 "--unlock",
380 is_flag=True,
381 help="unlock user",
382 )
383 @click.option(
384 "--renew",
385 is_flag=True,
386 help="renew user",
387 )
388 @click.pass_context
389 def user_update(
390 ctx,
391 username,
392 password,
393 set_username,
394 set_project,
395 remove_project,
396 add_project_role,
397 remove_project_role,
398 change_password,
399 new_password,
400 unlock,
401 renew,
402 ):
403 """Update a user information
404
405 \b
406 USERNAME: name of the user
407 PASSWORD: new password
408 SET_USERNAME: new username
409 SET_PROJECT: creating mappings for project/role(s)
410 REMOVE_PROJECT: deleting mappings for project/role(s)
411 ADD_PROJECT_ROLE: adding mappings for project/role(s)
412 REMOVE_PROJECT_ROLE: removing mappings for project/role(s)
413 CHANGE_PASSWORD: user's current password to change
414 NEW_PASSWORD: user's new password to update in expiry condition
415 UNLOCK: unlock user
416 RENEW: renew user
417 """
418 logger.debug("")
419 user = {}
420 user["password"] = password
421 user["username"] = set_username
422 user["set-project"] = set_project
423 user["remove-project"] = remove_project
424 user["add-project-role"] = add_project_role
425 user["remove-project-role"] = remove_project_role
426 user["change_password"] = change_password
427 user["new_password"] = new_password
428 user["unlock"] = unlock
429 user["renew"] = renew
430
431 utils.check_client_version(ctx.obj, ctx.command.name)
432 ctx.obj.user.update(username, user)
433 if not user.get("change_password"):
434 ctx.obj.user.update(username, user)
435 else:
436 ctx.obj.user.update(username, user, pwd_change=True)
437
438
439 @click.command(name="user-delete", short_help="deletes a user")
440 @click.argument("name")
441 # @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions')
442 @click.pass_context
443 def user_delete(ctx, name):
444 """deletes a user
445
446 \b
447 NAME: name or ID of the user to be deleted
448 """
449 logger.debug("")
450 utils.check_client_version(ctx.obj, ctx.command.name)
451 ctx.obj.user.delete(name)
452
453
454 @click.command(name="user-list", short_help="list all users")
455 @click.option(
456 "--filter",
457 default=None,
458 multiple=True,
459 help="restricts the list to the users matching the filter",
460 )
461 @click.pass_context
462 def user_list(ctx, filter):
463 """list all users"""
464 utils.check_client_version(ctx.obj, ctx.command.name)
465 if filter:
466 filter = "&".join(filter)
467 resp, admin_show = ctx.obj.user.list(filter)
468 for user in resp:
469 if user["username"] == "admin":
470 user["_admin"]["account_expire_time"] = "N/A"
471 if admin_show:
472 table = PrettyTable(["name", "id", "user_status", "expires_in"])
473 for user in resp:
474 table.add_row(
475 [
476 user["username"],
477 user["_id"],
478 user["_admin"]["user_status"].upper(),
479 time.strftime(
480 "%b-%d-%Y %X",
481 time.gmtime(user["_admin"]["account_expire_time"]),
482 )
483 if not user["username"] == "admin"
484 else user["_admin"]["account_expire_time"],
485 ]
486 )
487 else:
488 table = PrettyTable(["name", "id"])
489 for user in resp:
490 table.add_row([user["username"], user["_id"]])
491 table.align = "l"
492 print(table)
493
494
495 @click.command(name="user-show", short_help="shows the details of a user")
496 @click.argument("name")
497 @click.pass_context
498 def user_show(ctx, name):
499 """shows the details of a user
500
501 NAME: name or ID of the user
502 """
503 logger.debug("")
504 utils.check_client_version(ctx.obj, ctx.command.name)
505 resp = ctx.obj.user.get(name)
506 if "password" in resp:
507 resp["password"] = "********"
508
509 table = PrettyTable(["key", "attribute"])
510 for k, v in resp.items():
511 table.add_row([k, json.dumps(v, indent=2)])
512 table.align = "l"
513 print(table)