Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/db/model/auth.py: 100%

65 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-12-31 19:12 -0600

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2025 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24Auth Models 

25 

26The :term:`auth handler` is primarily responsible for managing the 

27data for these models. 

28 

29Basic design/structure is as follows: 

30 

31* :class:`User` may be assigned to multiple roles 

32* :class:`Role` may contain multiple users (cf. :class:`UserRole`) 

33* :class:`Role` may be granted multiple permissions 

34* :class:`Permission` is a permission granted to a role 

35* roles are not nested/grouped; each is independent 

36* a few roles are built-in, e.g. Administrators 

37 

38So a user's permissions are "inherited" from the role(s) to which they 

39belong. 

40""" 

41 

42import sqlalchemy as sa 

43from sqlalchemy import orm 

44from sqlalchemy.ext.associationproxy import association_proxy 

45 

46from wuttjamaican.db.util import uuid_column, uuid_fk_column 

47from wuttjamaican.db.model.base import Base 

48from wuttjamaican.util import make_utc 

49 

50 

51class Role(Base): # pylint: disable=too-few-public-methods 

52 """ 

53 Represents an authentication role within the system; used for 

54 permission management. 

55 

56 .. attribute:: permissions 

57 

58 List of keys (string names) for permissions granted to this 

59 role. 

60 

61 See also :attr:`permission_refs`. 

62 

63 .. attribute:: users 

64 

65 List of :class:`User` instances belonging to this role. 

66 

67 See also :attr:`user_refs`. 

68 """ 

69 

70 __tablename__ = "role" 

71 __versioned__ = {} 

72 

73 uuid = uuid_column() 

74 

75 name = sa.Column( 

76 sa.String(length=100), 

77 nullable=False, 

78 unique=True, 

79 doc=""" 

80 Name for the role. Each role must have a name, which must be 

81 unique. 

82 """, 

83 ) 

84 

85 notes = sa.Column( 

86 sa.Text(), 

87 nullable=True, 

88 doc=""" 

89 Arbitrary notes for the role. 

90 """, 

91 ) 

92 

93 permission_refs = orm.relationship( 

94 "Permission", 

95 back_populates="role", 

96 cascade="all, delete-orphan", 

97 cascade_backrefs=False, 

98 doc=""" 

99 List of :class:`Permission` references for the role. 

100 

101 See also :attr:`permissions`. 

102 """, 

103 ) 

104 

105 permissions = association_proxy( 

106 "permission_refs", 

107 "permission", 

108 creator=lambda p: Permission(permission=p), 

109 # TODO 

110 # getset_factory=getset_factory, 

111 ) 

112 

113 user_refs = orm.relationship( 

114 "UserRole", 

115 back_populates="role", 

116 cascade="all, delete-orphan", 

117 cascade_backrefs=False, 

118 doc=""" 

119 List of :class:`UserRole` instances belonging to the role. 

120 

121 See also :attr:`users`. 

122 """, 

123 ) 

124 

125 users = association_proxy( 

126 "user_refs", 

127 "user", 

128 creator=lambda u: UserRole(user=u), 

129 # TODO 

130 # getset_factory=getset_factory, 

131 ) 

132 

133 def __str__(self): 

134 return self.name or "" 

135 

136 

137class Permission(Base): # pylint: disable=too-few-public-methods 

138 """ 

139 Represents a permission granted to a role. 

140 """ 

141 

142 __tablename__ = "permission" 

143 __versioned__ = {} 

144 

145 role_uuid = uuid_fk_column("role.uuid", primary_key=True, nullable=False) 

146 role = orm.relationship( 

147 Role, 

148 back_populates="permission_refs", 

149 cascade_backrefs=False, 

150 doc=""" 

151 Reference to the :class:`Role` for which the permission is 

152 granted. 

153 """, 

154 ) 

155 

156 permission = sa.Column( 

157 sa.String(length=254), 

158 primary_key=True, 

159 doc=""" 

160 Key (name) of the permission which is granted. 

161 """, 

162 ) 

163 

164 def __str__(self): 

165 return self.permission or "" 

166 

167 

168class User(Base): # pylint: disable=too-few-public-methods 

169 """ 

170 Represents a user of the system. 

171 

172 This may or may not correspond to a real person, i.e. some users 

173 may exist solely for automated tasks. 

174 

175 .. attribute:: roles 

176 

177 List of :class:`Role` instances to which the user belongs. 

178 

179 See also :attr:`role_refs`. 

180 """ 

181 

182 __tablename__ = "user" 

183 __versioned__ = {"exclude": ["password"]} 

184 

185 uuid = uuid_column() 

186 

187 username = sa.Column( 

188 sa.String(length=25), 

189 nullable=False, 

190 unique=True, 

191 doc=""" 

192 Account username. This is required and must be unique. 

193 """, 

194 ) 

195 

196 password = sa.Column( 

197 sa.String(length=60), 

198 nullable=True, 

199 doc=""" 

200 Hashed password for login. (The raw password is not stored.) 

201 """, 

202 ) 

203 

204 person_uuid = uuid_fk_column("person.uuid", nullable=True) 

205 person = orm.relationship( 

206 "Person", 

207 # TODO: seems like this is not needed? 

208 # uselist=False, 

209 back_populates="users", 

210 cascade_backrefs=False, 

211 doc=""" 

212 Reference to the :class:`~wuttjamaican.db.model.base.Person` 

213 whose user account this is. 

214 """, 

215 ) 

216 

217 active = sa.Column( 

218 sa.Boolean(), 

219 nullable=False, 

220 default=True, 

221 doc=""" 

222 Flag indicating whether the user account is "active" - it is 

223 ``True`` by default. 

224 

225 The default auth logic will prevent login for "inactive" user accounts. 

226 """, 

227 ) 

228 

229 prevent_edit = sa.Column( 

230 sa.Boolean(), 

231 nullable=True, 

232 doc=""" 

233 If set, this user account can only be edited by root. User cannot 

234 change their own password. 

235 """, 

236 ) 

237 

238 role_refs = orm.relationship( 

239 "UserRole", 

240 back_populates="user", 

241 cascade="all, delete-orphan", 

242 cascade_backrefs=False, 

243 doc=""" 

244 List of :class:`UserRole` instances belonging to the user. 

245 

246 See also :attr:`roles`. 

247 """, 

248 ) 

249 

250 roles = association_proxy( 

251 "role_refs", 

252 "role", 

253 creator=lambda r: UserRole(role=r), 

254 # TODO 

255 # getset_factory=getset_factory, 

256 ) 

257 

258 api_tokens = orm.relationship( 

259 "UserAPIToken", 

260 back_populates="user", 

261 order_by="UserAPIToken.created", 

262 cascade="all, delete-orphan", 

263 cascade_backrefs=False, 

264 doc=""" 

265 List of :class:`UserAPIToken` instances belonging to the user. 

266 """, 

267 ) 

268 

269 def __str__(self): 

270 if self.person: 

271 name = str(self.person) 

272 if name: 

273 return name 

274 return self.username or "" 

275 

276 

277class UserRole(Base): # pylint: disable=too-few-public-methods 

278 """ 

279 Represents the association between a user and a role; i.e. the 

280 user "belongs" or "is assigned" to the role. 

281 """ 

282 

283 __tablename__ = "user_x_role" 

284 __versioned__ = {} 

285 __wutta_hint__ = { 

286 "model_title": "User Role", 

287 "model_title_plural": "User Roles", 

288 } 

289 

290 uuid = uuid_column() 

291 

292 user_uuid = uuid_fk_column("user.uuid", nullable=False) 

293 user = orm.relationship( 

294 User, 

295 back_populates="role_refs", 

296 cascade_backrefs=False, 

297 doc=""" 

298 Reference to the :class:`User` involved. 

299 """, 

300 ) 

301 

302 role_uuid = uuid_fk_column("role.uuid", nullable=False) 

303 role = orm.relationship( 

304 Role, 

305 back_populates="user_refs", 

306 cascade_backrefs=False, 

307 doc=""" 

308 Reference to the :class:`Role` involved. 

309 """, 

310 ) 

311 

312 

313class UserAPIToken(Base): # pylint: disable=too-few-public-methods 

314 """ 

315 User authentication token for use with HTTP API 

316 """ 

317 

318 __tablename__ = "user_api_token" 

319 __wutta_hint__ = { 

320 "model_title": "User API Token", 

321 "model_title_plural": "User API Tokens", 

322 } 

323 

324 uuid = uuid_column() 

325 

326 user_uuid = uuid_fk_column("user.uuid", nullable=False) 

327 user = orm.relationship( 

328 User, 

329 back_populates="api_tokens", 

330 cascade_backrefs=False, 

331 doc=""" 

332 Reference to the :class:`User` whose token this is. 

333 """, 

334 ) 

335 

336 description = sa.Column( 

337 sa.String(length=255), 

338 nullable=False, 

339 doc=""" 

340 Description of the token. 

341 """, 

342 ) 

343 

344 token_string = sa.Column( 

345 sa.String(length=255), 

346 nullable=False, 

347 doc=""" 

348 Raw token string, to be used by API clients. 

349 """, 

350 ) 

351 

352 created = sa.Column( 

353 sa.DateTime(), 

354 nullable=False, 

355 default=make_utc, 

356 doc=""" 

357 Date/time when the token was created. 

358 """, 

359 ) 

360 

361 def __str__(self): 

362 return self.description or ""