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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [2.4.0] Unreleased

### Added
- Federated login for multiple instances. Defaults to using Hashicorp Vault for secret management.

### Changed

Expand Down
38 changes: 38 additions & 0 deletions docs/authn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,41 @@ Authenticating
Once the client side authentication is setup, you should be able to trigger the authentication process with ``!session_start my_secret_word`` which will return a url to complete the login processes. This is how the page looks like:

.. image:: images/authentication_screen.jpg


Federated authentication
------------------------------------------------------------------------

This feature is experimental and may not be suitable for all enviornments that would like to use such a feature.

The federated authentication is intended for use when independent instances of StackStorm are run with an err-stackstorm instance attached to each one.

This creates a problem when users must identify per st2 instance if they want to run an action-alias. To enable scaling the login process, a remote authentication end point is added to err-stackstorm. The endpoint recieves notifications when a chat user successfully authenticates. The receiving err-stackstorm instance, will query a fixed path in the secret store to retreive a one time use token, that will give access to the credentials which will be used to authenticate against the local st2 api instance.

`unwrap_path` string can have a token `{federation_id}` that will be resolved to the id defined in the list of federated peers.

Currently, only vault is supported as a secret backend.

```
"secrets_backend": {
"vault": {
"url": "https://vault.example.com",
"auth": {
"token": "a.bdcdeed",
"approle": {
"role_id": "123456",
"secret_id": "456789"
}
},
"unwrap_path": "/kv/{federation_id}/err-stackstorm/remote-auth"
}
},
"federation":{
"id": "dc2",
"peers": {
"dc1": {
"api": "https://st2.dc1.example.com/"
}
}
},
```
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "err-stackstorm"
version = "2.3.2"
version = "2.4.0"
authors = [{ name="Err-stackstorm maintainers", email="nzlosh@yahoo.com" }]
keywords = [
"errbot",
Expand All @@ -28,6 +28,7 @@ dependencies = [
"requests>=2.20.0",
"sseclient",
"jsonschema",
"hvac",
]

[project.urls]
Expand Down
16 changes: 16 additions & 0 deletions src/err-stackstorm/errst2lib/chat_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,19 @@ def get_username(self, msg):
else:
name = f"@{username}"
return name

def normalise_user_id(self, user):
return str(user.aclattr)

def present_sessions(self, sessions):
res = "**Sessions**:\n"
for session in sessions:
userid = session.attributes()["UserID"]
if session.attributes().get("UserID") == "errbot%service":
res += "- {}\n".format(str(session))
else:
user = self.bot_plugin.build_identifier(
"<@{}>".format(session.attributes().get("UserID"))
)
res += "- `{}` `{}`\n".format(user.person, str(session))
return res
30 changes: 30 additions & 0 deletions src/err-stackstorm/errst2lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from errst2lib.authentication_handler import AuthHandlerFactory
from errst2lib.credentials_adapters import CredentialsFactory
from errst2lib.secrets_adapters import SecretsAdapterFactory

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -38,6 +39,9 @@ def setup(self, bot_conf):
self._configure_credentials(bot_conf)
self._configure_rbac_auth(bot_conf)
self._configure_stackstorm(bot_conf)
self._configure_secret_backend(bot_conf.STACKSTORM.get("secrets_backend", {}))
self._configure_federated_authentication(bot_conf.STACKSTORM.get("federation", {}))

self.oob_auth_url = bot_conf.STACKSTORM.get("oob_auth_url", "https://localhost:8888/")
self.timer_update = bot_conf.STACKSTORM.get("timer_update", 60)
self.verify_cert = bot_conf.STACKSTORM.get("verify_cert", True)
Expand Down Expand Up @@ -96,3 +100,29 @@ def _configure_credentials(self, bot_conf):
LOG.warning(
"Failed to find any valid st2 credentials for the bot, check configuration file."
)

def _configure_secret_backend(self, bot_conf):
"""
Get the secret definition.
"""
secret_backend = list(bot_conf.keys())
LOG.debug("Secrets backend %s", secret_backend)
if len(secret_backend) == 0:
LOG.warning(
"Unable to configure secrets backend. 'secrets_backend' "
"not found in config or no backend defined."
)
elif len(secret_backend) > 1:
LOG.warning(
"Only one secret backend definition is supported, "
"multiple secret backends have been detected."
)
else:
self.secret_backend = secret_backend[0]
self.secret_backend_cfg = bot_conf[self.secret_backend]

def _configure_federated_authentication(self, bot_conf):
"""
Configuration for Federated authentication.
"""
self.federation = bot_conf
168 changes: 168 additions & 0 deletions src/err-stackstorm/errst2lib/secrets_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# coding:utf-8
import abc
import logging

try:
import hvac

HAVE_HVAC = True
except ModuleNotFoundError:
HAVE_HVAC = False

LOG = logging.getLogger(__name__)


class AbstractSecretsAdapterFactory(metaclass=abc.ABCMeta):
@abc.abstractmethod
def instantiate(store_type):
raise NotImplementedError


class SecretsAdapterFactory(AbstractSecretsAdapterFactory):
@staticmethod
def instantiate(store_type):
LOG.debug("Establish connection with secret store service. '{}'".format(store_type))
return {"vault": VaultSecretsAdapter}.get(store_type, NoOpSecretsAdapter)


class AbstractSecretsAdapter(metaclass=abc.ABCMeta):
@abc.abstractmethod
def setup(self, cfg):
raise NotImplementedError

@abc.abstractmethod
def wrap_secret(self, secret, ttl):
raise NotImplementedError

@abc.abstractmethod
def unwrap(self, token):
raise NotImplementedError

@abc.abstractmethod
def read_kv(self, path, mount_point):
raise NotImplementedError

@abc.abstractmethod
def write_kv(self, k, v, mount_point):
raise NotImplementedError

@abc.abstractmethod
def renew_token(self):
raise NotImplementedError

@abc.abstractmethod
def teardown(self):
raise NotImplementedError


class NoOpSecretsAdapter(AbstractSecretsAdapter):
"""
No Op Secret Adapater does nothing.
"""

def setup(self):
pass

def renew_token(self):
pass

def wrap_secret(self, secret, ttl):
pass

def unwrap(self, token):
pass

def read_kv(self, path, mount_point):
pass

def write_kv(self, k, v, mount_point):
pass

def teardown(self):
pass


class VaultSecretsAdapter(AbstractSecretsAdapter):
"""
Vault secrets store. kv read/write/list and wrap/unwrap.
"""

def __init__(self, cfg):
self.client = None
self.unwrap_path = None
self.cfg = cfg

def setup(self):
if not HAVE_HVAC:
LOG.warning(
"hvac module could not be found. The Vault Secrets "
"adapter can't function without it."
)
return
self.client = hvac.Client(url=self.cfg["url"])
self.unwrap_path = self.cfg["unwrap_path"]

# supported credentials are token or approle.
token = self.cfg.get("auth", {}).get("token")
if "token" in self.cfg["auth"]:
LOG.debug("Vault authentication using Token")
self.client.token = self.cfg["auth"]["token"]
elif "approle" in self.cfg["auth"]:
LOG.debug("Vault authentication using AppRole")
self.client.auth.approle.login(
role_id=self.cfg["auth"]["approle"].get("role_id"),
secret_id=self.cfg["auth"]["approle"].get("secret_id"),
)
else:
raise RuntimeError(
"Vault Secret backend only supports token or approle authentication."
)
LOG.info("Vault token is authenticated = %s", self.client.is_authenticated())

def renew_token(self, threshold=3600):
"""
threshold: the minimum number of seconds remaining to request a token.
"""
# to do, add threshold check
if True:
self.teardown()
self.setup()

def get_wrap_path(self):
return self.unwrap_path

def wrap_secret(self, secret, ttl=30):
"""
Given a secret and the path
"""
return self.client.sys.wrap(payload=secret, ttl=ttl)

def unwrap(self, token):
"""
Given a chat userid, fetch an unwrap token from unwrap_path and perform the unwrap procedure
to return the secret contents.
"""
return self.client.sys.unwrap(token)

def read_kv(self, path, mount_point="kv"):
"""
Read a vault secret from kv path.
"""
return self.client.secrets.kv.v1.read_secret(
path=path,
mount_point=mount_point,
)["data"]

def write_kv(self, k, v, mount_point="kv"):
"""
Write a vault secret to the kv path.
"""
self.client.secrets.kv.v1.create_or_update_secret(k, secret=v, mount_point="kv")

def teardown(self):
"""
Close connection with secrets backend.
"""
if self.client:
self.client.logout()
self.client = None
2 changes: 1 addition & 1 deletion src/err-stackstorm/errst2lib/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def __init__(self, user_id, user_secret, session_ttl=3600):
self.bot_secret = None
self.user_id = user_id
self._is_sealed = True
self.session_id = uuid.uuid4()
self.session_id = str(uuid.uuid4())
self.create_date = int(dt.now().timestamp())
self.modified_date = self.create_date
self.ttl_in_seconds = session_ttl
Expand Down
11 changes: 10 additions & 1 deletion src/err-stackstorm/errst2lib/store_adapters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# coding:utf-8
import abc
import hashlib
import logging

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -115,8 +116,16 @@ def put_by_id(self, session_id, session):
self.id_to_user_map[session_id] = session.user_id

def get_by_uuid(self, session_id):
"""
Return a session given its UUID.
Return False if the session id is not found.
"""
user_id = self.id_to_user_map.get(session_id, False)
if user_id is False:
LOG.debug("Error: Session id '{}' doesn't exist in user mapping.".format(session_id))

session = self.memory.get(user_id, False)
if session is False:
LOG.debug("Error: Session id '{}' points to a missing session.".format(""))
LOG.debug("Error: User id '{}' doesn't exist.".format(user_id))

return session
2 changes: 1 addition & 1 deletion src/err-stackstorm/errst2lib/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ERR_STACKSTORM_VERSION = "2.3.2"
ERR_STACKSTORM_VERSION = "2.4.0"
2 changes: 1 addition & 1 deletion src/err-stackstorm/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.3/semantic.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js"></script>
<script>
console.log("err-stackstorm v2.3.2")
console.log("err-stackstorm v2.4.0")
function get_uuid_from_querystring() {
uuid = undefined;
params = window.location.href.split("?");
Expand Down
Loading
Loading