Skip to content

Add support to the client (CLI, http client) for sending additional basic auth credentials (aka MFA) #5152

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

Merged
merged 12 commits into from
Mar 27, 2022
Merged
13 changes: 13 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ Added

Contributed by @Kami.

* Add new ``credentials.basic_auth = username:password`` CLI configuration option.

This argument allows client to use additional set of basic auth credentials when talking to the
StackStorm API endpoints (api, auth, stream) - that is, in addition to the token / api key
native StackStorm auth.

This allows for simple basic auth based multi factor authentication implementation for
installations which don't utilize SSO.

#5152

Contributed by @Kami.

Fixed
~~~~~

Expand Down
5 changes: 5 additions & 0 deletions conf/st2rc.sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ username = test1
password = testpassword
# or authenticate with an api key.
# api_key = <key>
# Optional additional http basic auth credentials which are sent with each HTTP
# request except the auth request to /v1/auth/tokens endpoint.
# Available in StackStorm >= v3.4.0
# NOTE: Username can't contain colon (:) character.
# basic_auth = username:password

[api]
url = http://127.0.0.1:9101/v1
Expand Down
2 changes: 2 additions & 0 deletions st2client/st2client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"api_key": ["credentials", "api_key"],
"cacert": ["general", "cacert"],
"debug": ["cli", "debug"],
"basic_auth": ["credentials", "basic_auth"],
}


Expand Down Expand Up @@ -87,6 +88,7 @@ def get_client(self, args, debug=False):
"stream_url",
"api_version",
"cacert",
"basic_auth",
]
cli_options = {opt: getattr(args, opt, None) for opt in cli_options}
if cli_options.get("cacert", None) is not None:
Expand Down
125 changes: 108 additions & 17 deletions st2client/st2client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def __init__(
debug=False,
token=None,
api_key=None,
basic_auth=None,
):
# Get CLI options. If not given, then try to get it from the environment.
self.endpoints = dict()
Expand Down Expand Up @@ -129,115 +130,195 @@ def __init__(

self.api_key = api_key

if basic_auth:
# NOTE: We assume username can't contain colons
if len(basic_auth.split(":", 1)) != 2:
raise ValueError(
"basic_auth config options needs to be in the "
"username:password notation"
)

self.basic_auth = tuple(basic_auth.split(":", 1))
else:
self.basic_auth = None

# Instantiate resource managers and assign appropriate API endpoint.
self.managers = dict()
self.managers["Token"] = ResourceManager(
models.Token, self.endpoints["auth"], cacert=self.cacert, debug=self.debug
models.Token,
self.endpoints["auth"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["RunnerType"] = ResourceManager(
models.RunnerType,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Action"] = ActionResourceManager(
models.Action, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Action,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["ActionAlias"] = ActionAliasResourceManager(
models.ActionAlias,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["ActionAliasExecution"] = ActionAliasExecutionManager(
models.ActionAliasExecution,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["ApiKey"] = ResourceManager(
models.ApiKey, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.ApiKey,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Config"] = ConfigManager(
models.Config, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Config,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["ConfigSchema"] = ResourceManager(
models.ConfigSchema,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Execution"] = ExecutionResourceManager(
models.Execution,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
# NOTE: LiveAction has been deprecated in favor of Execution. It will be left here for
# backward compatibility reasons until v3.2.0
self.managers["LiveAction"] = self.managers["Execution"]
self.managers["Inquiry"] = InquiryResourceManager(
models.Inquiry, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Inquiry,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Pack"] = PackResourceManager(
models.Pack, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Pack,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Policy"] = ResourceManager(
models.Policy, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Policy,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["PolicyType"] = ResourceManager(
models.PolicyType,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Rule"] = ResourceManager(
models.Rule, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Rule,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Sensor"] = ResourceManager(
models.Sensor, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Sensor,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["TriggerType"] = ResourceManager(
models.TriggerType,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Trigger"] = ResourceManager(
models.Trigger, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Trigger,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["TriggerInstance"] = TriggerInstanceResourceManager(
models.TriggerInstance,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["KeyValuePair"] = ResourceManager(
models.KeyValuePair,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Webhook"] = WebhookManager(
models.Webhook, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Webhook,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Timer"] = ResourceManager(
models.Timer, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Timer,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Trace"] = ResourceManager(
models.Trace, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Trace,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["RuleEnforcement"] = ResourceManager(
models.RuleEnforcement,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Stream"] = StreamManager(
self.endpoints["stream"], cacert=self.cacert, debug=self.debug
self.endpoints["stream"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["Workflow"] = WorkflowManager(
self.endpoints["api"], cacert=self.cacert, debug=self.debug
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)

# Service Registry
Expand All @@ -246,24 +327,31 @@ def __init__(
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)

self.managers["ServiceRegistryMembers"] = ServiceRegistryMembersManager(
models.ServiceRegistryMember,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)

# RBAC
self.managers["Role"] = ResourceManager(
models.Role, self.endpoints["api"], cacert=self.cacert, debug=self.debug
models.Role,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
self.managers["UserRoleAssignment"] = ResourceManager(
models.UserRoleAssignment,
self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)

@add_auth_token_to_kwargs_from_env
Expand All @@ -275,7 +363,10 @@ def get_user_info(self, **kwargs):
"""
url = "/user"
client = httpclient.HTTPClient(
root=self.endpoints["api"], cacert=self.cacert, debug=self.debug
root=self.endpoints["api"],
cacert=self.cacert,
debug=self.debug,
basic_auth=self.basic_auth,
)
response = client.get(url=url, **kwargs)

Expand Down
5 changes: 5 additions & 0 deletions st2client/st2client/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"username": {"type": "string", "default": None},
"password": {"type": "string", "default": None},
"api_key": {"type": "string", "default": None},
"basic_auth": {
# Basic auth credentials in username:password notation
"type": "string",
"default": None,
},
},
"api": {"url": {"type": "string", "default": None}},
"auth": {"url": {"type": "string", "default": None}},
Expand Down
Loading