]> git.ipfire.org Git - ipfire.org.git/commitdiff
accounts: Add Bugzilla API to delete accounts in Bugzilla
authorMichael Tremer <michael.tremer@ipfire.org>
Wed, 28 Jun 2023 09:55:18 +0000 (09:55 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Wed, 28 Jun 2023 09:55:18 +0000 (09:55 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/backend/accounts.py
src/backend/base.py
src/backend/bugzilla.py [new file with mode: 0644]

index 59a5cf8d6dca040b69dc39766792b9bfa25cc088..02cb985d43e818b34277e8fe0c0cc85669e8b93c 100644 (file)
@@ -51,6 +51,7 @@ backend_PYTHON = \
        src/backend/accounts.py \
        src/backend/base.py \
        src/backend/blog.py \
+       src/backend/bugzilla.py \
        src/backend/campaigns.py \
        src/backend/countries.py \
        src/backend/database.py \
index 89b35595531c7357d9c3162a06d74e34d837d783..fffd692ff51cffec5b1d204230758193cadb30de 100644 (file)
@@ -1,6 +1,7 @@
 #!/usr/bin/python
 # encoding: utf-8
 
+import asyncio
 import base64
 import datetime
 import hashlib
@@ -607,6 +608,20 @@ class Accounts(Object):
                # 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):
@@ -849,6 +864,43 @@ class Account(LDAPObject):
        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):
@@ -1258,6 +1310,21 @@ class Account(LDAPObject):
                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):
index 476c2698cdaa643de0548ca86d64547983e3170a..a4285e11c88d528d824c050662f7087a1c88aead 100644 (file)
@@ -9,6 +9,7 @@ import tornado.httpclient
 
 from . import accounts
 from . import blog
+from . import bugzilla
 from . import campaigns
 from . import database
 from . import fireinfo
@@ -61,6 +62,7 @@ class Backend(object):
 
                # 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)
@@ -132,6 +134,7 @@ class Backend(object):
 
        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,
diff --git a/src/backend/bugzilla.py b/src/backend/bugzilla.py
new file mode 100644 (file)
index 0000000..0680bca
--- /dev/null
@@ -0,0 +1,126 @@
+#!/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,
+               })