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

1from __future__ import annotations 

2 

3from enum import Enum 

4from typing import Optional 

5 

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 

11 

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 

18 

19# Create a command group 

20app = typer.Typer( 

21 name="user", 

22 help="Manage users.", 

23 no_args_is_help=True, 

24) 

25 

26 

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 ) 

36 

37 

38def user_from_username(username: str) -> UserResp: 

39 """Fetches a Harbor user given a username. 

40 

41 Parameters 

42 ---------- 

43 username : str 

44 The username of the user to fetch. 

45 

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 

58 

59 

60def uid_from_username(username: str) -> int: 

61 """Fetches the User ID of a Harbor user given a username. 

62 

63 Parameters 

64 ---------- 

65 username : str 

66 The username of the user to fetch. 

67 

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 

77 

78 

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}.") 

117 

118 

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.") 

151 

152 

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.""" 

173 

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 ) 

181 

182 if is_id: 

183 uid = convert_uid(username_or_id) 

184 else: 

185 uid = uid_from_username(username_or_id) 

186 

187 state.run(state.client.delete_user(uid), "Deleting user...") 

188 logger.info(f"Deleted user with ID {uid}.") 

189 

190 

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) 

215 

216 

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) 

235 

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.") 

240 

241 

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) 

259 

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.") 

264 

265 

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) 

299 

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}.") 

309 

310 

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) 

336 

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}.") 

341 

342 

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) 

349 

350 

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) 

372 

373 

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 

383 

384 

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) 

407 

408 render_result(user_info, ctx) 

409 

410 

411class UserListSortMode(Enum): 

412 """Sort modes for the user list command.""" 

413 

414 ID = "id" 

415 USERNAME = "username" 

416 NAME = "name" 

417 

418 

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)