You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

sym-cli

Package Overview
Dependencies
Maintainers
1
Versions
176
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sym-cli - pypi Package Compare versions

Comparing version
0.5.1
to
0.6.0
+118
sym/cli/helpers/flags.py
"""
There is no client SDK for Python, Patrick provided some example code
for how to use the flag evaluation API from Python
"""
import base64
import json
from typing import List, NamedTuple, Optional, Tuple, TypeVar
import click
import requests
class FlagsConfig(NamedTuple):
"""Config for the LD Client SDK API"""
client_id: str
api_url: str = "app.launchdarkly.com"
def _cli_context(email: str) -> dict:
"""Construct a multitype LD context for flag evaluation"""
return {
"kind": "multi",
"sym-cli": {"key": "prod"},
"sym-user": {"key": email},
}
def _resource_context(email: str, env: str) -> dict:
"""
Construct a multitype LD contxt that includes the
currently requested SSH resource, so we can find the
correctly mapped flag
"""
cli_context = _cli_context(email)
cli_context["sym-ssh-environment"] = {"key": env}
return cli_context
def _encode_context(context_dict: dict) -> str:
"""Properly encode context (per Patrick)"""
json_str = json.dumps(context_dict)
base64_str = base64.urlsafe_b64encode(json_str.encode("utf-8"))
return base64_str.decode()
def _evaluate_context(context_json: dict, config: FlagsConfig) -> Optional[dict]:
"""There is no client SDK for Python, Patrick provided this example"""
ctx = _encode_context(context_json)
url = f"https://{config.api_url}/sdk/evalx/{config.client_id}/contexts/{ctx}"
try:
response = requests.get(url)
data = response.json()
if code := data.get("code"):
message = data.get("message")
click.secho(
(
f"Warning: unable to look up resource configuration in LaunchDarkly.\n"
f"Use --saml-config aws-config and specify the AWS profile name as your sym resource to connect.\n\n"
f"Code: '{code}'\n"
f"Message: '{message}'\n"
),
err=True,
fg="yellow",
bold=True,
)
return None
return data
except requests.exceptions.RequestException as ex:
click.secho(
(
f"Warning: unable to look up resource configuration in LaunchDarkly.\n"
f"Use --saml-config aws-config and specify the AWS profile name as your sym resource to connect.\n\n"
f"{ex}"
),
err=True,
fg="yellow",
bold=True,
)
return None
T = TypeVar("T")
def _flag_value(flags: dict, flag_name: str, default: T) -> T:
if flag := flags.get(flag_name):
return flag.get("value", default)
return default
def get_sso_profile(resource: str, email: str, config: FlagsConfig) -> Tuple[bool, str]:
"""
Look up the AWS SSO profile name for the given requested resource name
Users request access to things like "production" but the config in AWS config
has a different name.
"""
context_json = _resource_context(email, resource)
sso_enabled = False
sso_profile = ""
if flags := _evaluate_context(context_json, config):
sso_enabled = _flag_value(flags, "sym-sso-enabled", sso_enabled)
sso_profile = _flag_value(flags, "sym-ssh-sso-profile", sso_profile)
return (sso_enabled, sso_profile)
def get_resource_list(email: str, config: FlagsConfig) -> Tuple[bool, List[str]]:
"""
Look up all possible resources in the CLI context. This overrides
the hard-coded list in params
"""
context_json = _cli_context(email)
sso_enabled = False
resource_list: List[str] = []
if flags := _evaluate_context(context_json, config):
sso_enabled = _flag_value(flags, "sym-sso-enabled", sso_enabled)
resource_list = _flag_value(flags, "sym-cli-resource-list", resource_list)
return (sso_enabled, resource_list)
+2
-1
Metadata-Version: 2.1
Name: sym-cli
Version: 0.5.1
Version: 0.6.0
Summary: Sym's Official CLI for Users

@@ -34,2 +34,3 @@ Home-page: https://symops.com/

Requires-Dist: sym-shared-cli (>=0.2.3,<0.3.0)
Requires-Dist: urllib3 (==1.26.15)
Requires-Dist: validators (>=0.18.1,<0.19.0)

@@ -36,0 +37,0 @@ Project-URL: Documentation, https://docs.symops.com/docs/install-sym-cli

[tool.poetry]
name = "sym-cli"
version = "0.5.1"
version = "0.6.0"
description = "Sym's Official CLI for Users"

@@ -16,2 +16,5 @@ authors = ["SymOps, Inc. <pypi@symops.io>"]

[tool.poetry.dependencies]
# Temporarily pinned, see https://lists.wikimedia.org/hyperkitty/list/wikitech-l@lists.wikimedia.org/thread/2BIZMR3W2FHRTPVQXSJKM5BVKIFZ6ZIN/
urllib3 = "1.26.15"
python = "^3.8"

@@ -62,2 +65,3 @@ click = "^8.0.0"

pytest-xdist = { extras = ["psutil"], version = "^2.2.0" }
types-requests = "^2.31.0.1"

@@ -64,0 +68,0 @@ [tool.black]

@@ -0,1 +1,3 @@

import os
from dataclasses import InitVar, dataclass, field
from typing import Dict, List, NamedTuple, Optional, Tuple, TypedDict

@@ -7,2 +9,3 @@

from .config import Config
from .flags import FlagsConfig, get_resource_list, get_sso_profile

@@ -14,2 +17,3 @@

arn: str
sso_profile: Optional[str] = None
aws_saml_url: Optional[str] = None

@@ -20,3 +24,4 @@ ansible_bucket: Optional[str] = None

class OrgParams(TypedDict, total=False):
@dataclass
class OrgParams:
resource_env_var: Optional[str]

@@ -28,15 +33,86 @@ users: Dict[str, str]

saml2aws_params: Dict[str, str]
profiles: Dict[str, Profile]
flags_config: FlagsConfig
default_profiles: Dict[str, Profile]
# For legacy reasons, we need to maintain multiple org slugs for LD.
# This allows us to put the config in one place.
LaunchDarklyParams: OrgParams = {
"resource_env_var": "ENVIRONMENT",
"users": {"ssh": "ubuntu", "ansible": "ansible"},
"domain": "launchdarkly.com",
"aws_saml_url": (
_profiles: Dict[str, Profile] = field(init=False)
_sso_profiles: Dict[str, str] = field(init=False)
_sso_enabled: bool = field(init=False)
def __post_init__(self):
self._profiles = None
self._sso_profiles = {}
self._sso_enabled = False
@property
def sso_enabled(self) -> bool:
"""Find out whether we should be using sso as an option for this org"""
if not self._profiles:
self._profiles = self._load_profiles()
return self._sso_enabled
@property
def profiles(self) -> Dict[str, Profile]:
"""Get the list of profile options for this org"""
if not self._profiles:
self._profiles = self._load_profiles()
return self._profiles
def _load_profiles(self) -> Dict[str, Profile]:
"""
Use LaunchDarkly to look up a listing of resources, and overlay that
config on top of the hard-coded config if possible
"""
if not Config.should_analytics():
return self.default_profiles
email = Config.get_email()
sso_enabled, resources = get_resource_list(email, self.flags_config)
self._sso_enabled = sso_enabled
if not sso_enabled or not resources:
return self.default_profiles
result = {}
for resource in resources:
if profile := self.default_profiles.get(resource):
result[resource] = profile
else:
result[resource] = Profile(
display_name=resource,
region="us-east-1",
arn="n/a",
aliases=[],
)
return result
def sso_profile(self, resource: str) -> str:
"""
Use LaunchDarkly to look up an AWS SSO profile name for the current
resource, if possible.
"""
if result := self._sso_profiles.get(resource):
return result
if not Config.should_analytics():
return resource
email = Config.get_email()
_, profile = get_sso_profile(resource, email, self.flags_config)
if profile:
self._sso_profiles[resource] = profile
return profile
return resource
def __getitem__(self, item):
return getattr(self, item)
LaunchDarklyParams = OrgParams(
resource_env_var="ENVIRONMENT",
users={"ssh": "ubuntu", "ansible": "ansible"},
domain="launchdarkly.com",
aws_saml_url=(
"https://launchdarkly.okta.com/home/amazon_aws/0oaj4aow7gPk26Fy6356/272"
),
"aws_okta_params": {
aws_okta_params={
"mfa_provider": "OKTA",

@@ -47,4 +123,8 @@ "mfa_factor_type": "auto",

},
"saml2aws_params": {"mfa": "Auto", "aws_session_duration": "1800"},
"profiles": {
saml2aws_params={"mfa": "Auto", "aws_session_duration": "1800"},
flags_config=FlagsConfig(
client_id="647e348b49e9c114656d9506",
api_url="app.ld.catamorphic.com",
),
default_profiles={
"production": Profile(

@@ -56,2 +136,3 @@ display_name="Production",

aliases=["prod"],
sso_profile="launchdarkly-main-ssh",
),

@@ -63,2 +144,3 @@ "staging": Profile(

ansible_bucket="launchdarkly-sym-ansible-staging",
sso_profile="launchdarkly-main-ssh",
),

@@ -71,2 +153,3 @@ "dr": Profile(

aliases=["production_dr"],
sso_profile="launchdarkly-main-ssh",
),

@@ -79,2 +162,3 @@ "dev": Profile(

aliases=["development"],
sso_profile="launchdarkly-dev-ssh",
),

@@ -87,2 +171,3 @@ "shared-services": Profile(

aliases=["shared_services"],
sso_profile="launchdarkly-shared-services-ssh",
),

@@ -94,2 +179,3 @@ "catamorphic": Profile(

ansible_bucket="launchdarkly-sym-ansible-catamorphic",
sso_profile="launchdarkly-main-ssh",
),

@@ -102,2 +188,3 @@ "catamorphic-dr": Profile(

aliases=["catamorphic_dr"],
sso_profile="launchdarkly-main-ssh",
),

@@ -109,3 +196,8 @@ "intuit-production": Profile(

ansible_bucket="launchdarkly-sym-ansible-intuit-production",
aliases=["production-intuit", "intuit_production", "intuit_production_use2"],
aliases=[
"production-intuit",
"intuit_production",
"intuit_production_use2",
],
sso_profile="launchdarkly-intuit-ssh",
),

@@ -118,2 +210,3 @@ "intuit-dr": Profile(

aliases=["intuit_dr", "intuit_production_dr"],
sso_profile="launchdarkly-intuit-ssh",
),

@@ -126,2 +219,3 @@ "intuit-staging": Profile(

aliases=["staging-intuit", "intuit_staging", "intuit_staging_use2"],
sso_profile="launchdarkly-intuit-ssh",
),

@@ -133,2 +227,3 @@ "production_apac": Profile(

ansible_bucket="launchdarkly-sym-ansible-production",
sso_profile="launchdarkly-main-ssh-apac",
),

@@ -140,2 +235,3 @@ "production_euw1": Profile(

ansible_bucket="launchdarkly-sym-ansible-production",
sso_profile="launchdarkly-main-ssh-euw1",
),

@@ -147,2 +243,3 @@ "staging_apac": Profile(

ansible_bucket="launchdarkly-sym-ansible-staging",
sso_profile="launchdarkly-main-ssh-apac",
),

@@ -154,2 +251,3 @@ "staging_euw1": Profile(

ansible_bucket="launchdarkly-sym-ansible-staging",
sso_profile="launchdarkly-main-ssh-euw1",
),

@@ -162,43 +260,47 @@ "sdk": Profile(

aliases=["sdk"],
sso_profile="launchdarkly-sdk-ssh",
),
},
}
)
SymParams = OrgParams(
resource_env_var="ENVIRONMENT",
users={"ssh": "ubuntu", "ansible": "ubuntu"},
domain="symops.io",
aws_saml_url=("https://dev-291131.okta.com/home/amazon_aws/0oaqlmsn7GMVgAyBK4x6/272"),
aws_okta_params={
"mfa_provider": "OKTA",
"assume_role_ttl": "1h",
"session_ttl": "30m",
},
saml2aws_params={"mfa": "Auto", "aws_session_duration": "1800"},
flags_config=FlagsConfig(
client_id="61baa6c942189d14dec41c78",
),
default_profiles={
"test": Profile(
display_name="Test",
region="us-east-1",
arn="arn:aws:iam::838419636750:role/SSMTestRole",
aliases=["this_is_an_alias"],
),
"test_euw1": Profile(
display_name="Test: EU West 1",
region="eu-west-1",
arn="arn:aws:iam::838419636750:role/SSMTestRole",
),
"test-custom": Profile(
display_name="TestCustom",
region="us-east-1",
arn="arn:aws:iam::838419636750:role/SSMTestRoleCustomBucket",
ansible_bucket="sym-ansible-dev",
aliases=["test_custom", "test_custom2"],
),
},
)
PARAMS: Dict[str, OrgParams] = {
"launch-darkly": LaunchDarklyParams,
"launchdarkly": LaunchDarklyParams,
"sym": {
"resource_env_var": "ENVIRONMENT",
"users": {"ssh": "ubuntu", "ansible": "ubuntu"},
"domain": "symops.io",
"aws_saml_url": (
"https://dev-291131.okta.com/home/amazon_aws/0oaqlmsn7GMVgAyBK4x6/272"
),
"aws_okta_params": {
"mfa_provider": "OKTA",
"assume_role_ttl": "1h",
"session_ttl": "30m",
},
"saml2aws_params": {"mfa": "Auto", "aws_session_duration": "1800"},
"profiles": {
"test": Profile(
display_name="Test",
region="us-east-1",
arn="arn:aws:iam::838419636750:role/SSMTestRole",
aliases=["this_is_an_alias"],
),
"test_euw1": Profile(
display_name="Test: EU West 1",
region="eu-west-1",
arn="arn:aws:iam::838419636750:role/SSMTestRole",
),
"test-custom": Profile(
display_name="TestCustom",
region="us-east-1",
arn="arn:aws:iam::838419636750:role/SSMTestRoleCustomBucket",
ansible_bucket="sym-ansible-dev",
aliases=["test_custom", "test_custom2"],
),
},
},
"sym": SymParams,
}

@@ -211,2 +313,6 @@

def is_sso_enabled() -> bool:
return get_org_params().sso_enabled
def get_aws_saml_url(resource: str) -> str:

@@ -213,0 +319,0 @@ org_params = get_org_params()

@@ -15,3 +15,3 @@ import os

from ..errors import InvalidResource
from ..helpers.params import Profile
from ..helpers.params import Profile, get_org_params
from .saml_client import SAMLClient

@@ -27,6 +27,7 @@

option_value = "aws-config"
priority = 0
priority = 500
setup_help = f"Set up your profile in `{str(AwsConfigPath)}`."
resource: str
profile_name: str
options: "GlobalOptions"

@@ -37,4 +38,6 @@ boto_session: Session

super().__init__(resource, options=options)
self.resource = resource
self.profile_name = get_org_params().sso_profile(resource)
self.raise_if_invalid()
self.boto_session = Session(profile_name=self.resource)
self.boto_session = Session(profile_name=self.profile_name)

@@ -51,3 +54,3 @@ @classmethod

config = cls._read_creds_config()
return config.has_section(cls._profile_name(resource))
return config.has_section(cls._profile_config_name(resource))

@@ -57,13 +60,13 @@ @cached_property

config = self.__class__._read_creds_config()
return config[self.__class__._profile_name(self.resource)]
return config[self.__class__._profile_config_name(self.profile_name)]
@classmethod
def _profile_name(cls, resource: str):
return f"profile {resource}"
def _profile_config_name(cls, profile: str):
return f"profile {profile}"
def raise_if_invalid(self):
if self.__class__.validate_resource(self.resource):
if self.__class__.validate_resource(self.profile_name):
return
raise InvalidResource(
self.resource, self.__class__._read_creds_config().sections()
self.profile_name, self.__class__._read_creds_config().sections()
)

@@ -85,3 +88,3 @@

def _exec(self, *args: str, **opts: str) -> Iterator[Tuple[Argument, ...]]:
with push_env("AWS_PROFILE", self.resource):
with push_env("AWS_PROFILE", self.profile_name):
yield (*args, opts)

@@ -88,0 +91,0 @@

@@ -6,2 +6,5 @@ from typing import Literal, Optional, Type

from sym.cli.helpers.config import Config
from sym.cli.helpers.params import is_sso_enabled
from ..errors import SAMLClientNotFound

@@ -12,3 +15,3 @@ from . import import_all

SAMLClientName = Literal["auto", "aws-okta", "saml2aws", "aws-profile", "aws-config"]
AUTO_EXCLUDED_SAML_CLIENT_NAMES = ["aws-profile", "aws-config"]
AUTO_EXCLUDED_SAML_CLIENT_NAMES = ["aws-profile"]

@@ -26,7 +29,7 @@

if saml_client_name == "auto":
excludes = AUTO_EXCLUDED_SAML_CLIENT_NAMES
if Config.is_logged_in() and is_sso_enabled():
excludes.append("aws-config")
for client in SAMLClient.sorted_subclasses():
if (
client.option_value not in AUTO_EXCLUDED_SAML_CLIENT_NAMES
and has_command(client.binary)
):
if client.option_value not in excludes and has_command(client.binary):
return client

@@ -33,0 +36,0 @@ else:

@@ -163,1 +163,22 @@ from contextlib import contextmanager

init("sym")
@pytest.fixture(autouse=True)
def patch_is_sso_enabled(monkeypatch):
monkeypatch.setattr("sym.cli.helpers.params.is_sso_enabled", lambda: False)
monkeypatch.setattr("sym.cli.saml_clients.chooser.is_sso_enabled", lambda: False)
@pytest.fixture(autouse=True)
def patch_should_analytics(monkeypatch):
monkeypatch.setattr("sym.cli.helpers.config.Config.should_analytics", lambda: False)
@pytest.fixture(autouse=True)
def patch_aws_config_priority(monkeypatch):
monkeypatch.setattr("sym.cli.saml_clients.aws_config.AwsConfig.priority", 0)
@pytest.fixture(autouse=True)
def patch_aws_detault_region(monkeypatch):
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")

@@ -93,2 +93,3 @@ import tempfile

monkeypatch.setattr(SAMLClient, "check_is_setup", lambda self: ...)
monkeypatch.setattr(Config, "should_analytics", lambda: False)

@@ -95,0 +96,0 @@ with sandbox.push_xdg_config_home(), sandbox.push_exec_path(), capture_command():

@@ -1,1 +0,1 @@

__version__ = '0.5.1'
__version__ = '0.6.0'