Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions cycode/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ def app_callback(
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
id_token: Annotated[
Optional[str],
typer.Option(
help='Specify a Cycode OIDC ID token for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
_: Annotated[
Optional[bool],
typer.Option(
Expand Down Expand Up @@ -152,6 +159,7 @@ def app_callback(

ctx.obj['client_id'] = client_id
ctx.obj['client_secret'] = client_secret
ctx.obj['id_token'] = id_token

ctx.obj['progress_bar'] = get_progress_bar(hidden=no_progress_meter, sections=SCAN_PROGRESS_BAR_SECTIONS)

Expand Down
39 changes: 37 additions & 2 deletions cycode/cli/apps/auth/auth_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError
from cycode.cli.user_settings.credentials_manager import CredentialsManager
from cycode.cli.utils.jwt_utils import get_user_and_tenant_ids_from_access_token
from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient

if TYPE_CHECKING:
Expand All @@ -13,9 +14,23 @@
def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
printer = ctx.obj.get('console_printer')

client_id, client_secret = ctx.obj.get('client_id'), ctx.obj.get('client_secret')
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
id_token = ctx.obj.get('id_token')

credentials_manager = CredentialsManager()

auth_info = _try_oidc_authorization(ctx, printer, client_id, id_token)
if auth_info:
return auth_info

if not client_id or not client_secret:
client_id, client_secret = CredentialsManager().get_credentials()
stored_client_id, stored_id_token = credentials_manager.get_oidc_credentials()
auth_info = _try_oidc_authorization(ctx, printer, stored_client_id, stored_id_token)
if auth_info:
return auth_info

client_id, client_secret = credentials_manager.get_credentials()

if not client_id or not client_secret:
return None
Expand All @@ -32,3 +47,23 @@ def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
printer.print_exception()

return None


def _try_oidc_authorization(
ctx: 'Context', printer: any, client_id: Optional[str], id_token: Optional[str]
) -> Optional[AuthInfo]:
if not client_id or not id_token:
return None

try:
access_token = CycodeOidcBasedClient(client_id, id_token).get_access_token()
if not access_token:
return None

user_id, tenant_id = get_user_and_tenant_ids_from_access_token(access_token)
return AuthInfo(user_id=user_id, tenant_id=tenant_id)
except (RequestHttpError, HttpUnauthorizedError):
if ctx:
printer.print_exception()

return None
11 changes: 10 additions & 1 deletion cycode/cli/apps/configure/configure_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
get_app_url_input,
get_client_id_input,
get_client_secret_input,
get_id_token_input,
)
from cycode.cli.console import console
from cycode.cli.utils.sentry import add_breadcrumb
Expand All @@ -32,6 +33,7 @@ def configure_command() -> None:
* APP URL: The base URL for Cycode's web application (for on-premise or EU installations)
* Client ID: Your Cycode client ID for authentication
* Client Secret: Your Cycode client secret for authentication
* ID Token: Your Cycode ID token for authentication

Example usage:
* `cycode configure`: Start interactive configuration
Expand All @@ -55,15 +57,22 @@ def configure_command() -> None:
config_updated = True

current_client_id, current_client_secret = CREDENTIALS_MANAGER.get_credentials_from_file()
_, current_id_token = CREDENTIALS_MANAGER.get_oidc_credentials_from_file()
client_id = get_client_id_input(current_client_id)
client_secret = get_client_secret_input(current_client_secret)
id_token = get_id_token_input(current_id_token)

credentials_updated = False
if _should_update_value(current_client_id, client_id) or _should_update_value(current_client_secret, client_secret):
credentials_updated = True
CREDENTIALS_MANAGER.update_credentials(client_id, client_secret)

oidc_credentials_updated = False
if _should_update_value(current_client_id, client_id) or _should_update_value(current_id_token, id_token):
oidc_credentials_updated = True
CREDENTIALS_MANAGER.update_oidc_credentials(client_id, id_token)

if config_updated:
console.print(get_urls_update_result_message())
if credentials_updated:
if credentials_updated or oidc_credentials_updated:
console.print(get_credentials_update_result_message())
11 changes: 11 additions & 0 deletions cycode/cli/apps/configure/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,14 @@ def get_api_url_input(current_api_url: Optional[str]) -> str:
default = current_api_url

return typer.prompt(text=prompt_text, default=default, type=str)


def get_id_token_input(current_id_token: Optional[str]) -> Optional[str]:
prompt_text = 'Cycode ID Token'

prompt_suffix = ' []: '
if current_id_token:
prompt_suffix = f' [{obfuscate_text(current_id_token)}]: '

new_id_token = typer.prompt(text=prompt_text, prompt_suffix=prompt_suffix, default='', show_default=False)
return new_id_token or current_id_token
1 change: 1 addition & 0 deletions cycode/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
# env vars
CYCODE_CLIENT_ID_ENV_VAR_NAME = 'CYCODE_CLIENT_ID'
CYCODE_CLIENT_SECRET_ENV_VAR_NAME = 'CYCODE_CLIENT_SECRET'
CYCODE_ID_TOKEN_ENV_VAR_NAME = 'CYCODE_ID_TOKEN'
26 changes: 25 additions & 1 deletion cycode/cli/user_settings/credentials_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
from pathlib import Path
from typing import Optional

from cycode.cli.config import CYCODE_CLIENT_ID_ENV_VAR_NAME, CYCODE_CLIENT_SECRET_ENV_VAR_NAME
from cycode.cli.config import (
CYCODE_CLIENT_ID_ENV_VAR_NAME,
CYCODE_CLIENT_SECRET_ENV_VAR_NAME,
CYCODE_ID_TOKEN_ENV_VAR_NAME,
)
from cycode.cli.user_settings.base_file_manager import BaseFileManager
from cycode.cli.user_settings.jwt_creator import JwtCreator
from cycode.cli.utils.sentry import setup_scope_from_access_token
Expand All @@ -15,6 +19,7 @@ class CredentialsManager(BaseFileManager):

CLIENT_ID_FIELD_NAME: str = 'cycode_client_id'
CLIENT_SECRET_FIELD_NAME: str = 'cycode_client_secret'
ID_TOKEN_FIELD_NAME: str = 'cycode_id_token'
ACCESS_TOKEN_FIELD_NAME: str = 'cycode_access_token'
ACCESS_TOKEN_EXPIRES_IN_FIELD_NAME: str = 'cycode_access_token_expires_in'
ACCESS_TOKEN_CREATOR_FIELD_NAME: str = 'cycode_access_token_creator'
Expand All @@ -38,6 +43,25 @@ def get_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]:
client_secret = file_content.get(self.CLIENT_SECRET_FIELD_NAME)
return client_id, client_secret

def get_oidc_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]:
file_content = self.read_file()
client_id = file_content.get(self.CLIENT_ID_FIELD_NAME)
id_token = file_content.get(self.ID_TOKEN_FIELD_NAME)
return client_id, id_token

def get_oidc_credentials(self) -> tuple[Optional[str], Optional[str]]:
client_id = os.getenv(CYCODE_CLIENT_ID_ENV_VAR_NAME)
id_token = os.getenv(CYCODE_ID_TOKEN_ENV_VAR_NAME)

if client_id is not None and id_token is not None:
return client_id, id_token

return self.get_oidc_credentials_from_file()

def update_oidc_credentials(self, client_id: str, id_token: str) -> None:
file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.ID_TOKEN_FIELD_NAME: id_token}
self.write_content_to_file(file_content_to_update)

def update_credentials(self, client_id: str, client_secret: str) -> None:
file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.CLIENT_SECRET_FIELD_NAME: client_secret}
self.write_content_to_file(file_content_to_update)
Expand Down
31 changes: 26 additions & 5 deletions cycode/cli/utils/get_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,58 @@


def _get_cycode_client(
create_client_func: callable, client_id: Optional[str], client_secret: Optional[str], hide_response_log: bool
create_client_func: callable,
client_id: Optional[str],
client_secret: Optional[str],
hide_response_log: bool,
id_token: Optional[str] = None,
) -> Union['ScanClient', 'ReportClient']:
if id_token:
if not client_id:
raise click.ClickException('Cycode client id needed for OIDC authentication.')
return create_client_func(client_id, None, hide_response_log, id_token)

if not client_id or not client_secret:
oidc_client_id, oidc_id_token = _get_configured_oidc_credentials()
if oidc_client_id and oidc_id_token:
return create_client_func(oidc_client_id, None, hide_response_log, oidc_id_token)

client_id, client_secret = _get_configured_credentials()
if not client_id:
raise click.ClickException('Cycode client id needed.')
if not client_secret:
raise click.ClickException('Cycode client secret is needed.')

return create_client_func(client_id, client_secret, hide_response_log)
return create_client_func(client_id, client_secret, hide_response_log, None)


def get_scan_cycode_client(ctx: 'typer.Context') -> 'ScanClient':
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
id_token = ctx.obj.get('id_token')
hide_response_log = not ctx.obj.get('show_secret', False)
return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log)
return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log, id_token)


def get_report_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ReportClient':
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log)
id_token = ctx.obj.get('id_token')
return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log, id_token)


def get_import_sbom_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ImportSbomClient':
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log)
id_token = ctx.obj.get('id_token')
return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log, id_token)


def _get_configured_credentials() -> tuple[str, str]:
credentials_manager = CredentialsManager()
return credentials_manager.get_credentials()


def _get_configured_oidc_credentials() -> tuple[Optional[str], Optional[str]]:
credentials_manager = CredentialsManager()
return credentials_manager.get_oidc_credentials()
34 changes: 28 additions & 6 deletions cycode/cyclient/client_creator.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
from typing import Optional

from cycode.cyclient.config import dev_mode
from cycode.cyclient.config_dev import DEV_CYCODE_API_URL
from cycode.cyclient.cycode_dev_based_client import CycodeDevBasedClient
from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
from cycode.cyclient.import_sbom_client import ImportSbomClient
from cycode.cyclient.report_client import ReportClient
from cycode.cyclient.scan_client import ScanClient
from cycode.cyclient.scan_config_base import DefaultScanConfig, DevScanConfig


def create_scan_client(client_id: str, client_secret: str, hide_response_log: bool) -> ScanClient:
def create_scan_client(
client_id: str, client_secret: Optional[str] = None, hide_response_log: bool = False, id_token: Optional[str] = None
) -> ScanClient:
if dev_mode:
client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
scan_config = DevScanConfig()
else:
client = CycodeTokenBasedClient(client_id, client_secret)
if id_token:
client = CycodeOidcBasedClient(client_id, id_token)
else:
client = CycodeTokenBasedClient(client_id, client_secret)
scan_config = DefaultScanConfig()

return ScanClient(client, scan_config, hide_response_log)


def create_report_client(client_id: str, client_secret: str, _: bool) -> ReportClient:
client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
def create_report_client(
client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
) -> ReportClient:
if dev_mode:
client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
elif id_token:
client = CycodeOidcBasedClient(client_id, id_token)
else:
client = CycodeTokenBasedClient(client_id, client_secret)
return ReportClient(client)


def create_import_sbom_client(client_id: str, client_secret: str, _: bool) -> ImportSbomClient:
client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
def create_import_sbom_client(
client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
) -> ImportSbomClient:
if dev_mode:
client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
elif id_token:
client = CycodeOidcBasedClient(client_id, id_token)
else:
client = CycodeTokenBasedClient(client_id, client_secret)
return ImportSbomClient(client)
Loading