launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #30362
[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()