def post(self, request, project_slug): project = get_object_or_404(Project, slug=project_slug) if not AdminPermission.is_admin(request.user, project): return HttpResponseForbidden() version_slug = request.POST.get('version_slug') version = get_object_or_404( Version, project=project, slug=version_slug, ) update_docs_task, build = trigger_build(project=project, version=version) if (update_docs_task, build) == (None, None): # Build was skipped messages.add_message( request, messages.WARNING, "This project is currently disabled and can't trigger new builds.", ) return HttpResponseRedirect( reverse('builds_project_list', args=[project.slug]), ) return HttpResponseRedirect( reverse('builds_detail', args=[project.slug, build.pk]), )
def post(self, request, project_slug): project = get_object_or_404(Project, slug=project_slug) if not AdminPermission.is_admin(request.user, project): return HttpResponseForbidden() version_slug = request.POST.get('version_slug') version = get_object_or_404( Version, project=project, slug=version_slug, ) update_docs_task, build = trigger_build( project=project, version=version, ) if (update_docs_task, build) == (None, None): # Build was skipped messages.add_message( request, messages.WARNING, "This project is currently disabled and can't trigger new builds.", ) return HttpResponseRedirect( reverse('builds_project_list', args=[project.slug]), ) return HttpResponseRedirect( reverse('builds_detail', args=[project.slug, build.pk]), )
def serve_docs(request, project, subproject, lang_slug=None, version_slug=None, filename=''): """Exists to map existing proj, lang, version, filename views to the file format.""" if not version_slug: version_slug = project.get_default_version() try: version = project.versions.public(request.user).get(slug=version_slug) except Version.DoesNotExist: # Properly raise a 404 if the version doesn't exist & a 401 if it does if project.versions.filter(slug=version_slug).exists(): return _serve_401(request, project) raise Http404('Version does not exist.') filename = resolve_path( subproject or project, # Resolve the subproject if it exists version_slug=version_slug, language=lang_slug, filename=filename, subdomain= True, # subdomain will make it a "full" path without a URL prefix ) if (version.privacy_level == constants.PRIVATE and not AdminPermission.is_member(user=request.user, obj=project)): return _serve_401(request, project) return _serve_symlink_docs( request, filename=filename, project=project, privacy_level=version.privacy_level, )
def serve_docs( request, project, subproject, lang_slug=None, version_slug=None, filename='', ): """Map existing proj, lang, version, filename views to the file format.""" if not version_slug: version_slug = project.get_default_version() try: version = project.versions.public(request.user).get(slug=version_slug) except Version.DoesNotExist: # Properly raise a 404 if the version doesn't exist (or is inactive) and # a 401 if it does if project.versions.filter(slug=version_slug, active=True).exists(): return _serve_401(request, project) raise Http404('Version does not exist.') filename = resolve_path( subproject or project, # Resolve the subproject if it exists version_slug=version_slug, language=lang_slug, filename=filename, subdomain=True, # subdomain will make it a "full" path without a URL prefix ) if (version.privacy_level == constants.PRIVATE and not AdminPermission.is_member(user=request.user, obj=project)): return _serve_401(request, project) return _serve_symlink_docs( request, filename=filename, project=project, privacy_level=version.privacy_level, )
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) project = self.get_project() context['versions'] = self._get_versions(project) protocol = 'http' if self.request.is_secure(): protocol = 'https' version_slug = project.get_default_version() context['badge_url'] = ProjectBadgeView.get_badge_url( project.slug, version_slug, protocol=protocol, ) context['site_url'] = '{url}?badge={version}'.format( url=project.get_docs_url(version_slug), version=version_slug, ) context['is_project_admin'] = AdminPermission.is_admin( self.request.user, project, ) return context
def _add_user_projects(self, queryset, user, admin=False, member=False): """Add projects from where `user` is an `admin` or a `member`.""" projects = AdminPermission.projects( user=user, admin=admin, member=member, ) return queryset | projects
def test_project_admins_can_delete_subprojects_that_they_are_not_admin_of(self): self.project.users.add(self.user) self.assertFalse(AdminPermission.is_admin(self.user, self.subproject)) response = self.client.post( '/dashboard/my-mainproject/subprojects/my-subproject/delete/') self.assertEqual(response.status_code, 302) self.assertTrue(self.subproject not in [r.child for r in self.project.subprojects.all()])
def test_project_admins_can_delete_subprojects_that_they_are_not_admin_of( self): self.project.users.add(self.user) self.assertFalse(AdminPermission.is_admin(self.user, self.subproject)) response = self.client.post( '/dashboard/my-mainproject/subprojects/my-subproject/delete/') self.assertEqual(response.status_code, 302) self.assertTrue(self.subproject not in [r.child for r in self.project.subprojects.all()])
def _add_from_user_projects(self, queryset, user): if user and user.is_authenticated: projects_pk = (AdminPermission.projects( user=user, admin=True, member=True, ).values_list('pk', flat=True)) user_queryset = self.filter(build__project__in=projects_pk) queryset = user_queryset | queryset return queryset
def get(self, request, *args, **kwargs): # noqa """ Process GET from link click and let user in to team. If user is already logged in, link the team member to that account. If the user is not logged in, and doesn't have an account, the user will be prompted to sign up. """ # Linter doesn't like declaring `self.object` outside `__init__`. member = self.object = self.get_object() # noqa if member is not None: if not request.user.is_authenticated: member.invite.count += 1 member.invite.save() self.request.session.update({ 'invite:allow_signup': True, 'invite:email': member.invite.email, 'invite': member.invite.pk, # Auto-verify EmailAddress via django-allauth 'account_verified_email': member.invite.email, }) url = reverse('account_signup') if AdminPermission.has_sso_enabled(member.team.organization): url += f'?organization={member.team.organization.slug}' return HttpResponseRedirect(url) # If use is logged in, try to set the request user on the # fetched team member. If the member already exists on the team, # just delete the current member. Finally, get rid of the # invite too. org_slug = member.team.organization.slug invite = member.invite queryset = TeamMember.objects.filter( team=invite.team, member=self.request.user, ) if queryset.exists(): member.delete() else: member.member = self.request.user member.save() invite.delete() return HttpResponseRedirect( reverse( 'organization_detail', kwargs={'slug': org_slug}, ), ) return HttpResponseRedirect(reverse('homepage'))
def project_versions(request, project_slug): """ Project version list view. Shows the available versions and lets the user choose which ones to build. """ max_inactive_versions = 100 project = get_object_or_404( Project.objects.protected(request.user), slug=project_slug, ) versions = Version.internal.public( user=request.user, project=project, only_active=False, ) active_versions = versions.filter(active=True) # Limit inactive versions in case a project has a large number of branches or tags # Filter inactive versions based on the query string inactive_versions = versions.filter(active=False) version_filter = request.GET.get('version_filter', '') if version_filter: inactive_versions = inactive_versions.filter( verbose_name__icontains=version_filter) total_inactive_versions_count = inactive_versions.count() inactive_versions = inactive_versions[:max_inactive_versions] # If there's a wiped query string, check the string against the versions # list and display a success message. Deleting directories doesn't know how # to fail. :) wiped = request.GET.get('wipe', '') wiped_version = versions.filter(slug=wiped) if wiped and wiped_version.exists(): messages.success(request, 'Version wiped: ' + wiped) # Optimize project permission checks prefetch_related_objects([project], 'users') return render( request, 'projects/project_version_list.html', { 'inactive_versions': inactive_versions, 'active_versions': active_versions, 'project': project, 'is_project_admin': AdminPermission.is_admin( request.user, project), 'max_inactive_versions': max_inactive_versions, 'total_inactive_versions_count': total_inactive_versions_count, }, )
def _add_from_user_projects(self, queryset, user): if user and user.is_authenticated: projects_pk = (AdminPermission.projects( user=user, admin=True, member=True, ).values_list('pk', flat=True)) kwargs = {f'{self.project_field}__in': projects_pk} user_queryset = self.filter(**kwargs) queryset = user_queryset | queryset return queryset
def _add_from_user_projects(self, queryset, user, admin=False, member=False): """Add related objects from projects where `user` is an `admin` or a `member`.""" if user and user.is_authenticated: projects_pk = (AdminPermission.projects( user=user, admin=admin, member=member, ).values_list('pk', flat=True)) user_queryset = self.filter(project__in=projects_pk) queryset = user_queryset | queryset return queryset
def post(self, request, project_slug): project = get_object_or_404(Project, slug=project_slug) if not AdminPermission.is_admin(request.user, project): return HttpResponseForbidden() version_slug = request.POST.get('version_slug') version = get_object_or_404( Version, project=project, slug=version_slug, ) trigger_build(project=project, version=version) return HttpResponseRedirect(reverse('builds_project_list', args=[project.slug]))
def post(self, request, project_slug): project = get_object_or_404(Project, slug=project_slug) if not AdminPermission.is_admin(request.user, project): return HttpResponseForbidden() version_slug = request.POST.get('version_slug') version = get_object_or_404( Version, project=project, slug=version_slug, ) _, build = trigger_build(project=project, version=version) return HttpResponseRedirect( reverse('builds_detail', args=[project.slug, build.pk]), )
def notify_project_users(cls, projects): """ Notify project users of deprecated view. :param projects: List of project instances :type projects: [:py:class:`Project`] """ for project in projects: # Send one notification to each admin of the project for user in AdminPermission.admins(project): notification = cls( context_object=project, request=HttpRequest(), user=user, ) notification.send()
def sync_remote_repositories_organizations(organization_slugs=None): """ Re-sync users member of organizations. It will trigger one `sync_remote_repositories` task per user. :param organization_slugs: list containg organization's slugs to sync. If not passed, all organizations with ALLAUTH SSO enabled will be synced :type organization_slugs: list """ if organization_slugs: query = Organization.objects.filter(slug__in=organization_slugs) log.info( 'Triggering SSO re-sync for organizations.', organization_slugs=organization_slugs, count=query.count(), ) else: organization_ids = (SSOIntegration.objects.filter( provider=SSOIntegration.PROVIDER_ALLAUTH).values_list( 'organization', flat=True)) query = Organization.objects.filter(id__in=organization_ids) log.info( 'Triggering SSO re-sync for all organizations.', count=query.count(), ) n_task = -1 for organization in query: members = AdminPermission.members(organization) log.info( 'Triggering SSO re-sync for organization.', organization_slug=organization.slug, count=members.count(), ) for user in members: n_task += 1 sync_remote_repositories.apply_async( args=[user.pk], # delay the task by 0, 5, 10, 15, ... seconds countdown=n_task * 5, )
def resync_sso_user_accounts(self, request, queryset): # pylint: disable=no-self-use users_count = 0 organizations_count = queryset.count() for ssointegration in queryset.select_related('organization'): members = AdminPermission.members(ssointegration.organization) log.info( 'Triggering SSO re-sync for organization.', organization_slug=ssointegration.organization.slug, count=members.count(), ) users_count += members.count() for user in members: sync_remote_repositories.delay(user.pk) messages.add_message( request, messages.INFO, f'Triggered resync for {organizations_count} organizations and {users_count} users.' )
def clean_subproject(self): """Normalize subproject field Does lookup on against :py:class:`Project` to ensure matching project exists. Return the :py:class:`Project` object instead. """ subproject_name = self.cleaned_data['subproject'] subproject_qs = Project.objects.filter(slug=subproject_name) if not subproject_qs.exists(): raise forms.ValidationError( (_("Project %(name)s does not exist") % { 'name': subproject_name })) subproject = subproject_qs.first() if not AdminPermission.is_admin(self.user, subproject): raise forms.ValidationError( _('You need to be admin of {name} in order to add it as ' 'a subproject.'.format(name=subproject_name))) return subproject
def get_token_for_project(cls, project, force_local=False): """Get access token for project by iterating over project users.""" # TODO why does this only target GitHub? if not settings.ALLOW_PRIVATE_REPOS: return None token = None try: if settings.DONT_HIT_DB and not force_local: token = api.project(project.pk).token().get()['token'] else: for user in AdminPermission.admins(project): tokens = SocialToken.objects.filter( account__user=user, app__provider=cls.adapter.provider_id, ) if tokens.exists(): token = tokens[0].token except Exception: log.exception('Failed to get token for project') return token
def post(self, request, project_slug, build_pk): project = get_object_or_404(Project, slug=project_slug) build = get_object_or_404(Build, pk=build_pk) if not AdminPermission.is_admin(request.user, project): return HttpResponseForbidden() # NOTE: `terminate=True` is required for the child to attend our call # immediately when it's running the build. Otherwise, it finishes the # task. However, to revoke a task that has not started yet, we don't # need it. if build.state == BUILD_STATE_TRIGGERED: # Since the task won't be executed at all, we need to update the # Build object here. terminate = False build.state = BUILD_STATE_FINISHED build.success = False build.error = BuildCancelled.message build.length = 0 build.save() else: # In this case, we left the update of the Build object to the task # itself to be executed in the `on_failure` handler. terminate = True log.warning( 'Canceling build.', project_slug=project.slug, version_slug=build.version.slug, build_id=build.pk, build_task_id=build.task_id, terminate=terminate, ) app.control.revoke(build.task_id, signal=signal.SIGINT, terminate=terminate) return HttpResponseRedirect( reverse('builds_detail', args=[project.slug, build.pk]), )
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) project = self.get_project() # Get filtered and sorted versions versions = self._get_versions(project) if settings.RTD_EXT_THEME_ENABLED: filter = ProjectVersionListFilterSet( self.request.GET, queryset=versions, ) context['filter'] = filter versions = filter.qs context['versions'] = versions protocol = 'http' if self.request.is_secure(): protocol = 'https' version_slug = project.get_default_version() context['badge_url'] = ProjectBadgeView.get_badge_url( project.slug, version_slug, protocol=protocol, ) context['site_url'] = '{url}?badge={version}'.format( url=project.get_docs_url(version_slug), version=version_slug, ) context['is_project_admin'] = AdminPermission.is_admin( self.request.user, project, ) return context
def is_admin(user, project): return AdminPermission.is_admin(user, project)
def members(self): return AdminPermission.members(self)
def has_sso_enabled_filter(obj, provider=None): """Check if `obj` has sso enabled for `provider`.""" return AdminPermission.has_sso_enabled(obj, provider)
def is_member(user, project): return AdminPermission.is_member(user, project)
def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True # TODO: Similar logic to #1084 return AdminPermission.is_admin(request.user, obj.node.project)
def handle(self, *args, **options): if not options['email'] and not options['notification']: print("--email or --notification is required.") sys.exit(1) project = options['project'] organization = options['organization'] if project and organization: print("--project and --organization can\'t be used together.") sys.exit(1) if project: project = Project.objects.get(slug=project) users = AdminPermission.owners(project) elif organization: organization = Organization.objects.get(slug=organization) users = AdminPermission.owners(organization) elif settings.RTD_ALLOW_ORGANIZATIONS: users = (User.objects.filter( organizationowner__organization__disabled=False).distinct()) else: users = (User.objects.filter(projects__skip=False).distinct()) print('len(owners)={} production={} email={} notification={}'.format( users.count(), bool(options['production']), options['email'], options['notification'], )) if input('Continue? y/n: ') != 'y': print('Aborting run.') return notification_content = '' if options['notification']: file = Path(options['notification']) with file.open() as f: notification_content = f.read() email_subject = '' email_content = '' if options['email']: file = Path(options['email']) with file.open() as f: content = f.read().split('\n') email_subject = content[0].strip() email_content = '\n'.join(content[1:]).strip() resp = contact_users( users=users, email_subject=email_subject, email_content=email_content, notification_content=notification_content, sticky_notification=options['sticky'], dryrun=not options['production'], ) email = resp['email'] total = len(email['sent']) total_failed = len(email['failed']) print(f'Emails sent ({total}):') pprint(email['sent']) print(f'Failed emails ({total_failed}):') pprint(email['failed']) notification = resp['notification'] total = len(notification['sent']) total_failed = len(notification['failed']) print(f'Notifications sent ({total})') pprint(notification['sent']) print(f'Failed notifications ({total_failed})') pprint(notification['failed'])
def is_project_user(user, project): """Checks if the user has access to the project.""" return user in AdminPermission.members(project)
def send_build_status(build_pk, commit, status, link_to_build=False): """ Send Build Status to Git Status API for project external versions. It tries using these services' account in order: 1. user's account that imported the project 2. each user's account from the project's maintainers :param build_pk: Build primary key :param commit: commit sha of the pull/merge request :param status: build status failed, pending, or success to be sent. """ # TODO: Send build status for BitBucket. build = Build.objects.filter(pk=build_pk).first() if not build: return provider_name = build.project.git_provider_name log.info('Sending build status. build=%s, project=%s', build.pk, build.project.slug) if provider_name in [GITHUB_BRAND, GITLAB_BRAND]: # get the service class for the project e.g: GitHubService. service_class = build.project.git_service_class() users = build.project.users.all() try: remote_repository = build.project.remote_repository remote_repository_relations = ( remote_repository.remote_repository_relations.filter( account__isnull=False, # Use ``user_in=`` instead of ``user__projects=`` here # because User's are not related to Project's directly in # Read the Docs for Business user__in=AdminPermission.members(build.project), ).select_related('account', 'user').only('user', 'account')) # Try using any of the users' maintainer accounts # Try to loop through all remote repository relations for the projects users for relation in remote_repository_relations: service = service_class(relation.user, relation.account) # Send status report using the API. success = service.send_build_status( build=build, commit=commit, state=status, link_to_build=link_to_build, ) if success: log.info( 'Build status report sent correctly. ' 'project=%s build=%s status=%s commit=%s user=%s', build.project.slug, build.pk, status, commit, relation.user.username, ) return True except RemoteRepository.DoesNotExist: log.warning( 'Project does not have a RemoteRepository. project=%s', build.project.slug, ) # Try to send build status for projects with no RemoteRepository for user in users: services = service_class.for_user(user) # Try to loop through services for users all social accounts # to send successful build status for service in services: success = service.send_build_status(build, commit, status) if success: log.info( 'Build status report sent correctly using an user account. ' 'project=%s build=%s status=%s commit=%s user=%s', build.project.slug, build.pk, status, commit, user.username, ) return True for user in users: # Send Site notification about Build status reporting failure # to all the users of the project. notification = GitBuildStatusFailureNotification( context_object=build.project, extra_context={'provider_name': provider_name}, user=user, success=False, ) notification.send() log.info('No social account or repository permission available for %s', build.project.slug) return False
def post(self, request, project_slug): commit_to_retrigger = None project = get_object_or_404(Project, slug=project_slug) if not AdminPermission.is_admin(request.user, project): return HttpResponseForbidden() version_slug = request.POST.get('version_slug') build_pk = request.POST.get('build_pk') if build_pk: # Filter over external versions only when re-triggering a specific build version = get_object_or_404( Version.external.public(self.request.user), slug=version_slug, project=project, ) build_to_retrigger = get_object_or_404( Build.objects.all(), pk=build_pk, version=version, ) if build_to_retrigger != Build.objects.filter( version=version).first(): messages.add_message( request, messages.ERROR, "This build can't be re-triggered because it's " "not the latest build for this version.", ) return HttpResponseRedirect(request.path) # Set either the build to re-trigger it or None if build_to_retrigger: commit_to_retrigger = build_to_retrigger.commit log.info( 'Re-triggering build.', project_slug=project.slug, version_slug=version.slug, build_commit=build_to_retrigger.commit, build_id=build_to_retrigger.pk, ) else: # Use generic query when triggering a normal build version = get_object_or_404( self._get_versions(project), slug=version_slug, ) update_docs_task, build = trigger_build( project=project, version=version, commit=commit_to_retrigger, ) if (update_docs_task, build) == (None, None): # Build was skipped messages.add_message( request, messages.WARNING, "This project is currently disabled and can't trigger new builds.", ) return HttpResponseRedirect( reverse('builds_project_list', args=[project.slug]), ) return HttpResponseRedirect( reverse('builds_detail', args=[project.slug, build.pk]), )