Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support gh refresh tokens #69

Merged
merged 4 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,10 @@ def mock_submit_fn(metric, start, end):
mock_submit = mocker.Mock()
mock_submit.side_effect = mock_submit_fn

import helpers
from helpers.checkpoint_logger import CheckpointLogger

return mocker.patch.object(
helpers.checkpoint_logger.CheckpointLogger,
CheckpointLogger,
"submit_subflow",
mock_submit,
)

return mock_submit
35 changes: 35 additions & 0 deletions helpers/token_refresh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging
from typing import Callable, Dict

from shared.encryption.token import encode_token

from database.models.core import Owner
from services.encryption import encryptor

log = logging.getLogger(__name__)


def get_token_refresh_callback(owner: Owner) -> Callable[[Dict], None]:
"""
Produces a callback function that will encode and update the oauth token of a user.
This callback is passed to the TorngitAdapter for the service.
"""
# Some tokens don't have to be refreshed (GH integration, default bots)
# They don't belong to any owners.
if owner is None:
return None

service = owner.service
if service == "bitbucket" or service == "bitbucket_server":
return None

async def callback(new_token: Dict) -> None:
log.info(
"Saving new token after refresh",
extra=dict(owner=owner.username, ownerid=owner.ownerid),
)
string_to_save = encode_token(new_token)
oauth_token = encryptor.encode(string_to_save).decode()
owner.oauth_token = oauth_token

return callback
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
git+ssh://[email protected]/codecov/shared.git@6a8a33248804a9c101d34f417efda7c11e4bbe63#egg=shared
git+ssh://[email protected]/codecov/shared.git@f0174635ccaebc3463a5641b9ad640a46b5fd472#egg=shared
git+ssh://[email protected]/codecov/[email protected]#egg=codecovopentelem
boto3
celery
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ s3transfer==0.3.4
# via boto3
sentry-sdk==1.19.1
# via -r requirements.in
shared @ git+ssh://[email protected]/codecov/shared.git@4f65b0040ab6bb3dba2190ce544ade9a0ea3e354
shared @ git+ssh://[email protected]/codecov/shared.git@f0174635ccaebc3463a5641b9ad640a46b5fd472
# via -r requirements.in
six==1.15.0
# via
Expand Down
7 changes: 6 additions & 1 deletion services/owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import shared.torngit as torngit
from shared.config import get_config, get_verify_ssl

from database.models import Owner
from helpers.token_refresh import get_token_refresh_callback
from services.bots import get_owner_appropriate_bot_token

log = logging.getLogger(__name__)
Expand All @@ -27,6 +27,11 @@ def get_owner_provider_service(owner, using_integration=False):
key=get_config(service, "client_id"),
secret=get_config(service, "client_secret"),
),
# if using integration we will use the integration token
# not the owner's token
on_token_refresh=(
get_token_refresh_callback(owner) if not using_integration else None
),
)
return _get_owner_provider_service_instance(service, **adapter_params)

Expand Down
32 changes: 2 additions & 30 deletions services/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,28 @@
import re
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
from typing import Any, Mapping, Optional, Tuple

import shared.torngit as torngit
from shared.config import get_config, get_verify_ssl
from shared.encryption.token import encode_token
from shared.torngit.exceptions import (
TorngitClientError,
TorngitError,
TorngitObjectNotFoundError,
)
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from database.models import Commit, Owner, Pull, Repository
from helpers.token_refresh import get_token_refresh_callback
from services.bots import get_repo_appropriate_bot_token, get_token_type_mapping
from services.encryption import encryptor
from services.yaml import read_yaml_field

log = logging.getLogger(__name__)

merged_pull = re.compile(r".*Merged in [^\s]+ \(pull request \#(\d+)\).*").match


def get_token_refresh_callback(owner: Owner) -> Callable[[Dict], None]:
"""
Produces a callback function that will encode and update the oauth token of a user.
This callback is passed to the TorngitAdapter for the service.
"""
# Some tokens don't have to be refreshed (GH integration, default bots)
# They don't belong to any owners.
if owner is None:
return None

service = owner.service
if service != "gitlab" and service != "gitlab_enterprise":
return None

async def callback(new_token: Dict) -> None:
log.info(
"Saving new token after refresh",
extra=dict(owner=owner.username, ownerid=owner.ownerid),
)
string_to_save = encode_token(new_token)
oauth_token = encryptor.encode(string_to_save).decode()
owner.oauth_token = oauth_token

return callback


def get_repo_provider_service(
repository, commit=None
) -> torngit.base.TorngitBaseAdapter:
Expand Down
36 changes: 34 additions & 2 deletions services/tests/test_repository_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@


class TestRepositoryServiceTestCase(object):
def test_get_repo_provider_service(self, dbsession):
def test_get_repo_provider_service_github(self, dbsession):
repo = RepositoryFactory.create(
owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w",
owner__service="github",
Expand All @@ -55,7 +55,39 @@ def test_get_repo_provider_service(self, dbsession):
}
assert res.data == expected_data
assert repo.owner.service == "github"
assert res._on_token_refresh is None # GH doesn't have callback implemented
assert res._on_token_refresh is not None
assert inspect.isawaitable(res._on_token_refresh(None))
assert res.token == {
"username": repo.owner.username,
"key": "testyftq3ovzkb3zmt823u3t04lkrt9w",
"secret": None,
}

def test_get_repo_provider_service_bitbucket(self, dbsession):
repo = RepositoryFactory.create(
owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w",
owner__service="bitbucket",
name="example-python",
)
dbsession.add(repo)
dbsession.flush()
res = get_repo_provider_service(repo)
expected_data = {
"owner": {
"ownerid": repo.owner.ownerid,
"service_id": repo.owner.service_id,
"username": repo.owner.username,
},
"repo": {
"name": "example-python",
"using_integration": False,
"service_id": repo.service_id,
"repoid": repo.repoid,
},
}
assert res.data == expected_data
assert repo.owner.service == "bitbucket"
assert res._on_token_refresh is None
assert res.token == {
"username": repo.owner.username,
"key": "testyftq3ovzkb3zmt823u3t04lkrt9w",
Expand Down
Loading