Coverage for harbor_cli/commands/api/user.py: 31%
115 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3from enum import Enum
4from typing import Optional
6import typer
7from harborapi.exceptions import NotFound
8from harborapi.models.models import UserCreationReq
9from harborapi.models.models import UserProfile
10from harborapi.models.models import UserResp
12from ...exceptions import HarborCLIError
13from ...logs import logger
14from ...output.render import render_result
15from ...state import state
16from ...utils.args import create_updated_model
17from ...utils.commands import inject_resource_options
19# Create a command group
20app = typer.Typer(
21 name="user",
22 help="Manage users.",
23 no_args_is_help=True,
24)
27def convert_uid(uid: str | int) -> int:
28 """Utility function for converting a user ID to an integer for
29 commands that take a username or ID as it first argument."""
30 try:
31 return int(uid)
32 except ValueError:
33 raise HarborCLIError(
34 "The first argument must be an integer ID when --id is used."
35 )
38def user_from_username(username: str) -> UserResp:
39 """Fetches a Harbor user given a username.
41 Parameters
42 ----------
43 username : str
44 The username of the user to fetch.
46 Returns
47 -------
48 UserResp
49 The user object.
50 """
51 try:
52 user_resp = state.run(
53 state.client.get_user_by_username(username), "Fetching user..."
54 )
55 except NotFound:
56 raise HarborCLIError(f"User {username!r} not found.")
57 return user_resp
60def uid_from_username(username: str) -> int:
61 """Fetches the User ID of a Harbor user given a username.
63 Parameters
64 ----------
65 username : str
66 The username of the user to fetch.
68 Returns
69 -------
70 int
71 The User ID of the user.
72 """
73 user_resp = user_from_username(username)
74 if user_resp.user_id is None: # spec states ID can be None...
75 raise HarborCLIError(f"User {username!r} has no user ID.")
76 return user_resp.user_id
79# HarborAsyncClient.create_user()
80# There is no user object in the Harbor API schema with field descriptions
81# so we can't inject help here.
82@app.command("create")
83def create_user(
84 ctx: typer.Context,
85 username: str = typer.Argument(
86 ...,
87 help="Username of the user to create.",
88 ),
89 password: Optional[str] = typer.Option(
90 None,
91 help="Password of the user to create.",
92 ),
93 email: Optional[str] = typer.Option(
94 None,
95 help="Email of the user to create.",
96 ),
97 realname: Optional[str] = typer.Option(
98 None,
99 help="Real name of the user to create.",
100 ),
101 comment: Optional[str] = typer.Option(
102 None,
103 help="Comment of the user to create.",
104 ),
105) -> None:
106 """Create a new user."""
107 req = UserCreationReq(
108 username=username,
109 email=email,
110 realname=realname,
111 password=password,
112 comment=comment,
113 )
114 user_info = state.run(state.client.create_user(req), f"Creating user...")
115 render_result(user_info, ctx)
116 logger.info(f"Created user {username!r}.")
119# HarborAsyncClient.update_user()
120@app.command("update")
121def update_user(
122 ctx: typer.Context,
123 username_or_id: str = typer.Argument(
124 ...,
125 help="Username or ID of user to update. Use --id to update by ID.",
126 ),
127 is_id: bool = typer.Option(
128 False,
129 "--id",
130 help="Argument is a user ID.",
131 ),
132 email: Optional[str] = typer.Option(
133 None,
134 help="New email for the user.",
135 ),
136 realname: Optional[str] = typer.Option(
137 None,
138 help="New real name for the user.",
139 ),
140 comment: Optional[str] = typer.Option(
141 None,
142 help="New comment for the user.",
143 ),
144) -> None:
145 """Update an existing user."""
146 user = get_user(username_or_id, is_id)
147 assert user.user_id is not None, "User ID is None"
148 req = create_updated_model(user, UserProfile, ctx)
149 state.run(state.client.update_user(user.user_id, req), "Updating user...")
150 logger.info(f"Updated user.")
153# HarborAsyncClient.delete_user()
154@app.command("delete")
155def delete_user(
156 username_or_id: str = typer.Argument(
157 ...,
158 help="Username or ID of user to delete. Use --id to delete by ID.",
159 ),
160 is_id: bool = typer.Option(
161 False,
162 "--id",
163 help="Argument is a user ID.",
164 ),
165 force: bool = typer.Option(
166 False,
167 "--force",
168 "-f",
169 help="Skip confirmation prompt.",
170 ),
171) -> None:
172 """Delete a user."""
174 # Always confirm deletions unless --force is used
175 if not force:
176 typer.confirm(
177 f"Are you sure you want to delete user {username_or_id!r}?",
178 default=False,
179 abort=True,
180 )
182 if is_id:
183 uid = convert_uid(username_or_id)
184 else:
185 uid = uid_from_username(username_or_id)
187 state.run(state.client.delete_user(uid), "Deleting user...")
188 logger.info(f"Deleted user with ID {uid}.")
191# HarborAsyncClient.search_users_by_username()
192@app.command("search")
193@inject_resource_options()
194def search_users(
195 ctx: typer.Context,
196 page: int,
197 page_size: int,
198 limit: Optional[int],
199 username: str = typer.Argument(
200 ...,
201 help="Username or partial username to search for.",
202 ),
203) -> None:
204 """Search for users by username."""
205 users = state.run(
206 state.client.search_users_by_username(
207 username,
208 page=page,
209 page_size=page_size,
210 limit=limit,
211 ),
212 "Searching...",
213 )
214 render_result(users, ctx)
217# HarborAsyncClient.set_user_admin()
218@app.command("set-admin")
219def set_user_admin(
220 username_or_id: str = typer.Argument(
221 ...,
222 help="Username or ID of user to set as admin. Use --id to set by ID.",
223 ),
224 is_id: bool = typer.Option(
225 False,
226 "--id",
227 help="Argument is a user ID.",
228 ),
229) -> None:
230 """Sets a user as admin."""
231 if is_id:
232 uid = convert_uid(username_or_id)
233 else:
234 uid = uid_from_username(username_or_id)
236 state.run(
237 state.client.set_user_admin(uid, is_admin=True), "Setting user as admin..."
238 )
239 logger.info(f"Set user with ID {uid} as admin.")
242@app.command("unset-admin")
243def unset_user_admin(
244 username_or_id: str = typer.Argument(
245 ...,
246 help="Username or ID of user to unset as admin. Use --id to set by ID.",
247 ),
248 is_id: bool = typer.Option(
249 False,
250 "--id",
251 help="Argument is a user ID.",
252 ),
253) -> None:
254 """Unsets a user as admin."""
255 if is_id:
256 uid = convert_uid(username_or_id)
257 else:
258 uid = uid_from_username(username_or_id)
260 state.run(
261 state.client.set_user_admin(uid, is_admin=False), "Removing user as admin..."
262 )
263 logger.info(f"Removed user with ID {uid} as admin.")
266# HarborAsyncClient.set_user_password()
267@app.command("set-password")
268def set_user_password(
269 username_or_id: str = typer.Argument(
270 ...,
271 help="Username or ID of user to set password for. Use --id to set by ID.",
272 ),
273 is_id: bool = typer.Option(
274 False,
275 "--id",
276 help="Argument is a user ID.",
277 ),
278 old_password: str = typer.Option(
279 ...,
280 "--old-password",
281 prompt="Enter old password",
282 hide_input=True,
283 help="Old password for user.",
284 ),
285 new_password: str = typer.Option(
286 ...,
287 "--new-password",
288 prompt="Enter new password",
289 hide_input=True,
290 confirmation_prompt=True,
291 help="New password for user.",
292 ),
293) -> None:
294 """Set a user's password."""
295 if is_id:
296 uid = convert_uid(username_or_id)
297 else:
298 uid = uid_from_username(username_or_id)
300 state.run(
301 state.client.set_user_password(
302 uid,
303 new_password=new_password,
304 old_password=old_password,
305 ),
306 "Setting password for user...",
307 )
308 logger.info(f"Set password for user with ID {uid}.")
311# HarborAsyncClient.set_user_cli_secret()
312@app.command("set-cli-secret")
313def set_user_cli_secret(
314 username_or_id: str = typer.Argument(
315 ...,
316 help="Username or ID of user to set CLI secret for. Use --id to set by ID.",
317 ),
318 secret: str = typer.Option(
319 ...,
320 help="CLI secret to set for user. If omitted, a prompt will be shown.",
321 prompt="Enter CLI secret",
322 hide_input=True,
323 confirmation_prompt=True,
324 ),
325 is_id: bool = typer.Option(
326 False,
327 "--id",
328 help="Argument is a user ID.",
329 ),
330) -> None:
331 """Set a user's CLI secret."""
332 if is_id:
333 uid = convert_uid(username_or_id)
334 else:
335 uid = uid_from_username(username_or_id)
337 state.run(
338 state.client.set_user_cli_secret(uid, secret), "Setting CLI secret for user..."
339 )
340 logger.info(f"Set CLI secret for user with ID {uid}.")
343# HarborAsyncClient.get_current_user()
344@app.command("get-current")
345def get_current_user(ctx: typer.Context) -> None:
346 """Get information about the currently authenticated user."""
347 user_info = state.run(state.client.get_current_user(), "Fetching current user...")
348 render_result(user_info, ctx)
351# HarborAsyncClient.get_current_user_permissions()
352@app.command("get-current-permissions")
353def get_current_user_permissions(
354 ctx: typer.Context,
355 scope: Optional[str] = typer.Option(
356 None, "--scope", help="Scope to get permissions for."
357 ),
358 relative: bool = typer.Option(
359 False,
360 "--relative",
361 help="Show permissions relative to scope.",
362 ),
363) -> None:
364 """Get permissions for the currently authenticated user."""
365 permissions = state.run(
366 state.client.get_current_user_permissions(),
367 "Fetching current user permissions...",
368 )
369 # TODO: print a message here if format is table and no permissions exist?
370 # it's clear when using JSON, but not so much with table
371 render_result(permissions, ctx)
374def get_user(username_or_id: str, is_id: bool = False) -> UserResp:
375 """Get a user by username or ID."""
376 msg = "Fetching user..."
377 if is_id:
378 uid = convert_uid(username_or_id)
379 user_info = state.run(state.client.get_user(uid), msg)
380 else:
381 user_info = state.run(state.client.get_user_by_username(username_or_id), msg)
382 return user_info
385# HarborAsyncClient.get_user()
386# HarborAsyncClient.get_user_by_username()
387@app.command("get")
388def get_user_command(
389 ctx: typer.Context,
390 username_or_id: str = typer.Argument(
391 ...,
392 help="Username or ID of user to update. Add the --id flag to update by ID.",
393 ),
394 is_id: bool = typer.Option(
395 False,
396 "--id",
397 help="Argument is a user ID.",
398 ),
399) -> None:
400 """Get information about a specific user."""
401 msg = "Fetching user..."
402 if is_id:
403 uid = convert_uid(username_or_id)
404 user_info = state.run(state.client.get_user(uid), msg)
405 elif is_id is not None:
406 user_info = state.run(state.client.get_user_by_username(username_or_id), msg)
408 render_result(user_info, ctx)
411class UserListSortMode(Enum):
412 """Sort modes for the user list command."""
414 ID = "id"
415 USERNAME = "username"
416 NAME = "name"
419# HarborAsyncClient.get_users()
420@app.command("list")
421def list_users(
422 ctx: typer.Context,
423 sort: Optional[UserListSortMode] = typer.Option(
424 None,
425 case_sensitive=False,
426 help="Sort by field.",
427 ),
428) -> None:
429 """List all users in the system."""
430 users = state.run(state.client.get_users(), "Fetching users...")
431 if sort == UserListSortMode.ID:
432 users.sort(key=lambda u: u.user_id or "")
433 elif sort == UserListSortMode.USERNAME:
434 users.sort(key=lambda u: u.username or "")
435 elif sort == UserListSortMode.NAME:
436 users.sort(key=lambda u: u.realname or "")
437 render_result(users, ctx)