Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ MANIFEST
# Environments
.venv
venv/

# IDEs
.idea/
.vscode/
36 changes: 24 additions & 12 deletions keyrings/codeartifact.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
# codeartifact.py -- keyring backend

import re
import logging

import boto3
import boto3.session

import os
import re
from configparser import RawConfigParser
from datetime import datetime
from typing import NamedTuple
from urllib.parse import urlparse

import boto3
import boto3.session
from keyring import backend, credentials
from keyring.util.platform_ import config_root

from typing import NamedTuple
from configparser import RawConfigParser


class Qualifier(NamedTuple):
domain: str = None
Expand Down Expand Up @@ -180,11 +178,13 @@ def get_password(self, service, username):
# Options for the client callback.
options = {
# Pass in the region name.
"region_name": region,
"region_name": os.environ.get("AWS_REGION")
or os.environ.get("AWS_DEFAULT_REGION")
or region,
}

# Extract any AWS profile name we should use.
profile_name = config.get("profile_name")
profile_name = os.environ.get("AWS_PROFILE") or config.get("profile_name")
if profile_name:
options.update({"profile_name": profile_name})

Expand All @@ -202,15 +202,27 @@ def get_password(self, service, username):
options.update({"verify": verify.strip('"')})

# If static access/secret keys were provided, use them.
aws_access_key_id = config.get("aws_access_key_id")
aws_secret_access_key = config.get("aws_secret_access_key")
aws_access_key_id = os.environ.get("AWS_ACCESS_KEY_ID") or config.get(
"aws_access_key_id"
)
aws_secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY") or config.get(
"aws_secret_access_key"
)
aws_session_token = (
os.environ.get("AWS_SESSION_TOKEN")
or os.environ.get("AWS_SECURITY_TOKEN")
or config.get("aws_session_token")
)

if aws_access_key_id and aws_secret_access_key:
options.update(
{
"aws_access_key_id": aws_access_key_id,
"aws_secret_access_key": aws_secret_access_key,
}
)
if aws_session_token:
options["aws_session_token"] = aws_session_token

# Generate a CodeArtifact client using the callback.
client = self.make_client(options)
Expand Down
122 changes: 114 additions & 8 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
# test_backend.py -- backend tests

import pytest

from contextlib import contextmanager
from datetime import datetime, timedelta
from io import StringIO
from pathlib import Path
from tempfile import NamedTemporaryFile
from urllib.parse import urlunparse
from datetime import datetime, timedelta

import pytest
from botocore.stub import Stubber

from contextlib import contextmanager
from tempfile import NamedTemporaryFile

from keyrings.codeartifact import make_codeartifact_client
from keyrings.codeartifact import CodeArtifactBackend, CodeArtifactKeyringConfig
from keyrings.codeartifact import (
CodeArtifactBackend,
CodeArtifactKeyringConfig,
make_codeartifact_client,
)

REGION_NAME = "ca-central-1"
CONFIG_DIR = Path(__file__).parent / "config"
Expand Down Expand Up @@ -171,3 +172,108 @@ def make_client(options):
backend = CodeArtifactBackend(config=config, make_client=make_client)
url = codeartifact_pypi_url("domain", "000000000000", "region", "name")
credentials = backend.get_credential(url, None)


@pytest.mark.parametrize(
("env_vars", "config_content", "expected_options"),
[
# Environment variables override config file
(
{
"AWS_PROFILE": "env-profile",
"AWS_REGION": "env-region",
"AWS_ACCESS_KEY_ID": "env-key",
"AWS_SECRET_ACCESS_KEY": "env-secret",
"AWS_SESSION_TOKEN": "env-token",
},
"""
[codeartifact]
profile_name = config-profile
aws_access_key_id = config-key
aws_secret_access_key = config-secret
aws_session_token = config-token
""",
{
"profile_name": "env-profile",
"region_name": "env-region",
"aws_access_key_id": "env-key",
"aws_secret_access_key": "env-secret",
"aws_session_token": "env-token",
},
),
# AWS_DEFAULT_REGION fallback
(
{"AWS_DEFAULT_REGION": "default-region"},
"",
{"region_name": "default-region"},
),
# AWS_SECURITY_TOKEN fallback
(
{
"AWS_ACCESS_KEY_ID": "key",
"AWS_SECRET_ACCESS_KEY": "secret",
"AWS_SECURITY_TOKEN": "security-token",
},
"",
{
"aws_access_key_id": "key",
"aws_secret_access_key": "secret",
"aws_session_token": "security-token",
},
),
# Config file values when no env vars
(
{},
"""
[codeartifact]
profile_name = config-profile
aws_access_key_id = config-key
aws_secret_access_key = config-secret
""",
{
"profile_name": "config-profile",
"aws_access_key_id": "config-key",
"aws_secret_access_key": "config-secret",
},
),
# Region precedence: AWS_REGION takes priority over parsed region
({"AWS_REGION": "env-region"}, "", {"region_name": "env-region"}),
# Only session token without keys should not include token
({"AWS_SESSION_TOKEN": "token-only"}, "", {}),
],
)
def test_environment_variable_precedence(
monkeypatch, env_vars: dict, config_content: str, expected_options: str
):
# Clear existing environment variables and set test ones
for env_key in [
"AWS_PROFILE",
"AWS_REGION",
"AWS_DEFAULT_REGION",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"AWS_SECURITY_TOKEN",
]:
monkeypatch.delenv(env_key, raising=False)

for env_key, env_value in env_vars.items():
monkeypatch.setenv(env_key, env_value)

class DummyClient:
def get_authorization_token(self, *args, **kwargs):
return {}

def make_client(options):
# Assert that we received specific options.
for key, value in expected_options.items():
assert value == options.get(key)

# Ignore the rest.
return DummyClient()

with config_from_string(config_content) as config_file:
config = CodeArtifactKeyringConfig(config_file=config_file.name)
backend = CodeArtifactBackend(config=config, make_client=make_client)
url = codeartifact_pypi_url("domain", "000000000000", "region", "name")
backend.get_credential(url, None)
4 changes: 2 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# test_config.py -- config parsing tests

import pytest

from io import StringIO
from os.path import dirname, join

import pytest

from keyrings.codeartifact import CodeArtifactKeyringConfig


Expand Down
6 changes: 2 additions & 4 deletions tests/test_keyring.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# test_keyring.py -- keyring tests

import pytest
from urllib.parse import urlunparse

import keyring

from urllib.parse import urlunparse
from datetime import datetime, timedelta
import pytest

from keyrings.codeartifact import CodeArtifactBackend

Expand Down