diff --git a/.gitignore b/.gitignore index d485405fca..1a626d49fc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /.testrepository/ /cover/ /unreleased-*.txt +gerrit.ini diff --git a/openstack_releases/cmds/find_gerrit_acl_issues.py b/openstack_releases/cmds/find_gerrit_acl_issues.py new file mode 100644 index 0000000000..bcc5e58915 --- /dev/null +++ b/openstack_releases/cmds/find_gerrit_acl_issues.py @@ -0,0 +1,186 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Look for governed projects that have their tagging ACLs misconfigured. + +""" + +import argparse +import configparser +import json +import os.path +import urllib + +import appdirs +import requests +from requests.packages import urllib3 + +import openstack_releases +from openstack_releases import defaults +from openstack_releases import deliverable +from openstack_releases import governance + +# Disable warnings about insecure connections. +urllib3.disable_warnings() + + +IGNORED_TEAMS = [ + 'Infrastructure', + 'OpenStack Charms', +] + +ALLOWED = [ + 'library-release', + 'Release Managers', + 'openstack-chef-release', + 'xstatic-release', + 'release-tools-core', + 'releases-core', +] + + +class GerritClient(object): + + BASE = 'https://review.openstack.org:443/a/' + + def __init__(self, user, password): + self._user = user + self._password = password + self._auth = requests.auth.HTTPDigestAuth( + self._user, + self._password, + ) + self._groups = {} + + def _mk_url(self, api, *args): + encoded = [ + urllib.parse.quote_plus(a) + for a in args + ] + return self.BASE + api.format(*encoded) + + def _get(self, url): + response = requests.get(url, auth=self._auth) + if response.status_code == 404: + raise ValueError(404) + + # strip off first few chars because 'the JSON response body starts with + # a magic prefix line that must be stripped before feeding the rest of + # the response body to a JSON parser' + # https://review.openstack.org/Documentation/rest-api.html + # print(response.text) + return json.loads(response.text[5:]) + + def get_access(self, repo): + url = self._mk_url('projects/{}/access', repo) + return self._get(url) + + def get_group(self, group_id): + if group_id in self._groups: + return self._groups[group_id] + url = self._mk_url('groups/{}', group_id) + data = self._get(url) + self._groups[group_id] = data + return data + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--verbose', '-v', + action='store_true', + default=False, + ) + parser.add_argument( + '--deliverables-dir', + default=openstack_releases.deliverable_dir, + help='location of deliverable files', + ) + args = parser.parse_args() + + config_filename = os.path.join( + appdirs.user_config_dir('openstack-release', 'openstack'), + 'gerrit.ini', + ) + config = configparser.ConfigParser() + config.read(config_filename, encoding='utf-8') + + if not config.has_option('DEFAULT', 'username'): + parser.error('No username set in {}'.format(config_filename)) + if not config.has_option('DEFAULT', 'password'): + parser.error('No password set in {}'.format(config_filename)) + + team_data = governance.get_team_data() + + # Some deliverables were independent at one time but might not be + # any more, so compare the independent list with the current + # release series. + all_independent_deliverables = set( + name + for team, series, name, deliv in deliverable.Deliverables( + root_dir=args.deliverables_dir, + collapse_history=True, + ).get_deliverables(None, None) + ) + current_deliverables = set( + name + for team, series, name, deliv in deliverable.Deliverables( + root_dir=args.deliverables_dir, + collapse_history=True, + ).get_deliverables(None, defaults.RELEASE) + ) + independent_deliverables = all_independent_deliverables.difference( + current_deliverables) + + gerrit = GerritClient( + config['DEFAULT']['username'], + config['DEFAULT']['password'], + ) + + for repo in governance.get_repositories(team_data, code_only=True): + + if repo.deliverable.team.name in IGNORED_TEAMS: + if args.verbose: + print('{}: ignoring {} team'.format( + repo.name, repo.deliverable.team.name)) + continue + + if repo.deliverable.name in independent_deliverables: + if args.verbose: + print('{}: ignoring independent deliverable'.format( + repo.name)) + continue + + acls = gerrit.get_access(repo.name) + local_tag_acls = acls.get('local', {}).get('refs/tags/*', {}) + if local_tag_acls: + rules = local_tag_acls.get('permissions', {}).get( + 'pushSignedTag', {}).get('rules', {}) + if not rules and args.verbose: + print('{}: OK'.format(repo.name)) + + for group_id, permissions in rules.items(): + group_details = gerrit.get_group(group_id) + group_name = group_details['name'] + if group_name in ALLOWED: + if args.verbose: + print('{}: {} pushSignedTag OK'.format( + repo.name, group_name)) + continue + if args.verbose: + print('{}: {} pushSignedTag WARNING'.format( + repo.name, group_name)) + else: + print('{}: {} pushSignedTag'.format( + repo.name, group_name)) diff --git a/requirements.txt b/requirements.txt index 947b9ce864..c08692f322 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,5 @@ reno>=2.0.0 sphinx>=1.6.2 # BSD pyfiglet>=0.7.5 + +appdirs diff --git a/setup.cfg b/setup.cfg index 35dbdca009..a78fc251c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ console_scripts = send-mail = openstack_releases.cmds.mail:main release-notes = openstack_releases.cmds.release_notes:main check-schema = openstack_releases.cmds.check_schema:main + find-gerrit-acl-issues = openstack_releases.cmds.find_gerrit_acl_issues:main [extras] sphinxext =