Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
5f75ecf
adjusting sso creds callback :)
pimguilherme Mar 28, 2022
dc0f3b3
sso/saml implementation (wip)
pimguilherme Jul 2, 2022
8886beb
remving unecessary callback url in db
pimguilherme Apr 20, 2022
c70dd3c
fixing tests in debug mode with fake responses
pimguilherme Apr 20, 2022
09d3cda
adding type hinting and a sso response class
pimguilherme Jul 14, 2022
c9d3f43
adding role handling to proxyauth
pimguilherme Jul 14, 2022
baed32b
adding some tests :)
pimguilherme Jul 15, 2022
c4130e7
finalizing sso controller cli/web tests
pimguilherme Jul 18, 2022
ef66754
finalizing sso controller cli/web tests
pimguilherme Jul 18, 2022
3894251
finalizing sso controller cli/web tests
pimguilherme Jul 18, 2022
9ae0bcc
finalizing tests
pimguilherme Jul 19, 2022
bba0340
adding extra test :)
pimguilherme Jul 19, 2022
ea75281
fixing lint :)
pimguilherme Jul 19, 2022
cbf99ad
removing role mapping and using group mapping instead
pimguilherme Jul 19, 2022
aff1570
treating comparison
pimguilherme Jul 20, 2022
f494680
fixing lint :)
pimguilherme Jul 20, 2022
d967197
adding requirement
pimguilherme Jul 20, 2022
52851db
adding changelog
pimguilherme Jul 20, 2022
df972ec
adding saml to test
pimguilherme Jul 20, 2022
b083728
adding some rbac-related tests
pimguilherme Jul 22, 2022
e845059
Merge branch 'master' into feat/saml
pimguilherme Jul 22, 2022
982daa0
adjusting some tests
pimguilherme Jul 22, 2022
96834e8
adjusting tests
pimguilherme Jul 22, 2022
33b5abf
adjusting input parameters to proxy
pimguilherme Jul 22, 2022
157ad4e
adding proxy handler tests
pimguilherme Jul 25, 2022
2d42595
formatting and adding some tests :)
pimguilherme Jul 26, 2022
af0f694
minor formatting changes
pimguilherme Jul 26, 2022
f2cdaee
removing unused method and adding tests
pimguilherme Jul 26, 2022
016a94b
Merge branch 'master' into feat/saml
pimguilherme Jul 26, 2022
b694975
Merge branch 'feat/saml' of github.com:pimguilherme/st2 into feat/saml
pimguilherme Jul 26, 2022
cb013f7
fixing lint :)
pimguilherme Jul 26, 2022
88903fe
adjusting requirements
pimguilherme Jul 26, 2022
b7b5a48
fixing python 3.6 tets
pimguilherme Jul 26, 2022
97c18e8
fixing tests
pimguilherme Jul 26, 2022
0b9e389
trigger ci
pimguilherme Jul 26, 2022
7f1af01
adding sso port parameters to CLI
pimguilherme Sep 19, 2022
78b5427
adjusting timeout
pimguilherme Sep 19, 2022
f22dad5
removing debug
pimguilherme Sep 19, 2022
7cc4bc9
updating crypto :)
pimguilherme Sep 19, 2022
6579e8b
adding test for sso login via cli
pimguilherme Sep 19, 2022
014b249
final adjustment
pimguilherme Sep 19, 2022
a511d3b
adding browser launching config for sso
pimguilherme Sep 19, 2022
e9adafa
treating keyboard interrupt on sso flow
pimguilherme Sep 19, 2022
7f4a017
Merge branch 'master' into feat/saml
pimguilherme Sep 20, 2022
331e737
fixing lint
pimguilherme Sep 20, 2022
7ffbda5
fixing lint
pimguilherme Sep 21, 2022
eb17b7d
regenerating openapi spec
pimguilherme Sep 21, 2022
e01b7f3
fixing tests
pimguilherme Sep 21, 2022
79f7ab8
adjusting importing dependencies
pimguilherme Sep 21, 2022
9cef2aa
adding st2client build to ignore
pimguilherme Sep 21, 2022
1e8bc25
Merge branch 'master' into feat/saml
pimguilherme Sep 21, 2022
2657ac7
fixing lint
pimguilherme Sep 21, 2022
81d270a
Merge branch 'feat/saml' of ssh://github.com/pimguilherme/st2 into fe…
pimguilherme Sep 21, 2022
d453d28
removing st2client bson dependency
pimguilherme Sep 22, 2022
30c7c65
Merge remote-tracking branch 'origin/master' into feat/saml
pimguilherme Oct 6, 2022
dc0cc5f
retrigger cis
pimguilherme Oct 7, 2022
465afcd
Merge branch 'master' into feat/saml
pimguilherme Oct 19, 2022
66cd860
Merge branch 'master' into feat/saml
pimguilherme Nov 14, 2022
d53429a
Merge branch 'master' into feat/saml
pimguilherme Feb 24, 2023
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ virtualenv-components
virtualenv-components-osx
.venv-st2devbox

# st2client build files
st2client/build/*

# generated travis conf
conf/st2.travis.conf
# generated GitHub Actions conf
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ in development

Added
~~~~~

* Revised support for SSO backends + SAML2 included by default #5664

SAML backend on a separate repository, included as a dependecy , https://github.com/StackStorm/st2-auth-backend-sso-saml2

RBAC support also baked into SSO backends

Contributed by @pimguilherme

* Move `git clone` to `user_home/.st2packs` #5845

* Error on `st2ctl status` when running in Kubernetes. #5851
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ decorator==4.4.2
dnspython>=1.16.0,<2.0.0
eventlet==0.30.2
flex==6.14.1
git+https://github.com/pimguilherme/st2-auth-backend-sso-saml2.git@feat/saml#egg=st2-auth-backend-sso-saml2
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be changed as other PRs are merged

gitdb==4.0.2
gitpython==3.1.15
greenlet==1.0.0
Expand Down
2 changes: 2 additions & 0 deletions st2auth/in-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ passlib
pymongo
six
stevedore
# for SAML sso
git+https://github.com/pimguilherme/st2-auth-backend-sso-saml2.git@feat/saml#egg=st2-auth-backend-sso-saml2
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be changed as other PRs are merged

# For backward compatibility reasons, flat file backend is installed by default
st2-auth-backend-flat-file@ git+https://github.com/StackStorm/st2-auth-backend-flat-file.git@master
st2-auth-ldap@ git+https://github.com/StackStorm/st2-auth-ldap.git@master
Expand Down
1 change: 1 addition & 0 deletions st2auth/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# update the component requirements.txt
bcrypt==3.2.0
eventlet==0.30.2
git+https://github.com/pimguilherme/st2-auth-backend-sso-saml2.git@feat/saml#egg=st2-auth-backend-sso-saml2
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we must rememberd to change this to the default repository after merging the other repo!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be changed as other PRs are merge

gunicorn==20.1.0
oslo.config>=1.12.1,<1.13
passlib==1.7.4
Expand Down
230 changes: 214 additions & 16 deletions st2auth/st2auth/controllers/v1/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,30 @@

import datetime
import json
from uuid import uuid4

from oslo_config import cfg
from six.moves import http_client
from six.moves import urllib
from st2common.router import GenericRequestParam

import st2auth.handlers as handlers

from st2auth import sso as st2auth_sso
from st2auth.sso.base import BaseSingleSignOnBackendResponse
from st2common.exceptions import auth as auth_exc
from st2common import log as logging
from st2common import router

from st2common.models.db.auth import SSORequestDB
from st2common.services.access import (
create_cli_sso_request,
create_web_sso_request,
get_sso_request_by_request_id,
)
from st2common.exceptions.auth import SSORequestNotFoundError
from st2common.util.crypto import read_crypto_key_from_dict, symmetric_encrypt
from st2common.util.date import get_datetime_utc_now
from st2common.util.jsonify import json_decode

LOG = logging.getLogger(__name__)
SSO_BACKEND = st2auth_sso.get_sso_backend()
Expand All @@ -35,25 +47,96 @@ class IdentityProviderCallbackController(object):
def __init__(self):
self.st2_auth_handler = handlers.ProxyAuthHandler()

# Validates the incoming SSO response by getting its ID, checking against
# the database for outstanding SSO requests and checking to see if they have already expired
def _validate_and_delete_sso_request(self, response):

# Grabs the ID from the SSO response based on the backend
request_id = SSO_BACKEND.get_request_id_from_response(response)
if request_id is None:
raise ValueError("Invalid request id coming from SAML response")

LOG.debug("Validating SSO request %s from received response!", request_id)

# Grabs the original SSO request based on the ID
original_sso_request = None
try:
original_sso_request = get_sso_request_by_request_id(request_id)
except SSORequestNotFoundError:
pass

if original_sso_request is None:
raise ValueError(
"This SSO request is invalid (it may have already been used)"
)

# Verifies if the request has expired already
LOG.info(
"Incoming SSO response matching request: %s, with expiry: %s",
original_sso_request.request_id,
original_sso_request.expiry,
)
if original_sso_request.expiry <= get_datetime_utc_now():
raise ValueError(
"The SSO request associated with this response has already expired!"
)

# All done, we should not need to use this again :)
LOG.debug(
"Deleting original SSO request from database with ID %s",
original_sso_request.id,
)
original_sso_request.delete()

return original_sso_request

def post(self, response, **kwargs):
try:

original_sso_request = self._validate_and_delete_sso_request(response)

# Obtain user details from the SSO response from the backend
verified_user = SSO_BACKEND.verify_response(response)
if not isinstance(verified_user, BaseSingleSignOnBackendResponse):
return process_failure_response(
http_client.INTERNAL_SERVER_ERROR,
"Unexpected SSO backend response type. Expected "
"BaseSingleSignOnBackendResponse instance!",
)

LOG.info(
"Authenticating SSO user [%s] with groups [%s]",
verified_user.username,
verified_user.groups,
)

st2_auth_token_create_request = {
"user": verified_user["username"],
"ttl": None,
}
st2_auth_token_create_request = GenericRequestParam(
ttl=None,
groups=verified_user.groups,
)

st2_auth_token = self.st2_auth_handler.handle_auth(
request=st2_auth_token_create_request,
remote_addr=verified_user["referer"],
remote_user=verified_user["username"],
remote_addr=verified_user.referer,
remote_user=verified_user.username,
headers={},
)

return process_successful_authn_response(
verified_user["referer"], st2_auth_token
)
# Depending on the type of SSO request we should handle the response differently
# ie WEB gets redirected and CLI gets an encrypted callback
if original_sso_request.type == SSORequestDB.Type.WEB:
return process_successful_sso_web_response(
verified_user.referer, st2_auth_token
)
elif original_sso_request.type == SSORequestDB.Type.CLI:
return process_successful_sso_cli_response(
verified_user.referer, original_sso_request.key, st2_auth_token
)
else:
raise NotImplementedError(
"Unexpected SSO request type [%s] -- I can deal with web and cli"
% original_sso_request.type
)
except NotImplementedError as e:
return process_failure_response(http_client.INTERNAL_SERVER_ERROR, e)
except auth_exc.SSOVerificationError as e:
Expand All @@ -63,14 +146,76 @@ def post(self, response, **kwargs):


class SingleSignOnRequestController(object):
def get(self, referer):
def _create_sso_request(self, handler, **kwargs):

request_id = "id_%s" % str(uuid4())
sso_request = handler(request_id=request_id, **kwargs)
LOG.debug(
"Created SSO request with request id %s and expiry %s and type %s",
request_id,
sso_request.expiry,
sso_request.type,
)
return sso_request

# web-intended SSO
def get_web(self, referer):
try:
sso_request = self._create_sso_request(create_web_sso_request)

response = router.Response(status=http_client.TEMPORARY_REDIRECT)
response.location = SSO_BACKEND.get_request_redirect_url(referer)
response.location = SSO_BACKEND.get_request_redirect_url(
sso_request.request_id, referer
)
return response
except NotImplementedError as e:
if sso_request:
sso_request.delete()
return process_failure_response(http_client.INTERNAL_SERVER_ERROR, e)
except Exception as e:
if sso_request:
sso_request.delete()
raise e

# cli-intended SSO
def post_cli(self, response):
sso_request = None
try:
key = getattr(response, "key", None)
callback_url = getattr(response, "callback_url", None)
# This is already checked at the API level, but aanyway..
if not key or not callback_url:
raise ValueError("Missing either key and/or callback_url!")

try:
read_crypto_key_from_dict(json_decode(key))
except Exception:
LOG.warn("Could not decode incoming SSO CLI request key")
raise ValueError(
"The provided key is invalid! It should be stackstorm-compatible AES key"
)

sso_request = self._create_sso_request(create_cli_sso_request, key=key)
response = router.Response(status=http_client.OK)
response.content_type = "application/json"
response.json = {
"sso_url": SSO_BACKEND.get_request_redirect_url(
sso_request.request_id, callback_url
),
# this is needed because the db doesnt save microseconds
# pylint: disable=E1101
"expiry": sso_request.expiry.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ "000+00:00",
}

return response
except NotImplementedError as e:
if sso_request:
sso_request.delete()
return process_failure_response(http_client.INTERNAL_SERVER_ERROR, e)
except Exception as e:
if sso_request:
sso_request.delete()
raise e


Expand All @@ -94,22 +239,52 @@ def get(self):
CALLBACK_SUCCESS_RESPONSE_BODY = """
<html>
<script>
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
var v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return v ? v[2] : null;
}

data = JSON.parse(window.localStorage.getItem('st2Session'));
data['token'] = JSON.parse(decodeURIComponent(getCookie('st2-auth-token')));
// This cookie should've been set by the sso module
tokenDetails = JSON.parse(decodeURIComponent(getCookie('st2-auth-token')));

// Defining what to be set
data = JSON.parse(window.localStorage.getItem('st2Session') || "{}");
data['token'] = tokenDetails

if (!data['server']) {
serverPrefix = location.protocol + '//' + location.host;
data['server'] = {
"api": `${serverPrefix}/api`,
"auth": `${serverPrefix}/auth`,
"stream": `${serverPrefix}/stream`,
"token": null
}
console.log("Configured default server endpoints to [%%s]", data['server'])
}

// Persising data
console.log("Setting credentials to persistent stores")
window.localStorage.setItem('st2Session', JSON.stringify(data));
window.localStorage.setItem('logged_in', { "loggedIn": true })
setCookie("auth-token", tokenDetails.token)

window.location.replace("%s");
</script>
</html>
"""


def process_successful_authn_response(referer, token):
token_json = {
def token_to_json(token):
return {
"id": str(token.id),
"user": token.user,
"token": token.token,
Expand All @@ -118,6 +293,29 @@ def process_successful_authn_response(referer, token):
"metadata": {},
}


def process_successful_sso_cli_response(callback_url, key, token):
token_json = token_to_json(token)

aes_key = read_crypto_key_from_dict(json_decode(key))
encrypted_token = symmetric_encrypt(aes_key, json.dumps(token_json))

LOG.debug(
"Redirecting successfuly SSO CLI login to url [%s] "
"with extra parameters for the encrypted token",
callback_url,
)

# Response back to the browser has all the data in the query string, in an encrypted formta :)
resp = router.Response(status=http_client.FOUND)
resp.location = "%s?response=%s" % (callback_url, encrypted_token.decode("utf-8"))

return resp


def process_successful_sso_web_response(referer, token):
token_json = token_to_json(token)

body = CALLBACK_SUCCESS_RESPONSE_BODY % referer
resp = router.Response(body=body)
resp.headers["Content-Type"] = "text/html"
Expand Down
Loading