Coverage for src/gitlabracadabra/objects/user.py: 88%

34 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-14 23:10 +0200

1# 

2# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com> 

3# 

4# This program is free software: you can redistribute it and/or modify 

5# it under the terms of the GNU Lesser General Public License as published by 

6# the Free Software Foundation, either version 3 of the License, or 

7# (at your option) any later version. 

8# 

9# This program is distributed in the hope that it will be useful, 

10# but WITHOUT ANY WARRANTY; without even the implied warranty of 

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

12# GNU Lesser General Public License for more details. 

13# 

14# You should have received a copy of the GNU Lesser General Public License 

15# along with this program. If not, see <http://www.gnu.org/licenses/>. 

16 

17import logging 

18from typing import ClassVar 

19 

20from gitlabracadabra.objects.object import GitLabracadabraObject 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25class GitLabracadabraUser(GitLabracadabraObject): 

26 EXAMPLE_YAML_HEADER: ClassVar[str] = "mmyuser:\n type: user\n" 

27 DOC: ClassVar[list[str]] = [ 

28 "# User lifecycle", 

29 "gitlab_id", 

30 "create_object", 

31 "delete_object", 

32 "# Edit", 

33 "## Account", 

34 "name", 

35 # 'username', 

36 "email", 

37 "skip_confirmation", 

38 "skip_reconfirmation", 

39 "public_email", 

40 "state", 

41 "## Password", 

42 "password", 

43 "reset_password", 

44 "force_random_password", 

45 "## Access", 

46 "projects_limit", 

47 "can_create_group", 

48 "admin", 

49 "external", 

50 "provider", 

51 "extern_uid", 

52 "## Limits", 

53 "shared_runners_minutes_limit", 

54 "extra_shared_runners_minutes_limit", 

55 "## Profile", 

56 "avatar", 

57 "skype", 

58 "linkedin", 

59 "twitter", 

60 "website_url", 

61 "location", 

62 "organization", 

63 "bio", 

64 "private_profile", 

65 "note", 

66 ] 

67 SCHEMA: ClassVar[dict] = { 

68 "$schema": "http://json-schema.org/draft-04/schema#", 

69 "title": "User", 

70 "type": "object", 

71 "properties": { 

72 # Standard properties 

73 "gitlab_id": { 

74 "type": "string", 

75 "description": "GitLab id", 

76 "_example": "gitlab", 

77 "_doc_link": "action_file.md#gitlab_id", 

78 }, 

79 "create_object": { 

80 "type": "boolean", 

81 "description": "Create object if it does not exists", 

82 }, 

83 "delete_object": { 

84 "type": "boolean", 

85 "description": "Delete object if it exists", 

86 }, 

87 # From https://docs.gitlab.com/ee/api/users.html#user-creation 

88 # 'username': { 

89 # 'type': 'string', 

90 # 'description': 'Username', 

91 # }, 

92 "name": { 

93 "type": "string", 

94 "description": "Name", 

95 }, 

96 "email": { 

97 "type": "string", 

98 "description": "Email", 

99 }, 

100 "skip_confirmation": { 

101 "type": "boolean", 

102 "description": "Skip confirmation and assume e-mail is verified", 

103 }, 

104 "skip_reconfirmation": { 

105 "type": "boolean", 

106 "description": "Skip reconfirmation", 

107 }, 

108 "public_email": { 

109 "type": "string", 

110 "description": "The public email of the user", 

111 }, 

112 "state": { 

113 "type": "string", 

114 "description": "User state", 

115 "enum": [ 

116 "active", 

117 "banned", 

118 "blocked", 

119 "blocked_pending_approval", 

120 "deactivated", 

121 "ldap_blocked", 

122 ], 

123 }, 

124 "password": { 

125 "type": "string", 

126 "description": "Password", 

127 }, 

128 "reset_password": { 

129 "type": "boolean", 

130 "description": "Send user password reset link", 

131 }, 

132 "force_random_password": { 

133 "type": "boolean", 

134 "description": "Set user password to a random value ", 

135 }, 

136 "projects_limit": { 

137 "type": "integer", 

138 "description": "Number of projects user can create", 

139 "multipleOf": 1, 

140 "minimum": 0, 

141 }, 

142 "can_create_group": { 

143 "type": "boolean", 

144 "description": "User can create groups", 

145 }, 

146 "admin": { 

147 "type": "boolean", 

148 "description": "User is admin", 

149 }, 

150 "external": { 

151 "type": "boolean", 

152 "description": "Flags the user as external", 

153 }, 

154 "provider": { 

155 "type": "string", 

156 "description": "External provider name", 

157 }, 

158 "extern_uid": { 

159 "type": "string", 

160 "description": "External UID", 

161 }, 

162 "shared_runners_minutes_limit": { 

163 "type": "integer", 

164 "description": "Pipeline minutes quota for this user", 

165 "multipleOf": 1, 

166 "minimum": 0, 

167 }, 

168 "extra_shared_runners_minutes_limit": { 

169 "type": "integer", 

170 "description": "Extra pipeline minutes quota for this user", 

171 "multipleOf": 1, 

172 "minimum": 0, 

173 }, 

174 "avatar": { 

175 "type": "string", 

176 "description": "Image file for user's avatar", 

177 }, 

178 "skype": { 

179 "type": "string", 

180 "description": "Skype ID", 

181 }, 

182 "linkedin": { 

183 "type": "string", 

184 "description": "LinkedIn", 

185 }, 

186 "twitter": { 

187 "type": "string", 

188 "description": "Twitter account", 

189 }, 

190 "website_url": { 

191 "type": "string", 

192 "description": "Website URL", 

193 }, 

194 "location": { 

195 "type": "string", 

196 "description": "User's location", 

197 }, 

198 "organization": { 

199 "type": "string", 

200 "description": "Organization name", 

201 }, 

202 "bio": { 

203 "type": "string", 

204 "description": "User's biography", 

205 }, 

206 "private_profile": { 

207 "type": "boolean", 

208 "description": "User's profile is private", 

209 }, 

210 "note": { 

211 "type": "string", 

212 "description": "Admin note", 

213 }, 

214 }, 

215 "additionalProperties": False, 

216 "dependencies": { 

217 "email": ["skip_reconfirmation"], 

218 }, 

219 } 

220 

221 FIND_PARAM = "username" 

222 

223 CREATE_KEY = "username" 

224 

225 CREATE_PARAMS: ClassVar[list[str]] = [ 

226 "email", 

227 "password", 

228 "force_random_password", 

229 "reset_password", 

230 "skip_confirmation", 

231 "name", 

232 ] 

233 

234 IGNORED_PARAMS: ClassVar[list[str]] = [ 

235 "password", 

236 "force_random_password", 

237 "reset_password", 

238 "skip_confirmation", 

239 "skip_reconfirmation", 

240 ] 

241 

242 """"_get_param() 

243 

244 Get a param value. 

245 """ 

246 

247 def _get_param(self, param_name): 

248 if param_name == "admin": 

249 param_name = "is_admin" 

250 return super()._get_param(param_name) 

251 

252 """"_process_state() 

253 

254 Process the state param. 

255 """ 

256 

257 def _process_state(self, param_name, param_value, *, dry_run=False, skip_save=False): 

258 assert param_name == "state" # noqa: S101 

259 assert not skip_save # noqa: S101 

260 

261 current_value = getattr(self._obj, param_name) 

262 if current_value != param_value: 262 ↛ exitline 262 didn't return from function '_process_state' because the condition on line 262 was always true

263 # From Gitlab's state machine 

264 # https://gitlab.com/gitlab-org/gitlab/-/blob/8976bab138344e55e7feb1725cf63770d0a2741b/app/models/user.rb#L324-367 

265 action = self._state_action(current_value, param_value) 

266 if action is None: 266 ↛ 267line 266 didn't jump to line 267 because the condition on line 266 was never true

267 logger.warning( 

268 "[%s] No action found to change param %s: %s -> %s (dry-run)", 

269 self._name, 

270 param_name, 

271 current_value, 

272 param_value, 

273 ) 

274 elif dry_run: 274 ↛ 275line 274 didn't jump to line 275 because the condition on line 274 was never true

275 logger.info( 

276 "[%s] NOT doing %s to change param %s: %s -> %s (dry-run)", 

277 self._name, 

278 action, 

279 param_name, 

280 current_value, 

281 param_value, 

282 ) 

283 else: 

284 logger.info( 

285 "[%s] Doing %s to change param %s: %s -> %s (dry-run)", 

286 self._name, 

287 action, 

288 param_name, 

289 current_value, 

290 param_value, 

291 ) 

292 getattr(self._obj, action)() 

293 

294 """"_state_action() 

295 

296 Get action. 

297 """ 

298 

299 def _state_action(self, current: str, target: str) -> str | None: 

300 # https://gitlab.com/gitlab-org/gitlab/-/blob/1856858760a831a568d6ddae912ed1fc141d76cd/app/models/user.rb#L356-433 

301 # and https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/users.rb 

302 # (current, target): action 

303 transitions = { 

304 ("active", "blocked"): "block", 

305 ("deactivated", "blocked"): "block", 

306 ("ldap_blocked", "blocked"): "block", 

307 ("blocked_pending_approval", "blocked"): "block", 

308 ("blocked", "active"): "unblock", 

309 ("blocked_pending_approval", "active"): "approve", 

310 ("deactivated", "active"): "activate", 

311 ("active", "banned"): "ban", 

312 ("banned", "active"): "unban", 

313 ("active", "deactivated"): "deactivate", 

314 } 

315 return transitions.get((current, target), None)