launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28762
[Merge] ~lgp171188/launchpad:split-security.py-soyuz-translations into launchpad:master
Guruprasad has proposed merging ~lgp171188/launchpad:split-security.py-soyuz-translations into launchpad:master.
Commit message:
Move security adapters of soyuz, translations to their packages
Also move IOCIProject* adapters to lp.registry.security.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~lgp171188/launchpad/+git/launchpad/+merge/426604
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/launchpad:split-security.py-soyuz-translations into launchpad:master.
diff --git a/lib/lp/registry/security.py b/lib/lp/registry/security.py
index 7d29374..e80fa3b 100644
--- a/lib/lp/registry/security.py
+++ b/lib/lp/registry/security.py
@@ -51,6 +51,8 @@ from lp.registry.interfaces.nameblacklist import (
INameBlacklist,
INameBlacklistSet,
)
+from lp.registry.interfaces.ociproject import IOCIProject
+from lp.registry.interfaces.ociprojectseries import IOCIProjectSeries
from lp.registry.interfaces.packaging import IPackaging
from lp.registry.interfaces.person import (
IPerson,
@@ -1230,3 +1232,27 @@ class ViewIrcID(AnonymousAuthorization):
class ViewWikiName(AnonymousAuthorization):
usedfor = IWikiName
+
+
+class ViewOCIProject(AnonymousAuthorization):
+ """Anyone can view an `IOCIProject`."""
+ usedfor = IOCIProject
+
+
+class EditOCIProject(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IOCIProject
+
+ def checkAuthenticated(self, user):
+ """Maintainers, drivers, and admins can drive projects."""
+ return (user.in_admin or
+ user.isDriver(self.obj.pillar) or
+ self.obj.pillar.canAdministerOCIProjects(user))
+
+
+class EditOCIProjectSeries(DelegatedAuthorization):
+ permission = 'launchpad.Edit'
+ usedfor = IOCIProjectSeries
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.oci_project)
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 2e8d846..2e72d3e 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Security policies for using content objects."""
@@ -10,6 +10,7 @@ __all__ = [
'BugTargetOwnerOrBugSupervisorOrAdmins',
'EditByOwnersOrAdmins',
'EditByRegistryExpertsOrAdmins',
+ 'EditPackageBuild',
'is_commercial_case',
'ModerateByRegistryExpertsOrAdmins',
'OnlyBazaarExpertsAndAdmins',
@@ -21,14 +22,9 @@ from datetime import (
datetime,
timedelta,
)
-from operator import methodcaller
import pytz
-from storm.expr import And
-from zope.component import (
- getUtility,
- queryAdapter,
- )
+from zope.component import queryAdapter
from zope.interface import Interface
from lp.app.interfaces.security import IAuthorization
@@ -74,12 +70,9 @@ from lp.oci.interfaces.ocirecipe import (
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
from lp.oci.interfaces.ocirecipesubscription import IOCIRecipeSubscription
from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
-from lp.registry.interfaces.ociproject import IOCIProject
-from lp.registry.interfaces.ociprojectseries import IOCIProjectSeries
from lp.registry.interfaces.role import IHasOwner
from lp.services.auth.interfaces import IAccessToken
from lp.services.config import config
-from lp.services.database.interfaces import IStore
from lp.services.identity.interfaces.emailaddress import IEmailAddress
from lp.services.librarian.interfaces import ILibraryFileAliasWithParent
from lp.services.messages.interfaces.message import IMessage
@@ -95,10 +88,6 @@ from lp.services.webhooks.interfaces import (
IWebhookDeliveryJob,
)
from lp.services.worlddata.interfaces.country import ICountry
-from lp.services.worlddata.interfaces.language import (
- ILanguage,
- ILanguageSet,
- )
from lp.snappy.interfaces.snap import (
ISnap,
ISnapBuildRequest,
@@ -113,65 +102,6 @@ from lp.snappy.interfaces.snappyseries import (
ISnappySeriesSet,
)
from lp.snappy.interfaces.snapsubscription import ISnapSubscription
-from lp.soyuz.interfaces.archive import (
- IArchive,
- IArchiveSet,
- )
-from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
-from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
-from lp.soyuz.interfaces.archivesubscriber import (
- IArchiveSubscriber,
- IArchiveSubscriberSet,
- IPersonalArchiveSubscription,
- )
-from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
-from lp.soyuz.interfaces.binarypackagerelease import (
- IBinaryPackageReleaseDownloadCount,
- )
-from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
-from lp.soyuz.interfaces.distroarchseriesfilter import IDistroArchSeriesFilter
-from lp.soyuz.interfaces.livefs import ILiveFS
-from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
-from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJob
-from lp.soyuz.interfaces.packageset import (
- IPackageset,
- IPackagesetSet,
- )
-from lp.soyuz.interfaces.publishing import (
- IBinaryPackagePublishingHistory,
- IPublishingEdit,
- ISourcePackagePublishingHistory,
- )
-from lp.soyuz.interfaces.queue import (
- IPackageUpload,
- IPackageUploadLog,
- IPackageUploadQueue,
- )
-from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
-from lp.soyuz.model.archive import (
- Archive,
- get_enabled_archive_filter,
- )
-from lp.translations.interfaces.customlanguagecode import ICustomLanguageCode
-from lp.translations.interfaces.languagepack import ILanguagePack
-from lp.translations.interfaces.pofile import IPOFile
-from lp.translations.interfaces.potemplate import IPOTemplate
-from lp.translations.interfaces.translationgroup import (
- ITranslationGroup,
- ITranslationGroupSet,
- )
-from lp.translations.interfaces.translationimportqueue import (
- ITranslationImportQueue,
- ITranslationImportQueueEntry,
- )
-from lp.translations.interfaces.translationsperson import ITranslationsPerson
-from lp.translations.interfaces.translationtemplatesbuild import (
- ITranslationTemplatesBuild,
- )
-from lp.translations.interfaces.translator import (
- IEditTranslator,
- ITranslator,
- )
def is_commercial_case(obj, user):
@@ -515,15 +445,6 @@ class OnlyRosettaExpertsAndAdmins(AuthorizationBase):
return user.in_admin or user.in_rosetta_experts
-class EditTranslationsPersonByPerson(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = ITranslationsPerson
-
- def checkAuthenticated(self, user):
- person = self.obj.person
- return person == user.person or user.in_admin
-
-
class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
"""Product's owner and bug supervisor can set official bug tags."""
@@ -541,37 +462,6 @@ class ViewCountry(AnonymousAuthorization):
usedfor = ICountry
-class ViewDistroArchSeries(AnonymousAuthorization):
- """Anyone can view a DistroArchSeries."""
- usedfor = IDistroArchSeries
-
-
-class ModerateDistroArchSeries(AuthorizationBase):
- permission = 'launchpad.Moderate'
- usedfor = IDistroArchSeries
-
- def checkAuthenticated(self, user):
- return (
- user.isOwner(self.obj.distroseries.distribution.main_archive)
- or user.in_admin)
-
-
-class ViewDistroArchSeriesFilter(DelegatedAuthorization):
- permission = 'launchpad.View'
- usedfor = IDistroArchSeriesFilter
-
- def __init__(self, obj):
- super().__init__(obj, obj.distroarchseries, 'launchpad.View')
-
-
-class EditDistroArchSeriesFilter(DelegatedAuthorization):
- permission = 'launchpad.Edit'
- usedfor = IDistroArchSeriesFilter
-
- def __init__(self, obj):
- super().__init__(obj, obj.distroarchseries, 'launchpad.Moderate')
-
-
class EditStructuralSubscription(AuthorizationBase):
permission = 'launchpad.Edit'
usedfor = IStructuralSubscription
@@ -610,235 +500,6 @@ class OnlyVcsImportsAndAdmins(AuthorizationBase):
return user.in_admin or user.in_vcs_imports
-class ViewPOTemplates(AnonymousAuthorization):
- """Anyone can view an IPOTemplate."""
- usedfor = IPOTemplate
-
-
-class AdminPOTemplateDetails(OnlyRosettaExpertsAndAdmins):
- """Controls administration of an `IPOTemplate`.
-
- Allow all persons that can also administer the translations to
- which this template belongs to and also translation group owners.
-
- Product owners does not have administrative privileges.
- """
-
- permission = 'launchpad.Admin'
- usedfor = IPOTemplate
-
- def checkAuthenticated(self, user):
- template = self.obj
- if user.in_rosetta_experts or user.in_admin:
- return True
- if template.distroseries is not None:
- # Template is on a distribution.
- return (
- self.forwardCheckAuthenticated(user, template.distroseries,
- 'launchpad.TranslationsAdmin'))
- else:
- # Template is on a product.
- return False
-
-
-class EditPOTemplateDetails(AuthorizationBase):
- permission = 'launchpad.TranslationsAdmin'
- usedfor = IPOTemplate
-
- def checkAuthenticated(self, user):
- template = self.obj
- if template.distroseries is not None:
- # Template is on a distribution.
- return (
- user.isOwner(template) or
- self.forwardCheckAuthenticated(user, template.distroseries))
- else:
- # Template is on a product.
- return (
- user.isOwner(template) or
- self.forwardCheckAuthenticated(user, template.productseries))
-
-
-class ViewPOFile(AnonymousAuthorization):
- """Anyone can view an IPOFile."""
- usedfor = IPOFile
-
-
-class EditPOFile(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = IPOFile
-
- def checkAuthenticated(self, user):
- """The `POFile` itself keeps track of this permission."""
- return self.obj.canEditTranslations(user.person)
-
-
-class AdminTranslator(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.Admin'
- usedfor = ITranslator
-
- def checkAuthenticated(self, user):
- """Allow the owner of a translation group to edit the translator
- of any language in the group."""
- return (user.inTeam(self.obj.translationgroup.owner) or
- OnlyRosettaExpertsAndAdmins.checkAuthenticated(self, user))
-
-
-class EditTranslator(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.Edit'
- usedfor = IEditTranslator
-
- def checkAuthenticated(self, user):
- """Allow the translator and the group owner to edit parts of
- the translator entry."""
- return (user.inTeam(self.obj.translator) or
- user.inTeam(self.obj.translationgroup.owner) or
- OnlyRosettaExpertsAndAdmins.checkAuthenticated(self, user))
-
-
-class EditTranslationGroup(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.Edit'
- usedfor = ITranslationGroup
-
- def checkAuthenticated(self, user):
- """Allow the owner of a translation group to edit the translator
- of any language in the group."""
- return (user.inTeam(self.obj.owner) or
- OnlyRosettaExpertsAndAdmins.checkAuthenticated(self, user))
-
-
-class EditTranslationGroupSet(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.Admin'
- usedfor = ITranslationGroupSet
-
-
-class AdminTranslationImportQueueEntry(AuthorizationBase):
- permission = 'launchpad.Admin'
- usedfor = ITranslationImportQueueEntry
-
- def checkAuthenticated(self, user):
- if self.obj.distroseries is not None:
- series = self.obj.distroseries
- else:
- series = self.obj.productseries
- return (
- self.forwardCheckAuthenticated(user, series,
- 'launchpad.TranslationsAdmin'))
-
-
-class EditTranslationImportQueueEntry(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = ITranslationImportQueueEntry
-
- def checkAuthenticated(self, user):
- """Anyone who can admin an entry, plus its owner or the owner of the
- product or distribution, can edit it.
- """
- return (self.forwardCheckAuthenticated(
- user, self.obj, 'launchpad.Admin') or
- user.inTeam(self.obj.importer))
-
-
-class AdminTranslationImportQueue(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.Admin'
- usedfor = ITranslationImportQueue
-
-
-class EditPackageUploadQueue(AdminByAdminsTeam):
- permission = 'launchpad.Edit'
- usedfor = IPackageUploadQueue
-
- def checkAuthenticated(self, user):
- """Check user presence in admins or distroseries upload admin team."""
- if AdminByAdminsTeam.checkAuthenticated(self, user):
- return True
-
- permission_set = getUtility(IArchivePermissionSet)
- component_permissions = permission_set.componentsForQueueAdmin(
- self.obj.distroseries.distribution.all_distro_archives,
- user.person)
- if not component_permissions.is_empty():
- return True
- pocket_permissions = permission_set.pocketsForQueueAdmin(
- self.obj.distroseries.distribution.all_distro_archives,
- user.person)
- for permission in pocket_permissions:
- if permission.distroseries in (None, self.obj.distroseries):
- return True
- return False
-
-
-class EditPlainPackageCopyJob(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = IPlainPackageCopyJob
-
- def checkAuthenticated(self, user):
- archive = self.obj.target_archive
- if archive.is_ppa:
- return archive.checkArchivePermission(user.person)
-
- permission_set = getUtility(IArchivePermissionSet)
- permissions = permission_set.componentsForQueueAdmin(
- archive, user.person)
- return not permissions.is_empty()
-
-
-class ViewPackageUpload(AuthorizationBase):
- """Restrict viewing of package uploads.
-
- Anyone who can see the archive or the sourcepackagerelease can see the
- upload. The SPR may be visible without the archive being visible if the
- source package has been copied from a private archive.
- """
- permission = 'launchpad.View'
- usedfor = IPackageUpload
-
- def iter_adapters(self):
- yield ViewArchive(self.obj.archive)
- # We cannot use self.obj.sourcepackagerelease, as that causes
- # interference with the property cache if we are called in the
- # process of adding a source or a build.
- if self.obj.sources:
- spr = self.obj.sources[0].sourcepackagerelease
- elif self.obj.builds:
- spr = self.obj.builds[0].build.source_package_release
- else:
- spr = None
- if spr is not None:
- yield ViewSourcePackageRelease(spr)
-
- def checkAuthenticated(self, user):
- return any(map(
- methodcaller("checkAuthenticated", user), self.iter_adapters()))
-
- def checkUnauthenticated(self):
- return any(map(
- methodcaller("checkUnauthenticated"), self.iter_adapters()))
-
-
-class ViewPackageUploadLog(DelegatedAuthorization):
- """Anyone who can view a package upload can view its logs."""
- permission = 'launchpad.View'
- usedfor = IPackageUploadLog
-
- def __init__(self, obj):
- super().__init__(obj, obj.package_upload)
-
-
-class EditPackageUpload(AdminByAdminsTeam):
- permission = 'launchpad.Edit'
- usedfor = IPackageUpload
-
- def checkAuthenticated(self, user):
- """Return True if user has an ArchivePermission or is an admin."""
- if AdminByAdminsTeam.checkAuthenticated(self, user):
- return True
-
- return self.obj.archive.canAdministerQueue(
- user.person, self.obj.components, self.obj.pocket,
- self.obj.distroseries)
-
-
class AdminByBuilddAdmin(AuthorizationBase):
permission = 'launchpad.Admin'
@@ -893,444 +554,6 @@ class EditPackageBuild(EditBuildFarmJob):
user.inTeam(self.obj.archive.owner))
-class EditBinaryPackageBuild(EditPackageBuild):
- permission = 'launchpad.Edit'
- usedfor = IBinaryPackageBuild
-
- def checkAuthenticated(self, user):
- """Check write access for user and different kinds of archives.
-
- Allow
- * BuilddAdmins, for any archive.
- * The PPA owner for PPAs
- * users with upload permissions (for the respective distribution)
- otherwise.
- """
- if EditPackageBuild.checkAuthenticated(self, user):
- return True
-
- # Primary or partner section here: is the user in question allowed
- # to upload to the respective component, packageset or package? Allow
- # user to retry build if so.
- # strict_component is True because the source package already exists,
- # otherwise, how can they give it back?
- check_perms = self.obj.archive.checkUpload(
- user.person, self.obj.distro_series,
- self.obj.source_package_release.sourcepackagename,
- self.obj.current_component, self.obj.pocket,
- strict_component=True)
- return check_perms == None
-
-
-class ViewBinaryPackageBuild(EditBinaryPackageBuild):
- permission = 'launchpad.View'
-
- # This code MUST match the logic in
- # IBinaryPackageBuildSet.getBuildsForBuilder() otherwise users are
- # likely to get 403 errors, or worse.
- def checkAuthenticated(self, user):
- """Private restricts to admins and archive members."""
- if not self.obj.archive.private:
- # Anyone can see non-private archives.
- return True
-
- if user.inTeam(self.obj.archive.owner):
- # Anyone in the PPA team gets the nod.
- return True
-
- # LP admins may also see it.
- if user.in_admin:
- return True
-
- # If the permission check on the sourcepackagerelease for this
- # build passes then it means the build can be released from
- # privacy since the source package is published publicly.
- # This happens when Archive.copyPackage is used to re-publish a
- # private package in the primary archive.
- auth_spr = ViewSourcePackageRelease(self.obj.source_package_release)
- if auth_spr.checkAuthenticated(user):
- return True
-
- # You're not a celebrity, get out of here.
- return False
-
- def checkUnauthenticated(self):
- """Unauthenticated users can see the build if it's not private."""
- if not self.obj.archive.private:
- return True
-
- # See comment above.
- auth_spr = ViewSourcePackageRelease(self.obj.source_package_release)
- return auth_spr.checkUnauthenticated()
-
-
-class ModerateBinaryPackageBuild(ViewBinaryPackageBuild):
- permission = 'launchpad.Moderate'
-
- def checkAuthenticated(self, user):
- # Only people who can see the build and administer its archive can
- # edit restricted attributes of builds. (Currently this allows
- # setting BinaryPackageBuild.external_dependencies; people who can
- # administer the archive can already achieve the same effect by
- # setting Archive.external_dependencies.)
- return (
- super().checkAuthenticated(user) and
- AdminArchive(self.obj.archive).checkAuthenticated(user))
-
- def checkUnauthenticated(self, user):
- return False
-
-
-class ViewTranslationTemplatesBuild(DelegatedAuthorization):
- """Permission to view an `ITranslationTemplatesBuild`.
-
- Delegated to the build's branch.
- """
- permission = 'launchpad.View'
- usedfor = ITranslationTemplatesBuild
-
- def __init__(self, obj):
- super().__init__(obj, obj.branch)
-
-
-class ViewLanguageSet(AnonymousAuthorization):
- """Anyone can view an ILangaugeSet."""
- usedfor = ILanguageSet
-
-
-class AdminLanguageSet(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.Admin'
- usedfor = ILanguageSet
-
-
-class ViewLanguage(AnonymousAuthorization):
- """Anyone can view an ILangauge."""
- usedfor = ILanguage
-
-
-class AdminLanguage(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.Admin'
- usedfor = ILanguage
-
-
-class AdminCustomLanguageCode(AuthorizationBase):
- """Controls administration for a custom language code.
-
- Whoever can admin a product's or distribution's translations can also
- admin the custom language codes for it.
- """
- permission = 'launchpad.TranslationsAdmin'
- usedfor = ICustomLanguageCode
-
- def checkAuthenticated(self, user):
- return self.forwardCheckAuthenticated(
- user,
- self.obj.product or self.obj.distribution
- )
-
-
-class AdminLanguagePack(OnlyRosettaExpertsAndAdmins):
- permission = 'launchpad.LanguagePacksAdmin'
- usedfor = ILanguagePack
-
-
-class ViewArchive(AuthorizationBase):
- """Restrict viewing of private archives.
-
- Only admins or members of a private team can view the archive.
- """
- permission = 'launchpad.View'
- usedfor = IArchive
-
- def checkAuthenticated(self, user):
- """Verify that the user can view the archive."""
- archive_set = getUtility(IArchiveSet) # type: IArchiveSet
- return archive_set.checkViewPermission(
- [self.obj], user.person
- )[self.obj]
-
- def checkUnauthenticated(self):
- """Unauthenticated users can see the PPA if it's not private."""
- return not self.obj.private and self.obj.enabled
-
-
-class SubscriberViewArchive(ViewArchive):
- """Restrict viewing of private archives."""
- permission = 'launchpad.SubscriberView'
- usedfor = IArchive
-
- def checkAuthenticated(self, user):
- if user.person in self.obj._known_subscribers:
- return True
- if super().checkAuthenticated(user):
- return True
- filter = get_enabled_archive_filter(
- user.person, include_subscribed=True)
- return not IStore(self.obj).find(
- Archive.id, And(Archive.id == self.obj.id, filter)).is_empty()
-
-
-class LimitedViewArchive(AuthorizationBase):
- """Restricted existence knowledge of private archives.
-
- Just delegate to SubscriberView, since that includes View.
- """
- permission = 'launchpad.LimitedView'
- usedfor = IArchive
-
- def checkUnauthenticated(self):
- yield self.obj, 'launchpad.SubscriberView'
-
- def checkAuthenticated(self, user):
- yield self.obj, 'launchpad.SubscriberView'
-
-
-class EditArchive(AuthorizationBase):
- """Restrict archive editing operations.
-
- If the archive a primary archive then we check the user is in the
- distribution's owning team, otherwise we check the archive owner.
- """
- permission = 'launchpad.Edit'
- usedfor = IArchive
-
- def checkAuthenticated(self, user):
- if self.obj.is_main:
- return user.isOwner(self.obj.distribution) or user.in_admin
-
- return user.isOwner(self.obj) or user.in_admin
-
-
-class DeleteArchive(EditArchive):
- """Restrict archive deletion operations.
-
- People who can edit an archive can delete it. In addition, registry
- experts can delete non-main archives, as a spam control mechanism.
- """
- permission = 'launchpad.Delete'
- usedfor = IArchive
-
- def checkAuthenticated(self, user):
- return (
- super().checkAuthenticated(user) or
- (not self.obj.is_main and user.in_registry_experts))
-
-
-class AppendArchive(AuthorizationBase):
- """Restrict appending (upload and copy) operations on archives.
-
- No one can upload to disabled archives.
-
- PPA upload rights are managed via `IArchive.checkArchivePermission`;
-
- Appending to PRIMARY, PARTNER or COPY archives is restricted to owners.
- """
- permission = 'launchpad.Append'
- usedfor = IArchive
-
- def checkAuthenticated(self, user):
- if not self.obj.enabled:
- return False
-
- if user.inTeam(self.obj.owner):
- return True
-
- if self.obj.is_ppa and self.obj.checkArchivePermission(user.person):
- return True
-
- return False
-
-
-class ModerateArchive(AuthorizationBase):
- """Restrict changing the build score on archives.
-
- Buildd admins can change this, as a site-wide resource that requires
- arbitration, especially between distribution builds and builds in
- non-virtualized PPAs. PPA/commercial admins can also change this since
- it affects the relative priority of (private) PPAs.
- """
- permission = 'launchpad.Moderate'
- usedfor = IArchive
-
- def checkAuthenticated(self, user):
- return (user.in_buildd_admin or user.in_ppa_admin or
- user.in_commercial_admin or user.in_admin)
-
-
-class AdminArchive(AuthorizationBase):
- """Restrict changing privacy and build settings on archives.
-
- The security of the non-virtualised build farm depends on these
- settings, so they can only be changed by PPA/commercial admins, or by
- PPA self admins on PPAs that they can already edit.
- """
- permission = 'launchpad.Admin'
- usedfor = IArchive
-
- def checkAuthenticated(self, user):
- if user.in_ppa_admin or user.in_commercial_admin or user.in_admin:
- return True
- return (
- user.in_ppa_self_admins
- and EditArchive(self.obj).checkAuthenticated(user))
-
-
-class ViewArchiveAuthToken(AuthorizationBase):
- """Restrict viewing of archive tokens.
-
- The user just needs to be mentioned in the token, have append privilege
- to the archive or be an admin.
- """
- permission = "launchpad.View"
- usedfor = IArchiveAuthToken
-
- def checkAuthenticated(self, user):
- if user.person == self.obj.person:
- return True
- auth_edit = EditArchiveAuthToken(self.obj)
- return auth_edit.checkAuthenticated(user)
-
-
-class EditArchiveAuthToken(DelegatedAuthorization):
- """Restrict editing of archive tokens.
-
- The user should have append privileges to the context archive, or be an
- admin.
- """
- permission = "launchpad.Edit"
- usedfor = IArchiveAuthToken
-
- def __init__(self, obj):
- super().__init__(obj, obj.archive, 'launchpad.Append')
-
- def checkAuthenticated(self, user):
- return (user.in_admin or
- super().checkAuthenticated(user))
-
-
-class ViewPersonalArchiveSubscription(DelegatedAuthorization):
- """Restrict viewing of personal archive subscriptions (non-db class).
-
- The user should be the subscriber, have append privilege to the archive
- or be an admin.
- """
- permission = "launchpad.View"
- usedfor = IPersonalArchiveSubscription
-
- def __init__(self, obj):
- super().__init__(obj, obj.archive, 'launchpad.Append')
-
- def checkAuthenticated(self, user):
- if user.person == self.obj.subscriber or user.in_admin:
- return True
- return super().checkAuthenticated(user)
-
-
-class ViewArchiveSubscriber(DelegatedAuthorization):
- """Restrict viewing of archive subscribers.
-
- The user should be the subscriber, have append privilege to the
- archive or be an admin.
- """
- permission = "launchpad.View"
- usedfor = IArchiveSubscriber
-
- def __init__(self, obj):
- super().__init__(obj, obj, 'launchpad.Edit')
-
- def checkAuthenticated(self, user):
- return (user.inTeam(self.obj.subscriber) or
- user.in_commercial_admin or
- super().checkAuthenticated(user))
-
-
-class EditArchiveSubscriber(DelegatedAuthorization):
- """Restrict editing of archive subscribers.
-
- The user should have append privilege to the archive or be an admin.
- """
- permission = "launchpad.Edit"
- usedfor = IArchiveSubscriber
-
- def __init__(self, obj):
- super().__init__(obj, obj.archive, 'launchpad.Append')
-
- def checkAuthenticated(self, user):
- return (user.in_admin or
- user.in_commercial_admin or
- super().checkAuthenticated(user))
-
-
-class AdminArchiveSubscriberSet(AdminByCommercialTeamOrAdmins):
- """Only (commercial) admins can manipulate archive subscribers in bulk."""
- usedfor = IArchiveSubscriberSet
-
-
-class ViewSourcePackagePublishingHistory(AuthorizationBase):
- """Restrict viewing of source publications."""
- permission = "launchpad.View"
- usedfor = ISourcePackagePublishingHistory
-
- def checkUnauthenticated(self):
- yield self.obj.archive, 'launchpad.SubscriberView'
-
- def checkAuthenticated(self, user):
- yield self.obj.archive, 'launchpad.SubscriberView'
-
-
-class EditPublishing(DelegatedAuthorization):
- """Restrict editing of source and binary packages.."""
- permission = "launchpad.Edit"
- usedfor = IPublishingEdit
-
- def __init__(self, obj):
- super().__init__(obj, obj.archive, 'launchpad.Append')
-
-
-class ViewBinaryPackagePublishingHistory(ViewSourcePackagePublishingHistory):
- """Restrict viewing of binary publications."""
- usedfor = IBinaryPackagePublishingHistory
-
-
-class ViewBinaryPackageReleaseDownloadCount(
- ViewSourcePackagePublishingHistory):
- """Restrict viewing of binary package download counts."""
- usedfor = IBinaryPackageReleaseDownloadCount
-
-
-class ViewSourcePackageRelease(AuthorizationBase):
- """Restrict viewing of source packages.
-
- Packages that are only published in private archives are subject to the
- same viewing rules as the archive (see class ViewArchive).
-
- If the package is published in any non-private archive, then it is
- automatically viewable even if the package is also published in
- a private archive.
- """
- permission = 'launchpad.View'
- usedfor = ISourcePackageRelease
-
- def checkAuthenticated(self, user):
- """Verify that the user can view the sourcepackagerelease."""
- for archive in self.obj.published_archives:
- adapter = queryAdapter(archive, IAuthorization, self.permission)
- if adapter is not None and adapter.checkAuthenticated(user):
- return True
- return False
-
- def checkUnauthenticated(self):
- """Check unauthenticated users.
-
- Unauthenticated users can see the package as long as it's published
- in a non-private archive.
- """
- for archive in self.obj.published_archives:
- if not archive.private:
- return True
- return False
-
-
class ViewEmailAddress(AuthorizationBase):
permission = 'launchpad.View'
usedfor = IEmailAddress
@@ -1372,34 +595,6 @@ class EditEmailAddress(EditByOwnersOrAdmins):
return super().checkAuthenticated(user)
-class ViewPackageset(AnonymousAuthorization):
- """Anyone can view an IPackageset."""
- usedfor = IPackageset
-
-
-class EditPackageset(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = IPackageset
-
- def checkAuthenticated(self, user):
- """The owner of a package set can edit the object."""
- return user.isOwner(self.obj) or user.in_admin
-
-
-class ModeratePackageset(AdminByBuilddAdmin):
- permission = 'launchpad.Moderate'
- usedfor = IPackageset
-
-
-class EditPackagesetSet(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = IPackagesetSet
-
- def checkAuthenticated(self, user):
- """Users must be an admin or a member of the tech board."""
- return user.in_admin or user.in_ubuntu_techboard
-
-
class EditLibraryFileAliasWithParent(AuthorizationBase):
permission = 'launchpad.Edit'
usedfor = ILibraryFileAliasWithParent
@@ -1470,77 +665,6 @@ class ViewPublisherConfig(AdminByAdminsTeam):
usedfor = IPublisherConfig
-class ViewLiveFS(DelegatedAuthorization):
- permission = 'launchpad.View'
- usedfor = ILiveFS
-
- def __init__(self, obj):
- super().__init__(obj, obj.owner, 'launchpad.View')
-
-
-class EditLiveFS(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = ILiveFS
-
- def checkAuthenticated(self, user):
- return (
- user.isOwner(self.obj) or
- user.in_commercial_admin or user.in_admin)
-
-
-class ModerateLiveFS(ModerateArchive):
- """Restrict changing the build score on live filesystems."""
- usedfor = ILiveFS
-
-
-class AdminLiveFS(AuthorizationBase):
- """Restrict changing build settings on live filesystems.
-
- The security of the non-virtualised build farm depends on these
- settings, so they can only be changed by "PPA"/commercial admins, or by
- "PPA" self admins on live filesystems that they can already edit.
- """
- permission = 'launchpad.Admin'
- usedfor = ILiveFS
-
- def checkAuthenticated(self, user):
- if user.in_ppa_admin or user.in_commercial_admin or user.in_admin:
- return True
- return (
- user.in_ppa_self_admins
- and EditLiveFS(self.obj).checkAuthenticated(user))
-
-
-class ViewLiveFSBuild(DelegatedAuthorization):
- permission = 'launchpad.View'
- usedfor = ILiveFSBuild
-
- def iter_objects(self):
- yield self.obj.livefs
- yield self.obj.archive
-
-
-class EditLiveFSBuild(AdminByBuilddAdmin):
- permission = 'launchpad.Edit'
- usedfor = ILiveFSBuild
-
- def checkAuthenticated(self, user):
- """Check edit access for live filesystem builds.
-
- Allow admins, buildd admins, and the owner of the live filesystem.
- (Note that the requester of the build is required to be in the team
- that owns the live filesystem.)
- """
- auth_livefs = EditLiveFS(self.obj.livefs)
- if auth_livefs.checkAuthenticated(user):
- return True
- return super().checkAuthenticated(user)
-
-
-class AdminLiveFSBuild(AdminByBuilddAdmin):
- usedfor = ILiveFSBuild
-
-
class ViewWebhook(AuthorizationBase):
"""Webhooks can be viewed and edited by someone who can edit the target."""
permission = 'launchpad.View'
@@ -1697,30 +821,6 @@ class EditSnapBaseSet(EditByRegistryExpertsOrAdmins):
usedfor = ISnapBaseSet
-class ViewOCIProject(AnonymousAuthorization):
- """Anyone can view an `IOCIProject`."""
- usedfor = IOCIProject
-
-
-class EditOCIProject(AuthorizationBase):
- permission = 'launchpad.Edit'
- usedfor = IOCIProject
-
- def checkAuthenticated(self, user):
- """Maintainers, drivers, and admins can drive projects."""
- return (user.in_admin or
- user.isDriver(self.obj.pillar) or
- self.obj.pillar.canAdministerOCIProjects(user))
-
-
-class EditOCIProjectSeries(DelegatedAuthorization):
- permission = 'launchpad.Edit'
- usedfor = IOCIProjectSeries
-
- def __init__(self, obj):
- super().__init__(obj, obj.oci_project)
-
-
class ViewOCIRecipeBuildRequest(DelegatedAuthorization):
permission = 'launchpad.View'
usedfor = IOCIRecipeBuildRequest
diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml
index ae7407d..96cecb2 100644
--- a/lib/lp/soyuz/configure.zcml
+++ b/lib/lp/soyuz/configure.zcml
@@ -10,6 +10,8 @@
xmlns:webservice="http://namespaces.canonical.com/webservice"
xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
i18n_domain="launchpad">
+
+ <authorizations module=".security" />
<include
package=".browser"/>
<include
@@ -350,8 +352,8 @@
<require
permission="launchpad.Edit"
interface="lp.soyuz.interfaces.archive.IArchiveEdit"
- set_attributes="api_publish build_debug_symbols description
- displayname publish publish_debug_symbols
+ set_attributes="api_publish build_debug_symbols description
+ displayname publish publish_debug_symbols
status suppress_subscription_notifications"/>
<require
permission="launchpad.Delete"
diff --git a/lib/lp/soyuz/security.py b/lib/lp/soyuz/security.py
new file mode 100644
index 0000000..1cabec3
--- /dev/null
+++ b/lib/lp/soyuz/security.py
@@ -0,0 +1,677 @@
+# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Security adapters for the soyuz package."""
+
+__all__ = []
+
+from operator import methodcaller
+
+from storm.expr import And
+from zope.component import (
+ getUtility,
+ queryAdapter,
+ )
+
+from lp.app.interfaces.security import IAuthorization
+from lp.app.security import (
+ AnonymousAuthorization,
+ AuthorizationBase,
+ DelegatedAuthorization,
+ )
+from lp.security import (
+ AdminByAdminsTeam,
+ AdminByBuilddAdmin,
+ AdminByCommercialTeamOrAdmins,
+ EditPackageBuild,
+ )
+from lp.services.database.interfaces import IStore
+from lp.soyuz.interfaces.archive import (
+ IArchive,
+ IArchiveSet,
+ )
+from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
+from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
+from lp.soyuz.interfaces.archivesubscriber import (
+ IArchiveSubscriber,
+ IArchiveSubscriberSet,
+ IPersonalArchiveSubscription,
+ )
+from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
+from lp.soyuz.interfaces.binarypackagerelease import (
+ IBinaryPackageReleaseDownloadCount,
+ )
+from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
+from lp.soyuz.interfaces.distroarchseriesfilter import IDistroArchSeriesFilter
+from lp.soyuz.interfaces.livefs import ILiveFS
+from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
+from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJob
+from lp.soyuz.interfaces.packageset import (
+ IPackageset,
+ IPackagesetSet,
+ )
+from lp.soyuz.interfaces.publishing import (
+ IBinaryPackagePublishingHistory,
+ IPublishingEdit,
+ ISourcePackagePublishingHistory,
+ )
+from lp.soyuz.interfaces.queue import (
+ IPackageUpload,
+ IPackageUploadLog,
+ IPackageUploadQueue,
+ )
+from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
+from lp.soyuz.model.archive import (
+ Archive,
+ get_enabled_archive_filter,
+ )
+
+
+class ViewDistroArchSeries(AnonymousAuthorization):
+ """Anyone can view a DistroArchSeries."""
+ usedfor = IDistroArchSeries
+
+
+class ModerateDistroArchSeries(AuthorizationBase):
+ permission = 'launchpad.Moderate'
+ usedfor = IDistroArchSeries
+
+ def checkAuthenticated(self, user):
+ return (
+ user.isOwner(self.obj.distroseries.distribution.main_archive)
+ or user.in_admin)
+
+
+class ViewDistroArchSeriesFilter(DelegatedAuthorization):
+ permission = 'launchpad.View'
+ usedfor = IDistroArchSeriesFilter
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.distroarchseries, 'launchpad.View')
+
+
+class EditDistroArchSeriesFilter(DelegatedAuthorization):
+ permission = 'launchpad.Edit'
+ usedfor = IDistroArchSeriesFilter
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.distroarchseries, 'launchpad.Moderate')
+
+
+class EditPackageUploadQueue(AdminByAdminsTeam):
+ permission = 'launchpad.Edit'
+ usedfor = IPackageUploadQueue
+
+ def checkAuthenticated(self, user):
+ """Check user presence in admins or distroseries upload admin team."""
+ if AdminByAdminsTeam.checkAuthenticated(self, user):
+ return True
+
+ permission_set = getUtility(IArchivePermissionSet)
+ component_permissions = permission_set.componentsForQueueAdmin(
+ self.obj.distroseries.distribution.all_distro_archives,
+ user.person)
+ if not component_permissions.is_empty():
+ return True
+ pocket_permissions = permission_set.pocketsForQueueAdmin(
+ self.obj.distroseries.distribution.all_distro_archives,
+ user.person)
+ for permission in pocket_permissions:
+ if permission.distroseries in (None, self.obj.distroseries):
+ return True
+ return False
+
+
+class EditPlainPackageCopyJob(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IPlainPackageCopyJob
+
+ def checkAuthenticated(self, user):
+ archive = self.obj.target_archive
+ if archive.is_ppa:
+ return archive.checkArchivePermission(user.person)
+
+ permission_set = getUtility(IArchivePermissionSet)
+ permissions = permission_set.componentsForQueueAdmin(
+ archive, user.person)
+ return not permissions.is_empty()
+
+
+class ViewPackageUpload(AuthorizationBase):
+ """Restrict viewing of package uploads.
+
+ Anyone who can see the archive or the sourcepackagerelease can see the
+ upload. The SPR may be visible without the archive being visible if the
+ source package has been copied from a private archive.
+ """
+ permission = 'launchpad.View'
+ usedfor = IPackageUpload
+
+ def iter_adapters(self):
+ yield ViewArchive(self.obj.archive)
+ # We cannot use self.obj.sourcepackagerelease, as that causes
+ # interference with the property cache if we are called in the
+ # process of adding a source or a build.
+ if self.obj.sources:
+ spr = self.obj.sources[0].sourcepackagerelease
+ elif self.obj.builds:
+ spr = self.obj.builds[0].build.source_package_release
+ else:
+ spr = None
+ if spr is not None:
+ yield ViewSourcePackageRelease(spr)
+
+ def checkAuthenticated(self, user):
+ return any(map(
+ methodcaller("checkAuthenticated", user), self.iter_adapters()))
+
+ def checkUnauthenticated(self):
+ return any(map(
+ methodcaller("checkUnauthenticated"), self.iter_adapters()))
+
+
+class ViewPackageUploadLog(DelegatedAuthorization):
+ """Anyone who can view a package upload can view its logs."""
+ permission = 'launchpad.View'
+ usedfor = IPackageUploadLog
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.package_upload)
+
+
+class EditPackageUpload(AdminByAdminsTeam):
+ permission = 'launchpad.Edit'
+ usedfor = IPackageUpload
+
+ def checkAuthenticated(self, user):
+ """Return True if user has an ArchivePermission or is an admin."""
+ if AdminByAdminsTeam.checkAuthenticated(self, user):
+ return True
+
+ return self.obj.archive.canAdministerQueue(
+ user.person, self.obj.components, self.obj.pocket,
+ self.obj.distroseries)
+
+
+class EditBinaryPackageBuild(EditPackageBuild):
+ permission = 'launchpad.Edit'
+ usedfor = IBinaryPackageBuild
+
+ def checkAuthenticated(self, user):
+ """Check write access for user and different kinds of archives.
+
+ Allow
+ * BuilddAdmins, for any archive.
+ * The PPA owner for PPAs
+ * users with upload permissions (for the respective distribution)
+ otherwise.
+ """
+ if EditPackageBuild.checkAuthenticated(self, user):
+ return True
+
+ # Primary or partner section here: is the user in question allowed
+ # to upload to the respective component, packageset or package? Allow
+ # user to retry build if so.
+ # strict_component is True because the source package already exists,
+ # otherwise, how can they give it back?
+ check_perms = self.obj.archive.checkUpload(
+ user.person, self.obj.distro_series,
+ self.obj.source_package_release.sourcepackagename,
+ self.obj.current_component, self.obj.pocket,
+ strict_component=True)
+ return check_perms == None
+
+
+class ViewBinaryPackageBuild(EditBinaryPackageBuild):
+ permission = 'launchpad.View'
+
+ # This code MUST match the logic in
+ # IBinaryPackageBuildSet.getBuildsForBuilder() otherwise users are
+ # likely to get 403 errors, or worse.
+ def checkAuthenticated(self, user):
+ """Private restricts to admins and archive members."""
+ if not self.obj.archive.private:
+ # Anyone can see non-private archives.
+ return True
+
+ if user.inTeam(self.obj.archive.owner):
+ # Anyone in the PPA team gets the nod.
+ return True
+
+ # LP admins may also see it.
+ if user.in_admin:
+ return True
+
+ # If the permission check on the sourcepackagerelease for this
+ # build passes then it means the build can be released from
+ # privacy since the source package is published publicly.
+ # This happens when Archive.copyPackage is used to re-publish a
+ # private package in the primary archive.
+ auth_spr = ViewSourcePackageRelease(self.obj.source_package_release)
+ if auth_spr.checkAuthenticated(user):
+ return True
+
+ # You're not a celebrity, get out of here.
+ return False
+
+ def checkUnauthenticated(self):
+ """Unauthenticated users can see the build if it's not private."""
+ if not self.obj.archive.private:
+ return True
+
+ # See comment above.
+ auth_spr = ViewSourcePackageRelease(self.obj.source_package_release)
+ return auth_spr.checkUnauthenticated()
+
+
+class ModerateBinaryPackageBuild(ViewBinaryPackageBuild):
+ permission = 'launchpad.Moderate'
+
+ def checkAuthenticated(self, user):
+ # Only people who can see the build and administer its archive can
+ # edit restricted attributes of builds. (Currently this allows
+ # setting BinaryPackageBuild.external_dependencies; people who can
+ # administer the archive can already achieve the same effect by
+ # setting Archive.external_dependencies.)
+ return (
+ super().checkAuthenticated(user) and
+ AdminArchive(self.obj.archive).checkAuthenticated(user))
+
+ def checkUnauthenticated(self, user):
+ return False
+
+
+class ViewArchive(AuthorizationBase):
+ """Restrict viewing of private archives.
+
+ Only admins or members of a private team can view the archive.
+ """
+ permission = 'launchpad.View'
+ usedfor = IArchive
+
+ def checkAuthenticated(self, user):
+ """Verify that the user can view the archive."""
+ archive_set = getUtility(IArchiveSet) # type: IArchiveSet
+ return archive_set.checkViewPermission(
+ [self.obj], user.person
+ )[self.obj]
+
+ def checkUnauthenticated(self):
+ """Unauthenticated users can see the PPA if it's not private."""
+ return not self.obj.private and self.obj.enabled
+
+
+class SubscriberViewArchive(ViewArchive):
+ """Restrict viewing of private archives."""
+ permission = 'launchpad.SubscriberView'
+ usedfor = IArchive
+
+ def checkAuthenticated(self, user):
+ if user.person in self.obj._known_subscribers:
+ return True
+ if super().checkAuthenticated(user):
+ return True
+ filter = get_enabled_archive_filter(
+ user.person, include_subscribed=True)
+ return not IStore(self.obj).find(
+ Archive.id, And(Archive.id == self.obj.id, filter)).is_empty()
+
+
+class LimitedViewArchive(AuthorizationBase):
+ """Restricted existence knowledge of private archives.
+
+ Just delegate to SubscriberView, since that includes View.
+ """
+ permission = 'launchpad.LimitedView'
+ usedfor = IArchive
+
+ def checkUnauthenticated(self):
+ yield self.obj, 'launchpad.SubscriberView'
+
+ def checkAuthenticated(self, user):
+ yield self.obj, 'launchpad.SubscriberView'
+
+
+class EditArchive(AuthorizationBase):
+ """Restrict archive editing operations.
+
+ If the archive a primary archive then we check the user is in the
+ distribution's owning team, otherwise we check the archive owner.
+ """
+ permission = 'launchpad.Edit'
+ usedfor = IArchive
+
+ def checkAuthenticated(self, user):
+ if self.obj.is_main:
+ return user.isOwner(self.obj.distribution) or user.in_admin
+
+ return user.isOwner(self.obj) or user.in_admin
+
+
+class DeleteArchive(EditArchive):
+ """Restrict archive deletion operations.
+
+ People who can edit an archive can delete it. In addition, registry
+ experts can delete non-main archives, as a spam control mechanism.
+ """
+ permission = 'launchpad.Delete'
+ usedfor = IArchive
+
+ def checkAuthenticated(self, user):
+ return (
+ super().checkAuthenticated(user) or
+ (not self.obj.is_main and user.in_registry_experts))
+
+
+class AppendArchive(AuthorizationBase):
+ """Restrict appending (upload and copy) operations on archives.
+
+ No one can upload to disabled archives.
+
+ PPA upload rights are managed via `IArchive.checkArchivePermission`;
+
+ Appending to PRIMARY, PARTNER or COPY archives is restricted to owners.
+ """
+ permission = 'launchpad.Append'
+ usedfor = IArchive
+
+ def checkAuthenticated(self, user):
+ if not self.obj.enabled:
+ return False
+
+ if user.inTeam(self.obj.owner):
+ return True
+
+ if self.obj.is_ppa and self.obj.checkArchivePermission(user.person):
+ return True
+
+ return False
+
+
+class ModerateArchive(AuthorizationBase):
+ """Restrict changing the build score on archives.
+
+ Buildd admins can change this, as a site-wide resource that requires
+ arbitration, especially between distribution builds and builds in
+ non-virtualized PPAs. PPA/commercial admins can also change this since
+ it affects the relative priority of (private) PPAs.
+ """
+ permission = 'launchpad.Moderate'
+ usedfor = IArchive
+
+ def checkAuthenticated(self, user):
+ return (user.in_buildd_admin or user.in_ppa_admin or
+ user.in_commercial_admin or user.in_admin)
+
+
+class AdminArchive(AuthorizationBase):
+ """Restrict changing privacy and build settings on archives.
+
+ The security of the non-virtualised build farm depends on these
+ settings, so they can only be changed by PPA/commercial admins, or by
+ PPA self admins on PPAs that they can already edit.
+ """
+ permission = 'launchpad.Admin'
+ usedfor = IArchive
+
+ def checkAuthenticated(self, user):
+ if user.in_ppa_admin or user.in_commercial_admin or user.in_admin:
+ return True
+ return (
+ user.in_ppa_self_admins
+ and EditArchive(self.obj).checkAuthenticated(user))
+
+
+class ViewArchiveAuthToken(AuthorizationBase):
+ """Restrict viewing of archive tokens.
+
+ The user just needs to be mentioned in the token, have append privilege
+ to the archive or be an admin.
+ """
+ permission = "launchpad.View"
+ usedfor = IArchiveAuthToken
+
+ def checkAuthenticated(self, user):
+ if user.person == self.obj.person:
+ return True
+ auth_edit = EditArchiveAuthToken(self.obj)
+ return auth_edit.checkAuthenticated(user)
+
+
+class EditArchiveAuthToken(DelegatedAuthorization):
+ """Restrict editing of archive tokens.
+
+ The user should have append privileges to the context archive, or be an
+ admin.
+ """
+ permission = "launchpad.Edit"
+ usedfor = IArchiveAuthToken
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.archive, 'launchpad.Append')
+
+ def checkAuthenticated(self, user):
+ return (user.in_admin or
+ super().checkAuthenticated(user))
+
+
+class ViewPersonalArchiveSubscription(DelegatedAuthorization):
+ """Restrict viewing of personal archive subscriptions (non-db class).
+
+ The user should be the subscriber, have append privilege to the archive
+ or be an admin.
+ """
+ permission = "launchpad.View"
+ usedfor = IPersonalArchiveSubscription
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.archive, 'launchpad.Append')
+
+ def checkAuthenticated(self, user):
+ if user.person == self.obj.subscriber or user.in_admin:
+ return True
+ return super().checkAuthenticated(user)
+
+
+class ViewArchiveSubscriber(DelegatedAuthorization):
+ """Restrict viewing of archive subscribers.
+
+ The user should be the subscriber, have append privilege to the
+ archive or be an admin.
+ """
+ permission = "launchpad.View"
+ usedfor = IArchiveSubscriber
+
+ def __init__(self, obj):
+ super().__init__(obj, obj, 'launchpad.Edit')
+
+ def checkAuthenticated(self, user):
+ return (user.inTeam(self.obj.subscriber) or
+ user.in_commercial_admin or
+ super().checkAuthenticated(user))
+
+
+class EditArchiveSubscriber(DelegatedAuthorization):
+ """Restrict editing of archive subscribers.
+
+ The user should have append privilege to the archive or be an admin.
+ """
+ permission = "launchpad.Edit"
+ usedfor = IArchiveSubscriber
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.archive, 'launchpad.Append')
+
+ def checkAuthenticated(self, user):
+ return (user.in_admin or
+ user.in_commercial_admin or
+ super().checkAuthenticated(user))
+
+
+class AdminArchiveSubscriberSet(AdminByCommercialTeamOrAdmins):
+ """Only (commercial) admins can manipulate archive subscribers in bulk."""
+ usedfor = IArchiveSubscriberSet
+
+
+class ViewSourcePackagePublishingHistory(AuthorizationBase):
+ """Restrict viewing of source publications."""
+ permission = "launchpad.View"
+ usedfor = ISourcePackagePublishingHistory
+
+ def checkUnauthenticated(self):
+ yield self.obj.archive, 'launchpad.SubscriberView'
+
+ def checkAuthenticated(self, user):
+ yield self.obj.archive, 'launchpad.SubscriberView'
+
+
+class EditPublishing(DelegatedAuthorization):
+ """Restrict editing of source and binary packages.."""
+ permission = "launchpad.Edit"
+ usedfor = IPublishingEdit
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.archive, 'launchpad.Append')
+
+
+class ViewBinaryPackagePublishingHistory(ViewSourcePackagePublishingHistory):
+ """Restrict viewing of binary publications."""
+ usedfor = IBinaryPackagePublishingHistory
+
+
+class ViewBinaryPackageReleaseDownloadCount(
+ ViewSourcePackagePublishingHistory):
+ """Restrict viewing of binary package download counts."""
+ usedfor = IBinaryPackageReleaseDownloadCount
+
+
+class ViewSourcePackageRelease(AuthorizationBase):
+ """Restrict viewing of source packages.
+
+ Packages that are only published in private archives are subject to the
+ same viewing rules as the archive (see class ViewArchive).
+
+ If the package is published in any non-private archive, then it is
+ automatically viewable even if the package is also published in
+ a private archive.
+ """
+ permission = 'launchpad.View'
+ usedfor = ISourcePackageRelease
+
+ def checkAuthenticated(self, user):
+ """Verify that the user can view the sourcepackagerelease."""
+ for archive in self.obj.published_archives:
+ adapter = queryAdapter(archive, IAuthorization, self.permission)
+ if adapter is not None and adapter.checkAuthenticated(user):
+ return True
+ return False
+
+ def checkUnauthenticated(self):
+ """Check unauthenticated users.
+
+ Unauthenticated users can see the package as long as it's published
+ in a non-private archive.
+ """
+ for archive in self.obj.published_archives:
+ if not archive.private:
+ return True
+ return False
+
+
+class ViewPackageset(AnonymousAuthorization):
+ """Anyone can view an IPackageset."""
+ usedfor = IPackageset
+
+
+class EditPackageset(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IPackageset
+
+ def checkAuthenticated(self, user):
+ """The owner of a package set can edit the object."""
+ return user.isOwner(self.obj) or user.in_admin
+
+
+class ModeratePackageset(AdminByBuilddAdmin):
+ permission = 'launchpad.Moderate'
+ usedfor = IPackageset
+
+
+class EditPackagesetSet(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IPackagesetSet
+
+ def checkAuthenticated(self, user):
+ """Users must be an admin or a member of the tech board."""
+ return user.in_admin or user.in_ubuntu_techboard
+
+
+class ViewLiveFS(DelegatedAuthorization):
+ permission = 'launchpad.View'
+ usedfor = ILiveFS
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.owner, 'launchpad.View')
+
+
+class EditLiveFS(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = ILiveFS
+
+ def checkAuthenticated(self, user):
+ return (
+ user.isOwner(self.obj) or
+ user.in_commercial_admin or user.in_admin)
+
+
+class ModerateLiveFS(ModerateArchive):
+ """Restrict changing the build score on live filesystems."""
+ usedfor = ILiveFS
+
+
+class AdminLiveFS(AuthorizationBase):
+ """Restrict changing build settings on live filesystems.
+
+ The security of the non-virtualised build farm depends on these
+ settings, so they can only be changed by "PPA"/commercial admins, or by
+ "PPA" self admins on live filesystems that they can already edit.
+ """
+ permission = 'launchpad.Admin'
+ usedfor = ILiveFS
+
+ def checkAuthenticated(self, user):
+ if user.in_ppa_admin or user.in_commercial_admin or user.in_admin:
+ return True
+ return (
+ user.in_ppa_self_admins
+ and EditLiveFS(self.obj).checkAuthenticated(user))
+
+
+class ViewLiveFSBuild(DelegatedAuthorization):
+ permission = 'launchpad.View'
+ usedfor = ILiveFSBuild
+
+ def iter_objects(self):
+ yield self.obj.livefs
+ yield self.obj.archive
+
+
+class EditLiveFSBuild(AdminByBuilddAdmin):
+ permission = 'launchpad.Edit'
+ usedfor = ILiveFSBuild
+
+ def checkAuthenticated(self, user):
+ """Check edit access for live filesystem builds.
+
+ Allow admins, buildd admins, and the owner of the live filesystem.
+ (Note that the requester of the build is required to be in the team
+ that owns the live filesystem.)
+ """
+ auth_livefs = EditLiveFS(self.obj.livefs)
+ if auth_livefs.checkAuthenticated(user):
+ return True
+ return super().checkAuthenticated(user)
+
+
+class AdminLiveFSBuild(AdminByBuilddAdmin):
+ usedfor = ILiveFSBuild
diff --git a/lib/lp/translations/configure.zcml b/lib/lp/translations/configure.zcml
index a5422e3..4999f12 100644
--- a/lib/lp/translations/configure.zcml
+++ b/lib/lp/translations/configure.zcml
@@ -11,6 +11,7 @@
xmlns:lp="http://namespaces.canonical.com/lp"
i18n_domain="launchpad">
+ <authorizations module=".security" />
<include package=".browser"/>
<include file="vocabularies.zcml"/>
diff --git a/lib/lp/translations/security.py b/lib/lp/translations/security.py
new file mode 100644
index 0000000..97b6947
--- /dev/null
+++ b/lib/lp/translations/security.py
@@ -0,0 +1,233 @@
+# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Security adapters for the translations module."""
+
+__all__ = []
+
+from lp.app.security import (
+ AnonymousAuthorization,
+ AuthorizationBase,
+ DelegatedAuthorization,
+ )
+from lp.security import OnlyRosettaExpertsAndAdmins
+from lp.services.worlddata.interfaces.language import (
+ ILanguage,
+ ILanguageSet,
+ )
+from lp.translations.interfaces.customlanguagecode import ICustomLanguageCode
+from lp.translations.interfaces.languagepack import ILanguagePack
+from lp.translations.interfaces.pofile import IPOFile
+from lp.translations.interfaces.potemplate import IPOTemplate
+from lp.translations.interfaces.translationgroup import (
+ ITranslationGroup,
+ ITranslationGroupSet,
+ )
+from lp.translations.interfaces.translationimportqueue import (
+ ITranslationImportQueue,
+ ITranslationImportQueueEntry,
+ )
+from lp.translations.interfaces.translationsperson import ITranslationsPerson
+from lp.translations.interfaces.translationtemplatesbuild import (
+ ITranslationTemplatesBuild,
+ )
+from lp.translations.interfaces.translator import (
+ IEditTranslator,
+ ITranslator,
+ )
+
+
+class EditTranslationsPersonByPerson(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = ITranslationsPerson
+
+ def checkAuthenticated(self, user):
+ person = self.obj.person
+ return person == user.person or user.in_admin
+
+
+class ViewPOTemplates(AnonymousAuthorization):
+ """Anyone can view an IPOTemplate."""
+ usedfor = IPOTemplate
+
+
+class AdminPOTemplateDetails(OnlyRosettaExpertsAndAdmins):
+ """Controls administration of an `IPOTemplate`.
+
+ Allow all persons that can also administer the translations to
+ which this template belongs to and also translation group owners.
+
+ Product owners does not have administrative privileges.
+ """
+
+ permission = 'launchpad.Admin'
+ usedfor = IPOTemplate
+
+ def checkAuthenticated(self, user):
+ template = self.obj
+ if user.in_rosetta_experts or user.in_admin:
+ return True
+ if template.distroseries is not None:
+ # Template is on a distribution.
+ return (
+ self.forwardCheckAuthenticated(user, template.distroseries,
+ 'launchpad.TranslationsAdmin'))
+ else:
+ # Template is on a product.
+ return False
+
+
+class EditPOTemplateDetails(AuthorizationBase):
+ permission = 'launchpad.TranslationsAdmin'
+ usedfor = IPOTemplate
+
+ def checkAuthenticated(self, user):
+ template = self.obj
+ if template.distroseries is not None:
+ # Template is on a distribution.
+ return (
+ user.isOwner(template) or
+ self.forwardCheckAuthenticated(user, template.distroseries))
+ else:
+ # Template is on a product.
+ return (
+ user.isOwner(template) or
+ self.forwardCheckAuthenticated(user, template.productseries))
+
+
+class ViewPOFile(AnonymousAuthorization):
+ """Anyone can view an IPOFile."""
+ usedfor = IPOFile
+
+
+class EditPOFile(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IPOFile
+
+ def checkAuthenticated(self, user):
+ """The `POFile` itself keeps track of this permission."""
+ return self.obj.canEditTranslations(user.person)
+
+
+class AdminTranslator(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.Admin'
+ usedfor = ITranslator
+
+ def checkAuthenticated(self, user):
+ """Allow the owner of a translation group to edit the translator
+ of any language in the group."""
+ return (user.inTeam(self.obj.translationgroup.owner) or
+ OnlyRosettaExpertsAndAdmins.checkAuthenticated(self, user))
+
+
+class EditTranslator(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.Edit'
+ usedfor = IEditTranslator
+
+ def checkAuthenticated(self, user):
+ """Allow the translator and the group owner to edit parts of
+ the translator entry."""
+ return (user.inTeam(self.obj.translator) or
+ user.inTeam(self.obj.translationgroup.owner) or
+ OnlyRosettaExpertsAndAdmins.checkAuthenticated(self, user))
+
+
+class EditTranslationGroup(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.Edit'
+ usedfor = ITranslationGroup
+
+ def checkAuthenticated(self, user):
+ """Allow the owner of a translation group to edit the translator
+ of any language in the group."""
+ return (user.inTeam(self.obj.owner) or
+ OnlyRosettaExpertsAndAdmins.checkAuthenticated(self, user))
+
+
+class EditTranslationGroupSet(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.Admin'
+ usedfor = ITranslationGroupSet
+
+
+class AdminTranslationImportQueueEntry(AuthorizationBase):
+ permission = 'launchpad.Admin'
+ usedfor = ITranslationImportQueueEntry
+
+ def checkAuthenticated(self, user):
+ if self.obj.distroseries is not None:
+ series = self.obj.distroseries
+ else:
+ series = self.obj.productseries
+ return (
+ self.forwardCheckAuthenticated(user, series,
+ 'launchpad.TranslationsAdmin'))
+
+
+class EditTranslationImportQueueEntry(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = ITranslationImportQueueEntry
+
+ def checkAuthenticated(self, user):
+ """Anyone who can admin an entry, plus its owner or the owner of the
+ product or distribution, can edit it.
+ """
+ return (self.forwardCheckAuthenticated(
+ user, self.obj, 'launchpad.Admin') or
+ user.inTeam(self.obj.importer))
+
+
+class AdminTranslationImportQueue(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.Admin'
+ usedfor = ITranslationImportQueue
+
+
+class ViewTranslationTemplatesBuild(DelegatedAuthorization):
+ """Permission to view an `ITranslationTemplatesBuild`.
+
+ Delegated to the build's branch.
+ """
+ permission = 'launchpad.View'
+ usedfor = ITranslationTemplatesBuild
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.branch)
+
+
+class ViewLanguageSet(AnonymousAuthorization):
+ """Anyone can view an ILangaugeSet."""
+ usedfor = ILanguageSet
+
+
+class AdminLanguageSet(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.Admin'
+ usedfor = ILanguageSet
+
+
+class ViewLanguage(AnonymousAuthorization):
+ """Anyone can view an ILangauge."""
+ usedfor = ILanguage
+
+
+class AdminLanguage(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.Admin'
+ usedfor = ILanguage
+
+
+class AdminCustomLanguageCode(AuthorizationBase):
+ """Controls administration for a custom language code.
+
+ Whoever can admin a product's or distribution's translations can also
+ admin the custom language codes for it.
+ """
+ permission = 'launchpad.TranslationsAdmin'
+ usedfor = ICustomLanguageCode
+
+ def checkAuthenticated(self, user):
+ return self.forwardCheckAuthenticated(
+ user,
+ self.obj.product or self.obj.distribution
+ )
+
+
+class AdminLanguagePack(OnlyRosettaExpertsAndAdmins):
+ permission = 'launchpad.LanguagePacksAdmin'
+ usedfor = ILanguagePack