#!/usr/bin/python
# encoding: utf-8
+import asyncio
import base64
import datetime
import hashlib
# Cleanup expired account password resets
self.db.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
+ async def _delete(self, *args, **kwargs):
+ """
+ Deletes given users
+ """
+ # Who is deleting?
+ who = self.get_by_uid("ms")
+
+ for uid in args:
+ account = self.get_by_uid(uid)
+
+ # Delete the account
+ with self.db.transaction():
+ await account.delete(who)
+
# Discourse
def decode_discourse_payload(self, payload, signature):
def name(self):
return self._get_string("cn")
+ # Delete
+
+ async def delete(self, user):
+ """
+ Deletes this user
+ """
+ # Check if this user can be deleted
+ if not self.can_be_deleted(user):
+ raise RuntimeError("Cannot delete user %s" % self)
+
+ async with asyncio.TaskGroup() as tasks:
+ t = datetime.datetime.now()
+
+ # Disable this account on Bugzilla
+ tasks.create_task(
+ self._disable_on_bugzilla("Deleted by %s, %s" % (user, t)),
+ )
+
+ # XXX Delete on Discourse
+
+ # XXX Delete on LDAP
+
+ def can_be_deleted(self, user):
+ """
+ Return True if the user can be deleted by user
+ """
+ # Check permissions
+ if not self.can_be_managed_by(user):
+ return False
+
+ # Cannot delete shell users
+ if self.has_shell():
+ return False
+
+ # Looks okay
+ return True
+
# Nickname
def get_nickname(self):
set_contents_to_promotional_emails,
)
+ # Bugzilla
+
+ async def _disable_on_bugzilla(self, text=None):
+ """
+ Disables the user on Bugzilla
+ """
+ user = await self.backend.bugzilla.get_user(self.email)
+
+ # Do nothing if the user does not exist
+ if not user:
+ return
+
+ # Disable the user
+ await user.disable(text)
+
class StopForumSpam(Object):
def init(self, email, address):
from . import accounts
from . import blog
+from . import bugzilla
from . import campaigns
from . import database
from . import fireinfo
# Initialize backend modules.
self.accounts = accounts.Accounts(self)
+ self.bugzilla = bugzilla.Bugzilla(self)
self.fireinfo = fireinfo.Fireinfo(self)
self.iuse = iuse.IUse(self)
self.mirrors = mirrors.Mirrors(self)
async def run_task(self, task, *args, **kwargs):
tasks = {
+ "accounts:delete" : self.accounts._delete,
"announce-blog-posts" : self.blog.announce,
"check-mirrors" : self.mirrors.check_all,
"check-spam" : self.accounts.check_spam,
--- /dev/null
+#!/usr/bin/python3
+
+import json
+import urllib.parse
+
+from . import misc
+from .decorators import *
+
+class BugzillaError(Exception):
+ pass
+
+class Bugzilla(misc.Object):
+ def init(self, api_key=None):
+ if api_key is None:
+ api_key = self.settings.get("bugzilla-api-key")
+
+ # Store the API key
+ self.api_key = api_key
+
+ @property
+ def url(self):
+ """
+ Returns the base URL of a Bugzilla instance
+ """
+ return self.settings.get("bugzilla-url")
+
+ def make_url(self, *args, **kwargs):
+ """
+ Composes a URL based on the base URL
+ """
+ url = urllib.parse.urljoin(self.url, *args)
+
+ # Append any query arguments
+ if kwargs:
+ url = "%s?%s" % (url, urllib.parse.urlencode(kwargs))
+
+ return url
+
+ async def _request(self, method, url, data=None):
+ if data is None:
+ data = {}
+
+ # Headers
+ headers = {
+ # Authenticate all requests
+ "X-BUGZILLA-API-KEY" : self.api_key,
+ }
+
+ # Make the URL
+ url = self.make_url(url)
+
+ # Fallback authentication because some API endpoints
+ # do not accept the API key in the header
+ data |= { "api_key" : self.api_key }
+
+ # Encode body
+ body = None
+
+ # For GET requests, append query arguments
+ if method == "GET":
+ if data:
+ url = "%s?%s" % (url, urllib.parse.urlencode(data))
+
+ # For POST/PUT encode all arguments as JSON
+ elif method in ("POST", "PUT"):
+ headers |= {
+ "Content-Type" : "application/json",
+ }
+
+ body = json.dumps(data)
+
+ # Send the request and wait for a response
+ res = await self.backend.http_client.fetch(
+ url, method=method, headers=headers, body=body)
+
+ # Decode JSON response
+ body = json.loads(res.body)
+
+ # Check for any errors
+ if "error" in body:
+ # Fetch code and message
+ code, message = body.get("code"), body.get("message")
+
+ # Handle any so far unhandled errors
+ raise BugzillaError(message)
+
+ # Return an empty response
+ return body
+
+ async def get_user(self, uid):
+ """
+ Fetches a user from Bugzilla
+ """
+ response = await self._request("GET", "/rest/user/%s" % uid)
+
+ # Return the user object
+ for data in response.get("users"):
+ return User(self.backend, data)
+
+
+
+class User(misc.Object):
+ def init(self, data):
+ self.data = data
+
+ @property
+ def id(self):
+ return self.data.get("id")
+
+ async def _update(self, **kwargs):
+ # Send the request
+ await self.backend.bugzilla._request("PUT", "/rest/user/%s" % self.id, **kwargs)
+
+ # XXX apply changes to the User object?
+
+ async def disable(self, text=None):
+ """
+ Disables this user
+ """
+ if not text:
+ text = "DISABLED"
+
+ # Update the user
+ await self._update(data={
+ "login_denied_text" : text,
+ })