← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:anoint-team-member into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:anoint-team-member into launchpad:master.

Commit message:
Add anoint-team-member script

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/448542

This is useful in development environments.  It will also replace the local `anoint-dogfood-admin` script on our dogfood instance.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:anoint-team-member into launchpad:master.
diff --git a/doc/how-to/new-user.rst b/doc/how-to/new-user.rst
index daff7a6..31529bc 100644
--- a/doc/how-to/new-user.rst
+++ b/doc/how-to/new-user.rst
@@ -1,5 +1,16 @@
 Creating additional user accounts in the development environment
 ================================================================
 
-You can create a new account using the ``utilities/make-lp-user`` script and log
-in to that account at ``https://launchpad.test``.
+In environments that use the test OpenID provider, such as the development
+appserver started via ``make run``, you can create a new account using the
+``utilities/make-lp-user`` script and log into that account at
+``https://launchpad.test/``.
+
+Some development environments, such as those deployed using the :doc:`Mojo
+spec <../explanation/charms>`, use production Single Sign-On for
+authentication.  In these environments, you should not use
+``utilities/make-lp-user``; instead, log into ``https://launchpad.test/``
+via SSO as you would on production to create your user, and then use
+``utilities/anoint-team-member`` as needed to add yourself to teams.  For
+example, you might want to temporarily add yourself to the ``admins`` team
+in order to edit feature rules.
diff --git a/lib/lp/scripts/utilities/anointteammember.py b/lib/lp/scripts/utilities/anointteammember.py
new file mode 100644
index 0000000..ec08084
--- /dev/null
+++ b/lib/lp/scripts/utilities/anointteammember.py
@@ -0,0 +1,70 @@
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Script to add a user to a team."""
+
+__all__ = ["AnointTeamMemberScript"]
+
+from textwrap import dedent
+
+import transaction
+from zope.component import getUtility
+
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.config import config
+from lp.services.scripts.base import (
+    LaunchpadScript,
+    LaunchpadScriptFailure,
+    SilentLaunchpadScriptFailure,
+)
+from lp.services.webapp.publisher import canonical_url
+
+
+class AnointTeamMemberScript(LaunchpadScript):
+    """
+    Add a user to a team, bypassing the normal workflows.
+
+    This is particularly useful for making a local user be a member of the
+    "admins" team in a development environment.
+
+    This script is for testing purposes only.  Do NOT use it in production
+    environments.
+    """
+
+    usage = "%(prog)s <person> <team>"
+    description = dedent(__doc__)
+
+    def main(self):
+        if len(self.args) != 2:
+            self.parser.print_help()
+            raise SilentLaunchpadScriptFailure(2)
+        if config.vhost.mainsite.hostname == "launchpad.net":
+            raise LaunchpadScriptFailure(
+                "This script may not be used on production.  Use normal team "
+                "processes instead, or ask an existing member of ~admins."
+            )
+
+        person_name, team_name = self.args
+        person = getUtility(IPersonSet).getByName(person_name)
+        if person is None:
+            raise LaunchpadScriptFailure(
+                "There is no person named '%s'." % person_name
+            )
+        team = getUtility(IPersonSet).getByName(team_name)
+        if team is None:
+            raise LaunchpadScriptFailure(
+                "There is no team named '%s'." % team_name
+            )
+        if not team.is_team:
+            raise LaunchpadScriptFailure(
+                "The person named '%s' is not a team." % team_name
+            )
+
+        team.addMember(person, person)
+        transaction.commit()
+        self.logger.info(
+            "Anointed ~%s as a member of ~%s.", person_name, team_name
+        )
+        self.logger.info(
+            "Use %s to leave.", canonical_url(team, view_name="+leave")
+        )
diff --git a/lib/lp/scripts/utilities/tests/test_anointteammember.py b/lib/lp/scripts/utilities/tests/test_anointteammember.py
new file mode 100644
index 0000000..01c209e
--- /dev/null
+++ b/lib/lp/scripts/utilities/tests/test_anointteammember.py
@@ -0,0 +1,75 @@
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import transaction
+
+from lp.scripts.utilities.anointteammember import AnointTeamMemberScript
+from lp.services.log.logger import BufferLogger
+from lp.services.scripts.base import LaunchpadScriptFailure
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import ZopelessDatabaseLayer
+
+
+class TestAnointTeamMember(TestCaseWithFactory):
+    layer = ZopelessDatabaseLayer
+
+    def makeScript(self, test_args):
+        script = AnointTeamMemberScript(test_args=test_args)
+        script.logger = BufferLogger()
+        script.txn = transaction
+        return script
+
+    def test_refuses_on_production(self):
+        self.pushConfig("vhost.mainsite", hostname="launchpad.net")
+        script = self.makeScript(["person", "team"])
+        self.assertRaisesWithContent(
+            LaunchpadScriptFailure,
+            "This script may not be used on production.  Use normal team "
+            "processes instead, or ask an existing member of ~admins.",
+            script.main,
+        )
+        self.assertEqual("", script.logger.getLogBuffer())
+
+    def test_no_such_person(self):
+        script = self.makeScript(["nonexistent-person", "admins"])
+        self.assertRaisesWithContent(
+            LaunchpadScriptFailure,
+            "There is no person named 'nonexistent-person'.",
+            script.main,
+        )
+        self.assertEqual("", script.logger.getLogBuffer())
+
+    def test_no_such_team(self):
+        script = self.makeScript(
+            [self.factory.makePerson().name, "nonexistent-team"]
+        )
+        self.assertRaisesWithContent(
+            LaunchpadScriptFailure,
+            "There is no team named 'nonexistent-team'.",
+            script.main,
+        )
+        self.assertEqual("", script.logger.getLogBuffer())
+
+    def test_person_not_a_team(self):
+        persons = [self.factory.makePerson() for _ in range(2)]
+        script = self.makeScript([persons[0].name, persons[1].name])
+        self.assertRaisesWithContent(
+            LaunchpadScriptFailure,
+            "The person named '%s' is not a team." % persons[1].name,
+            script.main,
+        )
+        self.assertEqual("", script.logger.getLogBuffer())
+
+    def test_success(self):
+        person = self.factory.makePerson()
+        team = self.factory.makeTeam()
+        self.assertFalse(person.inTeam(team))
+        script = self.makeScript([person.name, team.name])
+        script.main()
+        self.assertTrue(person.inTeam(team))
+        self.assertEqual(
+            "INFO Anointed ~%s as a member of ~%s.\n"
+            "INFO Use http://launchpad.test/~%s/+leave to leave.\n"
+            % (person.name, team.name, team.name),
+            script.logger.getLogBuffer(),
+        )
diff --git a/utilities/anoint-team-member b/utilities/anoint-team-member
new file mode 100755
index 0000000..ebc32c5
--- /dev/null
+++ b/utilities/anoint-team-member
@@ -0,0 +1,11 @@
+#! /usr/bin/python3 -S
+#
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import _pythonpath  # noqa: F401
+
+from lp.scripts.utilities.anointteammember import AnointTeamMemberScript
+
+if __name__ == "__main__":
+    AnointTeamMemberScript("anoint-team-member").run()