common.lib.user
User class
1""" 2User class 3""" 4import html2text 5import hashlib 6import smtplib 7import socket 8import bcrypt 9import json 10import time 11import os 12 13from email.mime.multipart import MIMEMultipart 14from email.mime.text import MIMEText 15from common.lib.helpers import send_email 16from common.lib.exceptions import DataSetException 17from common.config_manager import config as global_config 18 19 20class User: 21 """ 22 User class 23 24 Compatible with Flask-Login 25 """ 26 data = None 27 userdata = None 28 is_authenticated = False 29 is_active = False 30 is_anonymous = True 31 config = None 32 db = None 33 34 name = "anonymous" 35 36 @staticmethod 37 def get_by_login(db, name, password, config=None): 38 """ 39 Get user object, if login is correct 40 41 If the login data supplied to this method is correct, a new user object 42 that is marked as authenticated is returned. 43 44 :param db: Database connection object 45 :param name: User name 46 :param password: User password 47 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 48 use a global configuration manager. 49 :return: User object, or `None` if login was invalid 50 """ 51 user = db.fetchone("SELECT * FROM users WHERE name = %s", (name,)) 52 if not user or not user.get("password", None): 53 # registration not finished yet 54 return None 55 elif not user or not bcrypt.checkpw(password.encode("ascii"), user["password"].encode("ascii")): 56 # non-existing user or wrong password 57 return None 58 else: 59 # valid login! 60 return User(db, user, authenticated=True, config=config) 61 62 @staticmethod 63 def get_by_name(db, name, config=None): 64 """ 65 Get user object for given user name 66 67 :param db: Database connection object 68 :param str name: Username to get object for 69 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 70 use a global configuration manager. 71 :return: User object, or `None` for invalid user name 72 """ 73 user = db.fetchone("SELECT * FROM users WHERE name = %s", (name,)) 74 if not user: 75 return None 76 else: 77 return User(db, user, config=config) 78 79 @staticmethod 80 def get_by_token(db, token, config=None): 81 """ 82 Get user object for given token, if token is valid 83 84 :param db: Database connection object 85 :param str token: Token to get object for 86 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 87 use a global configuration manager. 88 :return: User object, or `None` for invalid token 89 """ 90 user = db.fetchone( 91 "SELECT * FROM users WHERE register_token = %s AND (timestamp_token = 0 OR timestamp_token > %s)", 92 (token, int(time.time()) - (7 * 86400))) 93 if not user: 94 return None 95 else: 96 return User(db, user, config=config) 97 98 def __init__(self, db, data, authenticated=False, config=None): 99 """ 100 Instantiate user object 101 102 Also sets the properties required by Flask-Login. 103 104 :param db: Database connection object 105 :param data: User data 106 :param authenticated: Whether the user should be marked as authenticated 107 """ 108 self.db = db 109 self.data = data 110 111 self.config = config if config else global_config 112 113 try: 114 self.userdata = json.loads(self.data["userdata"]) 115 except (TypeError, json.JSONDecodeError): 116 self.userdata = {} 117 118 if self.data["name"] != "anonymous": 119 self.is_anonymous = False 120 self.is_active = True 121 122 self.name = self.data["name"] 123 self.is_authenticated = authenticated 124 125 self.userdata = json.loads(self.data.get("userdata", "{}")) 126 127 if not self.is_anonymous and self.is_authenticated: 128 self.db.update("users", where={"name": self.data["name"]}, data={"timestamp_seen": int(time.time())}) 129 130 def authenticate(self): 131 """ 132 Mark user object as authenticated. 133 """ 134 self.is_authenticated = True 135 136 def get_id(self): 137 """ 138 Get user ID 139 140 :return: User ID 141 """ 142 return self.data["name"] 143 144 def get_name(self): 145 """ 146 Get user name 147 148 This is usually the user ID. For the two special users, provide a nicer 149 name to display in interfaces, etc. 150 151 :return: User name 152 """ 153 if self.data["name"] == "anonymous": 154 return "Anonymous" 155 elif self.data["name"] == "autologin": 156 return self.config.get("flask.autologin.name") 157 else: 158 return self.data["name"] 159 160 def get_token(self): 161 """ 162 Get password reset token 163 164 May be empty or invalid! 165 166 :return str: Password reset token 167 """ 168 return self.generate_token(regenerate=False) 169 170 def with_config(self, config): 171 """ 172 Connect user to configuration manager 173 174 By default, the user object reads from the global configuration 175 manager. For frontend operations it may be desireable to use a 176 request-aware configuration manager, but this is only available after 177 the user has been instantiated. This method can thus be used to connect 178 the user to that config manager later when it is available. 179 180 :param config: Configuration manager object 181 :return: 182 """ 183 self.config = config 184 185 def clear_token(self): 186 """ 187 Reset password rest token 188 189 Clears the token and token timestamp. This allows requesting a new one 190 even if the old one had not expired yet. 191 192 :return: 193 """ 194 self.db.update("users", data={"register_token": "", "timestamp_token": 0}, where={"name": self.get_id()}) 195 196 def can_access_dataset(self, dataset, role=None): 197 """ 198 Check if this user should be able to access a given dataset. 199 200 This depends mostly on the dataset's owner, which should match the 201 user if the dataset is private. If the dataset is not private, or 202 if the user is an admin or the dataset is private but assigned to 203 an anonymous user, the dataset can be accessed. 204 205 :param dataset: The dataset to check access to 206 :return bool: 207 """ 208 if not dataset.is_private: 209 return True 210 211 elif self.is_admin: 212 return True 213 214 elif self.config.get("privileges.can_view_private_datasets", user=self): 215 # Allowed to see dataset, but perhaps not run processors (need privileges.admin.can_manipulate_all_datasets or dataset ownership) 216 return True 217 218 elif dataset.is_accessible_by(self, role=role): 219 return True 220 221 elif dataset.get_owners == ("anonymous",): 222 return True 223 224 else: 225 return False 226 227 @property 228 def is_special(self): 229 """ 230 Check if user is special user 231 232 :return: Whether the user is the anonymous user, or the automatically 233 logged in user. 234 """ 235 return self.get_id() in ("autologin", "anonymous") 236 237 @property 238 def is_admin(self): 239 """ 240 Check if user is an administrator 241 242 :return bool: 243 """ 244 try: 245 return "admin" in self.data["tags"] 246 except (ValueError, TypeError) as e: 247 # invalid JSON? 248 return False 249 250 @property 251 def is_deactivated(self): 252 """ 253 Check if user has been deactivated 254 255 :return bool: 256 """ 257 return self.data.get("is_deactivated", False) 258 259 def email_token(self, new=False): 260 """ 261 Send user an e-mail with a password reset link 262 263 Generates a token that the user can use to reset their password. The 264 token is valid for 72 hours. 265 266 Note that this requires a mail server to be configured, or a 267 `RuntimeError` will be raised. If a server is configured but the mail 268 still fails to send, it will also raise a `RuntimeError`. Note that 269 in these cases a token is still created and valid (the user just gets 270 no notification, but an admin could forward the correct link). 271 272 If the user is a 'special' user, a `ValueError` is raised. 273 274 :param bool new: Is this the first time setting a password for this 275 account? 276 :return str: Link for the user to set their password with 277 """ 278 if not self.config.get('mail.server'): 279 raise RuntimeError("No e-mail server configured. 4CAT cannot send any e-mails.") 280 281 if self.is_special: 282 raise ValueError("Cannot send password reset e-mails for a special user.") 283 284 username = self.get_id() 285 286 # generate a password reset token 287 register_token = self.generate_token(regenerate=True) 288 289 # prepare welcome e-mail 290 sender = self.config.get('mail.noreply') 291 message = MIMEMultipart("alternative") 292 message["From"] = sender 293 message["To"] = username 294 295 # the actual e-mail... 296 url_base = self.config.get("flask.server_name") 297 protocol = "https" if self.config.get("flask.https") else "http" 298 url = "%s://%s/reset-password/?token=%s" % (protocol, url_base, register_token) 299 300 # we use slightly different e-mails depending on whether this is the first time setting a password 301 if new: 302 303 message["Subject"] = "Account created" 304 mail = """ 305 <p>Hello %s,</p> 306 <p>A 4CAT account has been created for you. You can now log in to 4CAT at <a href="%s://%s">%s</a>.</p> 307 <p>Note that before you log in, you will need to set a password. You can do so via the following link:</p> 308 <p><a href="%s">%s</a></p> 309 <p>Please complete your registration within 72 hours as the link above will become invalid after this time.</p> 310 """ % (username, protocol, url_base, url_base, url, url) 311 else: 312 313 message["Subject"] = "Password reset" 314 mail = """ 315 <p>Hello %s,</p> 316 <p>Someone has requested a password reset for your 4CAT account. If that someone is you, great! If not, feel free to ignore this e-mail.</p> 317 <p>You can change your password via the following link:</p> 318 <p><a href="%s">%s</a></p> 319 <p>Please do this within 72 hours as the link above will become invalid after this time.</p> 320 """ % (username, url, url) 321 322 # provide a plain-text alternative as well 323 html_parser = html2text.HTML2Text() 324 message.attach(MIMEText(html_parser.handle(mail), "plain")) 325 message.attach(MIMEText(mail, "html")) 326 327 # try to send it 328 try: 329 send_email([username], message) 330 return url 331 except (smtplib.SMTPException, ConnectionRefusedError, socket.timeout, socket.gaierror) as e: 332 raise RuntimeError("Could not send password reset e-mail: %s" % e) 333 334 def generate_token(self, username=None, regenerate=True): 335 """ 336 Generate and store a new registration token for this user 337 338 Tokens are not re-generated if they exist already 339 340 :param username: Username to generate for: if left empty, it will be 341 inferred from self.data 342 :param regenerate: Force regenerating even if token exists 343 :return str: The token 344 """ 345 if self.data.get("register_token", None) and not regenerate: 346 return self.data["register_token"] 347 348 if not username: 349 username = self.data["name"] 350 351 register_token = hashlib.sha256() 352 register_token.update(os.urandom(128)) 353 register_token = register_token.hexdigest() 354 self.db.update("users", data={"register_token": register_token, "timestamp_token": int(time.time())}, 355 where={"name": username}) 356 357 return register_token 358 359 def get_value(self, key, default=None): 360 """ 361 Get persistently stored user property 362 363 :param key: Name of item to get 364 :param default: What to return if key is not avaiable (default None) 365 :return: 366 """ 367 return self.userdata.get(key, default) 368 369 def set_value(self, key, value): 370 """ 371 Set persistently stored user property 372 373 :param key: Name of item to store 374 :param value: Value 375 :return: 376 """ 377 self.userdata[key] = value 378 self.data["userdata"] = json.dumps(self.userdata) 379 380 self.db.update("users", where={"name": self.get_id()}, data={"userdata": json.dumps(self.userdata)}) 381 382 def set_password(self, password): 383 """ 384 Set user password 385 386 :param password: Password to set 387 """ 388 if self.is_anonymous: 389 raise Exception("Cannot set password for anonymous user") 390 391 salt = bcrypt.gensalt() 392 password_hash = bcrypt.hashpw(password.encode("ascii"), salt) 393 394 self.db.update("users", where={"name": self.data["name"]}, data={"password": password_hash.decode("utf-8")}) 395 396 def add_notification(self, notification, expires=None, allow_dismiss=True): 397 """ 398 Add a notification for this user 399 400 Notifications that already exist with the same parameters are not added 401 again. 402 403 :param str notification: The content of the notification. Can contain 404 Markdown. 405 :param int expires: Timestamp when the notification should expire. If 406 not provided, the notification does not expire 407 :param bool allow_dismiss: Whether to allow a user to dismiss the 408 notification. 409 """ 410 self.db.insert("users_notifications", { 411 "username": self.get_id(), 412 "notification": notification, 413 "timestamp_expires": expires, 414 "allow_dismiss": allow_dismiss 415 }, safe=True) 416 417 def dismiss_notification(self, notification_id): 418 """ 419 Dismiss a notification 420 421 The user can only dismiss notifications they can see! 422 423 :param int notification_id: ID of the notification to dismiss 424 """ 425 current_notifications = [n["id"] for n in self.get_notifications() if n["allow_dismiss"]] 426 if notification_id not in current_notifications: 427 return 428 429 self.db.delete("users_notifications", where={"id": notification_id}) 430 431 def get_notifications(self): 432 """ 433 Get all notifications for this user 434 435 That is all the user's own notifications, plus those for the groups of 436 users this user belongs to 437 438 :return list: Notifications, as a list of dictionaries 439 """ 440 tag_recipients = ["!everyone", *[f"!{tag}" for tag in self.config.get_active_tags(self)]] 441 if self.is_admin: 442 # for backwards compatibility - used to be called '!admins' even if the tag is 'admin' 443 tag_recipients.append("!admins") 444 445 notifications = self.db.fetchall( 446 "SELECT n.* FROM users_notifications AS n, users AS u " 447 "WHERE u.name = %s " 448 "AND (u.name = n.username OR n.username IN %s)", (self.get_id(), tuple(tag_recipients))) 449 450 return notifications 451 452 def add_tag(self, tag): 453 """ 454 Add tag to user 455 456 If the tag is already in the tag list, nothing happens. 457 458 :param str tag: Tag 459 """ 460 if tag not in self.data["tags"]: 461 self.data["tags"].append(tag) 462 self.sort_user_tags() 463 464 def remove_tag(self, tag): 465 """ 466 Remove tag from user 467 468 If the tag is not part of the tag list, nothing happens. 469 470 :param str tag: Tag 471 """ 472 if tag in self.data["tags"]: 473 self.data["tags"].remove(tag) 474 self.sort_user_tags() 475 476 def sort_user_tags(self): 477 """ 478 Ensure user tags are stored in the correct order 479 480 The order of the tags matters, since it decides which one will get to 481 override the global configuration. To avoid having to cross-reference 482 the canonical order every time the tags are queried, we ensure that the 483 tags are stored in the database in the right order to begin with. This 484 method ensures that. 485 """ 486 tags = self.data["tags"] 487 sorted_tags = [] 488 489 for tag in self.config.get("flask.tag_order"): 490 if tag in tags: 491 sorted_tags.append(tag) 492 493 for tag in tags: 494 if tag not in sorted_tags: 495 sorted_tags.append(tag) 496 497 # whitespace isn't a tag 498 sorted_tags = [tag for tag in sorted_tags if tag.strip()] 499 500 self.data["tags"] = sorted_tags 501 self.db.update("users", where={"name": self.get_id()}, data={"tags": json.dumps(sorted_tags)}) 502 503 def delete(self, also_datasets=True): 504 from common.lib.dataset import DataSet 505 506 username = self.data["name"] 507 508 self.db.delete("users_favourites", where={"name": username}, commit=False), 509 self.db.delete("users_notifications", where={"username": username}, commit=False) 510 self.db.delete("access_tokens", where={"name": username}, commit=False) 511 512 # find datasets and delete 513 datasets = self.db.fetchall("SELECT key FROM datasets_owners WHERE name = %s", (username,)) 514 515 # delete any datasets and jobs related to deleted datasets 516 if datasets: 517 for dataset in datasets: 518 try: 519 dataset = DataSet(key=dataset["key"], db=self.db) 520 except DataSetException: 521 # dataset already deleted? 522 continue 523 524 if len(dataset.get_owners()) == 1 and also_datasets: 525 dataset.delete(commit=False) 526 self.db.delete("jobs", where={"remote_id": dataset.key}, commit=False) 527 else: 528 dataset.remove_owner(self) 529 530 # and finally the user 531 self.db.delete("users", where={"name": username}, commit=False) 532 self.db.commit() 533 534 def __getattr__(self, attr): 535 """ 536 Getter so we don't have to use .data all the time 537 538 :param attr: Data key to get 539 :return: Value 540 """ 541 542 if attr in dir(self): 543 # an explicitly defined attribute should always be called in favour 544 # of this passthrough 545 attribute = getattr(self, attr) 546 return attribute 547 elif attr in self.data: 548 return self.data[attr] 549 else: 550 raise AttributeError("User object has no attribute %s" % attr) 551 552 def __setattr__(self, attr, value): 553 """ 554 Setter so we can flexibly update the database 555 556 Also updates internal data stores (.data etc). If the attribute is 557 unknown, it is stored within the 'userdata' attribute. 558 559 :param str attr: Attribute to update 560 :param value: New value 561 """ 562 563 # don't override behaviour for *actual* class attributes 564 if attr in dir(self): 565 super().__setattr__(attr, value) 566 return 567 568 if attr not in self.data: 569 self.userdata[attr] = value 570 attr = "userdata" 571 value = self.userdata 572 573 if attr == "userdata": 574 value = json.dumps(value) 575 576 self.db.update("users", where={"name": self.get_id()}, data={attr: value}) 577 578 self.data[attr] = value 579 580 if attr == "userdata": 581 self.userdata = json.loads(value)
21class User: 22 """ 23 User class 24 25 Compatible with Flask-Login 26 """ 27 data = None 28 userdata = None 29 is_authenticated = False 30 is_active = False 31 is_anonymous = True 32 config = None 33 db = None 34 35 name = "anonymous" 36 37 @staticmethod 38 def get_by_login(db, name, password, config=None): 39 """ 40 Get user object, if login is correct 41 42 If the login data supplied to this method is correct, a new user object 43 that is marked as authenticated is returned. 44 45 :param db: Database connection object 46 :param name: User name 47 :param password: User password 48 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 49 use a global configuration manager. 50 :return: User object, or `None` if login was invalid 51 """ 52 user = db.fetchone("SELECT * FROM users WHERE name = %s", (name,)) 53 if not user or not user.get("password", None): 54 # registration not finished yet 55 return None 56 elif not user or not bcrypt.checkpw(password.encode("ascii"), user["password"].encode("ascii")): 57 # non-existing user or wrong password 58 return None 59 else: 60 # valid login! 61 return User(db, user, authenticated=True, config=config) 62 63 @staticmethod 64 def get_by_name(db, name, config=None): 65 """ 66 Get user object for given user name 67 68 :param db: Database connection object 69 :param str name: Username to get object for 70 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 71 use a global configuration manager. 72 :return: User object, or `None` for invalid user name 73 """ 74 user = db.fetchone("SELECT * FROM users WHERE name = %s", (name,)) 75 if not user: 76 return None 77 else: 78 return User(db, user, config=config) 79 80 @staticmethod 81 def get_by_token(db, token, config=None): 82 """ 83 Get user object for given token, if token is valid 84 85 :param db: Database connection object 86 :param str token: Token to get object for 87 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 88 use a global configuration manager. 89 :return: User object, or `None` for invalid token 90 """ 91 user = db.fetchone( 92 "SELECT * FROM users WHERE register_token = %s AND (timestamp_token = 0 OR timestamp_token > %s)", 93 (token, int(time.time()) - (7 * 86400))) 94 if not user: 95 return None 96 else: 97 return User(db, user, config=config) 98 99 def __init__(self, db, data, authenticated=False, config=None): 100 """ 101 Instantiate user object 102 103 Also sets the properties required by Flask-Login. 104 105 :param db: Database connection object 106 :param data: User data 107 :param authenticated: Whether the user should be marked as authenticated 108 """ 109 self.db = db 110 self.data = data 111 112 self.config = config if config else global_config 113 114 try: 115 self.userdata = json.loads(self.data["userdata"]) 116 except (TypeError, json.JSONDecodeError): 117 self.userdata = {} 118 119 if self.data["name"] != "anonymous": 120 self.is_anonymous = False 121 self.is_active = True 122 123 self.name = self.data["name"] 124 self.is_authenticated = authenticated 125 126 self.userdata = json.loads(self.data.get("userdata", "{}")) 127 128 if not self.is_anonymous and self.is_authenticated: 129 self.db.update("users", where={"name": self.data["name"]}, data={"timestamp_seen": int(time.time())}) 130 131 def authenticate(self): 132 """ 133 Mark user object as authenticated. 134 """ 135 self.is_authenticated = True 136 137 def get_id(self): 138 """ 139 Get user ID 140 141 :return: User ID 142 """ 143 return self.data["name"] 144 145 def get_name(self): 146 """ 147 Get user name 148 149 This is usually the user ID. For the two special users, provide a nicer 150 name to display in interfaces, etc. 151 152 :return: User name 153 """ 154 if self.data["name"] == "anonymous": 155 return "Anonymous" 156 elif self.data["name"] == "autologin": 157 return self.config.get("flask.autologin.name") 158 else: 159 return self.data["name"] 160 161 def get_token(self): 162 """ 163 Get password reset token 164 165 May be empty or invalid! 166 167 :return str: Password reset token 168 """ 169 return self.generate_token(regenerate=False) 170 171 def with_config(self, config): 172 """ 173 Connect user to configuration manager 174 175 By default, the user object reads from the global configuration 176 manager. For frontend operations it may be desireable to use a 177 request-aware configuration manager, but this is only available after 178 the user has been instantiated. This method can thus be used to connect 179 the user to that config manager later when it is available. 180 181 :param config: Configuration manager object 182 :return: 183 """ 184 self.config = config 185 186 def clear_token(self): 187 """ 188 Reset password rest token 189 190 Clears the token and token timestamp. This allows requesting a new one 191 even if the old one had not expired yet. 192 193 :return: 194 """ 195 self.db.update("users", data={"register_token": "", "timestamp_token": 0}, where={"name": self.get_id()}) 196 197 def can_access_dataset(self, dataset, role=None): 198 """ 199 Check if this user should be able to access a given dataset. 200 201 This depends mostly on the dataset's owner, which should match the 202 user if the dataset is private. If the dataset is not private, or 203 if the user is an admin or the dataset is private but assigned to 204 an anonymous user, the dataset can be accessed. 205 206 :param dataset: The dataset to check access to 207 :return bool: 208 """ 209 if not dataset.is_private: 210 return True 211 212 elif self.is_admin: 213 return True 214 215 elif self.config.get("privileges.can_view_private_datasets", user=self): 216 # Allowed to see dataset, but perhaps not run processors (need privileges.admin.can_manipulate_all_datasets or dataset ownership) 217 return True 218 219 elif dataset.is_accessible_by(self, role=role): 220 return True 221 222 elif dataset.get_owners == ("anonymous",): 223 return True 224 225 else: 226 return False 227 228 @property 229 def is_special(self): 230 """ 231 Check if user is special user 232 233 :return: Whether the user is the anonymous user, or the automatically 234 logged in user. 235 """ 236 return self.get_id() in ("autologin", "anonymous") 237 238 @property 239 def is_admin(self): 240 """ 241 Check if user is an administrator 242 243 :return bool: 244 """ 245 try: 246 return "admin" in self.data["tags"] 247 except (ValueError, TypeError) as e: 248 # invalid JSON? 249 return False 250 251 @property 252 def is_deactivated(self): 253 """ 254 Check if user has been deactivated 255 256 :return bool: 257 """ 258 return self.data.get("is_deactivated", False) 259 260 def email_token(self, new=False): 261 """ 262 Send user an e-mail with a password reset link 263 264 Generates a token that the user can use to reset their password. The 265 token is valid for 72 hours. 266 267 Note that this requires a mail server to be configured, or a 268 `RuntimeError` will be raised. If a server is configured but the mail 269 still fails to send, it will also raise a `RuntimeError`. Note that 270 in these cases a token is still created and valid (the user just gets 271 no notification, but an admin could forward the correct link). 272 273 If the user is a 'special' user, a `ValueError` is raised. 274 275 :param bool new: Is this the first time setting a password for this 276 account? 277 :return str: Link for the user to set their password with 278 """ 279 if not self.config.get('mail.server'): 280 raise RuntimeError("No e-mail server configured. 4CAT cannot send any e-mails.") 281 282 if self.is_special: 283 raise ValueError("Cannot send password reset e-mails for a special user.") 284 285 username = self.get_id() 286 287 # generate a password reset token 288 register_token = self.generate_token(regenerate=True) 289 290 # prepare welcome e-mail 291 sender = self.config.get('mail.noreply') 292 message = MIMEMultipart("alternative") 293 message["From"] = sender 294 message["To"] = username 295 296 # the actual e-mail... 297 url_base = self.config.get("flask.server_name") 298 protocol = "https" if self.config.get("flask.https") else "http" 299 url = "%s://%s/reset-password/?token=%s" % (protocol, url_base, register_token) 300 301 # we use slightly different e-mails depending on whether this is the first time setting a password 302 if new: 303 304 message["Subject"] = "Account created" 305 mail = """ 306 <p>Hello %s,</p> 307 <p>A 4CAT account has been created for you. You can now log in to 4CAT at <a href="%s://%s">%s</a>.</p> 308 <p>Note that before you log in, you will need to set a password. You can do so via the following link:</p> 309 <p><a href="%s">%s</a></p> 310 <p>Please complete your registration within 72 hours as the link above will become invalid after this time.</p> 311 """ % (username, protocol, url_base, url_base, url, url) 312 else: 313 314 message["Subject"] = "Password reset" 315 mail = """ 316 <p>Hello %s,</p> 317 <p>Someone has requested a password reset for your 4CAT account. If that someone is you, great! If not, feel free to ignore this e-mail.</p> 318 <p>You can change your password via the following link:</p> 319 <p><a href="%s">%s</a></p> 320 <p>Please do this within 72 hours as the link above will become invalid after this time.</p> 321 """ % (username, url, url) 322 323 # provide a plain-text alternative as well 324 html_parser = html2text.HTML2Text() 325 message.attach(MIMEText(html_parser.handle(mail), "plain")) 326 message.attach(MIMEText(mail, "html")) 327 328 # try to send it 329 try: 330 send_email([username], message) 331 return url 332 except (smtplib.SMTPException, ConnectionRefusedError, socket.timeout, socket.gaierror) as e: 333 raise RuntimeError("Could not send password reset e-mail: %s" % e) 334 335 def generate_token(self, username=None, regenerate=True): 336 """ 337 Generate and store a new registration token for this user 338 339 Tokens are not re-generated if they exist already 340 341 :param username: Username to generate for: if left empty, it will be 342 inferred from self.data 343 :param regenerate: Force regenerating even if token exists 344 :return str: The token 345 """ 346 if self.data.get("register_token", None) and not regenerate: 347 return self.data["register_token"] 348 349 if not username: 350 username = self.data["name"] 351 352 register_token = hashlib.sha256() 353 register_token.update(os.urandom(128)) 354 register_token = register_token.hexdigest() 355 self.db.update("users", data={"register_token": register_token, "timestamp_token": int(time.time())}, 356 where={"name": username}) 357 358 return register_token 359 360 def get_value(self, key, default=None): 361 """ 362 Get persistently stored user property 363 364 :param key: Name of item to get 365 :param default: What to return if key is not avaiable (default None) 366 :return: 367 """ 368 return self.userdata.get(key, default) 369 370 def set_value(self, key, value): 371 """ 372 Set persistently stored user property 373 374 :param key: Name of item to store 375 :param value: Value 376 :return: 377 """ 378 self.userdata[key] = value 379 self.data["userdata"] = json.dumps(self.userdata) 380 381 self.db.update("users", where={"name": self.get_id()}, data={"userdata": json.dumps(self.userdata)}) 382 383 def set_password(self, password): 384 """ 385 Set user password 386 387 :param password: Password to set 388 """ 389 if self.is_anonymous: 390 raise Exception("Cannot set password for anonymous user") 391 392 salt = bcrypt.gensalt() 393 password_hash = bcrypt.hashpw(password.encode("ascii"), salt) 394 395 self.db.update("users", where={"name": self.data["name"]}, data={"password": password_hash.decode("utf-8")}) 396 397 def add_notification(self, notification, expires=None, allow_dismiss=True): 398 """ 399 Add a notification for this user 400 401 Notifications that already exist with the same parameters are not added 402 again. 403 404 :param str notification: The content of the notification. Can contain 405 Markdown. 406 :param int expires: Timestamp when the notification should expire. If 407 not provided, the notification does not expire 408 :param bool allow_dismiss: Whether to allow a user to dismiss the 409 notification. 410 """ 411 self.db.insert("users_notifications", { 412 "username": self.get_id(), 413 "notification": notification, 414 "timestamp_expires": expires, 415 "allow_dismiss": allow_dismiss 416 }, safe=True) 417 418 def dismiss_notification(self, notification_id): 419 """ 420 Dismiss a notification 421 422 The user can only dismiss notifications they can see! 423 424 :param int notification_id: ID of the notification to dismiss 425 """ 426 current_notifications = [n["id"] for n in self.get_notifications() if n["allow_dismiss"]] 427 if notification_id not in current_notifications: 428 return 429 430 self.db.delete("users_notifications", where={"id": notification_id}) 431 432 def get_notifications(self): 433 """ 434 Get all notifications for this user 435 436 That is all the user's own notifications, plus those for the groups of 437 users this user belongs to 438 439 :return list: Notifications, as a list of dictionaries 440 """ 441 tag_recipients = ["!everyone", *[f"!{tag}" for tag in self.config.get_active_tags(self)]] 442 if self.is_admin: 443 # for backwards compatibility - used to be called '!admins' even if the tag is 'admin' 444 tag_recipients.append("!admins") 445 446 notifications = self.db.fetchall( 447 "SELECT n.* FROM users_notifications AS n, users AS u " 448 "WHERE u.name = %s " 449 "AND (u.name = n.username OR n.username IN %s)", (self.get_id(), tuple(tag_recipients))) 450 451 return notifications 452 453 def add_tag(self, tag): 454 """ 455 Add tag to user 456 457 If the tag is already in the tag list, nothing happens. 458 459 :param str tag: Tag 460 """ 461 if tag not in self.data["tags"]: 462 self.data["tags"].append(tag) 463 self.sort_user_tags() 464 465 def remove_tag(self, tag): 466 """ 467 Remove tag from user 468 469 If the tag is not part of the tag list, nothing happens. 470 471 :param str tag: Tag 472 """ 473 if tag in self.data["tags"]: 474 self.data["tags"].remove(tag) 475 self.sort_user_tags() 476 477 def sort_user_tags(self): 478 """ 479 Ensure user tags are stored in the correct order 480 481 The order of the tags matters, since it decides which one will get to 482 override the global configuration. To avoid having to cross-reference 483 the canonical order every time the tags are queried, we ensure that the 484 tags are stored in the database in the right order to begin with. This 485 method ensures that. 486 """ 487 tags = self.data["tags"] 488 sorted_tags = [] 489 490 for tag in self.config.get("flask.tag_order"): 491 if tag in tags: 492 sorted_tags.append(tag) 493 494 for tag in tags: 495 if tag not in sorted_tags: 496 sorted_tags.append(tag) 497 498 # whitespace isn't a tag 499 sorted_tags = [tag for tag in sorted_tags if tag.strip()] 500 501 self.data["tags"] = sorted_tags 502 self.db.update("users", where={"name": self.get_id()}, data={"tags": json.dumps(sorted_tags)}) 503 504 def delete(self, also_datasets=True): 505 from common.lib.dataset import DataSet 506 507 username = self.data["name"] 508 509 self.db.delete("users_favourites", where={"name": username}, commit=False), 510 self.db.delete("users_notifications", where={"username": username}, commit=False) 511 self.db.delete("access_tokens", where={"name": username}, commit=False) 512 513 # find datasets and delete 514 datasets = self.db.fetchall("SELECT key FROM datasets_owners WHERE name = %s", (username,)) 515 516 # delete any datasets and jobs related to deleted datasets 517 if datasets: 518 for dataset in datasets: 519 try: 520 dataset = DataSet(key=dataset["key"], db=self.db) 521 except DataSetException: 522 # dataset already deleted? 523 continue 524 525 if len(dataset.get_owners()) == 1 and also_datasets: 526 dataset.delete(commit=False) 527 self.db.delete("jobs", where={"remote_id": dataset.key}, commit=False) 528 else: 529 dataset.remove_owner(self) 530 531 # and finally the user 532 self.db.delete("users", where={"name": username}, commit=False) 533 self.db.commit() 534 535 def __getattr__(self, attr): 536 """ 537 Getter so we don't have to use .data all the time 538 539 :param attr: Data key to get 540 :return: Value 541 """ 542 543 if attr in dir(self): 544 # an explicitly defined attribute should always be called in favour 545 # of this passthrough 546 attribute = getattr(self, attr) 547 return attribute 548 elif attr in self.data: 549 return self.data[attr] 550 else: 551 raise AttributeError("User object has no attribute %s" % attr) 552 553 def __setattr__(self, attr, value): 554 """ 555 Setter so we can flexibly update the database 556 557 Also updates internal data stores (.data etc). If the attribute is 558 unknown, it is stored within the 'userdata' attribute. 559 560 :param str attr: Attribute to update 561 :param value: New value 562 """ 563 564 # don't override behaviour for *actual* class attributes 565 if attr in dir(self): 566 super().__setattr__(attr, value) 567 return 568 569 if attr not in self.data: 570 self.userdata[attr] = value 571 attr = "userdata" 572 value = self.userdata 573 574 if attr == "userdata": 575 value = json.dumps(value) 576 577 self.db.update("users", where={"name": self.get_id()}, data={attr: value}) 578 579 self.data[attr] = value 580 581 if attr == "userdata": 582 self.userdata = json.loads(value)
User class
Compatible with Flask-Login
99 def __init__(self, db, data, authenticated=False, config=None): 100 """ 101 Instantiate user object 102 103 Also sets the properties required by Flask-Login. 104 105 :param db: Database connection object 106 :param data: User data 107 :param authenticated: Whether the user should be marked as authenticated 108 """ 109 self.db = db 110 self.data = data 111 112 self.config = config if config else global_config 113 114 try: 115 self.userdata = json.loads(self.data["userdata"]) 116 except (TypeError, json.JSONDecodeError): 117 self.userdata = {} 118 119 if self.data["name"] != "anonymous": 120 self.is_anonymous = False 121 self.is_active = True 122 123 self.name = self.data["name"] 124 self.is_authenticated = authenticated 125 126 self.userdata = json.loads(self.data.get("userdata", "{}")) 127 128 if not self.is_anonymous and self.is_authenticated: 129 self.db.update("users", where={"name": self.data["name"]}, data={"timestamp_seen": int(time.time())})
Instantiate user object
Also sets the properties required by Flask-Login.
Parameters
- db: Database connection object
- data: User data
- authenticated: Whether the user should be marked as authenticated
37 @staticmethod 38 def get_by_login(db, name, password, config=None): 39 """ 40 Get user object, if login is correct 41 42 If the login data supplied to this method is correct, a new user object 43 that is marked as authenticated is returned. 44 45 :param db: Database connection object 46 :param name: User name 47 :param password: User password 48 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 49 use a global configuration manager. 50 :return: User object, or `None` if login was invalid 51 """ 52 user = db.fetchone("SELECT * FROM users WHERE name = %s", (name,)) 53 if not user or not user.get("password", None): 54 # registration not finished yet 55 return None 56 elif not user or not bcrypt.checkpw(password.encode("ascii"), user["password"].encode("ascii")): 57 # non-existing user or wrong password 58 return None 59 else: 60 # valid login! 61 return User(db, user, authenticated=True, config=config)
Get user object, if login is correct
If the login data supplied to this method is correct, a new user object that is marked as authenticated is returned.
Parameters
- db: Database connection object
- name: User name
- password: User password
- config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to use a global configuration manager.
Returns
User object, or
None
if login was invalid
63 @staticmethod 64 def get_by_name(db, name, config=None): 65 """ 66 Get user object for given user name 67 68 :param db: Database connection object 69 :param str name: Username to get object for 70 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 71 use a global configuration manager. 72 :return: User object, or `None` for invalid user name 73 """ 74 user = db.fetchone("SELECT * FROM users WHERE name = %s", (name,)) 75 if not user: 76 return None 77 else: 78 return User(db, user, config=config)
Get user object for given user name
Parameters
- db: Database connection object
- str name: Username to get object for
- config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to use a global configuration manager.
Returns
User object, or
None
for invalid user name
80 @staticmethod 81 def get_by_token(db, token, config=None): 82 """ 83 Get user object for given token, if token is valid 84 85 :param db: Database connection object 86 :param str token: Token to get object for 87 :param config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to 88 use a global configuration manager. 89 :return: User object, or `None` for invalid token 90 """ 91 user = db.fetchone( 92 "SELECT * FROM users WHERE register_token = %s AND (timestamp_token = 0 OR timestamp_token > %s)", 93 (token, int(time.time()) - (7 * 86400))) 94 if not user: 95 return None 96 else: 97 return User(db, user, config=config)
Get user object for given token, if token is valid
Parameters
- db: Database connection object
- str token: Token to get object for
- config: Configuration manager. Can be used for request-aware user objects using ConfigWrapper. Empty to use a global configuration manager.
Returns
User object, or
None
for invalid token
131 def authenticate(self): 132 """ 133 Mark user object as authenticated. 134 """ 135 self.is_authenticated = True
Mark user object as authenticated.
137 def get_id(self): 138 """ 139 Get user ID 140 141 :return: User ID 142 """ 143 return self.data["name"]
Get user ID
Returns
User ID
145 def get_name(self): 146 """ 147 Get user name 148 149 This is usually the user ID. For the two special users, provide a nicer 150 name to display in interfaces, etc. 151 152 :return: User name 153 """ 154 if self.data["name"] == "anonymous": 155 return "Anonymous" 156 elif self.data["name"] == "autologin": 157 return self.config.get("flask.autologin.name") 158 else: 159 return self.data["name"]
Get user name
This is usually the user ID. For the two special users, provide a nicer name to display in interfaces, etc.
Returns
User name
161 def get_token(self): 162 """ 163 Get password reset token 164 165 May be empty or invalid! 166 167 :return str: Password reset token 168 """ 169 return self.generate_token(regenerate=False)
Get password reset token
May be empty or invalid!
Returns
Password reset token
171 def with_config(self, config): 172 """ 173 Connect user to configuration manager 174 175 By default, the user object reads from the global configuration 176 manager. For frontend operations it may be desireable to use a 177 request-aware configuration manager, but this is only available after 178 the user has been instantiated. This method can thus be used to connect 179 the user to that config manager later when it is available. 180 181 :param config: Configuration manager object 182 :return: 183 """ 184 self.config = config
Connect user to configuration manager
By default, the user object reads from the global configuration manager. For frontend operations it may be desireable to use a request-aware configuration manager, but this is only available after the user has been instantiated. This method can thus be used to connect the user to that config manager later when it is available.
Parameters
- config: Configuration manager object
Returns
186 def clear_token(self): 187 """ 188 Reset password rest token 189 190 Clears the token and token timestamp. This allows requesting a new one 191 even if the old one had not expired yet. 192 193 :return: 194 """ 195 self.db.update("users", data={"register_token": "", "timestamp_token": 0}, where={"name": self.get_id()})
Reset password rest token
Clears the token and token timestamp. This allows requesting a new one even if the old one had not expired yet.
Returns
197 def can_access_dataset(self, dataset, role=None): 198 """ 199 Check if this user should be able to access a given dataset. 200 201 This depends mostly on the dataset's owner, which should match the 202 user if the dataset is private. If the dataset is not private, or 203 if the user is an admin or the dataset is private but assigned to 204 an anonymous user, the dataset can be accessed. 205 206 :param dataset: The dataset to check access to 207 :return bool: 208 """ 209 if not dataset.is_private: 210 return True 211 212 elif self.is_admin: 213 return True 214 215 elif self.config.get("privileges.can_view_private_datasets", user=self): 216 # Allowed to see dataset, but perhaps not run processors (need privileges.admin.can_manipulate_all_datasets or dataset ownership) 217 return True 218 219 elif dataset.is_accessible_by(self, role=role): 220 return True 221 222 elif dataset.get_owners == ("anonymous",): 223 return True 224 225 else: 226 return False
Check if this user should be able to access a given dataset.
This depends mostly on the dataset's owner, which should match the user if the dataset is private. If the dataset is not private, or if the user is an admin or the dataset is private but assigned to an anonymous user, the dataset can be accessed.
Parameters
- dataset: The dataset to check access to
Returns
228 @property 229 def is_special(self): 230 """ 231 Check if user is special user 232 233 :return: Whether the user is the anonymous user, or the automatically 234 logged in user. 235 """ 236 return self.get_id() in ("autologin", "anonymous")
Check if user is special user
Returns
Whether the user is the anonymous user, or the automatically logged in user.
238 @property 239 def is_admin(self): 240 """ 241 Check if user is an administrator 242 243 :return bool: 244 """ 245 try: 246 return "admin" in self.data["tags"] 247 except (ValueError, TypeError) as e: 248 # invalid JSON? 249 return False
Check if user is an administrator
Returns
251 @property 252 def is_deactivated(self): 253 """ 254 Check if user has been deactivated 255 256 :return bool: 257 """ 258 return self.data.get("is_deactivated", False)
Check if user has been deactivated
Returns
260 def email_token(self, new=False): 261 """ 262 Send user an e-mail with a password reset link 263 264 Generates a token that the user can use to reset their password. The 265 token is valid for 72 hours. 266 267 Note that this requires a mail server to be configured, or a 268 `RuntimeError` will be raised. If a server is configured but the mail 269 still fails to send, it will also raise a `RuntimeError`. Note that 270 in these cases a token is still created and valid (the user just gets 271 no notification, but an admin could forward the correct link). 272 273 If the user is a 'special' user, a `ValueError` is raised. 274 275 :param bool new: Is this the first time setting a password for this 276 account? 277 :return str: Link for the user to set their password with 278 """ 279 if not self.config.get('mail.server'): 280 raise RuntimeError("No e-mail server configured. 4CAT cannot send any e-mails.") 281 282 if self.is_special: 283 raise ValueError("Cannot send password reset e-mails for a special user.") 284 285 username = self.get_id() 286 287 # generate a password reset token 288 register_token = self.generate_token(regenerate=True) 289 290 # prepare welcome e-mail 291 sender = self.config.get('mail.noreply') 292 message = MIMEMultipart("alternative") 293 message["From"] = sender 294 message["To"] = username 295 296 # the actual e-mail... 297 url_base = self.config.get("flask.server_name") 298 protocol = "https" if self.config.get("flask.https") else "http" 299 url = "%s://%s/reset-password/?token=%s" % (protocol, url_base, register_token) 300 301 # we use slightly different e-mails depending on whether this is the first time setting a password 302 if new: 303 304 message["Subject"] = "Account created" 305 mail = """ 306 <p>Hello %s,</p> 307 <p>A 4CAT account has been created for you. You can now log in to 4CAT at <a href="%s://%s">%s</a>.</p> 308 <p>Note that before you log in, you will need to set a password. You can do so via the following link:</p> 309 <p><a href="%s">%s</a></p> 310 <p>Please complete your registration within 72 hours as the link above will become invalid after this time.</p> 311 """ % (username, protocol, url_base, url_base, url, url) 312 else: 313 314 message["Subject"] = "Password reset" 315 mail = """ 316 <p>Hello %s,</p> 317 <p>Someone has requested a password reset for your 4CAT account. If that someone is you, great! If not, feel free to ignore this e-mail.</p> 318 <p>You can change your password via the following link:</p> 319 <p><a href="%s">%s</a></p> 320 <p>Please do this within 72 hours as the link above will become invalid after this time.</p> 321 """ % (username, url, url) 322 323 # provide a plain-text alternative as well 324 html_parser = html2text.HTML2Text() 325 message.attach(MIMEText(html_parser.handle(mail), "plain")) 326 message.attach(MIMEText(mail, "html")) 327 328 # try to send it 329 try: 330 send_email([username], message) 331 return url 332 except (smtplib.SMTPException, ConnectionRefusedError, socket.timeout, socket.gaierror) as e: 333 raise RuntimeError("Could not send password reset e-mail: %s" % e)
Send user an e-mail with a password reset link
Generates a token that the user can use to reset their password. The token is valid for 72 hours.
Note that this requires a mail server to be configured, or a
RuntimeError
will be raised. If a server is configured but the mail
still fails to send, it will also raise a RuntimeError
. Note that
in these cases a token is still created and valid (the user just gets
no notification, but an admin could forward the correct link).
If the user is a 'special' user, a ValueError
is raised.
Parameters
- bool new: Is this the first time setting a password for this account?
Returns
Link for the user to set their password with
335 def generate_token(self, username=None, regenerate=True): 336 """ 337 Generate and store a new registration token for this user 338 339 Tokens are not re-generated if they exist already 340 341 :param username: Username to generate for: if left empty, it will be 342 inferred from self.data 343 :param regenerate: Force regenerating even if token exists 344 :return str: The token 345 """ 346 if self.data.get("register_token", None) and not regenerate: 347 return self.data["register_token"] 348 349 if not username: 350 username = self.data["name"] 351 352 register_token = hashlib.sha256() 353 register_token.update(os.urandom(128)) 354 register_token = register_token.hexdigest() 355 self.db.update("users", data={"register_token": register_token, "timestamp_token": int(time.time())}, 356 where={"name": username}) 357 358 return register_token
Generate and store a new registration token for this user
Tokens are not re-generated if they exist already
Parameters
- username: Username to generate for: if left empty, it will be inferred from self.data
- regenerate: Force regenerating even if token exists
Returns
The token
360 def get_value(self, key, default=None): 361 """ 362 Get persistently stored user property 363 364 :param key: Name of item to get 365 :param default: What to return if key is not avaiable (default None) 366 :return: 367 """ 368 return self.userdata.get(key, default)
Get persistently stored user property
Parameters
- key: Name of item to get
- default: What to return if key is not avaiable (default None)
Returns
370 def set_value(self, key, value): 371 """ 372 Set persistently stored user property 373 374 :param key: Name of item to store 375 :param value: Value 376 :return: 377 """ 378 self.userdata[key] = value 379 self.data["userdata"] = json.dumps(self.userdata) 380 381 self.db.update("users", where={"name": self.get_id()}, data={"userdata": json.dumps(self.userdata)})
Set persistently stored user property
Parameters
- key: Name of item to store
- value: Value
Returns
383 def set_password(self, password): 384 """ 385 Set user password 386 387 :param password: Password to set 388 """ 389 if self.is_anonymous: 390 raise Exception("Cannot set password for anonymous user") 391 392 salt = bcrypt.gensalt() 393 password_hash = bcrypt.hashpw(password.encode("ascii"), salt) 394 395 self.db.update("users", where={"name": self.data["name"]}, data={"password": password_hash.decode("utf-8")})
Set user password
Parameters
- password: Password to set
397 def add_notification(self, notification, expires=None, allow_dismiss=True): 398 """ 399 Add a notification for this user 400 401 Notifications that already exist with the same parameters are not added 402 again. 403 404 :param str notification: The content of the notification. Can contain 405 Markdown. 406 :param int expires: Timestamp when the notification should expire. If 407 not provided, the notification does not expire 408 :param bool allow_dismiss: Whether to allow a user to dismiss the 409 notification. 410 """ 411 self.db.insert("users_notifications", { 412 "username": self.get_id(), 413 "notification": notification, 414 "timestamp_expires": expires, 415 "allow_dismiss": allow_dismiss 416 }, safe=True)
Add a notification for this user
Notifications that already exist with the same parameters are not added again.
Parameters
- str notification: The content of the notification. Can contain Markdown.
- int expires: Timestamp when the notification should expire. If not provided, the notification does not expire
- bool allow_dismiss: Whether to allow a user to dismiss the notification.
418 def dismiss_notification(self, notification_id): 419 """ 420 Dismiss a notification 421 422 The user can only dismiss notifications they can see! 423 424 :param int notification_id: ID of the notification to dismiss 425 """ 426 current_notifications = [n["id"] for n in self.get_notifications() if n["allow_dismiss"]] 427 if notification_id not in current_notifications: 428 return 429 430 self.db.delete("users_notifications", where={"id": notification_id})
Dismiss a notification
The user can only dismiss notifications they can see!
Parameters
- int notification_id: ID of the notification to dismiss
432 def get_notifications(self): 433 """ 434 Get all notifications for this user 435 436 That is all the user's own notifications, plus those for the groups of 437 users this user belongs to 438 439 :return list: Notifications, as a list of dictionaries 440 """ 441 tag_recipients = ["!everyone", *[f"!{tag}" for tag in self.config.get_active_tags(self)]] 442 if self.is_admin: 443 # for backwards compatibility - used to be called '!admins' even if the tag is 'admin' 444 tag_recipients.append("!admins") 445 446 notifications = self.db.fetchall( 447 "SELECT n.* FROM users_notifications AS n, users AS u " 448 "WHERE u.name = %s " 449 "AND (u.name = n.username OR n.username IN %s)", (self.get_id(), tuple(tag_recipients))) 450 451 return notifications
Get all notifications for this user
That is all the user's own notifications, plus those for the groups of users this user belongs to
Returns
Notifications, as a list of dictionaries
453 def add_tag(self, tag): 454 """ 455 Add tag to user 456 457 If the tag is already in the tag list, nothing happens. 458 459 :param str tag: Tag 460 """ 461 if tag not in self.data["tags"]: 462 self.data["tags"].append(tag) 463 self.sort_user_tags()
Add tag to user
If the tag is already in the tag list, nothing happens.
Parameters
- str tag: Tag
465 def remove_tag(self, tag): 466 """ 467 Remove tag from user 468 469 If the tag is not part of the tag list, nothing happens. 470 471 :param str tag: Tag 472 """ 473 if tag in self.data["tags"]: 474 self.data["tags"].remove(tag) 475 self.sort_user_tags()
Remove tag from user
If the tag is not part of the tag list, nothing happens.
Parameters
- str tag: Tag
504 def delete(self, also_datasets=True): 505 from common.lib.dataset import DataSet 506 507 username = self.data["name"] 508 509 self.db.delete("users_favourites", where={"name": username}, commit=False), 510 self.db.delete("users_notifications", where={"username": username}, commit=False) 511 self.db.delete("access_tokens", where={"name": username}, commit=False) 512 513 # find datasets and delete 514 datasets = self.db.fetchall("SELECT key FROM datasets_owners WHERE name = %s", (username,)) 515 516 # delete any datasets and jobs related to deleted datasets 517 if datasets: 518 for dataset in datasets: 519 try: 520 dataset = DataSet(key=dataset["key"], db=self.db) 521 except DataSetException: 522 # dataset already deleted? 523 continue 524 525 if len(dataset.get_owners()) == 1 and also_datasets: 526 dataset.delete(commit=False) 527 self.db.delete("jobs", where={"remote_id": dataset.key}, commit=False) 528 else: 529 dataset.remove_owner(self) 530 531 # and finally the user 532 self.db.delete("users", where={"name": username}, commit=False) 533 self.db.commit()