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

Feature: Improve Python API for Keychain #315

Merged
merged 4 commits into from
Mar 24, 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: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version 0.39.2
-------------

**Features**
- Improve Python API for module `codemagic.tools.keychain`. Allow passing passwords as strings in addition to `codemagic.tools.keychain.Password` for `Keychain` methods. [PR #315](https://github.com/codemagic-ci-cd/cli-tools/pull/315)

Version 0.39.1
-------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "codemagic-cli-tools"
version = "0.39.1"
version = "0.39.2"
description = "CLI tools used in Codemagic builds"
readme = "README.md"
authors = [
Expand Down
2 changes: 1 addition & 1 deletion src/codemagic/__version__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__title__ = 'codemagic-cli-tools'
__description__ = 'CLI tools used in Codemagic builds'
__version__ = '0.39.1.dev'
__version__ = '0.39.2.dev'
__url__ = 'https://github.com/codemagic-ci-cd/cli-tools'
__licence__ = 'GNU General Public License v3.0'
83 changes: 54 additions & 29 deletions src/codemagic/tools/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import List
from typing import Optional
from typing import Sequence
from typing import Union

from codemagic import cli
from codemagic.cli import Colors
Expand Down Expand Up @@ -124,15 +125,17 @@ def path(self) -> pathlib.Path:
return self._path

@cli.action('create', KeychainArgument.PASSWORD)
def create(self, password: Password = Password('')) -> pathlib.Path:
def create(self, password: Union[str, Password] = '') -> pathlib.Path:
"""
Create a macOS keychain, add it to the search list
"""
_password: str = password.value if isinstance(password, Password) else password

self.logger.info(f'Create keychain {self.path}')
process = self.execute(
('security', 'create-keychain', '-p', password.value, self.path),
obfuscate_patterns=[password.value])
('security', 'create-keychain', '-p', _password, self.path),
obfuscate_patterns=[_password],
)
if process.returncode != 0:
raise KeychainError(f'Unable to create keychain {self.path}', process)

Expand Down Expand Up @@ -200,15 +203,17 @@ def lock(self):
raise KeychainError(f'Unable to unlock keychain {self.path}', process)

@cli.action('unlock', KeychainArgument.PASSWORD)
def unlock(self, password: Password = Password('')):
def unlock(self, password: Union[str, Password] = ''):
"""
Unlock the specified keychain
"""
_password: str = password.value if isinstance(password, Password) else password

self.logger.info(f'Unlock keychain {self.path}')
process = self.execute(
('security', 'unlock-keychain', '-p', password.value, self.path),
obfuscate_patterns=[password.value])
('security', 'unlock-keychain', '-p', _password, self.path),
obfuscate_patterns=[_password],
)
if process.returncode != 0:
raise KeychainError(f'Unable to unlock keychain {self.path}', process)

Expand Down Expand Up @@ -261,22 +266,27 @@ def use_login_keychain(self) -> Keychain:
return self

@cli.action('initialize', KeychainArgument.PASSWORD, KeychainArgument.TIMEOUT)
def initialize(self, password: Password = Password(''), timeout: Optional[Seconds] = None) -> Keychain:
def initialize(
self,
password: Union[str, Password] = '',
timeout: Optional[Seconds] = None,
) -> Keychain:
"""
Set up the keychain to be used for code signing. Create the keychain
at specified path with specified password with given timeout.
Make it default and unlock it for upcoming use
"""
_password: str = password.value if isinstance(password, Password) else password

if not self._path:
self._generate_path()

message = f'Initialize new keychain to store code signing certificates at {self.path}'
self.logger.info(Colors.GREEN(message))
self.create(password)
self.create(_password)
self.set_timeout(timeout=timeout)
self.make_default()
self.unlock(password)
self.unlock(_password)
return self

@cli.action('list-certificates')
Expand All @@ -299,19 +309,22 @@ def _generate_path(self):
with NamedTemporaryFile(prefix=f'{date}_', suffix='.keychain-db', dir=keychain_dir) as tf:
self._path = pathlib.Path(tf.name)

@cli.action('add-certificates',
KeychainArgument.CERTIFICATE_PATHS,
KeychainArgument.CERTIFICATE_PASSWORD,
KeychainArgument.ALLOWED_APPLICATIONS,
KeychainArgument.ALLOW_ALL_APPLICATIONS,
KeychainArgument.DISALLOW_ALL_APPLICATIONS)
@cli.action(
'add-certificates',
KeychainArgument.CERTIFICATE_PATHS,
KeychainArgument.CERTIFICATE_PASSWORD,
KeychainArgument.ALLOWED_APPLICATIONS,
KeychainArgument.ALLOW_ALL_APPLICATIONS,
KeychainArgument.DISALLOW_ALL_APPLICATIONS,
)
def add_certificates(
self,
certificate_path_patterns: Sequence[pathlib.Path] = KeychainArgument.CERTIFICATE_PATHS.get_default(),
certificate_password: Password = Password(''),
certificate_password: Union[str, Password] = '',
allowed_applications: Sequence[pathlib.Path] = KeychainArgument.ALLOWED_APPLICATIONS.get_default(),
allow_all_applications: Optional[bool] = KeychainArgument.ALLOW_ALL_APPLICATIONS.get_default(),
disallow_all_applications: Optional[bool] = KeychainArgument.DISALLOW_ALL_APPLICATIONS.get_default()):
disallow_all_applications: Optional[bool] = KeychainArgument.DISALLOW_ALL_APPLICATIONS.get_default(),
):
"""
Add p12 certificate to specified keychain
"""
Expand All @@ -322,7 +335,8 @@ def add_certificates(
raise KeychainArgument.ALLOW_ALL_APPLICATIONS.raise_argument_error(
f'Using mutually exclusive options '
f'{KeychainArgument.ALLOWED_APPLICATIONS.flag!r} and '
f'{KeychainArgument.DISALLOW_ALL_APPLICATIONS.flag!r}')
f'{KeychainArgument.DISALLOW_ALL_APPLICATIONS.flag!r}',
)
elif allow_all_applications:
add_for_all_apps = True
elif not disallow_all_applications:
Expand All @@ -332,42 +346,53 @@ def add_certificates(
certificate_paths = list(self.find_paths(*certificate_path_patterns))
if not certificate_paths:
raise KeychainError('Did not find any certificates from specified locations')

if isinstance(certificate_password, Password):
_certificate_password = certificate_password.value
else:
_certificate_password = certificate_password

for certificate_path in certificate_paths:
self._add_certificate(certificate_path, certificate_password, add_for_all_apps, add_for_apps)
self._add_certificate(certificate_path, _certificate_password, add_for_all_apps, add_for_apps)

@classmethod
def _get_certificate_allowed_applications(
cls, given_allowed_applications: Sequence[pathlib.Path]) -> Iterable[str]:
cls,
given_allowed_applications: Sequence[pathlib.Path],
) -> Iterable[str]:
for application in given_allowed_applications:
resolved_path = shutil.which(application)
if resolved_path is None:
# Only raise exception if user-specified path is not present
if application not in KeychainArgument.ALLOWED_APPLICATIONS.get_default():
raise KeychainArgument.ALLOWED_APPLICATIONS.raise_argument_error(
f'Application "{application}" does not exist or is not in PATH')
f'Application "{application}" does not exist or is not in PATH',
)
else:
yield str(resolved_path)

def _add_certificate(self,
certificate_path: pathlib.Path,
certificate_password: Optional[Password] = None,
allow_for_all_apps: bool = False,
allowed_applications: Sequence[str] = tuple()):
def _add_certificate(
self,
certificate_path: pathlib.Path,
certificate_password: Optional[str] = None,
allow_for_all_apps: bool = False,
allowed_applications: Sequence[str] = tuple(),
):
self.logger.info(f'Add certificate {certificate_path} to keychain {self.path}')
# If case of no password, we need to explicitly set -P '' flag. Otherwise,
# security tries to open an interactive dialog to prompt the user for a password,
# which fails in non-interactive CI environment.
if certificate_password is not None:
obfuscate_patterns = [certificate_password.value]
obfuscate_patterns = [certificate_password]
else:
certificate_password = Password('')
certificate_password = ''
obfuscate_patterns = []

import_cmd = [
'security', 'import', certificate_path,
'-f', 'pkcs12',
'-k', self.path,
'-P', certificate_password.value,
'-P', certificate_password,
]
if allow_for_all_apps:
import_cmd.append('-A')
Expand Down