Skip to content

Commit

Permalink
Merge pull request #1291 from conda-forge/token-rot
Browse files Browse the repository at this point in the history
ENH added code to rotate anaconda tokens
  • Loading branch information
beckermr authored Apr 26, 2020
2 parents 6f6ef85 + f0e3175 commit 062787c
Show file tree
Hide file tree
Showing 5 changed files with 679 additions and 3 deletions.
388 changes: 388 additions & 0 deletions conda_smithy/anaconda_token_rotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
"""This module updates/rotates anaconda/binstar tokens.
The correct way to use this module is to call its functions via the command
line utility. The relevant one is
conda-smithy update-anaconda-token
Note that if you are using appveyor, you will need to push the changes to the
conda-forge.yml in your feedstock to GitHub.
"""
import os
import sys
from contextlib import redirect_stderr, redirect_stdout

import requests

from .utils import update_conda_forge_config


def _get_anaconda_token():
"""use this helper to enable easier patching for tests"""
try:
from .ci_register import anaconda_token

return anaconda_token
except ImportError:
raise RuntimeError(
"You must have the anaconda token defined to do token rotation!"
)


def rotate_anaconda_token(
user,
project,
feedstock_directory,
drone=True,
circle=True,
travis=True,
azure=True,
appveyor=True,
token_name="BINSTAR_TOKEN",
):
"""Rotate the anaconda (binstar) token used by the CI providers
All exceptions are swallowed and stdout/stderr from this function is
redirected to `/dev/null`. Sanitized error messages are
displayed at the end.
If you need to debug this function, define `DEBUG_ANACONDA_TOKENS` in
your environment before calling this function.
"""
# we are swallong all of the logs below, so we do a test import here
# to generate the proper errors for missing tokens
# note that these imports cover all providers
from .ci_register import travis_endpoint # noqa
from .azure_ci_utils import default_config # noqa

anaconda_token = _get_anaconda_token()

# capture stdout, stderr and suppress all exceptions so we don't
# spill tokens
failed = False
err_msg = None
with open(os.devnull, "w") as fp:
if "DEBUG_ANACONDA_TOKENS" in os.environ:
fpo = sys.stdout
fpe = sys.stderr
else:
fpo = fp
fpe = fp

with redirect_stdout(fpo), redirect_stderr(fpe):
try:
if circle:
try:
rotate_token_in_circle(
user, project, anaconda_token, token_name
)
except Exception as e:
if "DEBUG_ANACONDA_TOKENS" in os.environ:
raise e
else:
err_msg = (
"Failed to rotate token for %s/%s"
" on circle!"
) % (user, project)
failed = True
raise RuntimeError(err_msg)

if drone:
try:
rotate_token_in_drone(
user, project, anaconda_token, token_name
)
except Exception as e:
if "DEBUG_ANACONDA_TOKENS" in os.environ:
raise e
else:
err_msg = (
"Failed to rotate token for %s/%s" " on drone!"
) % (user, project)
failed = True
raise RuntimeError(err_msg)

if travis:
try:
rotate_token_in_travis(
user,
project,
feedstock_directory,
anaconda_token,
token_name,
)
except Exception as e:
if "DEBUG_ANACONDA_TOKENS" in os.environ:
raise e
else:
err_msg = (
"Failed to rotate token for %s/%s"
" on travis!"
) % (user, project)
failed = True
raise RuntimeError(err_msg)

if azure:
try:
rotate_token_in_azure(
user, project, anaconda_token, token_name
)
except Exception as e:
if "DEBUG_ANACONDA_TOKENS" in os.environ:
raise e
else:
err_msg = (
"Failed to rotate token for %s/%s" " on azure!"
) % (user, project)
failed = True
raise RuntimeError(err_msg)

if appveyor:
try:
rotate_token_in_appveyor(
feedstock_directory, anaconda_token, token_name
)
except Exception as e:
if "DEBUG_ANACONDA_TOKENS" in os.environ:
raise e
else:
err_msg = (
"Failed to rotate token for %s/%s"
" on appveyor!"
) % (user, project)
failed = True
raise RuntimeError(err_msg)

except Exception as e:
if "DEBUG_ANACONDA_TOKENS" in os.environ:
raise e
failed = True
if failed:
if err_msg:
raise RuntimeError(err_msg)
else:
raise RuntimeError(
(
"Rotating the feedstock token in proviers for %s/%s failed!"
" Try the command locally with DEBUG_ANACONDA_TOKENS"
" defined in the environment to investigate!"
)
% (user, project)
)


def rotate_token_in_circle(user, project, binstar_token, token_name):
from .ci_register import circle_token

url_template = (
"https://circleci.com/api/v1.1/project/github/{user}/{project}/envvar{extra}?"
"circle-token={token}"
)

r = requests.get(
url_template.format(
token=circle_token, user=user, project=project, extra=""
)
)
if r.status_code != 200:
r.raise_for_status()

have_binstar_token = False
for evar in r.json():
if evar["name"] == token_name:
have_binstar_token = True

if have_binstar_token:
r = requests.delete(
url_template.format(
token=circle_token,
user=user,
project=project,
extra="/%s" % token_name,
)
)
if r.status_code != 200:
r.raise_for_status()

data = {"name": token_name, "value": binstar_token}
response = requests.post(
url_template.format(
token=circle_token, user=user, project=project, extra=""
),
data,
)
if response.status_code != 201:
raise ValueError(response)


def rotate_token_in_drone(user, project, binstar_token, token_name):
from .ci_register import drone_session

session = drone_session()

r = session.get(f"/api/repos/{user}/{project}/secrets")
r.raise_for_status()
have_binstar_token = False
for secret in r.json():
if token_name == secret["name"]:
have_binstar_token = True

if have_binstar_token:
r = session.patch(
f"/api/repos/{user}/{project}/secrets/{token_name}",
json={"data": binstar_token, "pull_request": False},
)
r.raise_for_status()
else:
response = session.post(
f"/api/repos/{user}/{project}/secrets",
json={
"name": token_name,
"data": binstar_token,
"pull_request": False,
},
)
if response.status_code != 200:
response.raise_for_status()


def rotate_token_in_travis(
user, project, feedstock_directory, binstar_token, token_name
):
"""update the binstar token in travis."""
from .ci_register import (
travis_endpoint,
travis_headers,
travis_get_repo_info,
)

headers = travis_headers()

repo_info = travis_get_repo_info(user, project)
repo_id = repo_info["id"]

r = requests.get(
"{}/repo/{repo_id}/env_vars".format(travis_endpoint, repo_id=repo_id),
headers=headers,
)
if r.status_code != 200:
r.raise_for_status()

have_binstar_token = False
ev_id = None
for ev in r.json()["env_vars"]:
if ev["name"] == token_name:
have_binstar_token = True
ev_id = ev["id"]

data = {
"env_var.name": token_name,
"env_var.value": binstar_token,
"env_var.public": "false",
}

if have_binstar_token:
r = requests.patch(
"{}/repo/{repo_id}/env_var/{ev_id}".format(
travis_endpoint, repo_id=repo_id, ev_id=ev_id,
),
headers=headers,
json=data,
)
r.raise_for_status()
else:
r = requests.post(
"{}/repo/{repo_id}/env_vars".format(
travis_endpoint, repo_id=repo_id
),
headers=headers,
json=data,
)
if r.status_code != 201:
r.raise_for_status()

# we remove the token in the conda-forge.yml since on travis the
# encrypted values override any value we put in the API
with update_conda_forge_config(feedstock_directory) as code:
if (
"travis" in code
and "secure" in code["travis"]
and token_name in code["travis"]["secure"]
):
del code["travis"]["secure"][token_name]

if len(code["travis"]["secure"]) == 0:
del code["travis"]["secure"]

if len(code["travis"]) == 0:
del code["travis"]

print(
"An old value of the variable %s for travis was found in the "
"conda-forge.yml. You may need to rerender this feedstock to "
"use the new value since encrypted secrets inserted in travis.yml "
"files override those set in the UI/API!"
)


def rotate_token_in_azure(user, project, binstar_token, token_name):
from .azure_ci_utils import build_client, get_default_build_definition
from .azure_ci_utils import default_config as config
from vsts.build.v4_1.models import BuildDefinitionVariable

bclient = build_client()

existing_definitions = bclient.get_definitions(
project=config.project_name, name=project
)
if existing_definitions:
assert len(existing_definitions) == 1
ed = existing_definitions[0]
else:
raise RuntimeError(
"Cannot add %s to a repo that is not already registerd on azure CI!"
% token_name
)

ed = bclient.get_definition(ed.id, project=config.project_name)

if not hasattr(ed, "variables") or ed.variables is None:
variables = {}
else:
variables = ed.variables

variables[token_name] = BuildDefinitionVariable(
allow_override=False, is_secret=True, value=binstar_token,
)

build_definition = get_default_build_definition(
user,
project,
config=config,
variables=variables,
id=ed.id,
revision=ed.revision,
)

bclient.update_definition(
definition=build_definition,
definition_id=ed.id,
project=ed.project.name,
)


def rotate_token_in_appveyor(feedstock_directory, binstar_token, token_name):
from .ci_register import appveyor_token

headers = {"Authorization": "Bearer {}".format(appveyor_token)}
url = "https://ci.appveyor.com/api/account/encrypt"
response = requests.post(
url, headers=headers, data={"plainValue": binstar_token}
)
if response.status_code != 200:
raise ValueError(response)

with update_conda_forge_config(feedstock_directory) as code:
code.setdefault("appveyor", {}).setdefault("secure", {})[
token_name
] = response.content.decode("utf-8")
Loading

0 comments on commit 062787c

Please sign in to comment.