Manage repository collaborator invites

Fixes GH-57

Invitations can also be extended to outside collaborators at the
repository level. These changes extend the existing code to poll for all
such invitations (each repository in the organization must be
interrogated).
This commit is contained in:
Hal Wine 2019-12-10 17:08:01 -08:00 коммит произвёл Hal Wine
Родитель 150e67214d
Коммит 61eb739587
3 изменённых файлов: 72 добавлений и 18 удалений

Просмотреть файл

@ -1,8 +1,7 @@
# github org helper scripts
**NOTE: branch 'invitations' requires a version of github3.py which has
not yet been released.** See
[PR](https://github.com/sigmavirus24/github3.py/pull/675) for details.
**NOTE: the main helper library, GitHub3.py, has been updated to version 1.3.0.
Not all scripts have been verified against this version.**
These are some API helper scripts for sanely managing a github org. For now this is somewhat hardcoded for the mozilla org; no need for it to remain that way though.
@ -30,15 +29,19 @@ Find all hooks configured for an organization -- see --help for details
### get_org_info.py
Output basic info about an org, more if you have permissions. See --help for details
### team_update.py
Update administrative teams so they can be used for the new GitHub discussion
feature. Use the ``--help`` option for more information.
### lfs.py
Get current LFS Billing values using a headless firefox via selenium
(``geckodriver`` must be installed). Credentials as environment
variables, and 2FA token passed as input.
### manage_invitations.py
Cancel all org & repository invitations older than a specified age (default 2
weeks). See --help for details.
### team_update.py
Update administrative teams so they can be used for the new GitHub discussion
feature. Use the ``--help`` option for more information.
### Audit logs
Sadly, the org audit log does not have an API, so we'll screen scrape a little.

Просмотреть файл

@ -14,6 +14,12 @@ those with:
manage_invitations | cut -d ' ' -f1
Or invitee & inviter:
manage_invitations | awk '!/^Proc/ {print $1 " by " $NF;}'
ToDo:
- Better handling of Security Advisory private repos. These return a 404
on invitation calls, which are simply reported now. These repos
usually have a name ending in the string '-ghsa-' followed by hex
digits.
"""
# hwine believes keeping the doc above together is more important than PEP-8
import argparse # NOQA
@ -28,6 +34,9 @@ if not hasattr(github3.orgs.Organization, 'invitations'):
raise NotImplementedError("Your version of github3.py does not support "
"invitations. Try "
"https://github.com/hwine/github3.py/tree/invitations") # NOQA
if (1,3,0) > github3.__version_info__:
raise NotImplementedError("Your version of github3.py does not support "
"collaborator invitations. Version '1.3.0' or later is known to work.")
logger = logging.getLogger(__name__)
@ -49,28 +58,70 @@ def check_invites(gh, org_name, cancel=False, cutoff_delta="weeks=-2"):
cutoff_time = get_cutoff_time(cutoff_delta)
try:
for invite in org.invitations():
extended_at = arrow.get(invite['created_at'])
extended_at = arrow.get(invite.created_at)
line_end = ": " if cancel else "\n"
if extended_at < cutoff_time:
invite['ago'] = extended_at.humanize()
context = invite.as_dict()
context['ago'] = extended_at.humanize()
print('{login} ({email}) was invited {ago} by '
'{inviter[login]}'.format(**invite),
'{inviter[login]}'.format(**context),
end=line_end)
if cancel:
success = org.remove_membership(username=invite['login'])
success = org.remove_membership(invite.id)
if success:
print("Cancelled")
else:
print("FAILED to cancel")
logger.warning("Couldn't cancel invite for {login} "
"from {created_at}".format(**invite))
"from {created_at}".format(**context))
except ForbiddenError:
logger.error("You don't have 'admin:org' permissions for org '%s'",
org_name)
else:
# now handle collaborator invitations (GH-57)
for repo in org.repositories():
# occasionally get a 404 when looking for invitations.
# Assume this is a race condition and ignore. That may leave
# some invites uncanceled, but a 2nd run should catch.
try:
for invite in repo.invitations():
extended_at = arrow.get(invite.created_at)
line_end = ": " if cancel else "\n"
if extended_at < cutoff_time:
context = invite.as_dict()
context['ago'] = extended_at.humanize()
context['repo'] = repo.name
context['inviter'] = invite.inviter.login
context['invitee'] = invite.invitee.login
print('{invitee} was invited to {repo} {ago} by '
'{inviter} for {permissions} access.'.format(**context),
end=line_end)
if cancel:
# Deletion not directly supported, so hack url &
# use send delete verb directly
delete_url = repo.url + "/invitations/" + str(invite.id)
success = repo._delete(delete_url)
if success:
print("Cancelled")
else:
print("FAILED to cancel")
logger.warning("Couldn't cancel invite for {login} "
"from {created_at}".format(**context))
except (github3.exceptions.NotFoundError,
github3.exceptions.ConnectionError) as e:
# just report
logger.warning("Got 404 for invitation in {}, may be unhandled inviations. '{}'".format(repo.name,
str(e)))
def parse_args():
parser = argparse.ArgumentParser(description=__doc__, epilog=_epilog)
# from
# https://stackoverflow.com/questions/18462610/argumentparser-epilog-and-description-formatting-in-conjunction-with-argumentdef
class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
pass
parser = argparse.ArgumentParser(description=__doc__,
epilog=_epilog, formatter_class=CustomFormatter)
parser.add_argument('--cancel', action='store_true',
help='Cancel stale invitations')
parser.add_argument('--cutoff', help='When invitations go stale '
@ -100,4 +151,7 @@ def main():
if __name__ == '__main__':
logging.basicConfig(level=logging.WARN, format='%(asctime)s %(message)s')
main()
try:
main()
except KeyboardInterrupt:
raise SystemExit("\nCancelled by user")

Просмотреть файл

@ -1,8 +1,5 @@
jsonstreams==0.4.1
# we need a version of github3.py with
# https://github.com/sigmavirus24/github3.py/pull/675 landed
# github3.py==1.0.0a4
git+https://github.com/hwine/github3.py.git@invitations
github3.py==1.3.0
PyYaml==5.1
tinydb==3.2.1
# until up on pypa