From 5f75ecf385c3c6ca93d4e226ba98305225fef029 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 28 Mar 2022 11:34:24 -0300 Subject: [PATCH 01/49] adjusting sso creds callback :) --- st2auth/st2auth/controllers/v1/sso.py | 34 +++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index ef1096462c..4f2a9e4287 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -94,14 +94,44 @@ def get(self): CALLBACK_SUCCESS_RESPONSE_BODY = """ From dc0f3b322804334c247a4b35915cffdc335d27db Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Sat, 2 Jul 2022 16:17:51 -0300 Subject: [PATCH 02/49] sso/saml implementation (wip) --- st2auth/st2auth/controllers/v1/sso.py | 126 ++++- st2client/st2client/client.py | 11 +- st2client/st2client/commands/auth.py | 110 +++-- st2client/st2client/models/core.py | 50 +- st2client/st2client/utils/crypto.py | 485 +++++++++++++++++++ st2client/st2client/utils/sso_interceptor.py | 163 +++++++ st2client/tests/unit/test_auth.py | 34 ++ st2common/st2common/exceptions/auth.py | 4 + st2common/st2common/models/db/auth.py | 30 +- st2common/st2common/openapi.yaml | 31 +- st2common/st2common/openapi.yaml.j2 | 29 +- st2common/st2common/persistence/auth.py | 42 +- st2common/st2common/services/access.py | 72 ++- st2common/st2common/util/crypto.py | 28 +- 14 files changed, 1144 insertions(+), 71 deletions(-) create mode 100644 st2client/st2client/utils/crypto.py create mode 100644 st2client/st2client/utils/sso_interceptor.py diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index 4f2a9e4287..e07eb5904b 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -14,6 +14,8 @@ import datetime import json +from subprocess import call +from uuid import uuid4 from oslo_config import cfg from six.moves import http_client @@ -25,7 +27,12 @@ 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() @@ -35,8 +42,44 @@ 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) st2_auth_token_create_request = { @@ -50,10 +93,17 @@ def post(self, response, **kwargs): 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: @@ -63,10 +113,48 @@ 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: + return process_failure_response(http_client.INTERNAL_SERVER_ERROR, e) + except Exception as e: + raise e + + # cli-intended SSO + def post_cli(self, response): + try: + key = getattr(response, 'key', None) + callback_url = getattr(response, 'callback_url', None) + if not key or not callback_url: + raise ValueError("Missing either key or callback_url!") + + try: + aes_key = read_crypto_key_from_dict(json_decode(key)) + except Exception: + LOG.warn("Could not decode incoming SSO CLI request key") + raise + + sso_request = self._create_sso_request(create_cli_sso_request, key=key, callback_url=callback_url) + response = router.Response(status=http_client.OK) + response.json = { + "sso_url": SSO_BACKEND.get_request_redirect_url(sso_request.request_id, callback_url), + "expiry": sso_request.expiry + } + return response except NotImplementedError as e: return process_failure_response(http_client.INTERNAL_SERVER_ERROR, e) @@ -137,9 +225,8 @@ def get(self): """ - -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, @@ -148,6 +235,25 @@ 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" diff --git a/st2client/st2client/client.py b/st2client/st2client/client.py index ec2878758d..cdc7cd0e35 100644 --- a/st2client/st2client/client.py +++ b/st2client/st2client/client.py @@ -23,7 +23,7 @@ from st2client import models from st2client.utils import httpclient -from st2client.models.core import ResourceManager +from st2client.models.core import ResourceManager, TokenResourceManager from st2client.models.core import ActionAliasResourceManager from st2client.models.core import ActionAliasExecutionManager from st2client.models.core import ActionResourceManager @@ -37,6 +37,7 @@ from st2client.models.core import WorkflowManager from st2client.models.core import ServiceRegistryGroupsManager from st2client.models.core import ServiceRegistryMembersManager +from st2client.models.core import TokenResourceManager from st2client.models.core import add_auth_token_to_kwargs_from_env @@ -144,10 +145,10 @@ def __init__( # Instantiate resource managers and assign appropriate API endpoint. self.managers = dict() - self.managers["Token"] = ResourceManager( - models.Token, - self.endpoints["auth"], - cacert=self.cacert, + self.managers["Token"] = TokenResourceManager( + models.Token, + self.endpoints["auth"], + cacert=self.cacert, debug=self.debug, basic_auth=self.basic_auth, ) diff --git a/st2client/st2client/commands/auth.py b/st2client/st2client/commands/auth.py index 71437a1751..8c1cbe19b5 100644 --- a/st2client/st2client/commands/auth.py +++ b/st2client/st2client/commands/auth.py @@ -19,9 +19,10 @@ import json import logging import os - +import dateutil import requests import six +from dateutil import tz from six.moves.configparser import ConfigParser from six.moves import http_client @@ -32,10 +33,13 @@ from st2client.commands.noop import NoopCommand from st2client.exceptions.operations import OperationFailureException from st2client.formatters import table +from st2client.utils.date import format_isodate_for_user_timezone LOG = logging.getLogger(__name__) +class MissingUserNameException(Exception): + pass class TokenCreateCommand(resource.ResourceCommand): @@ -118,8 +122,16 @@ def __init__(self, resource, *args, **kwargs): **kwargs, ) - self.parser.add_argument("username", help="Name of the user to authenticate.") + self.parser.add_argument("username", nargs='?', default=None, help="Name of the user to authenticate (not needed if --sso is used).") + self.parser.add_argument( + "-s", + "--sso", + dest="sso", + action='store_true', + help="Whether to use SSO authentication or not. " + "If chosen, bypasses username/password.", + ) self.parser.add_argument( "-p", "--password", @@ -143,13 +155,11 @@ def __init__(self, resource, *args, **kwargs): default=False, dest="write_password", help="Write the password in plain text to the config file " - "(default is to omit it)", + "(only applicable to username/password login, and default is to omit it)", ) def run(self, args, **kwargs): - if not args.password: - args.password = getpass.getpass() instance = self.resource(ttl=args.ttl) if args.ttl else self.resource() cli = BaseCLIApp() @@ -161,11 +171,33 @@ def run(self, args, **kwargs): # config file not found in args or in env, defaulting config_file = config_parser.ST2_CONFIG_PATH - # Retrieve token - manager = self.manager.create( - instance, auth=(args.username, args.password), **kwargs - ) - cli._cache_auth_token(token_obj=manager) + # Retrieve token based on whether we're using SSO or username/password login :) + if args.sso: + LOG.debug("Logging in with SSO") + # Retrieve token from SSO backend + sso_proxy = self.manager.create_sso_request(**kwargs) + + print("Please finish your SSO login by visiting: %s" % (sso_proxy.get_proxy_url())) + token = self.manager.wait_for_sso_token(sso_proxy) + + # Defaults to username/password if not SSO + else: + LOG.debug("Logging in with username/password") + if not args.username: + raise MissingUserNameException( + "Username expected when not using SSO login" + ) + + if not args.password: + args.password = getpass.getpass() + + # Retrieve token from username/password auth api + token = self.manager.create( + instance, auth=(args.username, args.password), **kwargs + ) + + + cli._cache_auth_token(token_obj=token) # Update existing configuration with new credentials config = ConfigParser() @@ -175,8 +207,9 @@ def run(self, args, **kwargs): if not config.has_section("credentials"): config.add_section("credentials") - config.set("credentials", "username", args.username) - if args.write_password: + config.set("credentials", "username", token.user) + + if args.write_password and not args.sso: config.set("credentials", "password", args.password) else: # Remove any existing password from config @@ -189,37 +222,42 @@ def run(self, args, **kwargs): if not config_existed: os.chmod(config_file, 0o660) - return manager + return token def run_and_print(self, args, **kwargs): + try: - self.run(args, **kwargs) + token = self.run(args, **kwargs) + formatted_expiry = format_isodate_for_user_timezone(token.expiry) + print("Logged in as %s until %s" % (token.user, formatted_expiry)) + + if not args.write_password and not args.sso: + print("") + print( + "Note: You didn't use --write-password option so the password hasn't been " + "stored in the client config and you will need to login again after %s hours when " + "the auth token expires." % (formatted_expiry) + ) + print( + 'As an alternative, you can run st2 login command with the "--write-password" ' + "flag, but keep it mind this will cause it to store the password in plain-text " + "in the client config file (~/.st2/config)." + ) + except MissingUserNameException as e: + raise except Exception as e: if self.app.client.debug: raise - raise Exception( - "Failed to log in as %s: %s" % (args.username, six.text_type(e)) - ) - - print("Logged in as %s" % (args.username)) - - if not args.write_password: - # Note: Client can't depend and import from common so we need to hard-code this - # default value - token_expire_hours = 24 - - print("") - print( - "Note: You didn't use --write-password option so the password hasn't been " - "stored in the client config and you will need to login again in %s hours when " - "the auth token expires." % (token_expire_hours) - ) - print( - 'As an alternative, you can run st2 login command with the "--write-password" ' - "flag, but keep it mind this will cause it to store the password in plain-text " - "in the client config file (~/.st2/config)." - ) + if args.sso: + raise Exception( + "Could not perform SSO login: %s" % (six.text_type(e)) + ) + + else: + raise Exception( + "Failed to log in as %s: %s" % (args.username, six.text_type(e)) + ) class WhoamiCommand(resource.ResourceCommand): diff --git a/st2client/st2client/models/core.py b/st2client/st2client/models/core.py index 412abd99be..1f40baef95 100644 --- a/st2client/st2client/models/core.py +++ b/st2client/st2client/models/core.py @@ -28,8 +28,11 @@ from six.moves import http_client import requests -from st2client.utils import httpclient +from st2client.utils.crypto import AESKey + +from st2client.utils import httpclient +from st2client.utils import sso_interceptor LOG = logging.getLogger(__name__) @@ -879,3 +882,48 @@ def list(self, group_id, **kwargs): result.append(item) return result + + + +class TokenResourceManager(ResourceManager): + + # This will spin up a local web server to mediate the requests from/to the sso + # endpoint, so that we can intercept the callback and token :) + # + # This function will not retrieve the token directly because we still need + # to print out some interaction with the user and that's best done elsewhere, so + # we'll just provide back the "interceptor" object, that is able to provide + # the URL and wait for the token to be ready :) + def create_sso_request(self, **kwargs) -> sso_interceptor.SSOInterceptorProxy: + url = "/sso/request/cli" + + key = AESKey.generate() + sso_proxy = sso_interceptor.SSOInterceptorProxy(key) + + response = self.client.post( + url, + { + "key": key.to_json(), + "callback_url": sso_proxy.get_callback_url() + }, + **kwargs + ) + if response.status_code != http_client.OK: + self.handle_error(response) + + json_response = response.json() + if not type(json_response) is dict: + raise ValueError("Expected response body from SSO CLI request, but couldn't find one :( ") + + sso_url = response.json().get("sso_url", None) + if sso_url is None: + raise ValueError("Expected SSO URL to be present in SSO login request response!") + + sso_proxy.set_sso_url(sso_url) + + LOG.debug("Received SSO URL with lenght %d", len(sso_url)) + return sso_proxy + + + def wait_for_sso_token(self, sso_proxy): + return self.resource.deserialize(sso_proxy.get_token()) \ No newline at end of file diff --git a/st2client/st2client/utils/crypto.py b/st2client/st2client/utils/crypto.py new file mode 100644 index 0000000000..d1d66d3581 --- /dev/null +++ b/st2client/st2client/utils/crypto.py @@ -0,0 +1,485 @@ +# Copyright 2020 The StackStorm Authors. +# Copyright 2019 Extreme Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for handling symmetric encryption and decryption of short text values (mostly used for +encrypted datastore values aka secrets). + +NOTE: In the past, this module used and relied on keyczar, but since keyczar doesn't support +Python 3, we moved to cryptography library. + +symmetric_encrypt and symmetric_decrypt functions except values as returned by the AESKey.Encrypt() +and AESKey.Decrypt() methods in keyczar. Those functions follow the same approach (AES in CBC mode +with SHA1 HMAC signature) as keyczar methods, but they use and rely on primitives and methods from +the cryptography library. + +This was done to make the keyczar -> cryptography migration fully backward compatible. + +Eventually, we should move to Fernet (https://cryptography.io/en/latest/fernet/) recipe for +symmetric encryption / decryption, because it offers more robustness and safer defaults (SHA256 +instead of SHA1, etc.). +""" + +from __future__ import absolute_import + +import os +import binascii +import base64 + +import json + +from hashlib import sha1 + +import six + +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives.ciphers import modes +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hmac +from cryptography.hazmat.backends import default_backend + +__all__ = [ + "KEYCZAR_HEADER_SIZE", + "KEYCZAR_AES_BLOCK_SIZE", + "KEYCZAR_HLEN", + "read_crypto_key", + "symmetric_encrypt", + "symmetric_decrypt", + "cryptography_symmetric_encrypt", + "cryptography_symmetric_decrypt", + # NOTE: Keyczar functions are here for testing reasons - they are only used by tests + "keyczar_symmetric_encrypt", + "keyczar_symmetric_decrypt", + "AESKey", +] + +# Keyczar related constants +KEYCZAR_HEADER_SIZE = 5 +KEYCZAR_AES_BLOCK_SIZE = 16 +KEYCZAR_HLEN = sha1().digest_size + +# Minimum key size which can be used for symmetric crypto +MINIMUM_AES_KEY_SIZE = 128 + +DEFAULT_AES_KEY_SIZE = 256 + +if DEFAULT_AES_KEY_SIZE < MINIMUM_AES_KEY_SIZE: + raise ValueError( + 'AES key size "%s" is smaller than minimun key size "%s".' + % (DEFAULT_AES_KEY_SIZE, MINIMUM_AES_KEY_SIZE) + ) + + +class AESKey(object): + """ + Class representing AES key object. + """ + + aes_key_string = None + hmac_key_string = None + hmac_key_size = None + mode = None + size = None + + def __init__( + self, + aes_key_string, + hmac_key_string, + hmac_key_size, + mode="CBC", + size=DEFAULT_AES_KEY_SIZE, + ): + if mode not in ["CBC"]: + raise ValueError("Unsupported mode: %s" % (mode)) + + if size < MINIMUM_AES_KEY_SIZE: + raise ValueError("Unsafe key size: %s" % (size)) + + self.aes_key_string = aes_key_string + self.hmac_key_string = hmac_key_string + self.hmac_key_size = int(hmac_key_size) + self.mode = mode.upper() + self.size = int(size) + + # We also store bytes version of the key since bytes are needed by encrypt and decrypt + # methods + self.hmac_key_bytes = Base64WSDecode(self.hmac_key_string) + self.aes_key_bytes = Base64WSDecode(self.aes_key_string) + + @classmethod + def generate(self, key_size=DEFAULT_AES_KEY_SIZE): + """ + Generate a new AES key with the corresponding HMAC key. + + :rtype: :class:`AESKey` + """ + if key_size < MINIMUM_AES_KEY_SIZE: + raise ValueError("Unsafe key size: %s" % (key_size)) + + aes_key_bytes = os.urandom(int(key_size / 8)) + aes_key_string = Base64WSEncode(aes_key_bytes) + + hmac_key_bytes = os.urandom(int(key_size / 8)) + hmac_key_string = Base64WSEncode(hmac_key_bytes) + + return AESKey( + aes_key_string=aes_key_string, + hmac_key_string=hmac_key_string, + hmac_key_size=key_size, + mode="CBC", + size=key_size, + ) + + def to_json(self): + """ + Return JSON representation of this key which is fully compatible with keyczar JSON key + file format. + + :rtype: ``str`` + """ + data = { + "hmacKey": { + "hmacKeyString": self.hmac_key_string, + "size": self.hmac_key_size, + }, + "aesKeyString": self.aes_key_string, + "mode": self.mode.upper(), + "size": int(self.size), + } + return json.dumps(data) + + def __repr__(self): + return "" % ( + self.hmac_key_size, + self.mode, + self.size, + ) + + +def read_crypto_key(key_path): + """ + Read crypto key from keyczar JSON key file format and return parsed AESKey object. + + :param key_path: Absolute path to file containing crypto key in Keyczar JSON format. + :type key_path: ``str`` + + :rtype: :class:`AESKey` + """ + with open(key_path, "r") as fp: + content = fp.read() + + content = json.loads(content) + + try: + aes_key = AESKey( + aes_key_string=content["aesKeyString"], + hmac_key_string=content["hmacKey"]["hmacKeyString"], + hmac_key_size=content["hmacKey"]["size"], + mode=content["mode"].upper(), + size=content["size"], + ) + except KeyError as e: + msg = 'Invalid or malformed key file "%s": %s' % (key_path, six.text_type(e)) + raise KeyError(msg) + + return aes_key + + +def symmetric_encrypt(encrypt_key, plaintext): + return cryptography_symmetric_encrypt(encrypt_key=encrypt_key, plaintext=plaintext) + + +def symmetric_decrypt(decrypt_key, ciphertext): + return cryptography_symmetric_decrypt( + decrypt_key=decrypt_key, ciphertext=ciphertext + ) + + +def cryptography_symmetric_encrypt(encrypt_key, plaintext): + """ + Encrypt the provided plaintext using AES encryption. + + NOTE 1: This function return a string which is fully compatible with Keyczar.Encrypt() method. + + NOTE 2: This function is loosely based on keyczar AESKey.Encrypt() (Apache 2.0 license). + + The final encrypted string value consists of: + + [message bytes][HMAC signature bytes for the message] where message consists of + [keyczar header plaintext][IV bytes][ciphertext bytes] + + NOTE: Header itself is unused, but it's added so the format is compatible with keyczar format. + + """ + if not isinstance(encrypt_key, AESKey): + raise TypeError( + "Encrypted key needs to be an AESkey class instance" + f" (was {type(encrypt_key)})." + ) + if not isinstance(plaintext, (six.text_type, six.string_types, six.binary_type)): + raise TypeError( + "Plaintext needs to either be a string/unicode or bytes" + f" (was {type(plaintext)})." + ) + + aes_key_bytes = encrypt_key.aes_key_bytes + hmac_key_bytes = encrypt_key.hmac_key_bytes + + if not isinstance(aes_key_bytes, six.binary_type): + raise TypeError(f"AESKey is not bytes (it is {type(aes_key_bytes)}).") + if not isinstance(hmac_key_bytes, six.binary_type): + raise TypeError(f"HMACKey is not bytes (it is {type(hmac_key_bytes)}).") + + if isinstance(plaintext, (six.text_type, six.string_types)): + # Convert data to bytes + data = plaintext.encode("utf-8") + else: + data = plaintext + + # Pad data + data = pkcs5_pad(data) + + # Generate IV + iv_bytes = os.urandom(KEYCZAR_AES_BLOCK_SIZE) + + backend = default_backend() + cipher = Cipher(algorithms.AES(aes_key_bytes), modes.CBC(iv_bytes), backend=backend) + encryptor = cipher.encryptor() + + # NOTE: We don't care about actual Keyczar header value, we only care about the length (5 + # bytes) so we simply add 5 0's + header_bytes = b"00000" + + ciphertext_bytes = encryptor.update(data) + encryptor.finalize() + msg_bytes = header_bytes + iv_bytes + ciphertext_bytes + + # Generate HMAC signature for the message (header + IV + ciphertext) + h = hmac.HMAC(hmac_key_bytes, hashes.SHA1(), backend=backend) + h.update(msg_bytes) + sig_bytes = h.finalize() + + result = msg_bytes + sig_bytes + + # Convert resulting byte string to hex notation ASCII string + result = binascii.hexlify(result).upper() + + return result + + +def cryptography_symmetric_decrypt(decrypt_key, ciphertext): + """ + Decrypt the provided ciphertext which has been encrypted using symmetric_encrypt() method (it + assumes input is in hex notation as returned by binascii.hexlify). + + NOTE 1: This function assumes ciphertext has been encrypted using symmetric AES crypto from + keyczar library. Underneath it uses crypto primitives from cryptography library which is Python + 3 compatible. + + NOTE 2: This function is loosely based on keyczar AESKey.Decrypt() (Apache 2.0 license). + """ + if not isinstance(decrypt_key, AESKey): + raise TypeError( + "Decrypted key needs to be an AESKey class instance" + f" (was {type(decrypt_key)})." + ) + if not isinstance(ciphertext, (six.text_type, six.string_types, six.binary_type)): + raise TypeError( + "Ciphertext needs to either be a string/unicode or bytes" + f" (was {type(ciphertext)})." + ) + aes_key_bytes = decrypt_key.aes_key_bytes + hmac_key_bytes = decrypt_key.hmac_key_bytes + + if not isinstance(aes_key_bytes, six.binary_type): + raise TypeError(f"AESKey is not bytes (it is {type(aes_key_bytes)}).") + if not isinstance(hmac_key_bytes, six.binary_type): + raise TypeError(f"HMACKey is not bytes (it is {type(hmac_key_bytes)}).") + + # Convert from hex notation ASCII string to bytes + ciphertext = binascii.unhexlify(ciphertext) + + data_bytes = ciphertext[KEYCZAR_HEADER_SIZE:] # remove header + + # Verify ciphertext contains IV + HMAC signature + if len(data_bytes) < (KEYCZAR_AES_BLOCK_SIZE + KEYCZAR_HLEN): + raise ValueError("Invalid or malformed ciphertext (too short)") + + iv_bytes = data_bytes[:KEYCZAR_AES_BLOCK_SIZE] # first block is IV + ciphertext_bytes = data_bytes[ + KEYCZAR_AES_BLOCK_SIZE:-KEYCZAR_HLEN + ] # strip IV and signature + signature_bytes = data_bytes[-KEYCZAR_HLEN:] # last 20 bytes are signature + + # Verify HMAC signature + backend = default_backend() + h = hmac.HMAC(hmac_key_bytes, hashes.SHA1(), backend=backend) + h.update(ciphertext[:-KEYCZAR_HLEN]) + h.verify(signature_bytes) + + # Decrypt ciphertext + cipher = Cipher(algorithms.AES(aes_key_bytes), modes.CBC(iv_bytes), backend=backend) + + decryptor = cipher.decryptor() + decrypted = decryptor.update(ciphertext_bytes) + decryptor.finalize() + + # Unpad + decrypted = pkcs5_unpad(decrypted) + return decrypted + + +### +# NOTE: Those methods below are deprecated and only used for testing purposes +## + + +def keyczar_symmetric_encrypt(encrypt_key, plaintext): + """ + Encrypt the given message using the encrypt_key. Returns a UTF-8 str + ready to be stored in database. Note that we convert the hex notation + to a ASCII notation to produce a UTF-8 friendly string. + + Also, this method will not return the same output on multiple invocations + of same method. The reason is that the Encrypt method uses a different + 'Initialization Vector' per run and the IV is part of the output. + + :param encrypt_key: Symmetric AES key to use for encryption. + :type encrypt_key: :class:`AESKey` + + :param plaintext: Plaintext / message to be encrypted. + :type plaintext: ``str`` + + :rtype: ``str`` + """ + from keyczar.keys import AesKey as KeyczarAesKey # pylint: disable=import-error + from keyczar.keys import HmacKey as KeyczarHmacKey # pylint: disable=import-error + from keyczar.keyinfo import GetMode # pylint: disable=import-error + + encrypt_key = KeyczarAesKey( + encrypt_key.aes_key_string, + KeyczarHmacKey(encrypt_key.hmac_key_string, encrypt_key.hmac_key_size), + encrypt_key.size, + GetMode(encrypt_key.mode), + ) + + return binascii.hexlify(encrypt_key.Encrypt(plaintext)).upper() + + +def keyczar_symmetric_decrypt(decrypt_key, ciphertext): + """ + Decrypt the given crypto text into plain text. Returns the original + string input. Note that we first convert the string to hex notation + and then decrypt. This is reverse of the encrypt operation. + + :param decrypt_key: Symmetric AES key to use for decryption. + :type decrypt_key: :class:`keyczar.keys.AESKey` + + :param crypto: Crypto text to be decrypted. + :type crypto: ``str`` + + :rtype: ``str`` + """ + from keyczar.keys import AesKey as KeyczarAesKey # pylint: disable=import-error + from keyczar.keys import HmacKey as KeyczarHmacKey # pylint: disable=import-error + from keyczar.keyinfo import GetMode # pylint: disable=import-error + + decrypt_key = KeyczarAesKey( + decrypt_key.aes_key_string, + KeyczarHmacKey(decrypt_key.hmac_key_string, decrypt_key.hmac_key_size), + decrypt_key.size, + GetMode(decrypt_key.mode), + ) + + return decrypt_key.Decrypt(binascii.unhexlify(ciphertext)) + + +def pkcs5_pad(data): + """ + Pad data using PKCS5 + """ + pad = KEYCZAR_AES_BLOCK_SIZE - len(data) % KEYCZAR_AES_BLOCK_SIZE + data = data + pad * chr(pad).encode("utf-8") + return data + + +def pkcs5_unpad(data): + """ + Unpad data padded using PKCS5. + """ + if isinstance(data, six.binary_type): + # Make sure we are operating with a string type + data = data.decode("utf-8") + + pad = ord(data[-1]) + data = data[:-pad] + return data + + +def Base64WSEncode(s): + """ + Return Base64 web safe encoding of s. Suppress padding characters (=). + + Uses URL-safe alphabet: - replaces +, _ replaces /. Will convert s of type + unicode to string type first. + + @param s: string to encode as Base64 + @type s: string + + @return: Base64 representation of s. + @rtype: string + + NOTE: Taken from keyczar (Apache 2.0 license) + """ + if isinstance(s, six.text_type): + # Make sure input string is always converted to bytes (if not already) + s = s.encode("utf-8") + + return base64.urlsafe_b64encode(s).decode("utf-8").replace("=", "") + + +def Base64WSDecode(s): + """ + Return decoded version of given Base64 string. Ignore whitespace. + + Uses URL-safe alphabet: - replaces +, _ replaces /. Will convert s of type + unicode to string type first. + + @param s: Base64 string to decode + @type s: string + + @return: original string that was encoded as Base64 + @rtype: string + + @raise Base64DecodingError: If length of string (ignoring whitespace) is one + more than a multiple of four. + + NOTE: Taken from keyczar (Apache 2.0 license) + """ + s = "".join(s.splitlines()) + s = str(s.replace(" ", "")) # kill whitespace, make string (not unicode) + d = len(s) % 4 + + if d == 1: + raise ValueError("Base64 decoding errors") + elif d == 2: + s += "==" + elif d == 3: + s += "=" + + try: + return base64.urlsafe_b64decode(s) + except TypeError as e: + # Decoding raises TypeError if s contains invalid characters. + raise ValueError("Base64 decoding error: %s" % (six.text_type(e))) diff --git a/st2client/st2client/utils/sso_interceptor.py b/st2client/st2client/utils/sso_interceptor.py new file mode 100644 index 0000000000..efad506b4a --- /dev/null +++ b/st2client/st2client/utils/sso_interceptor.py @@ -0,0 +1,163 @@ +# Copyright 2020 The StackStorm Authors. +# Copyright 2019 Extreme Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import json +import logging +from multiprocessing.sharedctypes import Value +from threading import Thread +import time +from urllib.parse import urlparse, parse_qs +import uuid +from http.server import BaseHTTPRequestHandler, HTTPServer + +from st2client.utils.crypto import symmetric_decrypt + +LOG = logging.getLogger(__name__) + +# Implements a local HTTP server used to intercept calls from/to SSO endpoints :) +# via callback URLs +class SSOInterceptorProxy: + + thread = None + server = None + # Identifier to be used to access the SSO proxy (e.g. localhost:31283/) + url_id = uuid.uuid4() + # where should the proxy redirect to upon hitting it? + sso_url = None + # key that is used to decrypt the response + key = None + # token object to receive the token once it's avaiable! + token = None + + def __init__(self, key): + + self.server = HTTPServer(('localhost', 0), createSSOProxyHandler(self)) + self.key = key + + LOG.debug("Initialized SSO interceptor proxy at port %d and url id %s, SSO URL is still pending", self.server.server_port, self.url_id) + + self.thread = Thread(target=self.server.serve_forever) + self.thread.setDaemon(True) + self.thread.start() + + def set_sso_url(self, sso_url): + self.sso_url = sso_url + LOG.debug("SSO URL set to [%s]", sso_url) + + def get_proxy_url(self): + return "http://localhost:%d/%s" % (self.server.server_port, self.url_id) + + def get_callback_url(self): + return "http://localhost:%d/callback" % (self.server.server_port) + + def callback_received(self, token): + LOG.debug("Callback received and intercepted, token is provided :)") + self.token = token + + def get_token(self, timeout=90): + LOG.debug("Waiting for token to be received from SSO flow.. will timeout after [%s]s", timeout) + timeout_at = time.time() + timeout + while time.time() < timeout_at: + if self.token is not None: + return self.token + time.sleep(0.5) + + raise TimeoutError("Token was not received from SSO flow before the timeout of %ss"%timeout) + + + +def createSSOProxyHandler(interceptor: SSOInterceptorProxy): + class SSOProxyServer(BaseHTTPRequestHandler): + + def do_GET(self): + + o = urlparse(self.path) + qs = parse_qs(o.query) + + try: + + if o.path == "/callback": + self._handle_callbakc(qs.get("response", [None])[0]) + if o.path == "/success": + self._handle_success() + elif o.path == "/%s" % interceptor.url_id: + self._handle_sso_login() + else: + self._handle_unexpected_request() + + except ValueError as e: + self.send_error(400, explain="Invalid parameter: %s" % str(e)) + except Exception as e: + LOG.debug("Unexpected internal server error! %e", e) + self.send_error(500, explain="Unexpected error!" % str(e)) + + + # This request is not expected by the sso proxy + def _handle_unexpected_request(self): + self.send_error(404, explain="The selected URL does not exist!") + self.end_headers() + + # This request is to redirect the user to the proper sso place + # -- can only be achieve with the proper key :) + def _handle_sso_login(self): + LOG.debug("Intercepting SSO begin flow from the user") + self.send_response(307) + self.send_header("Location", interceptor.sso_url) + self.end_headers() + + # This request should have all the callback data we are expecting + # -- this means an encrypted key to be decrypted and used by the CLI :) + def _handle_callbakc(self, response): + LOG.debug("Intercepting SSO callback response!") + + if (response is None): + raise ValueError("Expected 'response' field with encrypted key in callback!") + + token = symmetric_decrypt(interceptor.key, response.encode('utf-8')) + try: + token_json = json.loads(token) + LOG.debug("Successful SSO login for user %s, redirecting to successful page!", token_json.get('user', None)) + except: + raise ValueError("Could not understand the incoming SSO callback response") + + interceptor.callback_received(token) + self.send_response(302) + self.send_header("Location", "/success") + self.end_headers() + + # self.wfile.close() + + def _handle_success(self): + self.send_response(200) + self.end_headers() + self.wfile.write(bytes(""" + SSO Login Successful + +
+
Successfully logged into StackStorm using SSO!
+
Please check your terminal
+
You may now close this page
+
+ + """ + ,"utf-8")) + + + def log_message(self, format, *args): + LOG.debug("%s " + format, "SSO Proxy: ", *args) + return + + return SSOProxyServer diff --git a/st2client/tests/unit/test_auth.py b/st2client/tests/unit/test_auth.py index e59b31dfaf..f82813331d 100644 --- a/st2client/tests/unit/test_auth.py +++ b/st2client/tests/unit/test_auth.py @@ -164,6 +164,40 @@ def runTest(self): os.path.isfile("%stoken-%s" % (self.DOTST2_PATH, expected_username)) ) +class TestLoginWithMissingUsername(TestLoginBase): + + CONFIG_FILE_NAME = "logintest.cfg" + + TOKEN = { + "user": "st2admin", + "token": "44583f15945b4095afbf57058535ca64", + "expiry": "2017-02-12T00:53:09.632783Z", + "id": "589e607532ed3535707f10eb", + "metadata": {}, + } + + @mock.patch.object( + requests, + "post", + mock.MagicMock(return_value=base.FakeResponse(json.dumps(TOKEN), 200, "OK")), + ) + def runTest(self): + """Test 'st2 login' functionality missing the username and should fail""" + + expected_username = self.TOKEN["user"] + args = [ + "--config", + self.CONFIG_FILE, + "login", + "--password", + "Password1!", + ] + + self.shell.run(args) + self.assertIn( + "Username expected when not using SSO login", self.stdout.getvalue() + ) + class TestLoginIntPwdAndConfig(TestLoginBase): diff --git a/st2common/st2common/exceptions/auth.py b/st2common/st2common/exceptions/auth.py index 5eab1915f5..07ed8c2cc6 100644 --- a/st2common/st2common/exceptions/auth.py +++ b/st2common/st2common/exceptions/auth.py @@ -31,12 +31,16 @@ "AmbiguousUserError", "NotServiceUserError", "SSOVerificationError", + "SSORequestNotFoundError" ] class TokenNotProvidedError(StackStormBaseException): pass +class SSORequestNotFoundError(StackStormBaseException): + pass + class TokenNotFoundError(StackStormDBObjectNotFoundError): pass diff --git a/st2common/st2common/models/db/auth.py b/st2common/st2common/models/db/auth.py index 2531ecb11a..c502c2737d 100644 --- a/st2common/st2common/models/db/auth.py +++ b/st2common/st2common/models/db/auth.py @@ -16,6 +16,7 @@ from __future__ import absolute_import import copy +from enum import Enum import mongoengine as me from st2common.constants.secrets import MASKED_ATTRIBUTE_VALUE @@ -25,7 +26,7 @@ from st2common.rbac.backends import get_rbac_backend from st2common.util import date as date_utils -__all__ = ["UserDB", "TokenDB", "ApiKeyDB"] +__all__ = ["UserDB", "TokenDB", "ApiKeyDB", "SSORequestDB"] class UserDB(stormbase.StormFoundationDB): @@ -84,6 +85,31 @@ class TokenDB(stormbase.StormFoundationDB): ) service = me.BooleanField(required=True, default=False) +class SSORequestDB(stormbase.StormFoundationDB): + + class Type(Enum): + CLI = 'cli' + WEB = 'web' + + """ + An entity representing a SSO request. + + Attribute: + request_id: Reference to the SSO request unique ID + expiry: Time at which this request expires. + type: What type of SSO request is this? web/cli + + -- cli -- + key: Symmetric key used to encrypt/decrypt contents from/to the CLI. + callback_url: what URL to be used as a callback endpoint + """ + + request_id = me.StringField(required=True) + key = me.StringField(required=False, unique=False) + expiry = me.DateTimeField(required=True) + type = me.EnumField(Type, required=True) + callback_url = me.StringField(required= False) + class ApiKeyDB(stormbase.StormFoundationDB, stormbase.UIDFieldMixin): """ @@ -127,4 +153,4 @@ def mask_secrets(self, value): return result -MODELS = [UserDB, TokenDB, ApiKeyDB] +MODELS = [UserDB, TokenDB, ApiKeyDB, SSORequestDB] diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index 107b97a1c2..2775dee140 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -4526,10 +4526,11 @@ paths: schema: $ref: '#/definitions/Error' security: [] - /auth/v1/sso/request: + + /auth/v1/sso/request/web: get: - operationId: st2auth.controllers.v1.sso:sso_request_controller.get - description: Redirects to the SSO Idp login page. + operationId: st2auth.controllers.v1.sso:sso_request_controller.get_web + description: Redirects to the SSO Idp login page from a user that's using the browser. parameters: - name: referer in: header @@ -4539,6 +4540,29 @@ paths: '307': description: Temporary redirect security: [] + + /auth/v1/sso/request/cli: + post: + operationId: st2auth.controllers.v1.sso:sso_request_controller.post_cli + description: Provides data for a CLI to handle SSO authentication internally. + parameters: + - name: response + in: body + description: SSO request with callback and key encryption + schema: + type: object + properties: + key: + type: string + description: The symmetric key to be used to encrypt contents of callback + callback_url: + type: string + description: What URL to be called back once the response from SSO is received + responses: + '200': + description: SSO request valid + security: [] + /auth/v1/sso/callback: post: operationId: st2auth.controllers.v1.sso:idp_callback_controller.post @@ -5372,6 +5396,7 @@ definitions: type: - object - array + ActionParametersSubSchema: type: object description: Input parameters for the action. diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index a54bb423f1..14a1a5dd77 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -4522,10 +4522,11 @@ paths: schema: $ref: '#/definitions/Error' security: [] - /auth/v1/sso/request: + + /auth/v1/sso/request/web: get: - operationId: st2auth.controllers.v1.sso:sso_request_controller.get - description: Redirects to the SSO Idp login page. + operationId: st2auth.controllers.v1.sso:sso_request_controller.get_web + description: Redirects to the SSO Idp login page from a user that's using the browser. parameters: - name: referer in: header @@ -4535,6 +4536,28 @@ paths: '307': description: Temporary redirect security: [] + + /auth/v1/sso/request/cli: + post: + operationId: st2auth.controllers.v1.sso:sso_request_controller.post_cli + description: Provides data for a CLI to handle SSO authentication internally. + parameters: + - name: response + in: body + description: SSO request with callback and key encryption + schema: + type: object + properties: + key: + type: string + description: The symmetric key to be used to encrypt contents of callback + callback_url: + type: string + description: What URL to be called back once the response from SSO is received + responses: + '200': + description: SSO request valid + security: [] /auth/v1/sso/callback: post: operationId: st2auth.controllers.v1.sso:idp_callback_controller.post diff --git a/st2common/st2common/persistence/auth.py b/st2common/st2common/persistence/auth.py index 51f0a59ea1..99f25154f1 100644 --- a/st2common/st2common/persistence/auth.py +++ b/st2common/st2common/persistence/auth.py @@ -15,6 +15,7 @@ from __future__ import absolute_import from st2common.exceptions.auth import ( + SSORequestNotFoundError, TokenNotFoundError, ApiKeyNotFoundError, UserNotFoundError, @@ -22,7 +23,7 @@ NoNicknameOriginProvidedError, ) from st2common.models.db import MongoDBAccess -from st2common.models.db.auth import UserDB, TokenDB, ApiKeyDB +from st2common.models.db.auth import SSORequestDB, UserDB, TokenDB, ApiKeyDB from st2common.persistence.base import Access from st2common.util import hash as hash_utils @@ -59,6 +60,45 @@ def _get_by_object(cls, object): return cls.get_by_name(name) + +class SSORequest(Access): + impl = MongoDBAccess(SSORequestDB) + + @classmethod + def _get_impl(cls): + return cls.impl + + @classmethod + def add_or_update(cls, model_object, publish=True, validate=True): + if not getattr(model_object, "request_id", None): + raise ValueError("SSO Request ID is not provided in the object.") + if not getattr(model_object, "type", None): + raise ValueError("SSO request type is not defined in the object") + if not getattr(model_object, "expiry", None): + raise ValueError("SSO request expiry is not provided in the object.") + return super(SSORequest, cls).add_or_update( + model_object, publish=publish, validate=validate + ) + + @classmethod + def get(cls, value): + result = cls.query(id=value).first() + + if not result: + raise SSORequestNotFoundError() + + return result + + @classmethod + def get_by_request_id(cls, value): + result = cls.query(request_id=value).first() + + if not result: + raise SSORequestNotFoundError() + + return result + + class Token(Access): impl = MongoDBAccess(TokenDB) diff --git a/st2common/st2common/services/access.py b/st2common/st2common/services/access.py index 9d88c39c42..56cab61a17 100644 --- a/st2common/st2common/services/access.py +++ b/st2common/st2common/services/access.py @@ -21,13 +21,13 @@ from st2common.util import isotime from st2common.util import date as date_utils -from st2common.exceptions.auth import TokenNotFoundError, UserNotFoundError +from st2common.exceptions.auth import SSORequestNotFoundError, TokenNotFoundError, UserNotFoundError from st2common.exceptions.auth import TTLTooLargeException -from st2common.models.db.auth import TokenDB, UserDB -from st2common.persistence.auth import Token, User +from st2common.models.db.auth import SSORequestDB, TokenDB, UserDB +from st2common.persistence.auth import SSORequest, Token, User from st2common import log as logging -__all__ = ["create_token", "delete_token"] +__all__ = ["create_token", "delete_token", "create_cli_sso_request", "create_web_sso_request", "get_sso_request_by_request_id", "delete_sso_request"] LOG = logging.getLogger(__name__) @@ -105,3 +105,67 @@ def delete_token(token): pass except Exception: raise + +def create_cli_sso_request(request_id, key, callback_url, ttl=120): + """ + :param request_id: ID of the SSO request that is being created (usually uuid format prepended by _) + :type request_id: ``str`` + + :param key: Symmetric key used to encrypt/decrypt the request between the CLI and the server + :type key: ``str`` + + :param callback_url: Where should the SSO authentication response be sent to after successful logins? + :type callback_url: ``str`` + + :param ttl: SSO request TTL (in seconds). + :type ttl: ``int`` + """ + + return _create_sso_request(request_id, ttl, SSORequestDB.Type.CLI, key=key, callback_url=callback_url) + +def create_web_sso_request(request_id, ttl=120): + """ + :param request_id: ID of the SSO request that is being created (usually uuid format prepended by _) + :type request_id: ``str`` + + :param ttl: SSO request TTL (in seconds). + :type ttl: ``int`` + """ + + return _create_sso_request(request_id, ttl, SSORequestDB.Type.WEB) + +def _create_sso_request(request_id, ttl, type, **kwargs) -> SSORequestDB: + + expiry = date_utils.get_datetime_utc_now() + datetime.timedelta(seconds=ttl) + + request = SSORequestDB( + request_id=request_id, + expiry=expiry, + type=type, + **kwargs + ) + SSORequest.add_or_update(request) + + expire_string = isotime.format(expiry, offset=False) + + LOG.audit( + 'Created SAML request with ID "%s" set to expire at "%s" of type "%s".' + % (request_id, expire_string, type) + ) + + return request + + +def get_sso_request_by_request_id(request_id) -> SSORequestDB: + request_db = SSORequest.get_by_request_id(request_id) + return request_db + + +def delete_sso_request(id): + try: + request_db = SSORequest.get(id) + return SSORequest.delete(request_db) + except SSORequestNotFoundError: + pass + except Exception: + raise diff --git a/st2common/st2common/util/crypto.py b/st2common/st2common/util/crypto.py index 0aea24763c..5cca2bec5c 100644 --- a/st2common/st2common/util/crypto.py +++ b/st2common/st2common/util/crypto.py @@ -184,16 +184,32 @@ def read_crypto_key(key_path): content = json_decode(content) + try: + return read_crypto_key_from_dict(content) + except KeyError as e: + msg = 'Invalid or malformed key file "%s": %s' % (key_path, six.text_type(e)) + raise KeyError(msg) + +def read_crypto_key_from_dict(key_dict): + """ + Read crypto key from provided Keyczar JSON-format dict and return parsed AESKey object. + + :param key_dict: A dictionary with a key in Keyczar format (same keys as the JSON). + :type key_dict: ``dict`` + + :rtype: :class:`AESKey` + """ + try: aes_key = AESKey( - aes_key_string=content["aesKeyString"], - hmac_key_string=content["hmacKey"]["hmacKeyString"], - hmac_key_size=content["hmacKey"]["size"], - mode=content["mode"].upper(), - size=content["size"], + aes_key_string=key_dict["aesKeyString"], + hmac_key_string=key_dict["hmacKey"]["hmacKeyString"], + hmac_key_size=key_dict["hmacKey"]["size"], + mode=key_dict["mode"].upper(), + size=key_dict["size"], ) except KeyError as e: - msg = 'Invalid or malformed key file "%s": %s' % (key_path, six.text_type(e)) + msg = 'Invalid or malformed AES key dictionary: %s' % (six.text_type(e)) raise KeyError(msg) return aes_key From 8886beb26bcd560950661b87914c11256f015aa7 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 20 Apr 2022 17:44:44 -0300 Subject: [PATCH 03/49] remving unecessary callback url in db --- st2auth/st2auth/controllers/v1/sso.py | 2 +- st2common/st2common/models/db/auth.py | 2 -- st2common/st2common/services/access.py | 7 ++----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index e07eb5904b..cb239867ce 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -148,7 +148,7 @@ def post_cli(self, response): LOG.warn("Could not decode incoming SSO CLI request key") raise - sso_request = self._create_sso_request(create_cli_sso_request, key=key, callback_url=callback_url) + sso_request = self._create_sso_request(create_cli_sso_request, key=key) response = router.Response(status=http_client.OK) response.json = { "sso_url": SSO_BACKEND.get_request_redirect_url(sso_request.request_id, callback_url), diff --git a/st2common/st2common/models/db/auth.py b/st2common/st2common/models/db/auth.py index c502c2737d..ee795954a1 100644 --- a/st2common/st2common/models/db/auth.py +++ b/st2common/st2common/models/db/auth.py @@ -101,14 +101,12 @@ class Type(Enum): -- cli -- key: Symmetric key used to encrypt/decrypt contents from/to the CLI. - callback_url: what URL to be used as a callback endpoint """ request_id = me.StringField(required=True) key = me.StringField(required=False, unique=False) expiry = me.DateTimeField(required=True) type = me.EnumField(Type, required=True) - callback_url = me.StringField(required= False) class ApiKeyDB(stormbase.StormFoundationDB, stormbase.UIDFieldMixin): diff --git a/st2common/st2common/services/access.py b/st2common/st2common/services/access.py index 56cab61a17..4f4d59473c 100644 --- a/st2common/st2common/services/access.py +++ b/st2common/st2common/services/access.py @@ -106,7 +106,7 @@ def delete_token(token): except Exception: raise -def create_cli_sso_request(request_id, key, callback_url, ttl=120): +def create_cli_sso_request(request_id, key, ttl=120): """ :param request_id: ID of the SSO request that is being created (usually uuid format prepended by _) :type request_id: ``str`` @@ -114,14 +114,11 @@ def create_cli_sso_request(request_id, key, callback_url, ttl=120): :param key: Symmetric key used to encrypt/decrypt the request between the CLI and the server :type key: ``str`` - :param callback_url: Where should the SSO authentication response be sent to after successful logins? - :type callback_url: ``str`` - :param ttl: SSO request TTL (in seconds). :type ttl: ``int`` """ - return _create_sso_request(request_id, ttl, SSORequestDB.Type.CLI, key=key, callback_url=callback_url) + return _create_sso_request(request_id, ttl, SSORequestDB.Type.CLI, key=key) def create_web_sso_request(request_id, ttl=120): """ From c70dd3c881fec9abe51ef21dc7b2165dcdefdf39 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 20 Apr 2022 17:44:56 -0300 Subject: [PATCH 04/49] fixing tests in debug mode with fake responses --- st2client/st2client/utils/httpclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/st2client/st2client/utils/httpclient.py b/st2client/st2client/utils/httpclient.py index ec7bebdf64..2759ba13b6 100644 --- a/st2client/st2client/utils/httpclient.py +++ b/st2client/st2client/utils/httpclient.py @@ -165,7 +165,8 @@ def delete(self, url, **kwargs): return response def _response_hook(self, response): - if self.debug: + # in case we're in testing, FakeResponse does not have a request parameter :/ + if self.debug and hasattr(response, "request"): # Log cURL request line curl_line = self._get_curl_line_for_request(request=response.request) print("# -------- begin %d request ----------" % id(self)) From 09d3cda4cfab7e20616e4f141c98a34e844dd212 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Thu, 14 Jul 2022 15:33:28 -0300 Subject: [PATCH 05/49] adding type hinting and a sso response class --- st2auth/st2auth/controllers/v1/sso.py | 13 ++++++----- st2auth/st2auth/sso/__init__.py | 5 +++-- st2auth/st2auth/sso/base.py | 31 ++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index cb239867ce..f597d11091 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -24,6 +24,7 @@ import st2auth.handlers as handlers from st2auth import sso as st2auth_sso +from st2auth.sso.base import BaseSingleSignOnBackend, BaseSingleSignOnBackendResponse from st2common.exceptions import auth as auth_exc from st2common import log as logging from st2common import router @@ -81,16 +82,18 @@ def post(self, response, **kwargs): # 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!") st2_auth_token_create_request = { - "user": verified_user["username"], + "user": verified_user.username, "ttl": None, } 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={}, ) @@ -98,10 +101,10 @@ def post(self, response, **kwargs): # 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 + 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) + 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: diff --git a/st2auth/st2auth/sso/__init__.py b/st2auth/st2auth/sso/__init__.py index b6d0df930a..62403e2106 100644 --- a/st2auth/st2auth/sso/__init__.py +++ b/st2auth/st2auth/sso/__init__.py @@ -19,6 +19,7 @@ import traceback from oslo_config import cfg +from st2auth.sso.base import BaseSingleSignOnBackend from st2common import log as logging @@ -36,7 +37,7 @@ def get_available_backends(): return driver_loader.get_available_backends(namespace=BACKENDS_NAMESPACE) -def get_backend_instance(name): +def get_backend_instance(name) -> BaseSingleSignOnBackend: sso_backend_cls = driver_loader.get_backend_driver( namespace=BACKENDS_NAMESPACE, name=name ) @@ -69,7 +70,7 @@ def get_backend_instance(name): return sso_backend -def get_sso_backend(): +def get_sso_backend() -> BaseSingleSignOnBackend: """ Return SingleSignOnBackend class instance. """ diff --git a/st2auth/st2auth/sso/base.py b/st2auth/st2auth/sso/base.py index 5e11199818..3e7787bcc1 100644 --- a/st2auth/st2auth/sso/base.py +++ b/st2auth/st2auth/sso/base.py @@ -14,10 +14,31 @@ import abc import six +from typing import List -__all__ = ["BaseSingleSignOnBackend"] +__all__ = ["BaseSingleSignOnBackend", "BaseSingleSignOnBackendResponse"] +# This defines the expected response to be communicated back from verify_response methods +@six.add_metaclass(abc.ABCMeta) +class BaseSingleSignOnBackendResponse(object): + username : str = None + referer : str = None + roles : List[str] = None + + def __init__(self, username=None, referer=None, roles=[]): + self.username = username + self.roles = roles + self.referer = referer + + def __eq__(self, other): + return self.username == other.username \ + and self.roles == other.roles \ + and self.referer == other.referer + + def __repr__(self): + return f"BaseSingleSignOnBackendResponse(username={self.username}, roles={self.roles}"\ + + f", referer={self.referer}" @six.add_metaclass(abc.ABCMeta) class BaseSingleSignOnBackend(object): @@ -25,11 +46,15 @@ class BaseSingleSignOnBackend(object): Base single sign on authentication class. """ - def get_request_redirect_url(self, referer): + def get_request_redirect_url(self, referer) -> str: msg = 'The function "get_request_redirect_url" is not implemented in the base SSO backend.' raise NotImplementedError(msg) - def verify_response(self, response): + def get_request_id_from_response(self, response) -> str: + msg = 'The function "get_request_id_from_response" is not implemented in the base SSO backend.' + raise NotImplementedError(msg) + + def verify_response(self, response) -> BaseSingleSignOnBackendResponse: msg = ( 'The function "verify_response" is not implemented in the base SSO backend.' ) From c9d3f4396197572425cac3f112191b34a84e40fe Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Thu, 14 Jul 2022 17:45:34 -0300 Subject: [PATCH 06/49] adding role handling to proxyauth --- st2auth/st2auth/controllers/v1/sso.py | 3 +++ st2auth/st2auth/handlers.py | 31 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index f597d11091..0a28915f21 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -85,9 +85,12 @@ def post(self, response, **kwargs): if not isinstance(verified_user, BaseSingleSignOnBackendResponse): return process_failure_response(http_client.INTERNAL_SERVER_ERROR, "Unexpected SSO backend response type. Expected BaseSingleSignOnBackendResponse instance!") + LOG.debug("Authenticating SSO user [%s] with roles [%s]", verified_user.username, verified_user.roles) + st2_auth_token_create_request = { "user": verified_user.username, "ttl": None, + "roles": verified_user.roles } st2_auth_token = self.st2_auth_handler.handle_auth( diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index f6540bcda7..36a4ce112f 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -24,6 +24,8 @@ from st2common.exceptions.db import StackStormDBObjectNotFoundError from st2common.exceptions.auth import NoNicknameOriginProvidedError, AmbiguousUserError from st2common.exceptions.auth import NotServiceUserError +from st2common.models.db.rbac import UserRoleAssignmentDB +from st2common.persistence.rbac import UserRoleAssignment from st2common.persistence.auth import User from st2common.router import abort from st2common.services.access import create_token @@ -53,6 +55,33 @@ def handle_auth( ): raise NotImplementedError() + def _get_roles_for_request(self, request): + if type(request) is dict: + return request.get("roles", []) + return getattr(request, "roles", None) + + def _sync_roles_for_user(self, username, roles): + LOG.debug("Syncing roles [%s] for user [%s] (deleting all " + "roles first and attaching them again)", roles, username) + # Delete all role assignments + role_assignments = UserRoleAssignment.get_all(user= username) + for role_assignment in role_assignments: + role_assignment.delete() + + # Assign roles for each role + for role in roles: + # Assign role to user + role_assignment_db = UserRoleAssignmentDB( + user=username, + source="API", + role=role, + description="Synced by ProxyAuth", + is_remote=True + ) + UserRoleAssignment.add_or_update(role_assignment_db) + + LOG.debug("Roles successfully synced for user [%s]", username) + def _create_token_for_user(self, username, ttl=None): tokendb = create_token(username=username, ttl=ttl) return TokenAPI.from_model(tokendb) @@ -135,6 +164,8 @@ def handle_auth( username = self._get_username_for_request(remote_user, request) try: token = self._create_token_for_user(username=username, ttl=ttl) + roles = self._get_roles_for_request(request) + self._sync_roles_for_user(username, roles) except TTLTooLargeException as e: abort_request( status_code=http_client.BAD_REQUEST, message=six.text_type(e) From baed32be089f441f174ac29bd8fac79b5b9440d0 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Fri, 15 Jul 2022 18:12:07 -0300 Subject: [PATCH 07/49] adding some tests :) --- st2auth/st2auth/controllers/v1/sso.py | 11 +- st2auth/st2auth/sso/noop.py | 4 +- st2auth/tests/unit/controllers/v1/test_sso.py | 140 ++++++++++++++++-- st2common/st2common/openapi.yaml.j2 | 2 + st2common/st2common/services/access.py | 6 +- 5 files changed, 143 insertions(+), 20 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index 0a28915f21..ea6350a82a 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -136,12 +136,17 @@ def get_web(self, 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) @@ -152,7 +157,7 @@ def post_cli(self, response): aes_key = read_crypto_key_from_dict(json_decode(key)) except Exception: LOG.warn("Could not decode incoming SSO CLI request key") - raise + 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) @@ -163,8 +168,12 @@ def post_cli(self, response): 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 diff --git a/st2auth/st2auth/sso/noop.py b/st2auth/st2auth/sso/noop.py index 6699e084f3..5bf6589b28 100644 --- a/st2auth/st2auth/sso/noop.py +++ b/st2auth/st2auth/sso/noop.py @@ -21,7 +21,7 @@ NOT_IMPLEMENTED_MESSAGE = ( 'The default "noop" SSO backend is not a proper implementation. ' - "Please refer to the enterprise version for configuring SSO." + "Please configure SSO accordingly by selecting a proper backend." ) @@ -30,7 +30,7 @@ class NoOpSingleSignOnBackend(BaseSingleSignOnBackend): NoOp SSO authentication backend. """ - def get_request_redirect_url(self, referer): + def get_request_redirect_url(self, request_id, referer): raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE) def verify_response(self, response): diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 5596b0fb01..642ec74524 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -12,8 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime +from typing import List +from st2common.models.db.auth import SSORequestDB +from st2common.persistence.auth import SSORequest +from st2common.services.access import DEFAULT_SSO_REQUEST_TTL import st2tests.config as tests_config +from st2common.util import date as date_utils + tests_config.parse_args() import json @@ -30,11 +37,21 @@ SSO_V1_PATH = "/v1/sso" -SSO_REQUEST_V1_PATH = SSO_V1_PATH + "/request" +SSO_REQUEST_WEB_V1_PATH = SSO_V1_PATH + "/request/web" +SSO_REQUEST_CLI_V1_PATH = SSO_V1_PATH + "/request/cli" SSO_CALLBACK_V1_PATH = SSO_V1_PATH + "/callback" MOCK_REFERER = "https://127.0.0.1" MOCK_USER = "stanley" - +MOCK_CALLBACK_URL = 'http://localhost:34999' +MOCK_CLI_REQUEST_KEY = json.dumps({ + "hmacKey": { + "hmacKeyString": "-qdRklvhm4xvzIfaL6Z2nmQ-2N-c4IUtNa1_BowCVfg", + "size": 256 + }, + "aesKeyString": "0UyXFjBTQ9PMyHZ0mqrvuqCSzesuFup1d6m-4Vi3vdo", + "mode": "CBC", + "size": 256 +}) class TestSingleSignOnController(FunctionalTest): def test_sso_enabled(self): @@ -63,35 +80,128 @@ def test_unknown_exception(self): sso_api_controller.SingleSignOnController._get_sso_enabled_config.called ) - +# Base SSO request test class, to be used by CLI/WEB class TestSingleSignOnRequestController(FunctionalTest): + + def _assert_response(self, response, status_code, expected_body): + self.assertTrue(response.status_code, status_code) + self.assertDictEqual(response.json, expected_body) + + + def _assert_sso_requests_len(self, expected): + sso_requests : List[SSORequestDB] = SSORequest.get_all() + self.assertEqual(len(sso_requests), expected) + return sso_requests + + def _assert_sso_request_success(self, sso_request, type): + self.assertEqual(sso_request.type, type) + self.assertLessEqual( + abs( + sso_request.expiry.timestamp() + - date_utils.get_datetime_utc_now().timestamp() + - DEFAULT_SSO_REQUEST_TTL + ), 2) + sso_api_controller.SSO_BACKEND.get_request_redirect_url.assert_called_with(sso_request.request_id, MOCK_REFERER) + + def _default_web_request(self, expect_errors): + return self.app.get(SSO_REQUEST_WEB_V1_PATH, + headers={"referer": MOCK_REFERER}, expect_errors=expect_errors) + def _default_cli_request( + self, + params={ + 'callback_url': MOCK_CALLBACK_URL, + 'key': MOCK_CLI_REQUEST_KEY + }, + expect_errors=False): + return self.app.post( + SSO_REQUEST_CLI_V1_PATH, + content_type="application/json", + params=json.dumps(params), + expect_errors=expect_errors + ) + + @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_redirect_url", mock.MagicMock(side_effect=Exception("fooobar")), ) - def test_default_backend_unknown_exception(self): - expected_error = {"faultstring": "Internal Server Error"} - response = self.app.get(SSO_REQUEST_V1_PATH, expect_errors=True) - self.assertTrue(response.status_code, http_client.INTERNAL_SERVER_ERROR) - self.assertDictEqual(response.json, expected_error) + def test_web_default_backend_unknown_exception(self): + response = self._default_web_request(True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": "Internal Server Error"}) + self._assert_sso_requests_len(0) + + def test_web_default_backend_invalid_key(self): + response = self._default_web_request(True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}) + self._assert_sso_requests_len(0) - def test_default_backend_not_implemented(self): - expected_error = {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE} - response = self.app.get(SSO_REQUEST_V1_PATH, expect_errors=True) - self.assertTrue(response.status_code, http_client.INTERNAL_SERVER_ERROR) - self.assertDictEqual(response.json, expected_error) + + def test_web_default_backend_not_implemented(self): + response = self._default_web_request(True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}) + self._assert_sso_requests_len(0) @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_redirect_url", mock.MagicMock(return_value="https://127.0.0.1"), ) - def test_idp_redirect(self): - response = self.app.get(SSO_REQUEST_V1_PATH, expect_errors=False) + def test_web_idp_redirect(self): + response = self._default_web_request(False) self.assertTrue(response.status_code, http_client.TEMPORARY_REDIRECT) self.assertEqual(response.location, "https://127.0.0.1") + # Make sure we have created a SSO request based on this call :) + sso_requests = self._assert_sso_requests_len(1) + sso_request = sso_requests[0] + self._assert_sso_request_success(sso_request, SSORequestDB.Type.WEB) + + + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_redirect_url", + mock.MagicMock(side_effect=Exception("fooobar")), + ) + def test_cli_default_backend_unknown_exception(self): + response = self._default_cli_request(expect_errors=True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": "Internal Server Error"}) + self._assert_sso_requests_len(0) + + def test_cli_default_backend_bad_key(self): + response = self._default_cli_request( + params={ + 'callback_url': MOCK_CALLBACK_URL, + 'key': 'bad-key' + }, + expect_errors=True + ) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": "The provided key is invalid! It should be stackstorm-compatible AES key"}) + self._assert_sso_requests_len(0) + + def test_cli_default_backend_not_implemented(self): + response = self._default_cli_request(expect_errors=True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}) + self._assert_sso_requests_len(0) + class TestIdentityProviderCallbackController(FunctionalTest): @mock.patch.object( diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index 14a1a5dd77..d73a88e39f 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -4551,9 +4551,11 @@ paths: key: type: string description: The symmetric key to be used to encrypt contents of callback + required: true callback_url: type: string description: What URL to be called back once the response from SSO is received + required: true responses: '200': description: SSO request valid diff --git a/st2common/st2common/services/access.py b/st2common/st2common/services/access.py index 4f4d59473c..157e9018a7 100644 --- a/st2common/st2common/services/access.py +++ b/st2common/st2common/services/access.py @@ -31,6 +31,8 @@ LOG = logging.getLogger(__name__) +DEFAULT_SSO_REQUEST_TTL = 120 + def create_token( username, ttl=None, metadata=None, add_missing_user=True, service=False @@ -106,7 +108,7 @@ def delete_token(token): except Exception: raise -def create_cli_sso_request(request_id, key, ttl=120): +def create_cli_sso_request(request_id, key, ttl=DEFAULT_SSO_REQUEST_TTL): """ :param request_id: ID of the SSO request that is being created (usually uuid format prepended by _) :type request_id: ``str`` @@ -120,7 +122,7 @@ def create_cli_sso_request(request_id, key, ttl=120): return _create_sso_request(request_id, ttl, SSORequestDB.Type.CLI, key=key) -def create_web_sso_request(request_id, ttl=120): +def create_web_sso_request(request_id, ttl=DEFAULT_SSO_REQUEST_TTL): """ :param request_id: ID of the SSO request that is being created (usually uuid format prepended by _) :type request_id: ``str`` From c4130e7f0dfffbabe3d0c87f3aa7200d32eeba3f Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 18 Jul 2022 11:38:59 -0300 Subject: [PATCH 08/49] finalizing sso controller cli/web tests --- st2auth/st2auth/controllers/v1/sso.py | 6 +- st2auth/tests/unit/controllers/v1/test_sso.py | 71 ++++++++++++++++--- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index ea6350a82a..ed8112221c 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -151,7 +151,7 @@ def post_cli(self, response): key = getattr(response, 'key', None) callback_url = getattr(response, 'callback_url', None) if not key or not callback_url: - raise ValueError("Missing either key or callback_url!") + raise ValueError("Missing either key and/or callback_url!") try: aes_key = read_crypto_key_from_dict(json_decode(key)) @@ -161,9 +161,11 @@ def post_cli(self, response): 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), - "expiry": sso_request.expiry + # this is needed because the db doesnt save microseconds + "expiry": sso_request.expiry.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "000+00:00" } return response diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 642ec74524..132e6a508f 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -103,6 +103,19 @@ def _assert_sso_request_success(self, sso_request, type): ), 2) sso_api_controller.SSO_BACKEND.get_request_redirect_url.assert_called_with(sso_request.request_id, MOCK_REFERER) + def _test_cli_request_bad_parameter_helper(self, params, expected_error): + response = self._default_cli_request( + params=params, + expect_errors=True + ) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": expected_error}) + self._assert_sso_requests_len(0) + + + def _default_web_request(self, expect_errors): return self.app.get(SSO_REQUEST_WEB_V1_PATH, headers={"referer": MOCK_REFERER}, expect_errors=expect_errors) @@ -181,18 +194,37 @@ def test_cli_default_backend_unknown_exception(self): self._assert_sso_requests_len(0) def test_cli_default_backend_bad_key(self): - response = self._default_cli_request( - params={ + self._test_cli_request_bad_parameter_helper( + { 'callback_url': MOCK_CALLBACK_URL, 'key': 'bad-key' }, - expect_errors=True + "The provided key is invalid! It should be stackstorm-compatible AES key" + ) + + def test_cli_default_backend_missing_key(self): + self._test_cli_request_bad_parameter_helper( + { + 'callback_url': MOCK_CALLBACK_URL, + }, + "Missing either key and/or callback_url!" + ) + + def test_cli_default_backend_missing_callback_url(self): + self._test_cli_request_bad_parameter_helper( + { + 'key': MOCK_CLI_REQUEST_KEY, + }, + "Missing either key and/or callback_url!" + ) + + def test_cli_default_backend_missing_key_and_callback_url(self): + self._test_cli_request_bad_parameter_helper( + { + 'ops': 'ops' + }, + "Missing either key and/or callback_url!" ) - self._assert_response( - response, - http_client.INTERNAL_SERVER_ERROR, - {"faultstring": "The provided key is invalid! It should be stackstorm-compatible AES key"}) - self._assert_sso_requests_len(0) def test_cli_default_backend_not_implemented(self): response = self._default_cli_request(expect_errors=True) @@ -201,6 +233,29 @@ def test_cli_default_backend_not_implemented(self): http_client.INTERNAL_SERVER_ERROR, {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}) self._assert_sso_requests_len(0) + + + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_redirect_url", + mock.MagicMock(return_value="https://127.0.0.1"), + ) + def test_cli_default_backend(self): + response = self._default_cli_request( + params={ + 'callback_url': MOCK_CALLBACK_URL, + 'key': MOCK_CLI_REQUEST_KEY + }, + expect_errors=False + ) + sso_request = self._assert_sso_requests_len(1)[0] + self._assert_response( + response, + http_client.OK, + { + "sso_url": "https://127.0.0.1", + "expiry": sso_request.expiry.isoformat() + }) class TestIdentityProviderCallbackController(FunctionalTest): From ef667545294c3e45a8953d01bf8b9d6a5ed0ccb1 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 18 Jul 2022 11:42:39 -0300 Subject: [PATCH 09/49] finalizing sso controller cli/web tests --- st2auth/tests/unit/controllers/v1/test_sso.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 132e6a508f..38ad9f96ef 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -83,6 +83,11 @@ def test_unknown_exception(self): # Base SSO request test class, to be used by CLI/WEB class TestSingleSignOnRequestController(FunctionalTest): + # Cleanup sso requests + def setUp(self): + for x in SSORequest.get_all(): + SSORequest.delete(x) + def _assert_response(self, response, status_code, expected_body): self.assertTrue(response.status_code, status_code) self.assertDictEqual(response.json, expected_body) From 38942513cb7d7a7f90f56485d97229ddafd72586 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 18 Jul 2022 11:46:44 -0300 Subject: [PATCH 10/49] finalizing sso controller cli/web tests --- st2auth/tests/unit/controllers/v1/test_sso.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 38ad9f96ef..0d1452ba0e 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -248,12 +248,16 @@ def test_cli_default_backend_not_implemented(self): def test_cli_default_backend(self): response = self._default_cli_request( params={ - 'callback_url': MOCK_CALLBACK_URL, + 'callback_url': MOCK_REFERER, 'key': MOCK_CLI_REQUEST_KEY }, expect_errors=False ) - sso_request = self._assert_sso_requests_len(1)[0] + + # Make sure we have created a SSO request based on this call :) + sso_requests = self._assert_sso_requests_len(1) + sso_request = sso_requests[0] + self._assert_sso_request_success(sso_request, SSORequestDB.Type.CLI) self._assert_response( response, http_client.OK, From 9ae0bcc454ce39c08ab95d85aca87300c3b6278e Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 19 Jul 2022 11:45:26 -0300 Subject: [PATCH 11/49] finalizing tests --- st2auth/st2auth/controllers/v1/sso.py | 2 +- st2auth/st2auth/sso/noop.py | 3 + st2auth/tests/unit/controllers/v1/test_sso.py | 293 +++++++++++++++--- st2common/st2common/openapi.yaml | 2 + st2common/st2common/openapi.yaml.j2 | 2 + 5 files changed, 262 insertions(+), 40 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index ed8112221c..bca82b4555 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -85,7 +85,7 @@ def post(self, response, **kwargs): if not isinstance(verified_user, BaseSingleSignOnBackendResponse): return process_failure_response(http_client.INTERNAL_SERVER_ERROR, "Unexpected SSO backend response type. Expected BaseSingleSignOnBackendResponse instance!") - LOG.debug("Authenticating SSO user [%s] with roles [%s]", verified_user.username, verified_user.roles) + LOG.info("Authenticating SSO user [%s] with roles [%s]", verified_user.username, verified_user.roles) st2_auth_token_create_request = { "user": verified_user.username, diff --git a/st2auth/st2auth/sso/noop.py b/st2auth/st2auth/sso/noop.py index 5bf6589b28..b0fca2d63a 100644 --- a/st2auth/st2auth/sso/noop.py +++ b/st2auth/st2auth/sso/noop.py @@ -35,3 +35,6 @@ def get_request_redirect_url(self, request_id, referer): def verify_response(self, response): raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE) + + def get_request_id_from_response(self, response) -> str: + raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE) \ No newline at end of file diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 0d1452ba0e..d3e2f79f6f 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -14,10 +14,12 @@ import datetime from typing import List +from st2auth.sso.base import BaseSingleSignOnBackendResponse from st2common.models.db.auth import SSORequestDB -from st2common.persistence.auth import SSORequest -from st2common.services.access import DEFAULT_SSO_REQUEST_TTL +from st2common.persistence.auth import SSORequest, Token +from st2common.services.access import DEFAULT_SSO_REQUEST_TTL, create_web_sso_request, create_cli_sso_request import st2tests.config as tests_config +from st2common.util.crypto import read_crypto_key_from_dict, symmetric_decrypt, symmetric_encrypt from st2common.util import date as date_utils @@ -43,7 +45,7 @@ MOCK_REFERER = "https://127.0.0.1" MOCK_USER = "stanley" MOCK_CALLBACK_URL = 'http://localhost:34999' -MOCK_CLI_REQUEST_KEY = json.dumps({ +MOCK_CLI_REQUEST_KEY = read_crypto_key_from_dict({ "hmacKey": { "hmacKeyString": "-qdRklvhm4xvzIfaL6Z2nmQ-2N-c4IUtNa1_BowCVfg", "size": 256 @@ -52,18 +54,26 @@ "mode": "CBC", "size": 256 }) +MOCK_CLI_REQUEST_KEY_JSON = MOCK_CLI_REQUEST_KEY.to_json() +MOCK_REQUEST_ID="test-id" +MOCK_GROUPS=["test", "test2"] +MOCK_VERIFIED_USER_OBJECT=BaseSingleSignOnBackendResponse( + referer=MOCK_REFERER, + roles=MOCK_GROUPS, + username=MOCK_USER +) class TestSingleSignOnController(FunctionalTest): def test_sso_enabled(self): cfg.CONF.set_override(group="auth", name="sso", override=True) response = self.app.get(SSO_V1_PATH, expect_errors=False) - self.assertTrue(response.status_code, http_client.OK) + self.assertEqual(response.status_code, http_client.OK) self.assertDictEqual(response.json, {"enabled": True}) def test_sso_disabled(self): cfg.CONF.set_override(group="auth", name="sso", override=False) response = self.app.get(SSO_V1_PATH, expect_errors=False) - self.assertTrue(response.status_code, http_client.OK) + self.assertEqual(response.status_code, http_client.OK) self.assertDictEqual(response.json, {"enabled": False}) @mock.patch.object( @@ -74,7 +84,7 @@ def test_sso_disabled(self): def test_unknown_exception(self): cfg.CONF.set_override(group="auth", name="sso", override=True) response = self.app.get(SSO_V1_PATH, expect_errors=False) - self.assertTrue(response.status_code, http_client.OK) + self.assertEqual(response.status_code, http_client.OK) self.assertDictEqual(response.json, {"enabled": False}) self.assertTrue( sso_api_controller.SingleSignOnController._get_sso_enabled_config.called @@ -83,13 +93,17 @@ def test_unknown_exception(self): # Base SSO request test class, to be used by CLI/WEB class TestSingleSignOnRequestController(FunctionalTest): + # + # Helpers + # + # Cleanup sso requests def setUp(self): for x in SSORequest.get_all(): SSORequest.delete(x) def _assert_response(self, response, status_code, expected_body): - self.assertTrue(response.status_code, status_code) + self.assertEqual(response.status_code, status_code) self.assertDictEqual(response.json, expected_body) @@ -128,7 +142,7 @@ def _default_cli_request( self, params={ 'callback_url': MOCK_CALLBACK_URL, - 'key': MOCK_CLI_REQUEST_KEY + 'key': MOCK_CLI_REQUEST_KEY_JSON }, expect_errors=False): return self.app.post( @@ -138,6 +152,9 @@ def _default_cli_request( expect_errors=expect_errors ) + # + # Tests :) + # @mock.patch.object( sso_api_controller.SSO_BACKEND, @@ -176,7 +193,7 @@ def test_web_default_backend_not_implemented(self): ) def test_web_idp_redirect(self): response = self._default_web_request(False) - self.assertTrue(response.status_code, http_client.TEMPORARY_REDIRECT) + self.assertEqual(response.status_code, http_client.TEMPORARY_REDIRECT) self.assertEqual(response.location, "https://127.0.0.1") # Make sure we have created a SSO request based on this call :) @@ -218,7 +235,7 @@ def test_cli_default_backend_missing_key(self): def test_cli_default_backend_missing_callback_url(self): self._test_cli_request_bad_parameter_helper( { - 'key': MOCK_CLI_REQUEST_KEY, + 'key': MOCK_CLI_REQUEST_KEY_JSON, }, "Missing either key and/or callback_url!" ) @@ -249,7 +266,7 @@ def test_cli_default_backend(self): response = self._default_cli_request( params={ 'callback_url': MOCK_REFERER, - 'key': MOCK_CLI_REQUEST_KEY + 'key': MOCK_CLI_REQUEST_KEY_JSON }, expect_errors=False ) @@ -268,60 +285,257 @@ def test_cli_default_backend(self): class TestIdentityProviderCallbackController(FunctionalTest): + + # Helpers + # + + def setUp(self): + for x in SSORequest.get_all(): + SSORequest.delete(x) + + + def _assert_response(self, response, status_code, expected_body, response_type='json'): + self.assertEqual(response.status_code, status_code) + if response_type == 'json': + self.assertDictEqual(response.json, expected_body) + else: + self.assertEqual(response.body.decode('utf-8'), expected_body) + + def _assert_sso_requests_len(self, expected): + sso_requests : List[SSORequestDB] = SSORequest.get_all() + self.assertEqual(len(sso_requests), expected) + return sso_requests + + def _assert_token_data_is_valid(self, token_data): + self.assertEqual(token_data["user"], MOCK_USER) + self.assertIsNotNone(token_data["expiry"]) + self.assertIsNotNone(token_data["token"]) + + # Validate actual token :) + token = Token.get(token_data["token"]) + self.assertIsNotNone(token) + self.assertEqual(token.user, MOCK_USER) + self.assertEqual(token.expiry.isoformat()[0:19], token_data["expiry"][0:19]) + + def _assert_response_has_token_cookie_only(self, response): + + set_cookies_list = [h for h in response.headerlist if h[0] == "Set-Cookie"] + self.assertEqual(len(set_cookies_list), 1) + self.assertIn("st2-auth-token", set_cookies_list[0][1]) + + cookie = urllib.parse.unquote(set_cookies_list[0][1]).split("=") + st2_auth_token = json.loads(cookie[1].split(";")[0]) + self.assertIn("token", st2_auth_token) + + return st2_auth_token + + def _default_callback_request( + self, + params={}, + expect_errors=False): + return self.app.post_json( + SSO_CALLBACK_V1_PATH, + params, + expect_errors=expect_errors + ) + + # + # Tests + # + @mock.patch.object( sso_api_controller.SSO_BACKEND, - "verify_response", + "get_request_id_from_response", mock.MagicMock(side_effect=Exception("fooobar")), ) def test_default_backend_unknown_exception(self): - expected_error = {"faultstring": "Internal Server Error"} - response = self.app.post_json( - SSO_CALLBACK_V1_PATH, {"foo": "bar"}, expect_errors=True + response = self._default_callback_request({"foo": "bar"}, expect_errors=True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": "Internal Server Error"} ) - self.assertTrue(response.status_code, http_client.INTERNAL_SERVER_ERROR) - self.assertDictEqual(response.json, expected_error) def test_default_backend_not_implemented(self): - expected_error = {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE} - response = self.app.post_json( - SSO_CALLBACK_V1_PATH, {"foo": "bar"}, expect_errors=True + response = self._default_callback_request({"foo": "bar"}, expect_errors=True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE} ) - self.assertTrue(response.status_code, http_client.INTERNAL_SERVER_ERROR) - self.assertDictEqual(response.json, expected_error) + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=None), + ) + def test_default_backend_invalid_request_id(self): + response = self._default_callback_request({"foo": "bar"}, expect_errors=True) + self._assert_response( + response, + http_client.BAD_REQUEST, + {"faultstring": "Invalid request id coming from SAML response"} + ) + + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) @mock.patch.object( sso_api_controller.SSO_BACKEND, "verify_response", - mock.MagicMock(return_value={"referer": MOCK_REFERER, "username": MOCK_USER}), + mock.MagicMock(return_value={'test': 'user'}), ) - def test_idp_callback(self): - expected_body = sso_api_controller.CALLBACK_SUCCESS_RESPONSE_BODY % MOCK_REFERER - response = self.app.post_json( - SSO_CALLBACK_V1_PATH, {"foo": "bar"}, expect_errors=False + def test_default_backend_invalid_backend_response(self): + create_web_sso_request(MOCK_REQUEST_ID) + response = self._default_callback_request({"foo": "bar"}, expect_errors=True) + self._assert_response( + response, + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": ( + "Unexpected SSO backend response type." + " Expected BaseSingleSignOnBackendResponse instance!" + )} + ) + + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) + def test_idp_callback_missing_sso_request(self): + self._assert_sso_requests_len(0) + response = self._default_callback_request({ + "foo": "bar" + + }, expect_errors=True) + + self._assert_response( + response, + http_client.BAD_REQUEST, + {"faultstring": "This SSO request is invalid (it may have already been used)"} ) - self.assertTrue(response.status_code, http_client.OK) - self.assertEqual(expected_body, response.body.decode("utf-8")) - set_cookies_list = [h for h in response.headerlist if h[0] == "Set-Cookie"] - self.assertEqual(len(set_cookies_list), 1) - self.assertIn("st2-auth-token", set_cookies_list[0][1]) + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) + def test_idp_callback_sso_request_expired(self): + # given + # Create fake expired request + create_web_sso_request(MOCK_REQUEST_ID, -20) + self._assert_sso_requests_len(1) + response = self._default_callback_request({ + "foo": "bar" + + }, expect_errors=True) + + self._assert_response( + response, + http_client.BAD_REQUEST, + {"faultstring": "The SSO request associated with this response has already expired!"} + ) + + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "verify_response", + mock.MagicMock(return_value=MOCK_VERIFIED_USER_OBJECT), + ) + def test_idp_callback_web(self): + # given + # Create fake request + create_web_sso_request(MOCK_REQUEST_ID) + self._assert_sso_requests_len(1) + + # when + # Callback based onthe fake request :) -- as mocked above + response = self._default_callback_request({ + "foo": "bar" + }, expect_errors=False) + + # then + # Validate request has been processed and response is as expected + self._assert_sso_requests_len(0) + self._assert_response( + response, + http_client.OK, + sso_api_controller.CALLBACK_SUCCESS_RESPONSE_BODY % MOCK_REFERER, + 'str' + ) + + # Validate token is valid + token_data = self._assert_response_has_token_cookie_only(response) + self._assert_token_data_is_valid(token_data) + + + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "verify_response", + mock.MagicMock(return_value=MOCK_VERIFIED_USER_OBJECT), + ) + def test_idp_callback_cli(self): + # given + # Create fake request + create_cli_sso_request(MOCK_REQUEST_ID, MOCK_CLI_REQUEST_KEY_JSON) + self._assert_sso_requests_len(1) + + # when + # Callback based onthe fake request :) -- as mocked above + response = self._default_callback_request({ + "foo": "bar" + }, expect_errors=False) + + # then + # Validate request has been processed and response is as expected + self._assert_sso_requests_len(0) + self.assertEqual(response.status_code, http_client.FOUND) + self.assertRegex(response.location, "^" + MOCK_REFERER + "\?response=[A-Z0-9]+$") + + # decrypt token + encrypted_response = response.location.split("response=")[1] + encrypted_token = symmetric_decrypt(MOCK_CLI_REQUEST_KEY, encrypted_response) + self.assertIsNotNone(encrypted_token) + + # Validate token is valid + token_data = json.loads(encrypted_token) + self._assert_token_data_is_valid(token_data) - cookie = urllib.parse.unquote(set_cookies_list[0][1]).split("=") - st2_auth_token = json.loads(cookie[1].split(";")[0]) - self.assertIn("token", st2_auth_token) - self.assertEqual(st2_auth_token["user"], MOCK_USER) + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) @mock.patch.object( sso_api_controller.SSO_BACKEND, "verify_response", - mock.MagicMock(return_value={"referer": MOCK_REFERER, "username": MOCK_USER}), + mock.MagicMock(return_value=MOCK_VERIFIED_USER_OBJECT), ) def test_callback_url_encoded_payload(self): + create_web_sso_request(MOCK_REQUEST_ID) data = {"foo": ["bar"]} headers = {"Content-Type": "application/x-www-form-urlencoded"} response = self.app.post(SSO_CALLBACK_V1_PATH, data, headers=headers) - self.assertTrue(response.status_code, http_client.OK) + self.assertEqual(response.status_code, http_client.OK) + + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) @mock.patch.object( sso_api_controller.SSO_BACKEND, "verify_response", @@ -330,9 +544,10 @@ def test_callback_url_encoded_payload(self): ), ) def test_idp_callback_verification_failed(self): + create_web_sso_request(MOCK_REQUEST_ID) expected_error = {"faultstring": "Verification Failed"} response = self.app.post_json( SSO_CALLBACK_V1_PATH, {"foo": "bar"}, expect_errors=True ) - self.assertTrue(response.status_code, http_client.UNAUTHORIZED) + self.assertEqual(response.status_code, http_client.UNAUTHORIZED) self.assertDictEqual(response.json, expected_error) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index 2775dee140..b573e385a6 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -4576,6 +4576,8 @@ paths: responses: '200': description: SSO response valid + '302': + description: SSO request valid and callback URL returned '401': description: Invalid or missing credentials has been provided schema: diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index d73a88e39f..4c4ffb5b9a 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -4573,6 +4573,8 @@ paths: responses: '200': description: SSO response valid + '302': + description: SSO request valid and callback URL returned '401': description: Invalid or missing credentials has been provided schema: From bba0340d4c8739d231e2d9574e8c738fc6caf057 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 19 Jul 2022 11:56:25 -0300 Subject: [PATCH 12/49] adding extra test :) --- st2auth/tests/unit/controllers/v1/test_sso.py | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index d3e2f79f6f..08cfd1c058 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -54,6 +54,15 @@ "mode": "CBC", "size": 256 }) +MOCK_CLI_REQUEST_KEY_ALTERNATIVE = read_crypto_key_from_dict({ + "hmacKey":{ + "hmacKeyString":"ENb-2COFGmdnshSnjjz3wePrxypVzCf9Jq2iuhXEgbc", + "size":256 + }, + "aesKeyString":"8TpT_RaA6dlharswjqVlJSw027B60UkgnQqcgGfmf08", + "mode":"CBC", + "size":256 + }) MOCK_CLI_REQUEST_KEY_JSON = MOCK_CLI_REQUEST_KEY.to_json() MOCK_REQUEST_ID="test-id" MOCK_GROUPS=["test", "test2"] @@ -505,14 +514,47 @@ def test_idp_callback_cli(self): # decrypt token encrypted_response = response.location.split("response=")[1] - encrypted_token = symmetric_decrypt(MOCK_CLI_REQUEST_KEY, encrypted_response) - self.assertIsNotNone(encrypted_token) + token_data_json = symmetric_decrypt(MOCK_CLI_REQUEST_KEY, encrypted_response) + self.assertIsNotNone(token_data_json) # Validate token is valid - token_data = json.loads(encrypted_token) + token_data = json.loads(token_data_json) self._assert_token_data_is_valid(token_data) + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "get_request_id_from_response", + mock.MagicMock(return_value=MOCK_REQUEST_ID), + ) + @mock.patch.object( + sso_api_controller.SSO_BACKEND, + "verify_response", + mock.MagicMock(return_value=MOCK_VERIFIED_USER_OBJECT), + ) + def test_idp_callback_cli_invalid_decryption_key(self): + # given + # Create fake request + create_cli_sso_request(MOCK_REQUEST_ID, MOCK_CLI_REQUEST_KEY_JSON) + self._assert_sso_requests_len(1) + + # when + # Callback based onthe fake request :) -- as mocked above + response = self._default_callback_request({ + "foo": "bar" + }, expect_errors=False) + + # then + # Validate request has been processed and response is as expected + self._assert_sso_requests_len(0) + self.assertEqual(response.status_code, http_client.FOUND) + self.assertRegex(response.location, "^" + MOCK_REFERER + "\?response=[A-Z0-9]+$") + + # decrypt token + encrypted_response = response.location.split("response=")[1] + with self.assertRaises(Exception): + token_data_json = symmetric_decrypt(MOCK_CLI_REQUEST_KEY_ALTERNATIVE, encrypted_response) + @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_id_from_response", From ea752810f6505b357e1da8a47fdd36e346955395 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 19 Jul 2022 13:26:19 -0300 Subject: [PATCH 13/49] fixing lint :) --- st2auth/st2auth/controllers/v1/sso.py | 101 +++++-- st2auth/st2auth/handlers.py | 20 +- st2auth/st2auth/sso/base.py | 24 +- st2auth/st2auth/sso/noop.py | 2 +- st2auth/tests/unit/controllers/v1/test_sso.py | 274 +++++++++--------- st2client/st2client/client.py | 7 +- st2client/st2client/commands/auth.py | 31 +- st2client/st2client/models/core.py | 25 +- st2client/st2client/utils/sso_interceptor.py | 63 ++-- st2client/tests/unit/test_auth.py | 3 +- st2common/st2common/models/db/auth.py | 12 +- st2common/st2common/persistence/auth.py | 1 - st2common/st2common/services/access.py | 27 +- st2common/st2common/util/crypto.py | 3 +- 14 files changed, 331 insertions(+), 262 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index bca82b4555..b78243614d 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -14,7 +14,6 @@ import datetime import json -from subprocess import call from uuid import uuid4 from oslo_config import cfg @@ -24,12 +23,16 @@ import st2auth.handlers as handlers from st2auth import sso as st2auth_sso -from st2auth.sso.base import BaseSingleSignOnBackend, BaseSingleSignOnBackendResponse +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.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 @@ -47,11 +50,11 @@ def __init__(self): # 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 + # 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 @@ -62,35 +65,53 @@ def _validate_and_delete_sso_request(self, response): pass if original_sso_request is None: - raise ValueError('This SSO request is invalid (it may have already been used)') + 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) + 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!') + 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) + 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!") + return process_failure_response( + http_client.INTERNAL_SERVER_ERROR, + "Unexpected SSO backend response type. Expected BaseSingleSignOnBackendResponse instance!", + ) - LOG.info("Authenticating SSO user [%s] with roles [%s]", verified_user.username, verified_user.roles) + LOG.info( + "Authenticating SSO user [%s] with roles [%s]", + verified_user.username, + verified_user.roles, + ) st2_auth_token_create_request = { "user": verified_user.username, "ttl": None, - "roles": verified_user.roles + "roles": verified_user.roles, } st2_auth_token = self.st2_auth_handler.handle_auth( @@ -99,17 +120,22 @@ def post(self, response, **kwargs): remote_user=verified_user.username, headers={}, ) - + # Depending on the type of SSO request we should handle the response differently - # ie WEB gets redirected and CLI gets an encrypted callback + # 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) + 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) + 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: @@ -119,21 +145,27 @@ def post(self, response, **kwargs): class SingleSignOnRequestController(object): - 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) + 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 + # 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(sso_request.request_id, referer) + response.location = SSO_BACKEND.get_request_redirect_url( + sso_request.request_id, referer + ) return response except NotImplementedError as e: if sso_request: @@ -148,24 +180,29 @@ def get_web(self, referer): def post_cli(self, response): sso_request = None try: - key = getattr(response, 'key', None) - callback_url = getattr(response, 'callback_url', None) + key = getattr(response, "key", None) + callback_url = getattr(response, "callback_url", None) if not key or not callback_url: raise ValueError("Missing either key and/or callback_url!") try: - aes_key = read_crypto_key_from_dict(json_decode(key)) + 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") + 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), + "sso_url": SSO_BACKEND.get_request_redirect_url( + sso_request.request_id, callback_url + ), # this is needed because the db doesnt save microseconds - "expiry": sso_request.expiry.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "000+00:00" + "expiry": sso_request.expiry.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + + "000+00:00", } return response @@ -242,6 +279,7 @@ def get(self): """ + def token_to_json(token): return { "id": str(token.id), @@ -255,15 +293,18 @@ def token_to_json(token): 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) + 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')) + resp.location = "%s?response=%s" % (callback_url, encrypted_token.decode("utf-8")) return resp diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index 36a4ce112f..a41223d7df 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -61,10 +61,14 @@ def _get_roles_for_request(self, request): return getattr(request, "roles", None) def _sync_roles_for_user(self, username, roles): - LOG.debug("Syncing roles [%s] for user [%s] (deleting all " - "roles first and attaching them again)", roles, username) + LOG.debug( + "Syncing roles [%s] for user [%s] (deleting all " + "roles first and attaching them again)", + roles, + username, + ) # Delete all role assignments - role_assignments = UserRoleAssignment.get_all(user= username) + role_assignments = UserRoleAssignment.get_all(user=username) for role_assignment in role_assignments: role_assignment.delete() @@ -72,11 +76,11 @@ def _sync_roles_for_user(self, username, roles): for role in roles: # Assign role to user role_assignment_db = UserRoleAssignmentDB( - user=username, - source="API", - role=role, - description="Synced by ProxyAuth", - is_remote=True + user=username, + source="API", + role=role, + description="Synced by ProxyAuth", + is_remote=True, ) UserRoleAssignment.add_or_update(role_assignment_db) diff --git a/st2auth/st2auth/sso/base.py b/st2auth/st2auth/sso/base.py index 3e7787bcc1..7692cce6ec 100644 --- a/st2auth/st2auth/sso/base.py +++ b/st2auth/st2auth/sso/base.py @@ -19,26 +19,32 @@ __all__ = ["BaseSingleSignOnBackend", "BaseSingleSignOnBackendResponse"] + # This defines the expected response to be communicated back from verify_response methods @six.add_metaclass(abc.ABCMeta) class BaseSingleSignOnBackendResponse(object): - username : str = None - referer : str = None - roles : List[str] = None + username: str = None + referer: str = None + roles: List[str] = None def __init__(self, username=None, referer=None, roles=[]): self.username = username self.roles = roles self.referer = referer - + def __eq__(self, other): - return self.username == other.username \ - and self.roles == other.roles \ + return ( + self.username == other.username + and self.roles == other.roles and self.referer == other.referer - + ) + def __repr__(self): - return f"BaseSingleSignOnBackendResponse(username={self.username}, roles={self.roles}"\ + return ( + f"BaseSingleSignOnBackendResponse(username={self.username}, roles={self.roles}" + f", referer={self.referer}" + ) + @six.add_metaclass(abc.ABCMeta) class BaseSingleSignOnBackend(object): @@ -53,7 +59,7 @@ def get_request_redirect_url(self, referer) -> str: def get_request_id_from_response(self, response) -> str: msg = 'The function "get_request_id_from_response" is not implemented in the base SSO backend.' raise NotImplementedError(msg) - + def verify_response(self, response) -> BaseSingleSignOnBackendResponse: msg = ( 'The function "verify_response" is not implemented in the base SSO backend.' diff --git a/st2auth/st2auth/sso/noop.py b/st2auth/st2auth/sso/noop.py index b0fca2d63a..55e764a809 100644 --- a/st2auth/st2auth/sso/noop.py +++ b/st2auth/st2auth/sso/noop.py @@ -37,4 +37,4 @@ def verify_response(self, response): raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE) def get_request_id_from_response(self, response) -> str: - raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE) \ No newline at end of file + raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 08cfd1c058..950b6e64d0 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -12,14 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime from typing import List from st2auth.sso.base import BaseSingleSignOnBackendResponse from st2common.models.db.auth import SSORequestDB from st2common.persistence.auth import SSORequest, Token -from st2common.services.access import DEFAULT_SSO_REQUEST_TTL, create_web_sso_request, create_cli_sso_request +from st2common.services.access import ( + DEFAULT_SSO_REQUEST_TTL, + create_web_sso_request, + create_cli_sso_request, +) import st2tests.config as tests_config -from st2common.util.crypto import read_crypto_key_from_dict, symmetric_decrypt, symmetric_encrypt +from st2common.util.crypto import read_crypto_key_from_dict, symmetric_decrypt from st2common.util import date as date_utils @@ -44,34 +47,37 @@ SSO_CALLBACK_V1_PATH = SSO_V1_PATH + "/callback" MOCK_REFERER = "https://127.0.0.1" MOCK_USER = "stanley" -MOCK_CALLBACK_URL = 'http://localhost:34999' -MOCK_CLI_REQUEST_KEY = read_crypto_key_from_dict({ - "hmacKey": { - "hmacKeyString": "-qdRklvhm4xvzIfaL6Z2nmQ-2N-c4IUtNa1_BowCVfg", - "size": 256 - }, - "aesKeyString": "0UyXFjBTQ9PMyHZ0mqrvuqCSzesuFup1d6m-4Vi3vdo", - "mode": "CBC", - "size": 256 -}) -MOCK_CLI_REQUEST_KEY_ALTERNATIVE = read_crypto_key_from_dict({ - "hmacKey":{ - "hmacKeyString":"ENb-2COFGmdnshSnjjz3wePrxypVzCf9Jq2iuhXEgbc", - "size":256 - }, - "aesKeyString":"8TpT_RaA6dlharswjqVlJSw027B60UkgnQqcgGfmf08", - "mode":"CBC", - "size":256 - }) +MOCK_CALLBACK_URL = "http://localhost:34999" +MOCK_CLI_REQUEST_KEY = read_crypto_key_from_dict( + { + "hmacKey": { + "hmacKeyString": "-qdRklvhm4xvzIfaL6Z2nmQ-2N-c4IUtNa1_BowCVfg", + "size": 256, + }, + "aesKeyString": "0UyXFjBTQ9PMyHZ0mqrvuqCSzesuFup1d6m-4Vi3vdo", + "mode": "CBC", + "size": 256, + } +) +MOCK_CLI_REQUEST_KEY_ALTERNATIVE = read_crypto_key_from_dict( + { + "hmacKey": { + "hmacKeyString": "ENb-2COFGmdnshSnjjz3wePrxypVzCf9Jq2iuhXEgbc", + "size": 256, + }, + "aesKeyString": "8TpT_RaA6dlharswjqVlJSw027B60UkgnQqcgGfmf08", + "mode": "CBC", + "size": 256, + } +) MOCK_CLI_REQUEST_KEY_JSON = MOCK_CLI_REQUEST_KEY.to_json() -MOCK_REQUEST_ID="test-id" -MOCK_GROUPS=["test", "test2"] -MOCK_VERIFIED_USER_OBJECT=BaseSingleSignOnBackendResponse( - referer=MOCK_REFERER, - roles=MOCK_GROUPS, - username=MOCK_USER +MOCK_REQUEST_ID = "test-id" +MOCK_GROUPS = ["test", "test2"] +MOCK_VERIFIED_USER_OBJECT = BaseSingleSignOnBackendResponse( + referer=MOCK_REFERER, roles=MOCK_GROUPS, username=MOCK_USER ) + class TestSingleSignOnController(FunctionalTest): def test_sso_enabled(self): cfg.CONF.set_override(group="auth", name="sso", override=True) @@ -99,6 +105,7 @@ def test_unknown_exception(self): sso_api_controller.SingleSignOnController._get_sso_enabled_config.called ) + # Base SSO request test class, to be used by CLI/WEB class TestSingleSignOnRequestController(FunctionalTest): @@ -115,9 +122,8 @@ def _assert_response(self, response, status_code, expected_body): self.assertEqual(response.status_code, status_code) self.assertDictEqual(response.json, expected_body) - def _assert_sso_requests_len(self, expected): - sso_requests : List[SSORequestDB] = SSORequest.get_all() + sso_requests: List[SSORequestDB] = SSORequest.get_all() self.assertEqual(len(sso_requests), expected) return sso_requests @@ -125,40 +131,40 @@ def _assert_sso_request_success(self, sso_request, type): self.assertEqual(sso_request.type, type) self.assertLessEqual( abs( - sso_request.expiry.timestamp() - - date_utils.get_datetime_utc_now().timestamp() + sso_request.expiry.timestamp() + - date_utils.get_datetime_utc_now().timestamp() - DEFAULT_SSO_REQUEST_TTL - ), 2) - sso_api_controller.SSO_BACKEND.get_request_redirect_url.assert_called_with(sso_request.request_id, MOCK_REFERER) + ), + 2, + ) + sso_api_controller.SSO_BACKEND.get_request_redirect_url.assert_called_with( + sso_request.request_id, MOCK_REFERER + ) def _test_cli_request_bad_parameter_helper(self, params, expected_error): - response = self._default_cli_request( - params=params, - expect_errors=True - ) + response = self._default_cli_request(params=params, expect_errors=True) self._assert_response( - response, - http_client.INTERNAL_SERVER_ERROR, - {"faultstring": expected_error}) + response, http_client.INTERNAL_SERVER_ERROR, {"faultstring": expected_error} + ) self._assert_sso_requests_len(0) - - def _default_web_request(self, expect_errors): - return self.app.get(SSO_REQUEST_WEB_V1_PATH, - headers={"referer": MOCK_REFERER}, expect_errors=expect_errors) + return self.app.get( + SSO_REQUEST_WEB_V1_PATH, + headers={"referer": MOCK_REFERER}, + expect_errors=expect_errors, + ) + def _default_cli_request( - self, - params={ - 'callback_url': MOCK_CALLBACK_URL, - 'key': MOCK_CLI_REQUEST_KEY_JSON - }, - expect_errors=False): + self, + params={"callback_url": MOCK_CALLBACK_URL, "key": MOCK_CLI_REQUEST_KEY_JSON}, + expect_errors=False, + ): return self.app.post( - SSO_REQUEST_CLI_V1_PATH, + SSO_REQUEST_CLI_V1_PATH, content_type="application/json", - params=json.dumps(params), - expect_errors=expect_errors + params=json.dumps(params), + expect_errors=expect_errors, ) # @@ -174,25 +180,27 @@ def test_web_default_backend_unknown_exception(self): response = self._default_web_request(True) self._assert_response( response, - http_client.INTERNAL_SERVER_ERROR, - {"faultstring": "Internal Server Error"}) + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": "Internal Server Error"}, + ) self._assert_sso_requests_len(0) def test_web_default_backend_invalid_key(self): response = self._default_web_request(True) self._assert_response( response, - http_client.INTERNAL_SERVER_ERROR, - {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}) + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}, + ) self._assert_sso_requests_len(0) - def test_web_default_backend_not_implemented(self): response = self._default_web_request(True) self._assert_response( response, - http_client.INTERNAL_SERVER_ERROR, - {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}) + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}, + ) self._assert_sso_requests_len(0) @mock.patch.object( @@ -210,7 +218,6 @@ def test_web_idp_redirect(self): sso_request = sso_requests[0] self._assert_sso_request_success(sso_request, SSORequestDB.Type.WEB) - @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_redirect_url", @@ -220,52 +227,47 @@ def test_cli_default_backend_unknown_exception(self): response = self._default_cli_request(expect_errors=True) self._assert_response( response, - http_client.INTERNAL_SERVER_ERROR, - {"faultstring": "Internal Server Error"}) + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": "Internal Server Error"}, + ) self._assert_sso_requests_len(0) def test_cli_default_backend_bad_key(self): self._test_cli_request_bad_parameter_helper( - { - 'callback_url': MOCK_CALLBACK_URL, - 'key': 'bad-key' - }, - "The provided key is invalid! It should be stackstorm-compatible AES key" + {"callback_url": MOCK_CALLBACK_URL, "key": "bad-key"}, + "The provided key is invalid! It should be stackstorm-compatible AES key", ) def test_cli_default_backend_missing_key(self): self._test_cli_request_bad_parameter_helper( { - 'callback_url': MOCK_CALLBACK_URL, + "callback_url": MOCK_CALLBACK_URL, }, - "Missing either key and/or callback_url!" + "Missing either key and/or callback_url!", ) def test_cli_default_backend_missing_callback_url(self): self._test_cli_request_bad_parameter_helper( { - 'key': MOCK_CLI_REQUEST_KEY_JSON, + "key": MOCK_CLI_REQUEST_KEY_JSON, }, - "Missing either key and/or callback_url!" + "Missing either key and/or callback_url!", ) def test_cli_default_backend_missing_key_and_callback_url(self): self._test_cli_request_bad_parameter_helper( - { - 'ops': 'ops' - }, - "Missing either key and/or callback_url!" + {"ops": "ops"}, "Missing either key and/or callback_url!" ) def test_cli_default_backend_not_implemented(self): response = self._default_cli_request(expect_errors=True) self._assert_response( response, - http_client.INTERNAL_SERVER_ERROR, - {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}) + http_client.INTERNAL_SERVER_ERROR, + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}, + ) self._assert_sso_requests_len(0) - @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_redirect_url", @@ -273,11 +275,8 @@ def test_cli_default_backend_not_implemented(self): ) def test_cli_default_backend(self): response = self._default_cli_request( - params={ - 'callback_url': MOCK_REFERER, - 'key': MOCK_CLI_REQUEST_KEY_JSON - }, - expect_errors=False + params={"callback_url": MOCK_REFERER, "key": MOCK_CLI_REQUEST_KEY_JSON}, + expect_errors=False, ) # Make sure we have created a SSO request based on this call :) @@ -286,12 +285,10 @@ def test_cli_default_backend(self): self._assert_sso_request_success(sso_request, SSORequestDB.Type.CLI) self._assert_response( response, - http_client.OK, - { - "sso_url": "https://127.0.0.1", - "expiry": sso_request.expiry.isoformat() - }) - + http_client.OK, + {"sso_url": "https://127.0.0.1", "expiry": sso_request.expiry.isoformat()}, + ) + class TestIdentityProviderCallbackController(FunctionalTest): @@ -302,16 +299,17 @@ def setUp(self): for x in SSORequest.get_all(): SSORequest.delete(x) - - def _assert_response(self, response, status_code, expected_body, response_type='json'): + def _assert_response( + self, response, status_code, expected_body, response_type="json" + ): self.assertEqual(response.status_code, status_code) - if response_type == 'json': + if response_type == "json": self.assertDictEqual(response.json, expected_body) else: - self.assertEqual(response.body.decode('utf-8'), expected_body) + self.assertEqual(response.body.decode("utf-8"), expected_body) def _assert_sso_requests_len(self, expected): - sso_requests : List[SSORequestDB] = SSORequest.get_all() + sso_requests: List[SSORequestDB] = SSORequest.get_all() self.assertEqual(len(sso_requests), expected) return sso_requests @@ -326,7 +324,7 @@ def _assert_token_data_is_valid(self, token_data): self.assertEqual(token.user, MOCK_USER) self.assertEqual(token.expiry.isoformat()[0:19], token_data["expiry"][0:19]) - def _assert_response_has_token_cookie_only(self, response): + def _assert_response_has_token_cookie_only(self, response): set_cookies_list = [h for h in response.headerlist if h[0] == "Set-Cookie"] self.assertEqual(len(set_cookies_list), 1) @@ -338,16 +336,11 @@ def _assert_response_has_token_cookie_only(self, response): return st2_auth_token - def _default_callback_request( - self, - params={}, - expect_errors=False): + def _default_callback_request(self, params={}, expect_errors=False): return self.app.post_json( - SSO_CALLBACK_V1_PATH, - params, - expect_errors=expect_errors + SSO_CALLBACK_V1_PATH, params, expect_errors=expect_errors ) - + # # Tests # @@ -362,7 +355,7 @@ def test_default_backend_unknown_exception(self): self._assert_response( response, http_client.INTERNAL_SERVER_ERROR, - {"faultstring": "Internal Server Error"} + {"faultstring": "Internal Server Error"}, ) def test_default_backend_not_implemented(self): @@ -370,7 +363,7 @@ def test_default_backend_not_implemented(self): self._assert_response( response, http_client.INTERNAL_SERVER_ERROR, - {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE} + {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}, ) @mock.patch.object( @@ -383,7 +376,7 @@ def test_default_backend_invalid_request_id(self): self._assert_response( response, http_client.BAD_REQUEST, - {"faultstring": "Invalid request id coming from SAML response"} + {"faultstring": "Invalid request id coming from SAML response"}, ) @mock.patch.object( @@ -394,7 +387,7 @@ def test_default_backend_invalid_request_id(self): @mock.patch.object( sso_api_controller.SSO_BACKEND, "verify_response", - mock.MagicMock(return_value={'test': 'user'}), + mock.MagicMock(return_value={"test": "user"}), ) def test_default_backend_invalid_backend_response(self): create_web_sso_request(MOCK_REQUEST_ID) @@ -402,12 +395,14 @@ def test_default_backend_invalid_backend_response(self): self._assert_response( response, http_client.INTERNAL_SERVER_ERROR, - {"faultstring": ( - "Unexpected SSO backend response type." - " Expected BaseSingleSignOnBackendResponse instance!" - )} + { + "faultstring": ( + "Unexpected SSO backend response type." + " Expected BaseSingleSignOnBackendResponse instance!" + ) + }, ) - + @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_id_from_response", @@ -415,15 +410,14 @@ def test_default_backend_invalid_backend_response(self): ) def test_idp_callback_missing_sso_request(self): self._assert_sso_requests_len(0) - response = self._default_callback_request({ - "foo": "bar" - - }, expect_errors=True) + response = self._default_callback_request({"foo": "bar"}, expect_errors=True) self._assert_response( response, http_client.BAD_REQUEST, - {"faultstring": "This SSO request is invalid (it may have already been used)"} + { + "faultstring": "This SSO request is invalid (it may have already been used)" + }, ) @mock.patch.object( @@ -436,15 +430,14 @@ def test_idp_callback_sso_request_expired(self): # Create fake expired request create_web_sso_request(MOCK_REQUEST_ID, -20) self._assert_sso_requests_len(1) - response = self._default_callback_request({ - "foo": "bar" - - }, expect_errors=True) + response = self._default_callback_request({"foo": "bar"}, expect_errors=True) self._assert_response( response, http_client.BAD_REQUEST, - {"faultstring": "The SSO request associated with this response has already expired!"} + { + "faultstring": "The SSO request associated with this response has already expired!" + }, ) @mock.patch.object( @@ -462,12 +455,10 @@ def test_idp_callback_web(self): # Create fake request create_web_sso_request(MOCK_REQUEST_ID) self._assert_sso_requests_len(1) - + # when # Callback based onthe fake request :) -- as mocked above - response = self._default_callback_request({ - "foo": "bar" - }, expect_errors=False) + response = self._default_callback_request({"foo": "bar"}, expect_errors=False) # then # Validate request has been processed and response is as expected @@ -476,14 +467,13 @@ def test_idp_callback_web(self): response, http_client.OK, sso_api_controller.CALLBACK_SUCCESS_RESPONSE_BODY % MOCK_REFERER, - 'str' + "str", ) # Validate token is valid token_data = self._assert_response_has_token_cookie_only(response) self._assert_token_data_is_valid(token_data) - @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_id_from_response", @@ -499,18 +489,18 @@ def test_idp_callback_cli(self): # Create fake request create_cli_sso_request(MOCK_REQUEST_ID, MOCK_CLI_REQUEST_KEY_JSON) self._assert_sso_requests_len(1) - + # when # Callback based onthe fake request :) -- as mocked above - response = self._default_callback_request({ - "foo": "bar" - }, expect_errors=False) + response = self._default_callback_request({"foo": "bar"}, expect_errors=False) # then # Validate request has been processed and response is as expected self._assert_sso_requests_len(0) self.assertEqual(response.status_code, http_client.FOUND) - self.assertRegex(response.location, "^" + MOCK_REFERER + "\?response=[A-Z0-9]+$") + self.assertRegex( + response.location, "^" + MOCK_REFERER + r"\?response=[A-Z0-9]+$" + ) # decrypt token encrypted_response = response.location.split("response=")[1] @@ -521,7 +511,6 @@ def test_idp_callback_cli(self): token_data = json.loads(token_data_json) self._assert_token_data_is_valid(token_data) - @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_id_from_response", @@ -537,23 +526,23 @@ def test_idp_callback_cli_invalid_decryption_key(self): # Create fake request create_cli_sso_request(MOCK_REQUEST_ID, MOCK_CLI_REQUEST_KEY_JSON) self._assert_sso_requests_len(1) - + # when # Callback based onthe fake request :) -- as mocked above - response = self._default_callback_request({ - "foo": "bar" - }, expect_errors=False) + response = self._default_callback_request({"foo": "bar"}, expect_errors=False) # then # Validate request has been processed and response is as expected self._assert_sso_requests_len(0) self.assertEqual(response.status_code, http_client.FOUND) - self.assertRegex(response.location, "^" + MOCK_REFERER + "\?response=[A-Z0-9]+$") + self.assertRegex( + response.location, "^" + MOCK_REFERER + r"\?response=[A-Z0-9]+$" + ) # decrypt token encrypted_response = response.location.split("response=")[1] with self.assertRaises(Exception): - token_data_json = symmetric_decrypt(MOCK_CLI_REQUEST_KEY_ALTERNATIVE, encrypted_response) + symmetric_decrypt(MOCK_CLI_REQUEST_KEY_ALTERNATIVE, encrypted_response) @mock.patch.object( sso_api_controller.SSO_BACKEND, @@ -572,7 +561,6 @@ def test_callback_url_encoded_payload(self): response = self.app.post(SSO_CALLBACK_V1_PATH, data, headers=headers) self.assertEqual(response.status_code, http_client.OK) - @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_id_from_response", diff --git a/st2client/st2client/client.py b/st2client/st2client/client.py index cdc7cd0e35..1e4d23ed5b 100644 --- a/st2client/st2client/client.py +++ b/st2client/st2client/client.py @@ -37,7 +37,6 @@ from st2client.models.core import WorkflowManager from st2client.models.core import ServiceRegistryGroupsManager from st2client.models.core import ServiceRegistryMembersManager -from st2client.models.core import TokenResourceManager from st2client.models.core import add_auth_token_to_kwargs_from_env @@ -146,9 +145,9 @@ def __init__( # Instantiate resource managers and assign appropriate API endpoint. self.managers = dict() self.managers["Token"] = TokenResourceManager( - models.Token, - self.endpoints["auth"], - cacert=self.cacert, + models.Token, + self.endpoints["auth"], + cacert=self.cacert, debug=self.debug, basic_auth=self.basic_auth, ) diff --git a/st2client/st2client/commands/auth.py b/st2client/st2client/commands/auth.py index 8c1cbe19b5..71d75febd0 100644 --- a/st2client/st2client/commands/auth.py +++ b/st2client/st2client/commands/auth.py @@ -19,10 +19,8 @@ import json import logging import os -import dateutil import requests import six -from dateutil import tz from six.moves.configparser import ConfigParser from six.moves import http_client @@ -38,9 +36,11 @@ LOG = logging.getLogger(__name__) + class MissingUserNameException(Exception): pass + class TokenCreateCommand(resource.ResourceCommand): display_attributes = ["user", "token", "expiry"] @@ -122,13 +122,18 @@ def __init__(self, resource, *args, **kwargs): **kwargs, ) - self.parser.add_argument("username", nargs='?', default=None, help="Name of the user to authenticate (not needed if --sso is used).") + self.parser.add_argument( + "username", + nargs="?", + default=None, + help="Name of the user to authenticate (not needed if --sso is used).", + ) self.parser.add_argument( "-s", "--sso", dest="sso", - action='store_true', + action="store_true", help="Whether to use SSO authentication or not. " "If chosen, bypasses username/password.", ) @@ -177,7 +182,10 @@ def run(self, args, **kwargs): # Retrieve token from SSO backend sso_proxy = self.manager.create_sso_request(**kwargs) - print("Please finish your SSO login by visiting: %s" % (sso_proxy.get_proxy_url())) + print( + "Please finish your SSO login by visiting: %s" + % (sso_proxy.get_proxy_url()) + ) token = self.manager.wait_for_sso_token(sso_proxy) # Defaults to username/password if not SSO @@ -190,13 +198,12 @@ def run(self, args, **kwargs): if not args.password: args.password = getpass.getpass() - + # Retrieve token from username/password auth api token = self.manager.create( instance, auth=(args.username, args.password), **kwargs ) - cli._cache_auth_token(token_obj=token) # Update existing configuration with new credentials @@ -208,7 +215,7 @@ def run(self, args, **kwargs): config.add_section("credentials") config.set("credentials", "username", token.user) - + if args.write_password and not args.sso: config.set("credentials", "password", args.password) else: @@ -244,16 +251,14 @@ def run_and_print(self, args, **kwargs): "in the client config file (~/.st2/config)." ) except MissingUserNameException as e: - raise + raise e except Exception as e: if self.app.client.debug: raise if args.sso: - raise Exception( - "Could not perform SSO login: %s" % (six.text_type(e)) - ) - + raise Exception("Could not perform SSO login: %s" % (six.text_type(e))) + else: raise Exception( "Failed to log in as %s: %s" % (args.username, six.text_type(e)) diff --git a/st2client/st2client/models/core.py b/st2client/st2client/models/core.py index 1f40baef95..217c20d7fd 100644 --- a/st2client/st2client/models/core.py +++ b/st2client/st2client/models/core.py @@ -884,9 +884,8 @@ def list(self, group_id, **kwargs): return result - class TokenResourceManager(ResourceManager): - + # This will spin up a local web server to mediate the requests from/to the sso # endpoint, so that we can intercept the callback and token :) # @@ -901,29 +900,29 @@ def create_sso_request(self, **kwargs) -> sso_interceptor.SSOInterceptorProxy: sso_proxy = sso_interceptor.SSOInterceptorProxy(key) response = self.client.post( - url, - { - "key": key.to_json(), - "callback_url": sso_proxy.get_callback_url() - }, - **kwargs + url, + {"key": key.to_json(), "callback_url": sso_proxy.get_callback_url()}, + **kwargs, ) if response.status_code != http_client.OK: self.handle_error(response) json_response = response.json() if not type(json_response) is dict: - raise ValueError("Expected response body from SSO CLI request, but couldn't find one :( ") - + raise ValueError( + "Expected response body from SSO CLI request, but couldn't find one :( " + ) + sso_url = response.json().get("sso_url", None) if sso_url is None: - raise ValueError("Expected SSO URL to be present in SSO login request response!") + raise ValueError( + "Expected SSO URL to be present in SSO login request response!" + ) sso_proxy.set_sso_url(sso_url) LOG.debug("Received SSO URL with lenght %d", len(sso_url)) return sso_proxy - def wait_for_sso_token(self, sso_proxy): - return self.resource.deserialize(sso_proxy.get_token()) \ No newline at end of file + return self.resource.deserialize(sso_proxy.get_token()) diff --git a/st2client/st2client/utils/sso_interceptor.py b/st2client/st2client/utils/sso_interceptor.py index efad506b4a..166e10b18e 100644 --- a/st2client/st2client/utils/sso_interceptor.py +++ b/st2client/st2client/utils/sso_interceptor.py @@ -15,7 +15,6 @@ # import json import logging -from multiprocessing.sharedctypes import Value from threading import Thread import time from urllib.parse import urlparse, parse_qs @@ -26,6 +25,7 @@ LOG = logging.getLogger(__name__) + # Implements a local HTTP server used to intercept calls from/to SSO endpoints :) # via callback URLs class SSOInterceptorProxy: @@ -43,10 +43,14 @@ class SSOInterceptorProxy: def __init__(self, key): - self.server = HTTPServer(('localhost', 0), createSSOProxyHandler(self)) + self.server = HTTPServer(("localhost", 0), createSSOProxyHandler(self)) self.key = key - LOG.debug("Initialized SSO interceptor proxy at port %d and url id %s, SSO URL is still pending", self.server.server_port, self.url_id) + LOG.debug( + "Initialized SSO interceptor proxy at port %d and url id %s, SSO URL is still pending", + self.server.server_port, + self.url_id, + ) self.thread = Thread(target=self.server.serve_forever) self.thread.setDaemon(True) @@ -67,20 +71,23 @@ def callback_received(self, token): self.token = token def get_token(self, timeout=90): - LOG.debug("Waiting for token to be received from SSO flow.. will timeout after [%s]s", timeout) + LOG.debug( + "Waiting for token to be received from SSO flow.. will timeout after [%s]s", + timeout, + ) timeout_at = time.time() + timeout while time.time() < timeout_at: if self.token is not None: return self.token time.sleep(0.5) - - raise TimeoutError("Token was not received from SSO flow before the timeout of %ss"%timeout) + raise TimeoutError( + "Token was not received from SSO flow before the timeout of %ss" % timeout + ) def createSSOProxyHandler(interceptor: SSOInterceptorProxy): class SSOProxyServer(BaseHTTPRequestHandler): - def do_GET(self): o = urlparse(self.path) @@ -103,12 +110,11 @@ def do_GET(self): LOG.debug("Unexpected internal server error! %e", e) self.send_error(500, explain="Unexpected error!" % str(e)) - # This request is not expected by the sso proxy def _handle_unexpected_request(self): self.send_error(404, explain="The selected URL does not exist!") self.end_headers() - + # This request is to redirect the user to the proper sso place # -- can only be achieve with the proper key :) def _handle_sso_login(self): @@ -116,22 +122,29 @@ def _handle_sso_login(self): self.send_response(307) self.send_header("Location", interceptor.sso_url) self.end_headers() - + # This request should have all the callback data we are expecting # -- this means an encrypted key to be decrypted and used by the CLI :) def _handle_callbakc(self, response): LOG.debug("Intercepting SSO callback response!") - if (response is None): - raise ValueError("Expected 'response' field with encrypted key in callback!") - - token = symmetric_decrypt(interceptor.key, response.encode('utf-8')) + if response is None: + raise ValueError( + "Expected 'response' field with encrypted key in callback!" + ) + + token = symmetric_decrypt(interceptor.key, response.encode("utf-8")) try: token_json = json.loads(token) - LOG.debug("Successful SSO login for user %s, redirecting to successful page!", token_json.get('user', None)) + LOG.debug( + "Successful SSO login for user %s, redirecting to successful page!", + token_json.get("user", None), + ) except: - raise ValueError("Could not understand the incoming SSO callback response") - + raise ValueError( + "Could not understand the incoming SSO callback response" + ) + interceptor.callback_received(token) self.send_response(302) self.send_header("Location", "/success") @@ -142,22 +155,26 @@ def _handle_callbakc(self, response): def _handle_success(self): self.send_response(200) self.end_headers() - self.wfile.write(bytes(""" + self.wfile.write( + bytes( + """ SSO Login Successful -
+
Successfully logged into StackStorm using SSO!
Please check your terminal
You may now close this page
- """ - ,"utf-8")) - + """, + "utf-8", + ) + ) def log_message(self, format, *args): - LOG.debug("%s " + format, "SSO Proxy: ", *args) + LOG.debug("%s " + format, "SSO Proxy: ", *args) return return SSOProxyServer diff --git a/st2client/tests/unit/test_auth.py b/st2client/tests/unit/test_auth.py index f82813331d..367a33c4ac 100644 --- a/st2client/tests/unit/test_auth.py +++ b/st2client/tests/unit/test_auth.py @@ -164,6 +164,7 @@ def runTest(self): os.path.isfile("%stoken-%s" % (self.DOTST2_PATH, expected_username)) ) + class TestLoginWithMissingUsername(TestLoginBase): CONFIG_FILE_NAME = "logintest.cfg" @@ -184,7 +185,7 @@ class TestLoginWithMissingUsername(TestLoginBase): def runTest(self): """Test 'st2 login' functionality missing the username and should fail""" - expected_username = self.TOKEN["user"] + expected_username = self.TOKEN["user"] # noqa args = [ "--config", self.CONFIG_FILE, diff --git a/st2common/st2common/models/db/auth.py b/st2common/st2common/models/db/auth.py index ee795954a1..ccb8558cea 100644 --- a/st2common/st2common/models/db/auth.py +++ b/st2common/st2common/models/db/auth.py @@ -85,19 +85,19 @@ class TokenDB(stormbase.StormFoundationDB): ) service = me.BooleanField(required=True, default=False) -class SSORequestDB(stormbase.StormFoundationDB): +class SSORequestDB(stormbase.StormFoundationDB): class Type(Enum): - CLI = 'cli' - WEB = 'web' - + CLI = "cli" + WEB = "web" + """ An entity representing a SSO request. Attribute: request_id: Reference to the SSO request unique ID expiry: Time at which this request expires. - type: What type of SSO request is this? web/cli + type: What type of SSO request is this? web/cli -- cli -- key: Symmetric key used to encrypt/decrypt contents from/to the CLI. @@ -107,7 +107,7 @@ class Type(Enum): key = me.StringField(required=False, unique=False) expiry = me.DateTimeField(required=True) type = me.EnumField(Type, required=True) - + class ApiKeyDB(stormbase.StormFoundationDB, stormbase.UIDFieldMixin): """ diff --git a/st2common/st2common/persistence/auth.py b/st2common/st2common/persistence/auth.py index 99f25154f1..376e6fffdb 100644 --- a/st2common/st2common/persistence/auth.py +++ b/st2common/st2common/persistence/auth.py @@ -60,7 +60,6 @@ def _get_by_object(cls, object): return cls.get_by_name(name) - class SSORequest(Access): impl = MongoDBAccess(SSORequestDB) diff --git a/st2common/st2common/services/access.py b/st2common/st2common/services/access.py index 157e9018a7..53b75ffe0d 100644 --- a/st2common/st2common/services/access.py +++ b/st2common/st2common/services/access.py @@ -21,13 +21,24 @@ from st2common.util import isotime from st2common.util import date as date_utils -from st2common.exceptions.auth import SSORequestNotFoundError, TokenNotFoundError, UserNotFoundError +from st2common.exceptions.auth import ( + SSORequestNotFoundError, + TokenNotFoundError, + UserNotFoundError, +) from st2common.exceptions.auth import TTLTooLargeException from st2common.models.db.auth import SSORequestDB, TokenDB, UserDB from st2common.persistence.auth import SSORequest, Token, User from st2common import log as logging -__all__ = ["create_token", "delete_token", "create_cli_sso_request", "create_web_sso_request", "get_sso_request_by_request_id", "delete_sso_request"] +__all__ = [ + "create_token", + "delete_token", + "create_cli_sso_request", + "create_web_sso_request", + "get_sso_request_by_request_id", + "delete_sso_request", +] LOG = logging.getLogger(__name__) @@ -108,6 +119,7 @@ def delete_token(token): except Exception: raise + def create_cli_sso_request(request_id, key, ttl=DEFAULT_SSO_REQUEST_TTL): """ :param request_id: ID of the SSO request that is being created (usually uuid format prepended by _) @@ -122,6 +134,7 @@ def create_cli_sso_request(request_id, key, ttl=DEFAULT_SSO_REQUEST_TTL): return _create_sso_request(request_id, ttl, SSORequestDB.Type.CLI, key=key) + def create_web_sso_request(request_id, ttl=DEFAULT_SSO_REQUEST_TTL): """ :param request_id: ID of the SSO request that is being created (usually uuid format prepended by _) @@ -133,16 +146,12 @@ def create_web_sso_request(request_id, ttl=DEFAULT_SSO_REQUEST_TTL): return _create_sso_request(request_id, ttl, SSORequestDB.Type.WEB) + def _create_sso_request(request_id, ttl, type, **kwargs) -> SSORequestDB: expiry = date_utils.get_datetime_utc_now() + datetime.timedelta(seconds=ttl) - - request = SSORequestDB( - request_id=request_id, - expiry=expiry, - type=type, - **kwargs - ) + + request = SSORequestDB(request_id=request_id, expiry=expiry, type=type, **kwargs) SSORequest.add_or_update(request) expire_string = isotime.format(expiry, offset=False) diff --git a/st2common/st2common/util/crypto.py b/st2common/st2common/util/crypto.py index 5cca2bec5c..e6be862101 100644 --- a/st2common/st2common/util/crypto.py +++ b/st2common/st2common/util/crypto.py @@ -190,6 +190,7 @@ def read_crypto_key(key_path): msg = 'Invalid or malformed key file "%s": %s' % (key_path, six.text_type(e)) raise KeyError(msg) + def read_crypto_key_from_dict(key_dict): """ Read crypto key from provided Keyczar JSON-format dict and return parsed AESKey object. @@ -209,7 +210,7 @@ def read_crypto_key_from_dict(key_dict): size=key_dict["size"], ) except KeyError as e: - msg = 'Invalid or malformed AES key dictionary: %s' % (six.text_type(e)) + msg = "Invalid or malformed AES key dictionary: %s" % (six.text_type(e)) raise KeyError(msg) return aes_key From cbf99ad418040c6e8bb9841ba056a625d57022c4 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 19 Jul 2022 16:17:07 -0300 Subject: [PATCH 14/49] removing role mapping and using group mapping instead --- st2auth/st2auth/controllers/v1/sso.py | 12 ++- st2auth/st2auth/handlers.py | 91 +++++++------------ st2auth/st2auth/sso/base.py | 15 +-- st2auth/tests/unit/controllers/v1/test_sso.py | 4 +- 4 files changed, 52 insertions(+), 70 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index b78243614d..86f73563c2 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -99,19 +99,20 @@ def post(self, response, **kwargs): if not isinstance(verified_user, BaseSingleSignOnBackendResponse): return process_failure_response( http_client.INTERNAL_SERVER_ERROR, - "Unexpected SSO backend response type. Expected BaseSingleSignOnBackendResponse instance!", + "Unexpected SSO backend response type. Expected " + "BaseSingleSignOnBackendResponse instance!", ) LOG.info( - "Authenticating SSO user [%s] with roles [%s]", + "Authenticating SSO user [%s] with groups [%s]", verified_user.username, - verified_user.roles, + verified_user.groups, ) st2_auth_token_create_request = { "user": verified_user.username, "ttl": None, - "roles": verified_user.roles, + "groups": verified_user.groups, } st2_auth_token = self.st2_auth_handler.handle_auth( @@ -298,7 +299,8 @@ def process_successful_sso_cli_response(callback_url, key, token): 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", + "Redirecting successfuly SSO CLI login to url [%s] " + "with extra parameters for the encrypted token", callback_url, ) diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index a41223d7df..d6784d750e 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -24,8 +24,6 @@ from st2common.exceptions.db import StackStormDBObjectNotFoundError from st2common.exceptions.auth import NoNicknameOriginProvidedError, AmbiguousUserError from st2common.exceptions.auth import NotServiceUserError -from st2common.models.db.rbac import UserRoleAssignmentDB -from st2common.persistence.rbac import UserRoleAssignment from st2common.persistence.auth import User from st2common.router import abort from st2common.services.access import create_token @@ -55,36 +53,38 @@ def handle_auth( ): raise NotImplementedError() - def _get_roles_for_request(self, request): - if type(request) is dict: - return request.get("roles", []) - return getattr(request, "roles", None) + def sync_user_groups(self, extra, username, groups): + + if groups is None or len(groups) == 0: + LOG.debug("No groups to sync for user '%s'", username) + return + + extra["username"] = username + extra["user_groups"] = groups - def _sync_roles_for_user(self, username, roles): LOG.debug( - "Syncing roles [%s] for user [%s] (deleting all " - "roles first and attaching them again)", - roles, - username, + 'Found "%s" groups for user "%s"' % (len(groups), username), + extra=extra, ) - # Delete all role assignments - role_assignments = UserRoleAssignment.get_all(user=username) - for role_assignment in role_assignments: - role_assignment.delete() - - # Assign roles for each role - for role in roles: - # Assign role to user - role_assignment_db = UserRoleAssignmentDB( - user=username, - source="API", - role=role, - description="Synced by ProxyAuth", - is_remote=True, - ) - UserRoleAssignment.add_or_update(role_assignment_db) - LOG.debug("Roles successfully synced for user [%s]", username) + user_db = UserDB(name=username) + + rbac_backend = get_rbac_backend() + syncer = rbac_backend.get_remote_group_to_role_syncer() + + try: + syncer.sync(user_db=user_db, groups=groups) + except Exception: + # Note: Failed sync is not fatal + LOG.exception( + 'Failed to synchronize remote groups for user "%s"' % (username), + extra=extra, + ) + else: + LOG.debug( + 'Successfully synchronized groups for user "%s"' % (username), + extra=extra, + ) def _create_token_for_user(self, username, ttl=None): tokendb = create_token(username=username, ttl=ttl) @@ -168,8 +168,11 @@ def handle_auth( username = self._get_username_for_request(remote_user, request) try: token = self._create_token_for_user(username=username, ttl=ttl) - roles = self._get_roles_for_request(request) - self._sync_roles_for_user(username, roles) + groups = getattr(request, "groups", None) + + if cfg.CONF.rbac.backend != "noop": + self.sync_user_groups(extra, username, groups) + except TTLTooLargeException as e: abort_request( status_code=http_client.BAD_REQUEST, message=six.text_type(e) @@ -263,33 +266,7 @@ def handle_auth( # No groups, return early return token - extra["username"] = username - extra["user_groups"] = user_groups - - LOG.debug( - 'Found "%s" groups for user "%s"' % (len(user_groups), username), - extra=extra, - ) - - user_db = UserDB(name=username) - - rbac_backend = get_rbac_backend() - syncer = rbac_backend.get_remote_group_to_role_syncer() - - try: - syncer.sync(user_db=user_db, groups=user_groups) - except Exception: - # Note: Failed sync is not fatal - LOG.exception( - 'Failed to synchronize remote groups for user "%s"' - % (username), - extra=extra, - ) - else: - LOG.debug( - 'Successfully synchronized groups for user "%s"' % (username), - extra=extra, - ) + self.sync_user_groups(extra, username, user_groups) return token return token diff --git a/st2auth/st2auth/sso/base.py b/st2auth/st2auth/sso/base.py index 7692cce6ec..c2a4dff1e5 100644 --- a/st2auth/st2auth/sso/base.py +++ b/st2auth/st2auth/sso/base.py @@ -25,23 +25,23 @@ class BaseSingleSignOnBackendResponse(object): username: str = None referer: str = None - roles: List[str] = None + groups: List[str] = None - def __init__(self, username=None, referer=None, roles=[]): + def __init__(self, username=None, referer=None, groups=[]): self.username = username - self.roles = roles + self.groups = groups self.referer = referer def __eq__(self, other): return ( self.username == other.username - and self.roles == other.roles + and self.groups == other.groups and self.referer == other.referer ) def __repr__(self): return ( - f"BaseSingleSignOnBackendResponse(username={self.username}, roles={self.roles}" + f"BaseSingleSignOnBackendResponse(username={self.username}, groups={self.groups}" + f", referer={self.referer}" ) @@ -57,7 +57,10 @@ def get_request_redirect_url(self, referer) -> str: raise NotImplementedError(msg) def get_request_id_from_response(self, response) -> str: - msg = 'The function "get_request_id_from_response" is not implemented in the base SSO backend.' + msg = ( + 'The function "get_request_id_from_response" is not implemented' + "in the base SSO backend." + ) raise NotImplementedError(msg) def verify_response(self, response) -> BaseSingleSignOnBackendResponse: diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 950b6e64d0..d12abae7e0 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -74,7 +74,7 @@ MOCK_REQUEST_ID = "test-id" MOCK_GROUPS = ["test", "test2"] MOCK_VERIFIED_USER_OBJECT = BaseSingleSignOnBackendResponse( - referer=MOCK_REFERER, roles=MOCK_GROUPS, username=MOCK_USER + referer=MOCK_REFERER, groups=MOCK_GROUPS, username=MOCK_USER ) @@ -144,7 +144,7 @@ def _assert_sso_request_success(self, sso_request, type): def _test_cli_request_bad_parameter_helper(self, params, expected_error): response = self._default_cli_request(params=params, expect_errors=True) self._assert_response( - response, http_client.INTERNAL_SERVER_ERROR, {"faultstring": expected_error} + response, http_client.BAD_REQUEST, {"faultstring": expected_error} ) self._assert_sso_requests_len(0) From aff15701fd284fc97b217f231f715b2e9c046566 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 20 Jul 2022 10:17:24 -0300 Subject: [PATCH 15/49] treating comparison --- st2auth/st2auth/sso/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/st2auth/st2auth/sso/base.py b/st2auth/st2auth/sso/base.py index c2a4dff1e5..970a798128 100644 --- a/st2auth/st2auth/sso/base.py +++ b/st2auth/st2auth/sso/base.py @@ -33,6 +33,8 @@ def __init__(self, username=None, referer=None, groups=[]): self.referer = referer def __eq__(self, other): + if other is None: + return False return ( self.username == other.username and self.groups == other.groups From f494680296de5501711811c6046a58919a6ab248 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 20 Jul 2022 13:52:04 -0300 Subject: [PATCH 16/49] fixing lint :) --- st2auth/st2auth/controllers/v1/sso.py | 1 + st2common/st2common/exceptions/auth.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index 86f73563c2..337332f562 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -202,6 +202,7 @@ def post_cli(self, response): 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", } diff --git a/st2common/st2common/exceptions/auth.py b/st2common/st2common/exceptions/auth.py index 07ed8c2cc6..15a04a5680 100644 --- a/st2common/st2common/exceptions/auth.py +++ b/st2common/st2common/exceptions/auth.py @@ -31,13 +31,14 @@ "AmbiguousUserError", "NotServiceUserError", "SSOVerificationError", - "SSORequestNotFoundError" + "SSORequestNotFoundError", ] class TokenNotProvidedError(StackStormBaseException): pass + class SSORequestNotFoundError(StackStormBaseException): pass From d96719781ea7f3aa320406d35765dc7bb60c6f79 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 20 Jul 2022 13:52:15 -0300 Subject: [PATCH 17/49] adding requirement --- st2common/st2common/openapi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index b573e385a6..d2eed861e4 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -4555,14 +4555,15 @@ paths: key: type: string description: The symmetric key to be used to encrypt contents of callback + required: true callback_url: type: string description: What URL to be called back once the response from SSO is received + required: true responses: '200': description: SSO request valid security: [] - /auth/v1/sso/callback: post: operationId: st2auth.controllers.v1.sso:idp_callback_controller.post @@ -5398,7 +5399,6 @@ definitions: type: - object - array - ActionParametersSubSchema: type: object description: Input parameters for the action. From 52851dbe8201d91122d886c61e2d54a5276d7337 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 20 Jul 2022 14:06:10 -0300 Subject: [PATCH 18/49] adding changelog --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 024cb6abc1..24ea85450f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,15 @@ Changelog in development -------------- +Added +~~~~~ + +* Added support for SSO backends #5664 + + SAML backend on a separate [repository](https://github.com/StackStorm/st2-auth-backend-sso-saml2) + + Contributed by @pimguilherme + Fixed ~~~~~ From df972ecc4c40c0cd5551bc5dcde1d542fc4bb023 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 20 Jul 2022 15:48:05 -0300 Subject: [PATCH 19/49] adding saml to test --- st2auth/in-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/st2auth/in-requirements.txt b/st2auth/in-requirements.txt index d6de70f4f8..77ee1f495b 100644 --- a/st2auth/in-requirements.txt +++ b/st2auth/in-requirements.txt @@ -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 # For backward compatibility reasons, flat file backend is installed by default git+https://github.com/StackStorm/st2-auth-backend-flat-file.git@master#egg=st2-auth-backend-flat-file git+https://github.com/StackStorm/st2-auth-ldap.git@master#egg=st2-auth-ldap From b08372896b3b11d2cb50ff5558034e41b75a4df5 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Fri, 22 Jul 2022 14:26:52 -0300 Subject: [PATCH 20/49] adding some rbac-related tests --- st2auth/st2auth/handlers.py | 3 +- st2auth/tests/unit/controllers/v1/test_sso.py | 101 ++++++++++++++++-- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index d6784d750e..f2a61c3650 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -162,13 +162,14 @@ def handle_auth( ): remote_addr = headers.get("x-forwarded-for", remote_addr) extra = {"remote_addr": remote_addr} + LOG.debug("Authenticating for proxy with request [%s]", request) if remote_user: ttl = getattr(request, "ttl", None) username = self._get_username_for_request(remote_user, request) try: token = self._create_token_for_user(username=username, ttl=ttl) - groups = getattr(request, "groups", None) + groups = request.get("groups", None) if cfg.CONF.rbac.backend != "noop": self.sync_user_groups(extra, username, groups) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index d12abae7e0..9016a28b17 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -16,6 +16,8 @@ from st2auth.sso.base import BaseSingleSignOnBackendResponse from st2common.models.db.auth import SSORequestDB from st2common.persistence.auth import SSORequest, Token +from st2common.persistence.rbac import GroupToRoleMapping, UserRoleAssignment, Role +from st2common.models.db.rbac import GroupToRoleMappingDB, UserRoleAssignmentDB, RoleDB from st2common.services.access import ( DEFAULT_SSO_REQUEST_TTL, create_web_sso_request, @@ -110,13 +112,17 @@ def test_unknown_exception(self): class TestSingleSignOnRequestController(FunctionalTest): # - # Helpers - # + # Settupers + # # Cleanup sso requests def setUp(self): for x in SSORequest.get_all(): SSORequest.delete(x) + + # + # Helpers + # def _assert_response(self, response, status_code, expected_body): self.assertEqual(response.status_code, status_code) @@ -292,13 +298,59 @@ def test_cli_default_backend(self): class TestIdentityProviderCallbackController(FunctionalTest): - # Helpers - # def setUp(self): for x in SSORequest.get_all(): SSORequest.delete(x) + def setUp_for_rbac(self): + # Set up standard roles + for x in Role.get_all(): + Role.delete(x) + + RoleDB(name="system_admin", system=True).save() + RoleDB(name="admin", system=True).save() + RoleDB(name="my-test", system=True).save() + + # Cleanup user assignments + for x in UserRoleAssignment.get_all(): + UserRoleAssignment.delete(x) + + # Set up assignment mappings + for x in GroupToRoleMapping.get_all(): + SSORequest.delete(x) + + GroupToRoleMappingDB( + group="test2", + roles=["system_admin", "admin"], + source="test", + enabled=True + ).save() + + GroupToRoleMappingDB( + group="test", + roles=["my-test"], + source="test", + enabled=True + ).save() + + cfg.CONF.set_override(group="rbac", name="enable", override=True) + cfg.CONF.set_override(group="rbac", name="backend", override="default") + + def tearDown_for_rbac(self): + + for x in UserRoleAssignment.get_all(): + UserRoleAssignment.delete(x) + + for x in GroupToRoleMapping.get_all(): + SSORequest.delete(x) + + for x in Role.get_all(): + Role.delete(x) + + # Helpers + # + def _assert_response( self, response, status_code, expected_body, response_type="json" ): @@ -313,6 +365,11 @@ def _assert_sso_requests_len(self, expected): self.assertEqual(len(sso_requests), expected) return sso_requests + def _assert_role_assignment_len(self, expected): + role_assignments: List[UserRoleAssignment] = UserRoleAssignment.get_all() + self.assertEqual(len(role_assignments), expected) + return role_assignments + def _assert_token_data_is_valid(self, token_data): self.assertEqual(token_data["user"], MOCK_USER) self.assertIsNotNone(token_data["expiry"]) @@ -450,7 +507,7 @@ def test_idp_callback_sso_request_expired(self): "verify_response", mock.MagicMock(return_value=MOCK_VERIFIED_USER_OBJECT), ) - def test_idp_callback_web(self): + def _test_idp_callback_web(self): # given # Create fake request create_web_sso_request(MOCK_REQUEST_ID) @@ -473,6 +530,22 @@ def test_idp_callback_web(self): # Validate token is valid token_data = self._assert_response_has_token_cookie_only(response) self._assert_token_data_is_valid(token_data) + + + def test_idp_callback_web_without_rbac(self): + self._assert_role_assignment_len(0) + self._test_idp_callback_web() + self._assert_role_assignment_len(0) + + def test_idp_callback_web_with_rbac(self): + self.setUp_for_rbac() + self._assert_role_assignment_len(0) + + self._test_idp_callback_web() + + self._assert_role_assignment_len(3) + self.tearDown_for_rbac() + @mock.patch.object( sso_api_controller.SSO_BACKEND, @@ -484,7 +557,7 @@ def test_idp_callback_web(self): "verify_response", mock.MagicMock(return_value=MOCK_VERIFIED_USER_OBJECT), ) - def test_idp_callback_cli(self): + def _test_idp_callback_cli(self): # given # Create fake request create_cli_sso_request(MOCK_REQUEST_ID, MOCK_CLI_REQUEST_KEY_JSON) @@ -511,6 +584,20 @@ def test_idp_callback_cli(self): token_data = json.loads(token_data_json) self._assert_token_data_is_valid(token_data) + def test_idp_callback_cli_without_rbac(self): + self._assert_role_assignment_len(0) + self._test_idp_callback_cli() + self._assert_role_assignment_len(0) + + def test_idp_callback_cli_with_rbac(self): + self.setUp_for_rbac() + self._assert_role_assignment_len(0) + + self._test_idp_callback_cli() + + self._assert_role_assignment_len(3) + self.tearDown_for_rbac() + @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_id_from_response", @@ -526,6 +613,7 @@ def test_idp_callback_cli_invalid_decryption_key(self): # Create fake request create_cli_sso_request(MOCK_REQUEST_ID, MOCK_CLI_REQUEST_KEY_JSON) self._assert_sso_requests_len(1) + self._assert_role_assignment_len(0) # when # Callback based onthe fake request :) -- as mocked above @@ -534,6 +622,7 @@ def test_idp_callback_cli_invalid_decryption_key(self): # then # Validate request has been processed and response is as expected self._assert_sso_requests_len(0) + self._assert_role_assignment_len(0) self.assertEqual(response.status_code, http_client.FOUND) self.assertRegex( response.location, "^" + MOCK_REFERER + r"\?response=[A-Z0-9]+$" From 982daa093b8819c8ad2aa3dbab328563040205e8 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Fri, 22 Jul 2022 14:58:23 -0300 Subject: [PATCH 21/49] adjusting some tests --- CHANGELOG.rst | 6 ++- requirements.txt | 1 + st2auth/requirements.txt | 1 + st2auth/tests/unit/controllers/v1/test_sso.py | 47 +++++++------------ 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 179cf30a3e..15f7c4ed50 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,9 +7,11 @@ in development Added ~~~~~ -* Added support for SSO backends #5664 +* Revised support for SSO backends + SAML2 included by default #5664 - SAML backend on a separate [repository](https://github.com/StackStorm/st2-auth-backend-sso-saml2) + 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 diff --git a/requirements.txt b/requirements.txt index e3890d0b80..6df7b5304d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ git+https://github.com/StackStorm/orquesta.git@v1.5.0#egg=orquesta git+https://github.com/StackStorm/st2-auth-backend-flat-file.git@master#egg=st2-auth-backend-flat-file git+https://github.com/StackStorm/st2-auth-ldap.git@master#egg=st2-auth-ldap git+https://github.com/StackStorm/st2-rbac-backend.git@master#egg=st2-rbac-backend +git+https://github.com/pimguilherme/st2-auth-backend-sso-saml2.git@feat/saml#egg=st2-auth-backend-sso-saml2 gitdb==4.0.2 gitpython==3.1.15 greenlet==1.0.0 diff --git a/st2auth/requirements.txt b/st2auth/requirements.txt index 6b6016bcb6..cb96607118 100644 --- a/st2auth/requirements.txt +++ b/st2auth/requirements.txt @@ -9,6 +9,7 @@ bcrypt==3.2.0 eventlet==0.30.2 git+https://github.com/StackStorm/st2-auth-backend-flat-file.git@master#egg=st2-auth-backend-flat-file git+https://github.com/StackStorm/st2-auth-ldap.git@master#egg=st2-auth-ldap +git+https://github.com/pimguilherme/st2-auth-backend-sso-saml2.git@feat/saml#egg=st2-auth-backend-sso-saml2 gunicorn==20.1.0 oslo.config>=1.12.1,<1.13 passlib==1.7.4 diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 9016a28b17..d0a0fffff0 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -12,12 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from tests.base import FunctionalTest +from st2common.exceptions import auth as auth_exc +from st2auth.sso import noop +from st2auth.controllers.v1 import sso as sso_api_controller +from six.moves import urllib +from six.moves import http_client +from oslo_config import cfg +import mock +import json from typing import List from st2auth.sso.base import BaseSingleSignOnBackendResponse from st2common.models.db.auth import SSORequestDB from st2common.persistence.auth import SSORequest, Token from st2common.persistence.rbac import GroupToRoleMapping, UserRoleAssignment, Role -from st2common.models.db.rbac import GroupToRoleMappingDB, UserRoleAssignmentDB, RoleDB +from st2common.models.db.rbac import GroupToRoleMappingDB, RoleDB from st2common.services.access import ( DEFAULT_SSO_REQUEST_TTL, create_web_sso_request, @@ -30,18 +39,6 @@ tests_config.parse_args() -import json -import mock - -from oslo_config import cfg -from six.moves import http_client -from six.moves import urllib - -from st2auth.controllers.v1 import sso as sso_api_controller -from st2auth.sso import noop -from st2common.exceptions import auth as auth_exc -from tests.base import FunctionalTest - SSO_V1_PATH = "/v1/sso" SSO_REQUEST_WEB_V1_PATH = SSO_V1_PATH + "/request/web" @@ -113,16 +110,16 @@ class TestSingleSignOnRequestController(FunctionalTest): # # Settupers - # + # # Cleanup sso requests def setUp(self): for x in SSORequest.get_all(): SSORequest.delete(x) - + # # Helpers - # + # def _assert_response(self, response, status_code, expected_body): self.assertEqual(response.status_code, status_code) @@ -297,8 +294,6 @@ def test_cli_default_backend(self): class TestIdentityProviderCallbackController(FunctionalTest): - - def setUp(self): for x in SSORequest.get_all(): SSORequest.delete(x) @@ -321,17 +316,11 @@ def setUp_for_rbac(self): SSORequest.delete(x) GroupToRoleMappingDB( - group="test2", - roles=["system_admin", "admin"], - source="test", - enabled=True + group="test2", roles=["system_admin", "admin"], source="test", enabled=True ).save() GroupToRoleMappingDB( - group="test", - roles=["my-test"], - source="test", - enabled=True + group="test", roles=["my-test"], source="test", enabled=True ).save() cfg.CONF.set_override(group="rbac", name="enable", override=True) @@ -530,7 +519,6 @@ def _test_idp_callback_web(self): # Validate token is valid token_data = self._assert_response_has_token_cookie_only(response) self._assert_token_data_is_valid(token_data) - def test_idp_callback_web_without_rbac(self): self._assert_role_assignment_len(0) @@ -542,11 +530,10 @@ def test_idp_callback_web_with_rbac(self): self._assert_role_assignment_len(0) self._test_idp_callback_web() - + self._assert_role_assignment_len(3) self.tearDown_for_rbac() - @mock.patch.object( sso_api_controller.SSO_BACKEND, "get_request_id_from_response", @@ -594,7 +581,7 @@ def test_idp_callback_cli_with_rbac(self): self._assert_role_assignment_len(0) self._test_idp_callback_cli() - + self._assert_role_assignment_len(3) self.tearDown_for_rbac() From 96834e8dcaf970a57bc446a2eab0ae0662a3a86f Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Fri, 22 Jul 2022 15:46:18 -0300 Subject: [PATCH 22/49] adjusting tests --- st2auth/st2auth/controllers/v1/sso.py | 11 ++++++----- st2auth/st2auth/handlers.py | 2 +- st2auth/tests/unit/controllers/v1/test_sso.py | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index 337332f562..543585265e 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -19,6 +19,7 @@ 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 @@ -109,11 +110,11 @@ def post(self, response, **kwargs): verified_user.groups, ) - st2_auth_token_create_request = { - "user": verified_user.username, - "ttl": None, - "groups": verified_user.groups, - } + st2_auth_token_create_request = GenericRequestParam( + user=verified_user.username, + ttl=None, + groups=verified_user.groups, + ) st2_auth_token = self.st2_auth_handler.handle_auth( request=st2_auth_token_create_request, diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index f2a61c3650..7328e886cd 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -169,7 +169,7 @@ def handle_auth( username = self._get_username_for_request(remote_user, request) try: token = self._create_token_for_user(username=username, ttl=ttl) - groups = request.get("groups", None) + groups = getattr(request, "groups", None) if cfg.CONF.rbac.backend != "noop": self.sync_user_groups(extra, username, groups) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index d0a0fffff0..9900c8c6d7 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -337,6 +337,9 @@ def tearDown_for_rbac(self): for x in Role.get_all(): Role.delete(x) + cfg.CONF.set_override(group="rbac", name="enable", override=False) + cfg.CONF.set_override(group="rbac", name="backend", override="default") + # Helpers # From 33b5abf2ffd57e28368a1eb727e0ce733588dd0c Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Fri, 22 Jul 2022 17:53:28 -0300 Subject: [PATCH 23/49] adjusting input parameters to proxy --- st2auth/st2auth/controllers/v1/sso.py | 10 ++++++---- st2auth/st2auth/handlers.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index 543585265e..381991d226 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -93,7 +93,8 @@ def _validate_and_delete_sso_request(self, response): def post(self, response, **kwargs): try: - original_sso_request = self._validate_and_delete_sso_request(response) + 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) @@ -111,7 +112,6 @@ def post(self, response, **kwargs): ) st2_auth_token_create_request = GenericRequestParam( - user=verified_user.username, ttl=None, groups=verified_user.groups, ) @@ -195,7 +195,8 @@ def post_cli(self, response): "The provided key is invalid! It should be stackstorm-compatible AES key" ) - sso_request = self._create_sso_request(create_cli_sso_request, key=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 = { @@ -308,7 +309,8 @@ def process_successful_sso_cli_response(callback_url, key, token): # 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")) + resp.location = "%s?response=%s" % ( + callback_url, encrypted_token.decode("utf-8")) return resp diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index 7328e886cd..dc3ab19e4e 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -162,7 +162,7 @@ def handle_auth( ): remote_addr = headers.get("x-forwarded-for", remote_addr) extra = {"remote_addr": remote_addr} - LOG.debug("Authenticating for proxy with request [%s]", request) + LOG.debug("Authenticating for proxy with request [%s]", request.__dict__ if request else None) if remote_user: ttl = getattr(request, "ttl", None) From 157ad4e8b34c1833cbafc7adecb74ee2708e18a8 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 25 Jul 2022 17:17:35 -0300 Subject: [PATCH 24/49] adding proxy handler tests --- st2auth/st2auth/handlers.py | 6 +- st2auth/tests/unit/controllers/v1/test_sso.py | 4 +- st2auth/tests/unit/test_handlers.py | 113 +++++++++++++++++- 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index dc3ab19e4e..ee55b12ec8 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -55,7 +55,7 @@ def handle_auth( def sync_user_groups(self, extra, username, groups): - if groups is None or len(groups) == 0: + if groups is None: LOG.debug("No groups to sync for user '%s'", username) return @@ -162,8 +162,8 @@ def handle_auth( ): remote_addr = headers.get("x-forwarded-for", remote_addr) extra = {"remote_addr": remote_addr} - LOG.debug("Authenticating for proxy with request [%s]", request.__dict__ if request else None) - + LOG.debug("Authenticating for proxy with request [%s]", getattr(request, "__dict__", None) if request else None) + if remote_user: ttl = getattr(request, "ttl", None) username = self._get_username_for_request(remote_user, request) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 9900c8c6d7..ad2b0e52c9 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -313,7 +313,7 @@ def setUp_for_rbac(self): # Set up assignment mappings for x in GroupToRoleMapping.get_all(): - SSORequest.delete(x) + GroupToRoleMapping.delete(x) GroupToRoleMappingDB( group="test2", roles=["system_admin", "admin"], source="test", enabled=True @@ -332,7 +332,7 @@ def tearDown_for_rbac(self): UserRoleAssignment.delete(x) for x in GroupToRoleMapping.get_all(): - SSORequest.delete(x) + GroupToRoleMapping.delete(x) for x in Role.get_all(): Role.delete(x) diff --git a/st2auth/tests/unit/test_handlers.py b/st2auth/tests/unit/test_handlers.py index cf00e642a6..be437f2c18 100644 --- a/st2auth/tests/unit/test_handlers.py +++ b/st2auth/tests/unit/test_handlers.py @@ -30,23 +30,130 @@ from st2tests.mocks.auth import MockRequest from st2tests.mocks.auth import get_mock_backend +from st2common.persistence.rbac import GroupToRoleMapping, UserRoleAssignment, Role +from st2common.models.db.rbac import GroupToRoleMappingDB, RoleDB + + __all__ = ["AuthHandlerTestCase"] +from st2common.router import GenericRequestParam + @mock.patch("st2auth.handlers.get_auth_backend_instance", get_mock_backend) -class AuthHandlerTestCase(CleanDbTestCase): +class ProxyHandlerRBACAndGroupsTestCase(CleanDbTestCase): + def setUp(self): - super(AuthHandlerTestCase, self).setUp() + super(ProxyHandlerRBACAndGroupsTestCase, self).setUp() cfg.CONF.auth.backend = "mock" - def test_proxy_handler(self): + RoleDB(name="role-1").save() + RoleDB(name="role-2").save() + + GroupToRoleMappingDB( + group="group-1", roles=["role-1"], source="test", enabled=True + ).save() + + GroupToRoleMappingDB( + group="group-2", roles=["role-2"], source="test", enabled=True + ).save() + + cfg.CONF.set_override(name="enable", group="rbac", override=False) + cfg.CONF.set_override(name="backend", group="rbac", override="noop") + + def test_proxy_handler_no_groups_no_rbac(self): h = handlers.ProxyAuthHandler() request = {} token = h.handle_auth( request, headers={}, remote_addr=None, remote_user="test_proxy_handler" ) + user_roles = UserRoleAssignment.get_all(user=token.user) + self.assertEqual(token.user, "test_proxy_handler") + self.assertEqual(len(user_roles), 0) + + def test_proxy_handler_with_groups_and_rbac_disabled(self): + + h = handlers.ProxyAuthHandler() + + request = GenericRequestParam( + groups=["group-1", "group-2"] + ) + token = h.handle_auth( + request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + ) + user_roles = UserRoleAssignment.get_all(user=token.user) + + self.assertEqual(token.user, "test_proxy_handler") + self.assertEqual(len(user_roles), 0) + + + def test_proxy_handler_with_groups_and_rbac_enabled(self): + + cfg.CONF.set_override(name="enable", group="rbac", override=True) + cfg.CONF.set_override(name="backend", group="rbac", override="default") + + h = handlers.ProxyAuthHandler() + + request = GenericRequestParam( + groups=["group-1", "group-2"] + ) + token = h.handle_auth( + request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + ) + user_roles = UserRoleAssignment.get_all(user=token.user) + + self.assertEqual(token.user, "test_proxy_handler") + self.assertEqual(len(user_roles), 2) + self.assertEqual(user_roles[0].role, 'role-1') + self.assertEqual(user_roles[1].role, 'role-2') + + def test_proxy_handler_no_groups_and_rbac_enabled_with_no_prior_roles(self): + + cfg.CONF.set_override(name="enable", group="rbac", override=True) + cfg.CONF.set_override(name="backend", group="rbac", override="default") + + h = handlers.ProxyAuthHandler() + + request = GenericRequestParam( + groups=[] + ) + token = h.handle_auth( + request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + ) + user_roles = UserRoleAssignment.get_all(user=token.user) + self.assertEqual(token.user, "test_proxy_handler") + self.assertEqual(len(user_roles), 0) + + def test_proxy_handler_no_groups_and_rbac_enabled_with_prior_roles(self): + + self.test_proxy_handler_with_groups_and_rbac_enabled() + + cfg.CONF.set_override(name="enable", group="rbac", override=True) + cfg.CONF.set_override(name="backend", group="rbac", override="default") + + h = handlers.ProxyAuthHandler() + + request = GenericRequestParam( + groups=[] + ) + token = h.handle_auth( + request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + ) + user_roles = UserRoleAssignment.get_all(user=token.user) + + self.assertEqual(token.user, "test_proxy_handler") + self.assertEqual(len(user_roles), 0) + + +@mock.patch("st2auth.handlers.get_auth_backend_instance", get_mock_backend) +class AuthHandlerTestCase(CleanDbTestCase): + + + def setUp(self): + super(AuthHandlerTestCase, self).setUp() + + cfg.CONF.auth.backend = "mock" def test_standalone_bad_auth_type(self): h = handlers.StandaloneAuthHandler() From 2d42595284b5e6b239f05535f64f664fc306cf0f Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 07:56:18 -0300 Subject: [PATCH 25/49] formatting and adding some tests :) --- st2auth/tests/unit/controllers/v1/test_sso.py | 2 +- st2auth/tests/unit/test_handlers.py | 40 ++++++++------ st2common/tests/unit/test_db_auth.py | 53 ++++++++++++++++++- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index ad2b0e52c9..dab79498d3 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -311,10 +311,10 @@ def setUp_for_rbac(self): for x in UserRoleAssignment.get_all(): UserRoleAssignment.delete(x) - # Set up assignment mappings for x in GroupToRoleMapping.get_all(): GroupToRoleMapping.delete(x) + # Set up assignment mappings GroupToRoleMappingDB( group="test2", roles=["system_admin", "admin"], source="test", enabled=True ).save() diff --git a/st2auth/tests/unit/test_handlers.py b/st2auth/tests/unit/test_handlers.py index be437f2c18..434e708ee3 100644 --- a/st2auth/tests/unit/test_handlers.py +++ b/st2auth/tests/unit/test_handlers.py @@ -38,18 +38,26 @@ from st2common.router import GenericRequestParam +MOCK_USER = "test_proxy_handler" @mock.patch("st2auth.handlers.get_auth_backend_instance", get_mock_backend) class ProxyHandlerRBACAndGroupsTestCase(CleanDbTestCase): + def _assert_roles_len(self, user, total): + user_roles = UserRoleAssignment.get_all(user=user) + self.assertEqual(len(user_roles), total) + return user_roles + def setUp(self): super(ProxyHandlerRBACAndGroupsTestCase, self).setUp() cfg.CONF.auth.backend = "mock" + # Create test roles RoleDB(name="role-1").save() RoleDB(name="role-2").save() + # Create tsts mappings GroupToRoleMappingDB( group="group-1", roles=["role-1"], source="test", enabled=True ).save() @@ -65,11 +73,10 @@ def test_proxy_handler_no_groups_no_rbac(self): h = handlers.ProxyAuthHandler() request = {} token = h.handle_auth( - request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + request, headers={}, remote_addr=None, remote_user=MOCK_USER ) - user_roles = UserRoleAssignment.get_all(user=token.user) - self.assertEqual(token.user, "test_proxy_handler") - self.assertEqual(len(user_roles), 0) + self._assert_roles_len(token.user, 0) + self.assertEqual(token.user, MOCK_USER) def test_proxy_handler_with_groups_and_rbac_disabled(self): @@ -79,12 +86,11 @@ def test_proxy_handler_with_groups_and_rbac_disabled(self): groups=["group-1", "group-2"] ) token = h.handle_auth( - request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + request, headers={}, remote_addr=None, remote_user=MOCK_USER ) - user_roles = UserRoleAssignment.get_all(user=token.user) + self._assert_roles_len(token.user, 0) - self.assertEqual(token.user, "test_proxy_handler") - self.assertEqual(len(user_roles), 0) + self.assertEqual(token.user, MOCK_USER) def test_proxy_handler_with_groups_and_rbac_enabled(self): @@ -98,12 +104,11 @@ def test_proxy_handler_with_groups_and_rbac_enabled(self): groups=["group-1", "group-2"] ) token = h.handle_auth( - request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + request, headers={}, remote_addr=None, remote_user=MOCK_USER ) - user_roles = UserRoleAssignment.get_all(user=token.user) - self.assertEqual(token.user, "test_proxy_handler") - self.assertEqual(len(user_roles), 2) + self.assertEqual(token.user, MOCK_USER) + user_roles = self._assert_roles_len(token.user, 2) self.assertEqual(user_roles[0].role, 'role-1') self.assertEqual(user_roles[1].role, 'role-2') @@ -118,16 +123,17 @@ def test_proxy_handler_no_groups_and_rbac_enabled_with_no_prior_roles(self): groups=[] ) token = h.handle_auth( - request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + request, headers={}, remote_addr=None, remote_user=MOCK_USER ) - user_roles = UserRoleAssignment.get_all(user=token.user) + user_roles = self._assert_roles_len(token.user, 0) - self.assertEqual(token.user, "test_proxy_handler") + self.assertEqual(token.user, MOCK_USER) self.assertEqual(len(user_roles), 0) def test_proxy_handler_no_groups_and_rbac_enabled_with_prior_roles(self): self.test_proxy_handler_with_groups_and_rbac_enabled() + self._assert_roles_len(MOCK_USER, 2) cfg.CONF.set_override(name="enable", group="rbac", override=True) cfg.CONF.set_override(name="backend", group="rbac", override="default") @@ -138,11 +144,11 @@ def test_proxy_handler_no_groups_and_rbac_enabled_with_prior_roles(self): groups=[] ) token = h.handle_auth( - request, headers={}, remote_addr=None, remote_user="test_proxy_handler" + request, headers={}, remote_addr=None, remote_user=MOCK_USER ) user_roles = UserRoleAssignment.get_all(user=token.user) - self.assertEqual(token.user, "test_proxy_handler") + self.assertEqual(token.user, MOCK_USER) self.assertEqual(len(user_roles), 0) diff --git a/st2common/tests/unit/test_db_auth.py b/st2common/tests/unit/test_db_auth.py index b159580505..c6b0e1669b 100644 --- a/st2common/tests/unit/test_db_auth.py +++ b/st2common/tests/unit/test_db_auth.py @@ -14,14 +14,16 @@ # limitations under the License. from __future__ import absolute_import -from st2common.models.db.auth import UserDB +import datetime +from st2common.models.db.auth import SSORequestDB, UserDB from st2common.models.db.auth import TokenDB from st2common.models.db.auth import ApiKeyDB -from st2common.persistence.auth import User +from st2common.persistence.auth import SSORequest, User from st2common.persistence.auth import Token from st2common.persistence.auth import ApiKey from st2common.util.date import get_datetime_utc_now from st2tests import DbTestCase +from mongoengine.errors import ValidationError from tests.unit.base import BaseDBModelCRUDTestCase @@ -58,3 +60,50 @@ class ApiKeyDBModelCRUDTestCase(BaseDBModelCRUDTestCase, DbTestCase): persistance_class = ApiKey model_class_kwargs = {"user": "pony", "key_hash": "token-token-token-token"} update_attribute_name = "user" + +class SSORequestDBModelCRUDTestCase(BaseDBModelCRUDTestCase, DbTestCase): + model_class = SSORequestDB + persistance_class = SSORequest + model_class_kwargs = { + "request_id": "48144c2b-7969-4708-ba1d-96fd7d05393f", + "expiry": datetime.datetime.strptime("2050-01-05T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + "type": SSORequestDB.Type.CLI, + } + update_attribute_name = "request_id" + + def _save_model(self, **kwargs): + model_db = self.model_class(**kwargs) + saved_db = self.persistance_class.add_or_update(model_db) + + def test_missing_parameters(self): + + self.assertRaises( + ValueError, self._save_model, **{ + "request_id": self.model_class_kwargs["request_id"], + "expiry": self.model_class_kwargs["expiry"], + } + ) + + self.assertRaises( + ValueError, self._save_model, **{ + "request_id": self.model_class_kwargs["request_id"], + "type": self.model_class_kwargs["type"], + } + ) + + self.assertRaises( + ValueError, self._save_model, **{ + "type": self.model_class_kwargs["type"], + "expiry": self.model_class_kwargs["expiry"], + } + ) + + def test_invalid_parameters(self): + + self.assertRaises( + ValidationError, self._save_model, **{ + "type": "invalid", + "expiry": self.model_class_kwargs["expiry"], + "request_id": self.model_class_kwargs["request_id"], + } + ) \ No newline at end of file From af0f69437fcdd6130a77253c933020fac78351da Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 08:03:09 -0300 Subject: [PATCH 26/49] minor formatting changes --- st2common/st2common/openapi.yaml | 6 +++--- st2common/st2common/openapi.yaml.j2 | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index d2eed861e4..064641ccc6 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -4544,7 +4544,7 @@ paths: /auth/v1/sso/request/cli: post: operationId: st2auth.controllers.v1.sso:sso_request_controller.post_cli - description: Provides data for a CLI to handle SSO authentication internally. + description: Issues an encrypted SSO login request for a CLI parameters: - name: response in: body @@ -4554,7 +4554,7 @@ paths: properties: key: type: string - description: The symmetric key to be used to encrypt contents of callback + description: The AES256 symmetric key to be used to encrypt contents of callback required: true callback_url: type: string @@ -4578,7 +4578,7 @@ paths: '200': description: SSO response valid '302': - description: SSO request valid and callback URL returned + description: SSO response valid and callback URL returned '401': description: Invalid or missing credentials has been provided schema: diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index 4c4ffb5b9a..f17b40de23 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -4540,7 +4540,7 @@ paths: /auth/v1/sso/request/cli: post: operationId: st2auth.controllers.v1.sso:sso_request_controller.post_cli - description: Provides data for a CLI to handle SSO authentication internally. + description: Issues an encrypted SSO login request for a CLI parameters: - name: response in: body @@ -4574,7 +4574,7 @@ paths: '200': description: SSO response valid '302': - description: SSO request valid and callback URL returned + description: SSO response valid and callback URL returned '401': description: Invalid or missing credentials has been provided schema: From f2cdaee64cd3d8f9191a53d16505ddfbcbb7b3c6 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 08:14:28 -0300 Subject: [PATCH 27/49] removing unused method and adding tests --- st2common/st2common/services/access.py | 12 +------ st2common/tests/unit/services/test_access.py | 38 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/st2common/st2common/services/access.py b/st2common/st2common/services/access.py index 53b75ffe0d..6e1e0c3bde 100644 --- a/st2common/st2common/services/access.py +++ b/st2common/st2common/services/access.py @@ -36,8 +36,7 @@ "delete_token", "create_cli_sso_request", "create_web_sso_request", - "get_sso_request_by_request_id", - "delete_sso_request", + "get_sso_request_by_request_id" ] LOG = logging.getLogger(__name__) @@ -168,12 +167,3 @@ def get_sso_request_by_request_id(request_id) -> SSORequestDB: request_db = SSORequest.get_by_request_id(request_id) return request_db - -def delete_sso_request(id): - try: - request_db = SSORequest.get(id) - return SSORequest.delete(request_db) - except SSORequestNotFoundError: - pass - except Exception: - raise diff --git a/st2common/tests/unit/services/test_access.py b/st2common/tests/unit/services/test_access.py index 4f7d8169b4..16ab3255e8 100644 --- a/st2common/tests/unit/services/test_access.py +++ b/st2common/tests/unit/services/test_access.py @@ -18,6 +18,7 @@ import uuid from oslo_config import cfg +from st2common.models.db.auth import SSORequestDB from st2tests.base import DbTestCase from st2common.util import isotime from st2common.util import date as date_utils @@ -30,6 +31,8 @@ USERNAME = "manas" +SSO_REQUEST_ID = "a58fa0cd-61c8-4bd9-a2e7-a4497d6aca68" +SSO_EXPIRY = datetime.datetime.strptime("2050-01-05T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") class AccessServiceTest(DbTestCase): @classmethod @@ -106,3 +109,38 @@ def test_create_token_service_token_can_use_arbitrary_ttl(self): self.assertRaises( TTLTooLargeException, access.create_token, USERNAME, ttl=ttl, service=False ) + + + def test_create_cli_sso_request(self): + request = access.create_cli_sso_request(SSO_REQUEST_ID, None, 20) + self.assertIsNotNone(request) + self.assertEqual(request.type, SSORequestDB.Type.CLI) + self.assertEqual(request.request_id, SSO_REQUEST_ID) + self.assertLessEqual( + abs( + request.expiry.timestamp() + - date_utils.get_datetime_utc_now().timestamp() + - 20 + ), + 2, + ) + + def test_create_web_sso_request(self): + request = access.create_web_sso_request(SSO_REQUEST_ID, 20) + self.assertIsNotNone(request) + self.assertEqual(request.type, SSORequestDB.Type.WEB) + self.assertEqual(request.request_id, SSO_REQUEST_ID) + self.assertLessEqual( + abs( + request.expiry.timestamp() + - date_utils.get_datetime_utc_now().timestamp() + - 20 + ), + 2, + ) + + def test_get_sso_request_by_id(self): + access.create_web_sso_request(SSO_REQUEST_ID, 20) + request = access.get_sso_request_by_request_id(SSO_REQUEST_ID) + self.assertIsNotNone(request) + self.assertEqual(request.request_id, SSO_REQUEST_ID) \ No newline at end of file From cb013f7dffa6a97a7957bd87de501809fc02cd88 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 08:34:58 -0300 Subject: [PATCH 28/49] fixing lint :) --- st2auth/st2auth/controllers/v1/sso.py | 9 ++-- st2auth/st2auth/handlers.py | 7 ++- st2auth/tests/unit/test_handlers.py | 45 ++++++++------------ st2common/st2common/openapi.yaml | 2 +- st2common/st2common/services/access.py | 4 +- st2common/tests/unit/services/test_access.py | 4 +- st2common/tests/unit/test_db_auth.py | 33 +++++++++----- 7 files changed, 51 insertions(+), 53 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index 381991d226..8bbc250805 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -93,8 +93,7 @@ def _validate_and_delete_sso_request(self, response): def post(self, response, **kwargs): try: - original_sso_request = self._validate_and_delete_sso_request( - response) + 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) @@ -195,8 +194,7 @@ def post_cli(self, response): "The provided key is invalid! It should be stackstorm-compatible AES key" ) - sso_request = self._create_sso_request( - create_cli_sso_request, key=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 = { @@ -309,8 +307,7 @@ def process_successful_sso_cli_response(callback_url, key, token): # 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")) + resp.location = "%s?response=%s" % (callback_url, encrypted_token.decode("utf-8")) return resp diff --git a/st2auth/st2auth/handlers.py b/st2auth/st2auth/handlers.py index ee55b12ec8..81b55c94be 100644 --- a/st2auth/st2auth/handlers.py +++ b/st2auth/st2auth/handlers.py @@ -162,8 +162,11 @@ def handle_auth( ): remote_addr = headers.get("x-forwarded-for", remote_addr) extra = {"remote_addr": remote_addr} - LOG.debug("Authenticating for proxy with request [%s]", getattr(request, "__dict__", None) if request else None) - + LOG.debug( + "Authenticating for proxy with request [%s]", + getattr(request, "__dict__", None) if request else None, + ) + if remote_user: ttl = getattr(request, "ttl", None) username = self._get_username_for_request(remote_user, request) diff --git a/st2auth/tests/unit/test_handlers.py b/st2auth/tests/unit/test_handlers.py index 434e708ee3..24e0012dc1 100644 --- a/st2auth/tests/unit/test_handlers.py +++ b/st2auth/tests/unit/test_handlers.py @@ -30,7 +30,7 @@ from st2tests.mocks.auth import MockRequest from st2tests.mocks.auth import get_mock_backend -from st2common.persistence.rbac import GroupToRoleMapping, UserRoleAssignment, Role +from st2common.persistence.rbac import UserRoleAssignment from st2common.models.db.rbac import GroupToRoleMappingDB, RoleDB @@ -40,9 +40,9 @@ MOCK_USER = "test_proxy_handler" + @mock.patch("st2auth.handlers.get_auth_backend_instance", get_mock_backend) class ProxyHandlerRBACAndGroupsTestCase(CleanDbTestCase): - def _assert_roles_len(self, user, total): user_roles = UserRoleAssignment.get_all(user=user) self.assertEqual(len(user_roles), total) @@ -79,54 +79,47 @@ def test_proxy_handler_no_groups_no_rbac(self): self.assertEqual(token.user, MOCK_USER) def test_proxy_handler_with_groups_and_rbac_disabled(self): - + h = handlers.ProxyAuthHandler() - request = GenericRequestParam( - groups=["group-1", "group-2"] - ) + request = GenericRequestParam(groups=["group-1", "group-2"]) token = h.handle_auth( request, headers={}, remote_addr=None, remote_user=MOCK_USER ) self._assert_roles_len(token.user, 0) - + self.assertEqual(token.user, MOCK_USER) - def test_proxy_handler_with_groups_and_rbac_enabled(self): - + cfg.CONF.set_override(name="enable", group="rbac", override=True) cfg.CONF.set_override(name="backend", group="rbac", override="default") - + h = handlers.ProxyAuthHandler() - request = GenericRequestParam( - groups=["group-1", "group-2"] - ) + request = GenericRequestParam(groups=["group-1", "group-2"]) token = h.handle_auth( request, headers={}, remote_addr=None, remote_user=MOCK_USER ) - + self.assertEqual(token.user, MOCK_USER) user_roles = self._assert_roles_len(token.user, 2) - self.assertEqual(user_roles[0].role, 'role-1') - self.assertEqual(user_roles[1].role, 'role-2') + self.assertEqual(user_roles[0].role, "role-1") + self.assertEqual(user_roles[1].role, "role-2") def test_proxy_handler_no_groups_and_rbac_enabled_with_no_prior_roles(self): - + cfg.CONF.set_override(name="enable", group="rbac", override=True) cfg.CONF.set_override(name="backend", group="rbac", override="default") h = handlers.ProxyAuthHandler() - request = GenericRequestParam( - groups=[] - ) + request = GenericRequestParam(groups=[]) token = h.handle_auth( request, headers={}, remote_addr=None, remote_user=MOCK_USER ) user_roles = self._assert_roles_len(token.user, 0) - + self.assertEqual(token.user, MOCK_USER) self.assertEqual(len(user_roles), 0) @@ -134,28 +127,24 @@ def test_proxy_handler_no_groups_and_rbac_enabled_with_prior_roles(self): self.test_proxy_handler_with_groups_and_rbac_enabled() self._assert_roles_len(MOCK_USER, 2) - + cfg.CONF.set_override(name="enable", group="rbac", override=True) cfg.CONF.set_override(name="backend", group="rbac", override="default") h = handlers.ProxyAuthHandler() - request = GenericRequestParam( - groups=[] - ) + request = GenericRequestParam(groups=[]) token = h.handle_auth( request, headers={}, remote_addr=None, remote_user=MOCK_USER ) user_roles = UserRoleAssignment.get_all(user=token.user) - + self.assertEqual(token.user, MOCK_USER) self.assertEqual(len(user_roles), 0) @mock.patch("st2auth.handlers.get_auth_backend_instance", get_mock_backend) class AuthHandlerTestCase(CleanDbTestCase): - - def setUp(self): super(AuthHandlerTestCase, self).setUp() diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index 064641ccc6..3e65182739 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -4554,7 +4554,7 @@ paths: properties: key: type: string - description: The AES256 symmetric key to be used to encrypt contents of callback + description: The symmetric key to be used to encrypt contents of callback required: true callback_url: type: string diff --git a/st2common/st2common/services/access.py b/st2common/st2common/services/access.py index 6e1e0c3bde..73433ee849 100644 --- a/st2common/st2common/services/access.py +++ b/st2common/st2common/services/access.py @@ -22,7 +22,6 @@ from st2common.util import isotime from st2common.util import date as date_utils from st2common.exceptions.auth import ( - SSORequestNotFoundError, TokenNotFoundError, UserNotFoundError, ) @@ -36,7 +35,7 @@ "delete_token", "create_cli_sso_request", "create_web_sso_request", - "get_sso_request_by_request_id" + "get_sso_request_by_request_id", ] LOG = logging.getLogger(__name__) @@ -166,4 +165,3 @@ def _create_sso_request(request_id, ttl, type, **kwargs) -> SSORequestDB: def get_sso_request_by_request_id(request_id) -> SSORequestDB: request_db = SSORequest.get_by_request_id(request_id) return request_db - diff --git a/st2common/tests/unit/services/test_access.py b/st2common/tests/unit/services/test_access.py index 16ab3255e8..568296affe 100644 --- a/st2common/tests/unit/services/test_access.py +++ b/st2common/tests/unit/services/test_access.py @@ -34,6 +34,7 @@ SSO_REQUEST_ID = "a58fa0cd-61c8-4bd9-a2e7-a4497d6aca68" SSO_EXPIRY = datetime.datetime.strptime("2050-01-05T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + class AccessServiceTest(DbTestCase): @classmethod def setUpClass(cls): @@ -110,7 +111,6 @@ def test_create_token_service_token_can_use_arbitrary_ttl(self): TTLTooLargeException, access.create_token, USERNAME, ttl=ttl, service=False ) - def test_create_cli_sso_request(self): request = access.create_cli_sso_request(SSO_REQUEST_ID, None, 20) self.assertIsNotNone(request) @@ -143,4 +143,4 @@ def test_get_sso_request_by_id(self): access.create_web_sso_request(SSO_REQUEST_ID, 20) request = access.get_sso_request_by_request_id(SSO_REQUEST_ID) self.assertIsNotNone(request) - self.assertEqual(request.request_id, SSO_REQUEST_ID) \ No newline at end of file + self.assertEqual(request.request_id, SSO_REQUEST_ID) diff --git a/st2common/tests/unit/test_db_auth.py b/st2common/tests/unit/test_db_auth.py index c6b0e1669b..a42b234a1c 100644 --- a/st2common/tests/unit/test_db_auth.py +++ b/st2common/tests/unit/test_db_auth.py @@ -61,49 +61,60 @@ class ApiKeyDBModelCRUDTestCase(BaseDBModelCRUDTestCase, DbTestCase): model_class_kwargs = {"user": "pony", "key_hash": "token-token-token-token"} update_attribute_name = "user" + class SSORequestDBModelCRUDTestCase(BaseDBModelCRUDTestCase, DbTestCase): model_class = SSORequestDB persistance_class = SSORequest model_class_kwargs = { "request_id": "48144c2b-7969-4708-ba1d-96fd7d05393f", - "expiry": datetime.datetime.strptime("2050-01-05T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + "expiry": datetime.datetime.strptime( + "2050-01-05T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z" + ), "type": SSORequestDB.Type.CLI, } update_attribute_name = "request_id" def _save_model(self, **kwargs): model_db = self.model_class(**kwargs) - saved_db = self.persistance_class.add_or_update(model_db) + self.persistance_class.add_or_update(model_db) def test_missing_parameters(self): self.assertRaises( - ValueError, self._save_model, **{ + ValueError, + self._save_model, + **{ "request_id": self.model_class_kwargs["request_id"], "expiry": self.model_class_kwargs["expiry"], - } + }, ) self.assertRaises( - ValueError, self._save_model, **{ + ValueError, + self._save_model, + **{ "request_id": self.model_class_kwargs["request_id"], "type": self.model_class_kwargs["type"], - } + }, ) self.assertRaises( - ValueError, self._save_model, **{ + ValueError, + self._save_model, + **{ "type": self.model_class_kwargs["type"], "expiry": self.model_class_kwargs["expiry"], - } + }, ) def test_invalid_parameters(self): self.assertRaises( - ValidationError, self._save_model, **{ + ValidationError, + self._save_model, + **{ "type": "invalid", "expiry": self.model_class_kwargs["expiry"], "request_id": self.model_class_kwargs["request_id"], - } - ) \ No newline at end of file + }, + ) From 88903fec3ef5b6d3ff011b35f7534bca8dfee71b Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 09:31:06 -0300 Subject: [PATCH 29/49] adjusting requirements --- requirements.txt | 1 + st2auth/requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 50e7cb7f01..dd7483f45d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,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 gitdb==4.0.2 gitpython==3.1.15 greenlet==1.0.0 diff --git a/st2auth/requirements.txt b/st2auth/requirements.txt index 1d6a06de81..13cf8ad85d 100644 --- a/st2auth/requirements.txt +++ b/st2auth/requirements.txt @@ -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 gunicorn==20.1.0 oslo.config>=1.12.1,<1.13 passlib==1.7.4 From b7b5a48c3632a6d66e5ba8aee2889edf4c9f6b88 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 09:41:39 -0300 Subject: [PATCH 30/49] fixing python 3.6 tets --- st2common/tests/unit/services/test_access.py | 1 - st2common/tests/unit/test_db_auth.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/st2common/tests/unit/services/test_access.py b/st2common/tests/unit/services/test_access.py index 568296affe..7ca61b358b 100644 --- a/st2common/tests/unit/services/test_access.py +++ b/st2common/tests/unit/services/test_access.py @@ -32,7 +32,6 @@ USERNAME = "manas" SSO_REQUEST_ID = "a58fa0cd-61c8-4bd9-a2e7-a4497d6aca68" -SSO_EXPIRY = datetime.datetime.strptime("2050-01-05T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") class AccessServiceTest(DbTestCase): diff --git a/st2common/tests/unit/test_db_auth.py b/st2common/tests/unit/test_db_auth.py index a42b234a1c..3cae8be5cb 100644 --- a/st2common/tests/unit/test_db_auth.py +++ b/st2common/tests/unit/test_db_auth.py @@ -21,7 +21,7 @@ from st2common.persistence.auth import SSORequest, User from st2common.persistence.auth import Token from st2common.persistence.auth import ApiKey -from st2common.util.date import get_datetime_utc_now +from st2common.util.date import add_utc_tz, get_datetime_utc_now from st2tests import DbTestCase from mongoengine.errors import ValidationError @@ -67,8 +67,8 @@ class SSORequestDBModelCRUDTestCase(BaseDBModelCRUDTestCase, DbTestCase): persistance_class = SSORequest model_class_kwargs = { "request_id": "48144c2b-7969-4708-ba1d-96fd7d05393f", - "expiry": datetime.datetime.strptime( - "2050-01-05T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z" + "expiry": add_utc_tz( + datetime.datetime.strptime("2050-01-05T10:00:00", "%Y-%m-%dT%H:%M:%S") ), "type": SSORequestDB.Type.CLI, } From 97c18e8c6cb6a27fd3aec2d7ebedb14720698fd2 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 10:20:03 -0300 Subject: [PATCH 31/49] fixing tests --- st2auth/tests/unit/controllers/v1/test_sso.py | 6 ++++++ st2auth/tests/unit/controllers/v1/test_token.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index dab79498d3..0f3ab29177 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -12,6 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +# NOTE: We need to perform monkeypatch before importing ssl module otherwise tests will fail. +# See https://github.com/StackStorm/st2/pull/4834 for details +from st2common.util.monkey_patch import monkey_patch + +monkey_patch() + from tests.base import FunctionalTest from st2common.exceptions import auth as auth_exc from st2auth.sso import noop diff --git a/st2auth/tests/unit/controllers/v1/test_token.py b/st2auth/tests/unit/controllers/v1/test_token.py index cd90a6cef1..cb45f62fe6 100644 --- a/st2auth/tests/unit/controllers/v1/test_token.py +++ b/st2auth/tests/unit/controllers/v1/test_token.py @@ -21,6 +21,12 @@ import mock from oslo_config import cfg +# NOTE: We need to perform monkeypatch before importing ssl module otherwise tests will fail. +# See https://github.com/StackStorm/st2/pull/4834 for details +from st2common.util.monkey_patch import monkey_patch + +monkey_patch() + from tests.base import FunctionalTest from st2common.util import isotime from st2common.util import date as date_utils From 0b9e38982941f87c241fc1d16723296365076d01 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 26 Jul 2022 13:18:49 -0300 Subject: [PATCH 32/49] trigger ci From 7f1af012f80013e6f97a781b6f1de85eeb35fc01 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 17:40:44 -0300 Subject: [PATCH 33/49] adding sso port parameters to CLI --- st2client/st2client/commands/auth.py | 12 ++++++++++-- st2client/st2client/models/core.py | 8 ++++++-- st2client/st2client/utils/sso_interceptor.py | 18 +++++++++++------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/st2client/st2client/commands/auth.py b/st2client/st2client/commands/auth.py index 71d75febd0..87aab90146 100644 --- a/st2client/st2client/commands/auth.py +++ b/st2client/st2client/commands/auth.py @@ -137,6 +137,14 @@ def __init__(self, resource, *args, **kwargs): help="Whether to use SSO authentication or not. " "If chosen, bypasses username/password.", ) + self.parser.add_argument( + "-P", + "--sso-port", + dest="sso_port", + type=int, + default=0, + help="Fixed SSO port to use for local callback server. Default is 0, which is random", + ) self.parser.add_argument( "-p", "--password", @@ -178,9 +186,9 @@ def run(self, args, **kwargs): # Retrieve token based on whether we're using SSO or username/password login :) if args.sso: - LOG.debug("Logging in with SSO") + LOG.debug("Logging in with SSO with fixed port [%d]", args.sso_port) # Retrieve token from SSO backend - sso_proxy = self.manager.create_sso_request(**kwargs) + sso_proxy = self.manager.create_sso_request(args.sso_port, **kwargs) print( "Please finish your SSO login by visiting: %s" diff --git a/st2client/st2client/models/core.py b/st2client/st2client/models/core.py index 217c20d7fd..99cc911270 100644 --- a/st2client/st2client/models/core.py +++ b/st2client/st2client/models/core.py @@ -893,17 +893,21 @@ class TokenResourceManager(ResourceManager): # to print out some interaction with the user and that's best done elsewhere, so # we'll just provide back the "interceptor" object, that is able to provide # the URL and wait for the token to be ready :) - def create_sso_request(self, **kwargs) -> sso_interceptor.SSOInterceptorProxy: + def create_sso_request( + self, sso_port=0, **kwargs + ) -> sso_interceptor.SSOInterceptorProxy: url = "/sso/request/cli" key = AESKey.generate() - sso_proxy = sso_interceptor.SSOInterceptorProxy(key) + print("KEY: %s", key.to_json()) + sso_proxy = sso_interceptor.SSOInterceptorProxy(key, sso_port) response = self.client.post( url, {"key": key.to_json(), "callback_url": sso_proxy.get_callback_url()}, **kwargs, ) + if response.status_code != http_client.OK: self.handle_error(response) diff --git a/st2client/st2client/utils/sso_interceptor.py b/st2client/st2client/utils/sso_interceptor.py index 166e10b18e..30ac9017f4 100644 --- a/st2client/st2client/utils/sso_interceptor.py +++ b/st2client/st2client/utils/sso_interceptor.py @@ -41,9 +41,9 @@ class SSOInterceptorProxy: # token object to receive the token once it's avaiable! token = None - def __init__(self, key): + def __init__(self, key, sso_port): - self.server = HTTPServer(("localhost", 0), createSSOProxyHandler(self)) + self.server = HTTPServer(("localhost", sso_port), createSSOProxyHandler(self)) self.key = key LOG.debug( @@ -70,7 +70,7 @@ def callback_received(self, token): LOG.debug("Callback received and intercepted, token is provided :)") self.token = token - def get_token(self, timeout=90): + def get_token(self, timeout=5): LOG.debug( "Waiting for token to be received from SSO flow.. will timeout after [%s]s", timeout, @@ -96,8 +96,8 @@ def do_GET(self): try: if o.path == "/callback": - self._handle_callbakc(qs.get("response", [None])[0]) - if o.path == "/success": + self._handle_callback(qs.get("response", [None])[0]) + elif o.path == "/success": self._handle_success() elif o.path == "/%s" % interceptor.url_id: self._handle_sso_login() @@ -110,6 +110,8 @@ def do_GET(self): LOG.debug("Unexpected internal server error! %e", e) self.send_error(500, explain="Unexpected error!" % str(e)) + return True + # This request is not expected by the sso proxy def _handle_unexpected_request(self): self.send_error(404, explain="The selected URL does not exist!") @@ -125,7 +127,7 @@ def _handle_sso_login(self): # This request should have all the callback data we are expecting # -- this means an encrypted key to be decrypted and used by the CLI :) - def _handle_callbakc(self, response): + def _handle_callback(self, response): LOG.debug("Intercepting SSO callback response!") if response is None: @@ -133,14 +135,16 @@ def _handle_callbakc(self, response): "Expected 'response' field with encrypted key in callback!" ) - token = symmetric_decrypt(interceptor.key, response.encode("utf-8")) + token = None try: + token = symmetric_decrypt(interceptor.key, response.encode("utf-8")) token_json = json.loads(token) LOG.debug( "Successful SSO login for user %s, redirecting to successful page!", token_json.get("user", None), ) except: + LOG.debug("Could not understand the SSO callback response!") raise ValueError( "Could not understand the incoming SSO callback response" ) From 78b5427559f3bc71d3b44d8db8a34735a955f298 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 17:41:06 -0300 Subject: [PATCH 34/49] adjusting timeout --- st2client/st2client/utils/sso_interceptor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2client/st2client/utils/sso_interceptor.py b/st2client/st2client/utils/sso_interceptor.py index 30ac9017f4..935a43394e 100644 --- a/st2client/st2client/utils/sso_interceptor.py +++ b/st2client/st2client/utils/sso_interceptor.py @@ -70,7 +70,7 @@ def callback_received(self, token): LOG.debug("Callback received and intercepted, token is provided :)") self.token = token - def get_token(self, timeout=5): + def get_token(self, timeout=90): LOG.debug( "Waiting for token to be received from SSO flow.. will timeout after [%s]s", timeout, From f22dad504c38bdf5806be551032844b31966be32 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 17:41:15 -0300 Subject: [PATCH 35/49] removing debug --- st2client/st2client/models/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/st2client/st2client/models/core.py b/st2client/st2client/models/core.py index 99cc911270..a3536d9a65 100644 --- a/st2client/st2client/models/core.py +++ b/st2client/st2client/models/core.py @@ -899,8 +899,7 @@ def create_sso_request( url = "/sso/request/cli" key = AESKey.generate() - print("KEY: %s", key.to_json()) - sso_proxy = sso_interceptor.SSOInterceptorProxy(key, sso_port) + sso_proxy = sso_interceptor.SSOInterceptorProxy(key) response = self.client.post( url, From 7cc4bc9abadb46b99338cf848dd6927291fc1062 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 17:41:27 -0300 Subject: [PATCH 36/49] updating crypto :) --- st2client/st2client/utils/crypto.py | 38 +++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/st2client/st2client/utils/crypto.py b/st2client/st2client/utils/crypto.py index d1d66d3581..e6be862101 100644 --- a/st2client/st2client/utils/crypto.py +++ b/st2client/st2client/utils/crypto.py @@ -38,8 +38,6 @@ import binascii import base64 -import json - from hashlib import sha1 import six @@ -51,6 +49,9 @@ from cryptography.hazmat.primitives import hmac from cryptography.hazmat.backends import default_backend +from st2common.util.jsonify import json_encode +from st2common.util.jsonify import json_decode + __all__ = [ "KEYCZAR_HEADER_SIZE", "KEYCZAR_AES_BLOCK_SIZE", @@ -159,7 +160,7 @@ def to_json(self): "mode": self.mode.upper(), "size": int(self.size), } - return json.dumps(data) + return json_encode(data) def __repr__(self): return "" % ( @@ -181,18 +182,35 @@ def read_crypto_key(key_path): with open(key_path, "r") as fp: content = fp.read() - content = json.loads(content) + content = json_decode(content) + + try: + return read_crypto_key_from_dict(content) + except KeyError as e: + msg = 'Invalid or malformed key file "%s": %s' % (key_path, six.text_type(e)) + raise KeyError(msg) + + +def read_crypto_key_from_dict(key_dict): + """ + Read crypto key from provided Keyczar JSON-format dict and return parsed AESKey object. + + :param key_dict: A dictionary with a key in Keyczar format (same keys as the JSON). + :type key_dict: ``dict`` + + :rtype: :class:`AESKey` + """ try: aes_key = AESKey( - aes_key_string=content["aesKeyString"], - hmac_key_string=content["hmacKey"]["hmacKeyString"], - hmac_key_size=content["hmacKey"]["size"], - mode=content["mode"].upper(), - size=content["size"], + aes_key_string=key_dict["aesKeyString"], + hmac_key_string=key_dict["hmacKey"]["hmacKeyString"], + hmac_key_size=key_dict["hmacKey"]["size"], + mode=key_dict["mode"].upper(), + size=key_dict["size"], ) except KeyError as e: - msg = 'Invalid or malformed key file "%s": %s' % (key_path, six.text_type(e)) + msg = "Invalid or malformed AES key dictionary: %s" % (six.text_type(e)) raise KeyError(msg) return aes_key From 6579e8b178b514d5efca5f7e234a89a6531a4617 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 17:41:51 -0300 Subject: [PATCH 37/49] adding test for sso login via cli --- st2client/tests/unit/test_auth.py | 129 +++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/st2client/tests/unit/test_auth.py b/st2client/tests/unit/test_auth.py index 367a33c4ac..9c3ea8c820 100644 --- a/st2client/tests/unit/test_auth.py +++ b/st2client/tests/unit/test_auth.py @@ -14,7 +14,11 @@ # limitations under the License. from __future__ import absolute_import +import io import os +import re +import sys +from time import sleep, time import uuid import json import mock @@ -22,19 +26,27 @@ import requests import argparse import logging +from threading import Thread +from datetime import datetime, timedelta import six +from st2client.utils.sso_interceptor import SSOInterceptorProxy from tests import base from st2client import shell from st2client.models.core import add_auth_token_to_kwargs_from_env from st2client.commands.resource import add_auth_token_to_kwargs_from_cli +from st2client.utils.crypto import ( + AESKey, + read_crypto_key_from_dict, + symmetric_encrypt, + symmetric_decrypt, +) from st2client.utils.httpclient import ( add_auth_token_to_headers, add_json_content_type_to_headers, ) - LOG = logging.getLogger(__name__) if six.PY3: @@ -165,6 +177,121 @@ def runTest(self): ) +class TestLoginSSO(TestLoginBase): + + ORIGINAL_POST_FN = requests.post + + CONFIG_FILE_NAME = "logintest.cfg" + + LOGIN_REQUEST_MOCK_KEY = read_crypto_key_from_dict( + { + "hmacKey": { + "hmacKeyString": "-qdRklvhm4xvzIfaL6Z2nmQ-2N-c4IUtNa1_BowCVfg", + "size": 256, + }, + "aesKeyString": "0UyXFjBTQ9PMyHZ0mqrvuqCSzesuFup1d6m-4Vi3vdo", + "mode": "CBC", + "size": 256, + } + ) + + TOKEN = { + "user": "stanley", + "token": "44583f15945b4095afbf57058535ca64", + "expiry": "2017-02-12T00:53:09.632783Z", + "id": "589e607532ed3535707f10eb", + "metadata": {}, + } + + ENCRYPTED_TOKEN = symmetric_encrypt( + LOGIN_REQUEST_MOCK_KEY, json.dumps(TOKEN) + ).decode("utf-8") + + LOGIN_REQUEST_RESPONSE = { + # This is just a placeholder name, it's all mocked :) + "sso_url": "http://keycloak/realms/StackStorm/protocol/saml?SAMLRequest=fZFRS8MwFIX%2FSsl7TJPV1Ya1MB3iYOJYqw%2B%2BSJpFF2yTmXsr%2Bu%2FNplNU2OM53HNzvtyJg1ROB9y4lXkZDGDy1ncOZLRLMgQnvQIbpeoNSNSynl4vpDhJ5TZ49Np35DvAjwcUgAlovSPJfFYSu34QOs9yk2f0LB8rmqm2oAUfjempUCLX3Ohx25LkzgSIqZLEJTEKMJi5A1QOo5UKQdOC8qLhuRRCZuKeJLOIYZ3CfWqDuJWMdV6rbuMB5SjlnAWjuh5YjUo%2F1%2BhDzw48DFQfoZZf8ty6tXVPx9HazyGQV02zpMubuiHJ9IB74R0MvQm1Ca9Wm9vV4n8ppuIFGIBn0ejaWIpUk%2Fijco8bksvYUOHxEjvHrunjflQahxbfSfX3pQn7WVvtxO%2FrVx8%3D&RelayState=%7B%22referer%22%3A+%22http%3A%2F%2Flocalhost%3A34000%2Fcallback%22%7D", + "expiry": (datetime.now() + timedelta(hours=3)).strftime( + "%Y-%m-%dT%H:%M:%S.%f" + )[:-3] + + "000+00:00", + } + + @mock.patch.object(AESKey, "generate", return_value=LOGIN_REQUEST_MOCK_KEY) + @mock.patch( + "requests.post", + return_value=base.FakeResponse(json.dumps(LOGIN_REQUEST_RESPONSE), 200, "OK"), + ) + def runTest(self, mock_aeskey_generate, mock_post): + """Test 'st2 login --sso' functionality""" + + expected_username = self.TOKEN["user"] + args = [ + "--config", + self.CONFIG_FILE, + "login", + "--sso", + "--sso-port", + "34000", + ] + + original_stdout = sys.stdout + out_buffer = io.StringIO() + + def handle_sso_flow(): + # Waiting for SSO link on the CLI + LOG.debug("Waiting for SSO link") + match = None + timeout_at = time() + 5 + while not match and timeout_at > time(): + sleep(0.5) + match = re.search(r"http://localhost:34000/\S+", out_buffer.getvalue()) + self.assertIsNotNone(match) + + LOG.debug("STDOUT buffer has: %s", out_buffer.getvalue()) + + # Hitting the localhost login url + login_url = match[0] + LOG.debug("GETting SSO login to %s", login_url) + response = requests.get(login_url, allow_redirects=False) + self.assertEquals(response.status_code, 307) + self.assertEquals( + response.headers["Location"], self.LOGIN_REQUEST_RESPONSE["sso_url"] + ) + + # Ignoring IDP flow and just hittin callback with proper response :) + response = requests.get( + "http://localhost:34000/callback", + params={"response": self.ENCRYPTED_TOKEN}, + allow_redirects=False, + ) + self.assertEquals(response.status_code, 302) + self.assertEquals(response.headers["Location"], "/success") + + # Calling the login proecss async + ssoFlowThread = Thread(target=handle_sso_flow, daemon=True) + ssoFlowThread.start() + + sys.stdout = out_buffer + self.shell.run(args) + sys.stdout = original_stdout + + with open(self.CONFIG_FILE, "r") as config_file: + for line in config_file.readlines(): + print(line) + # Make sure certain values are not present + self.assertNotIn("password", line) + self.assertNotIn("olduser", line) + + # Make sure configured username is what we expect + if "username" in line: + self.assertEqual(line.split(" ")[2][:-1], expected_username) + + # validate token was created + self.assertTrue( + os.path.isfile("%stoken-%s" % (self.DOTST2_PATH, expected_username)) + ) + + class TestLoginWithMissingUsername(TestLoginBase): CONFIG_FILE_NAME = "logintest.cfg" From 014b249ec3eff31af515cfd353916113994058eb Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 17:47:22 -0300 Subject: [PATCH 38/49] final adjustment --- st2client/st2client/models/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2client/st2client/models/core.py b/st2client/st2client/models/core.py index a3536d9a65..51f5dde05b 100644 --- a/st2client/st2client/models/core.py +++ b/st2client/st2client/models/core.py @@ -899,7 +899,7 @@ def create_sso_request( url = "/sso/request/cli" key = AESKey.generate() - sso_proxy = sso_interceptor.SSOInterceptorProxy(key) + sso_proxy = sso_interceptor.SSOInterceptorProxy(key, sso_port) response = self.client.post( url, From a511d3b237a6e21ff10ae7844481656d82cd1423 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 18:46:45 -0300 Subject: [PATCH 39/49] adding browser launching config for sso --- st2client/st2client/commands/auth.py | 24 ++++++++++++++++---- st2client/st2client/utils/sso_interceptor.py | 2 +- st2client/tests/unit/test_auth.py | 12 ++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/st2client/st2client/commands/auth.py b/st2client/st2client/commands/auth.py index 87aab90146..a7ed4a0326 100644 --- a/st2client/st2client/commands/auth.py +++ b/st2client/st2client/commands/auth.py @@ -19,6 +19,7 @@ import json import logging import os +import webbrowser import requests import six from six.moves.configparser import ConfigParser @@ -145,6 +146,13 @@ def __init__(self, resource, *args, **kwargs): default=0, help="Fixed SSO port to use for local callback server. Default is 0, which is random", ) + self.parser.add_argument( + "--no-sso-browser", + dest="no_sso_browser", + action="store_true", + default=False, + help="Prevents from automatically launching the browser for SSO", + ) self.parser.add_argument( "-p", "--password", @@ -190,10 +198,18 @@ def run(self, args, **kwargs): # Retrieve token from SSO backend sso_proxy = self.manager.create_sso_request(args.sso_port, **kwargs) - print( - "Please finish your SSO login by visiting: %s" - % (sso_proxy.get_proxy_url()) - ) + if args.no_sso_browser: + print( + "Please finish your SSO login by visiting: %s" + % (sso_proxy.get_proxy_url()) + ) + else: + print( + "Please finish the SSO login on your browser.\n" + "If the browser hasn't opened automatically, please visit: %s" + % (sso_proxy.get_proxy_url()) + ) + webbrowser.open(sso_proxy.get_proxy_url()) token = self.manager.wait_for_sso_token(sso_proxy) # Defaults to username/password if not SSO diff --git a/st2client/st2client/utils/sso_interceptor.py b/st2client/st2client/utils/sso_interceptor.py index 935a43394e..a469d3cf5a 100644 --- a/st2client/st2client/utils/sso_interceptor.py +++ b/st2client/st2client/utils/sso_interceptor.py @@ -70,7 +70,7 @@ def callback_received(self, token): LOG.debug("Callback received and intercepted, token is provided :)") self.token = token - def get_token(self, timeout=90): + def get_token(self, timeout=8): LOG.debug( "Waiting for token to be received from SSO flow.. will timeout after [%s]s", timeout, diff --git a/st2client/tests/unit/test_auth.py b/st2client/tests/unit/test_auth.py index 9c3ea8c820..79ed1bfb94 100644 --- a/st2client/tests/unit/test_auth.py +++ b/st2client/tests/unit/test_auth.py @@ -230,6 +230,7 @@ def runTest(self, mock_aeskey_generate, mock_post): self.CONFIG_FILE, "login", "--sso", + "--no-sso-browser", "--sso-port", "34000", ] @@ -243,12 +244,13 @@ def handle_sso_flow(): match = None timeout_at = time() + 5 while not match and timeout_at > time(): - sleep(0.5) - match = re.search(r"http://localhost:34000/\S+", out_buffer.getvalue()) + sleep(1) + LOG.debug("STDOUT buffer has: %s", out_buffer.getvalue()) + match = re.search( + r"http://localhost:34000/\S+", out_buffer.getvalue(), re.MULTILINE + ) self.assertIsNotNone(match) - LOG.debug("STDOUT buffer has: %s", out_buffer.getvalue()) - # Hitting the localhost login url login_url = match[0] LOG.debug("GETting SSO login to %s", login_url) @@ -259,6 +261,7 @@ def handle_sso_flow(): ) # Ignoring IDP flow and just hittin callback with proper response :) + LOG.debug("Calling back to local server") response = requests.get( "http://localhost:34000/callback", params={"response": self.ENCRYPTED_TOKEN}, @@ -266,6 +269,7 @@ def handle_sso_flow(): ) self.assertEquals(response.status_code, 302) self.assertEquals(response.headers["Location"], "/success") + LOG.debug("Finished SSO flow") # Calling the login proecss async ssoFlowThread = Thread(target=handle_sso_flow, daemon=True) From e9adafae7c4ab09c8428be5543282ed3a831b826 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Mon, 19 Sep 2022 18:52:00 -0300 Subject: [PATCH 40/49] treating keyboard interrupt on sso flow --- st2client/st2client/commands/auth.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/st2client/st2client/commands/auth.py b/st2client/st2client/commands/auth.py index a7ed4a0326..e8abfdd99a 100644 --- a/st2client/st2client/commands/auth.py +++ b/st2client/st2client/commands/auth.py @@ -210,7 +210,11 @@ def run(self, args, **kwargs): % (sso_proxy.get_proxy_url()) ) webbrowser.open(sso_proxy.get_proxy_url()) - token = self.manager.wait_for_sso_token(sso_proxy) + + try: + token = self.manager.wait_for_sso_token(sso_proxy) + except KeyboardInterrupt: + raise Exception("SSO Login aborted by user") # Defaults to username/password if not SSO else: From 331e73706f1fb443a0c440f51ff2e1dd908de0d6 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Tue, 20 Sep 2022 17:46:24 -0300 Subject: [PATCH 41/49] fixing lint --- st2client/tests/unit/test_auth.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/st2client/tests/unit/test_auth.py b/st2client/tests/unit/test_auth.py index 79ed1bfb94..22772c4e03 100644 --- a/st2client/tests/unit/test_auth.py +++ b/st2client/tests/unit/test_auth.py @@ -30,7 +30,6 @@ from datetime import datetime, timedelta import six -from st2client.utils.sso_interceptor import SSOInterceptorProxy from tests import base from st2client import shell @@ -40,7 +39,6 @@ AESKey, read_crypto_key_from_dict, symmetric_encrypt, - symmetric_decrypt, ) from st2client.utils.httpclient import ( add_auth_token_to_headers, From 7ffbda540376933527fc08b127e4e5e128914685 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 21 Sep 2022 10:21:38 -0300 Subject: [PATCH 42/49] fixing lint --- st2common/st2common/openapi.yaml | 1232 ++++++++++++++------------- st2common/st2common/openapi.yaml.j2 | 7 +- 2 files changed, 653 insertions(+), 586 deletions(-) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index 1d631c08d9..a1fa29f29b 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -2,13 +2,13 @@ # Edit st2common/st2common/openapi.yaml.j2 and then run # make .generate-api-spec # to generate the final spec file -swagger: '2.0' +swagger: "2.0" info: version: "1.0.0" title: StackStorm API description: | - + ## Welcome Welcome to the StackStorm API Reference documentation! You can use the StackStorm API to integrate StackStorm with 3rd-party systems and custom applications. Example integrations include writing your own self-service user portal, or integrating with other orquestation systems. @@ -197,7 +197,6 @@ info: Join our [Slack Community](https://stackstorm.com/community-signup) to get help from the engineering team and fellow users. You can also create issues against the main [StackStorm GitHub repo](https://github.com/StackStorm/st2/issues), or the [st2apidocs repo](https://github.com/StackStorm/st2apidocs) for documentation-specific issues. We also recommend reviewing the main [StackStorm documentation](https://docs.stackstorm.com/). - paths: /api/v1/: @@ -205,7 +204,7 @@ paths: operationId: st2api.controllers.root:root_controller.index description: General API info. responses: - '200': + "200": description: General API info. schema: type: object @@ -216,12 +215,12 @@ paths: type: string examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/user: get: operationId: st2api.controllers.v1.user:user_controller.get @@ -236,14 +235,14 @@ paths: x-as: auth_info description: Information on how user authenticated. responses: - '200': + "200": description: Metadata information about the authenticated user. schema: type: object default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actions: get: @@ -299,20 +298,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of actions schema: type: array items: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.actions:actions_controller.post description: | @@ -322,25 +321,25 @@ paths: in: body description: Action content schema: - $ref: '#/definitions/ActionCreateRequest' + $ref: "#/definitions/ActionCreateRequest" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Single action being created schema: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actions/{ref_or_id}: get: operationId: st2api.controllers.v1.actions:actions_controller.get_one @@ -358,18 +357,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action requested schema: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.actions:actions_controller.put description: | @@ -384,25 +383,25 @@ paths: in: body description: Action content schema: - $ref: '#/definitions/ActionUpdateRequest' + $ref: "#/definitions/ActionUpdateRequest" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action updated schema: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.actions:actions_controller.delete description: | @@ -417,19 +416,19 @@ paths: in: body description: Flag to remove action files from disk schema: - $ref: '#/definitions/ActionDeleteRequest' + $ref: "#/definitions/ActionDeleteRequest" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '204': + "204": description: Action deleted default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actions/{ref_or_id}/clone: post: operationId: st2api.controllers.v1.actions:actions_controller.clone @@ -445,7 +444,7 @@ paths: in: body description: Destination action content schema: - $ref: '#/definitions/ActionCloneRequest' + $ref: "#/definitions/ActionCloneRequest" required: true x-parameters: - name: user @@ -453,18 +452,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Single action being cloned schema: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actions/views/parameters/{ref_or_id}: get: operationId: st2api.controllers.v1.action_views:parameters_view_controller.get_one @@ -482,10 +481,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: An object containing action parameters schema: - $ref: '#/definitions/ActionParameters' + $ref: "#/definitions/ActionParameters" examples: application/json: parameters: @@ -495,7 +494,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actions/views/overview: get: operationId: st2api.controllers.v1.action_views:overview_controller.get_all @@ -544,20 +543,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of actions schema: type: array items: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actions/views/overview/{ref_or_id}: get: operationId: st2api.controllers.v1.action_views:overview_controller.get_one @@ -575,18 +574,18 @@ paths: x-as: requester_user description: User running the action responses: - '200': + "200": description: Action requested schema: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actions/views/entry_point/{ref_or_id}: get: operationId: st2api.controllers.v1.action_views:entry_point_controller.get_one @@ -604,7 +603,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Entry point code schema: type: string @@ -617,13 +616,13 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actionalias: get: operationId: st2api.controllers.v1.actionalias:action_alias_controller.get_all x-permissions: action_alias_list description: | - Get list of action-aliases. + Get list of action-aliases. parameters: - name: exclude_attributes in: query @@ -666,20 +665,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of action aliases. schema: type: array items: - $ref: '#/definitions/ActionAlias' + $ref: "#/definitions/ActionAlias" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.actionalias:action_alias_controller.post description: | @@ -689,25 +688,25 @@ paths: in: body description: Action alias file. schema: - $ref: '#/definitions/ActionAlias' + $ref: "#/definitions/ActionAlias" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Action alias created schema: - $ref: '#/definitions/ActionAlias' + $ref: "#/definitions/ActionAlias" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actionalias/{ref_or_id}: get: operationId: st2api.controllers.v1.actionalias:action_alias_controller.get_one @@ -727,18 +726,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action alias requested schema: - $ref: '#/definitions/ActionAlias' + $ref: "#/definitions/ActionAlias" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.actionalias:action_alias_controller.put description: Update action alias @@ -752,7 +751,7 @@ paths: in: body description: JSON/YAML file containing the action alias to update. schema: - $ref: '#/definitions/ActionAlias' + $ref: "#/definitions/ActionAlias" required: true x-parameters: - name: user @@ -760,18 +759,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action alias updated. schema: - $ref: '#/definitions/ActionAlias' + $ref: "#/definitions/ActionAlias" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.actionalias:action_alias_controller.delete description: | @@ -788,12 +787,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '204': + "204": description: Action alias deleted default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actionalias/match: post: operationId: st2api.controllers.v1.actionalias:action_alias_controller.match @@ -805,16 +804,16 @@ paths: in: body description: Object containing the format to be matched. schema: - $ref: '#/definitions/ActionAliasMatchRequest' + $ref: "#/definitions/ActionAliasMatchRequest" responses: - '200': + "200": description: Action alias match pattern schema: - $ref: '#/definitions/ActionAliasMatch' + $ref: "#/definitions/ActionAliasMatch" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/actionalias/help: get: operationId: st2api.controllers.v1.actionalias:action_alias_controller.help @@ -841,18 +840,18 @@ paths: type: integer default: 0 responses: - '200': + "200": description: Action alias match pattern schema: - $ref: '#/definitions/ActionAliasHelp' + $ref: "#/definitions/ActionAliasHelp" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/aliasexecution: post: operationId: st2api.controllers.v1.aliasexecution:action_alias_execution_controller.post @@ -863,7 +862,7 @@ paths: in: body description: Alias execution payload. schema: - $ref: '#/definitions/AliasExecution' + $ref: "#/definitions/AliasExecution" - name: show_secrets in: query description: Show secrets in plain text @@ -874,18 +873,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Action alias created schema: - $ref: '#/definitions/AliasExecution' + $ref: "#/definitions/AliasExecution" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/aliasexecution/match_and_execute: post: operationId: st2api.controllers.v1.aliasexecution:action_alias_execution_controller.match_and_execute @@ -896,7 +895,7 @@ paths: in: body description: Input data. schema: - $ref: '#/definitions/AliasMatchAndExecuteInputAPI' + $ref: "#/definitions/AliasMatchAndExecuteInputAPI" - name: show_secrets in: query description: Show secrets in plain text @@ -907,7 +906,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Action alias executions created schema: type: object @@ -917,15 +916,15 @@ paths: type: array items: type: object - $ref: '#/definitions/AliasExecution' + $ref: "#/definitions/AliasExecution" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions: get: @@ -1046,12 +1045,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of executions schema: type: array items: - $ref: '#/definitions/Execution' + $ref: "#/definitions/Execution" examples: application/json: - trigger: @@ -1062,7 +1061,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.post x-log-result: false @@ -1073,7 +1072,7 @@ paths: in: body description: Execution request schema: - $ref: '#/definitions/ExecutionRequest' + $ref: "#/definitions/ExecutionRequest" - name: show_secrets in: query description: Show secrets in plain text @@ -1089,10 +1088,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Execution being created schema: - $ref: '#/definitions/Execution' + $ref: "#/definitions/Execution" examples: application/json: trigger: @@ -1103,7 +1102,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions/{id}: get: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.get_one @@ -1145,10 +1144,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Execution requested schema: - $ref: '#/definitions/Execution' + $ref: "#/definitions/Execution" examples: application/json: trigger: @@ -1159,7 +1158,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.put description: | @@ -1174,7 +1173,7 @@ paths: in: body description: Execution update request schema: - $ref: '#/definitions/ExecutionUpdateRequest' + $ref: "#/definitions/ExecutionUpdateRequest" - name: show_secrets in: query description: Show secrets in plain text @@ -1185,14 +1184,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Execution that was updated schema: - $ref: '#/definitions/Execution' + $ref: "#/definitions/Execution" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.delete description: | @@ -1213,12 +1212,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Execution cancelled default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions/{id}/output: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_output_controller.get_one @@ -1247,12 +1246,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Execution output. default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions/{id}/re_run: post: operationId: st2api.controllers.v1.actionexecutions:action_execution_rerun_controller.post @@ -1283,7 +1282,7 @@ paths: default: [] user: type: string - default: '' + default: "" delay: description: How long (in milliseconds) to delay the execution before scheduling. type: integer @@ -1297,10 +1296,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Single action being created schema: - $ref: '#/definitions/Execution' + $ref: "#/definitions/Execution" examples: application/json: trigger: @@ -1311,7 +1310,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions/{id}/attribute/{attribute}: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_attribute_controller.get @@ -1336,7 +1335,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Execution attribute requested examples: application/json: @@ -1346,7 +1345,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions/{id}/children: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_children_controller.get_one @@ -1375,12 +1374,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Execution attribute requested schema: type: array items: - $ref: '#/definitions/Execution' + $ref: "#/definitions/Execution" examples: application/json: trigger: @@ -1389,7 +1388,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions/{id}/result: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_raw_result_controller.get @@ -1424,7 +1423,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Execution result schema: type: string @@ -1433,7 +1432,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/executions/views/filters: get: operationId: st2api.controllers.v1.execution_views:filters_controller.get_all @@ -1448,10 +1447,10 @@ paths: items: type: string responses: - '200': + "200": description: A number of distinct values for the requested filters schema: - $ref: '#/definitions/ExecutionFilters' + $ref: "#/definitions/ExecutionFilters" examples: application/json: trigger: @@ -1462,7 +1461,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/inquiries: get: operationId: st2api.controllers.v1.inquiries:inquiries_controller.get_all @@ -1493,16 +1492,16 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of inquries schema: type: array items: - $ref: '#/definitions/Inquiry' + $ref: "#/definitions/Inquiry" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/inquiries/{inquiry_id}: get: operationId: st2api.controllers.v1.inquiries:inquiries_controller.get_one @@ -1520,14 +1519,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Inquiry requested. schema: - $ref: '#/definitions/Inquiry' + $ref: "#/definitions/Inquiry" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.inquiries:inquiries_controller.put description: | @@ -1543,25 +1542,25 @@ paths: required: true description: Inquiry response schema: - $ref: '#/definitions/InquiryResponseResult' + $ref: "#/definitions/InquiryResponseResult" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Response received schema: - $ref: '#/definitions/InquiryResponseResult' + $ref: "#/definitions/InquiryResponseResult" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/keys: get: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.get_all @@ -1571,7 +1570,7 @@ paths: - name: prefix in: query description: | - Only return values which name starts with the provided prefix. + Only return values which name starts with the provided prefix. type: string - name: scope in: query @@ -1613,19 +1612,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of key value pairs schema: type: array items: - $ref: '#/definitions/KeyValuePair' + $ref: "#/definitions/KeyValuePair" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/keys/{name}: get: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.get_one @@ -1655,18 +1654,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Key value pair requested. schema: - $ref: '#/definitions/KeyValuePair' + $ref: "#/definitions/KeyValuePair" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.put description: | @@ -1681,7 +1680,7 @@ paths: in: body description: Key Value pair content. schema: - $ref: '#/definitions/KeyValuePairRequest' + $ref: "#/definitions/KeyValuePairRequest" required: true x-parameters: - name: user @@ -1689,18 +1688,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Key set/updated. schema: - $ref: '#/definitions/KeyValuePair' + $ref: "#/definitions/KeyValuePair" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.delete description: Delete a Key. @@ -1724,12 +1723,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '204': + "204": description: Key deleted. default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs: get: operationId: st2api.controllers.v1.packs:packs_controller.get_all @@ -1783,19 +1782,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of installed packs. schema: type: array items: - $ref: '#/definitions/PacksList' + $ref: "#/definitions/PacksList" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/{ref_or_id}: get: operationId: st2api.controllers.v1.packs:packs_controller.get_one @@ -1814,17 +1813,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Information about a pack. schema: - $ref: '#/definitions/PackView' + $ref: "#/definitions/PackView" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/install: post: operationId: st2api.controllers.v1.packs:packs_controller.install.post @@ -1836,25 +1835,25 @@ paths: in: body description: Packs to be installed schema: - $ref: '#/definitions/PacksInstall' + $ref: "#/definitions/PacksInstall" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '202': + "202": description: Pack installation request has been accepted schema: - $ref: '#/definitions/AsyncRequest' + $ref: "#/definitions/AsyncRequest" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/uninstall: post: operationId: st2api.controllers.v1.packs:packs_controller.uninstall.post @@ -1866,25 +1865,25 @@ paths: in: body description: Packs to be uninstalled schema: - $ref: '#/definitions/PacksUninstall' + $ref: "#/definitions/PacksUninstall" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '202': + "202": description: Pack uninstallation request has been accepted schema: - $ref: '#/definitions/AsyncRequest' + $ref: "#/definitions/AsyncRequest" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/register: post: operationId: st2api.controllers.v1.packs:packs_controller.register.post @@ -1896,37 +1895,37 @@ paths: in: body description: Pack(s) to be Registered schema: - $ref: '#/definitions/PacksRegister' + $ref: "#/definitions/PacksRegister" responses: - '200': + "200": description: Pack(s) Registered. schema: - $ref: '#/definitions/PacksList' + $ref: "#/definitions/PacksList" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/index: get: operationId: st2api.controllers.v1.packs:packs_controller.index.get_all x-permissions: pack_search description: To list all the packs of all indexes used by your StackStorm instance. responses: - '200': + "200": description: Pack index. schema: - $ref: '#/definitions/PackIndex' + $ref: "#/definitions/PackIndex" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/index/search: post: operationId: st2api.controllers.v1.packs:packs_controller.index.search.post @@ -1938,9 +1937,9 @@ paths: in: body description: A query to search a pack or a pack name to get its details schema: - $ref: '#/definitions/PacksSearchShow' + $ref: "#/definitions/PacksSearchShow" responses: - '200': + "200": description: Pack search results. schema: type: @@ -1952,12 +1951,12 @@ paths: # $ref: '#/definitions/PacksList' examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" # TODO: No unit test for /packs/index/health /api/v1/packs/index/health: get: @@ -1965,17 +1964,17 @@ paths: x-permissions: pack_views_index_health description: To get the state of all indexes used by your StackStorm instance. responses: - '200': + "200": description: Index health. schema: - $ref: '#/definitions/PackIndex' + $ref: "#/definitions/PackIndex" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/views/files/{ref_or_id}: get: operationId: st2api.controllers.v1.packs:packs_controller.views.files.get_one @@ -1993,19 +1992,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Information about a pack. schema: type: array items: - $ref: '#/definitions/DataFilesSubSchema' + $ref: "#/definitions/DataFilesSubSchema" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/packs/views/file/{ref_or_id}/{file_path}: get: operationId: st2api.controllers.v1.packs:packs_controller.views.file.get_one @@ -2041,14 +2040,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Content of the file. - '304': + "304": description: File has not been modified. default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/configs: get: operationId: st2api.controllers.v1.pack_configs:pack_configs_controller.get_all @@ -2091,19 +2090,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Get packs config. # schema: - # type: array - # items: - # $ref: '#/definitions/PackConfig' + # type: array + # items: + # $ref: '#/definitions/PackConfig' examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/configs/{pack_ref}: get: operationId: st2api.controllers.v1.pack_configs:pack_configs_controller.get_one @@ -2124,17 +2123,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Config for a particular pack. schema: - $ref: '#/definitions/PackConfigView' + $ref: "#/definitions/PackConfigView" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.pack_configs:pack_configs_controller.put x-permissions: pack_config @@ -2149,7 +2148,7 @@ paths: in: body description: Pack config content schema: - $ref: '#/definitions/PackConfigContent' + $ref: "#/definitions/PackConfigContent" - name: show_secrets in: query description: Show secrets in plain text @@ -2160,17 +2159,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Config for a particular pack. schema: - $ref: '#/definitions/PackConfigCreate' + $ref: "#/definitions/PackConfigCreate" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/config_schemas: get: operationId: st2api.controllers.v1.pack_config_schemas:pack_config_schema_controller.get_all @@ -2209,19 +2208,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Get packs config schema. # schema: - # type: array - # items: - # $ref: '#/definitions/PackConfig' + # type: array + # items: + # $ref: '#/definitions/PackConfig' examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/config_schemas/{pack_ref}: get: operationId: st2api.controllers.v1.pack_config_schemas:pack_config_schema_controller.get_one @@ -2238,17 +2237,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Config schema for a particular pack. schema: - $ref: '#/definitions/PackConfigView' + $ref: "#/definitions/PackConfigView" examples: application/json: - ref: 'core.local' + ref: "core.local" default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/policytypes: get: operationId: st2api.controllers.v1.policies:policy_type_controller.get_all @@ -2304,20 +2303,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of policy types schema: type: array items: - $ref: '#/definitions/PolicyTypeList' + $ref: "#/definitions/PolicyTypeList" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/policytypes/{ref_or_id}: get: operationId: st2api.controllers.v1.policies:policy_type_controller.get_one @@ -2334,18 +2333,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Specific policy type. schema: - $ref: '#/definitions/PolicyTypeList' + $ref: "#/definitions/PolicyTypeList" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/policies: get: operationId: st2api.controllers.v1.policies:policy_controller.get_all @@ -2390,20 +2389,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Policy list schema: type: array items: - - $ref: '#/definitions/PolicyList' + - $ref: "#/definitions/PolicyList" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.policies:policy_controller.post description: | @@ -2413,7 +2412,7 @@ paths: in: body description: Policy details in yaml/json file schema: - $ref: '#/definitions/PolicyCreate' + $ref: "#/definitions/PolicyCreate" required: true x-parameters: - name: user @@ -2421,18 +2420,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Policy created successfully. schema: - $ref: '#/definitions/PolicyList' + $ref: "#/definitions/PolicyList" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/policies/{ref_or_id}: get: operationId: st2api.controllers.v1.policies:policy_controller.get_one @@ -2450,18 +2449,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Policy found. schema: - $ref: '#/definitions/PolicyList' + $ref: "#/definitions/PolicyList" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.policies:policy_controller.put description: | @@ -2476,7 +2475,7 @@ paths: in: body description: Policy details in yaml/json file schema: - $ref: '#/definitions/PolicyCreate' + $ref: "#/definitions/PolicyCreate" required: true x-parameters: - name: user @@ -2484,18 +2483,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Policy updated successfully. schema: - $ref: '#/definitions/PolicyList' + $ref: "#/definitions/PolicyList" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.policies:policy_controller.delete description: | @@ -2512,12 +2511,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '204': + "204": description: Policy deleted default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rbac/permission_types: get: operationId: st2api.controllers.v1.rbac:permission_types_controller.get_all @@ -2528,14 +2527,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Dictionary of permission types by resource types. schema: type: object default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rbac/permission_types/{resource_type}: get: operationId: st2api.controllers.v1.rbac:permission_types_controller.get_one @@ -2552,14 +2551,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of permission types. schema: type: object default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rbac/roles: get: operationId: st2api.controllers.v1.rbac:roles_controller.get_all @@ -2575,7 +2574,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of all roles. schema: type: array @@ -2583,12 +2582,12 @@ paths: type: object examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rbac/roles/{name_or_id}: get: operationId: st2api.controllers.v1.rbac:roles_controller.get_one @@ -2605,18 +2604,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of roles. schema: type: object examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rbac/role_assignments: get: operationId: st2api.controllers.v1.rbac:role_assignments_controller.get_all @@ -2644,7 +2643,7 @@ paths: description: Only include remote role assignments. type: boolean responses: - '200': + "200": description: List of all role assignments. schema: type: array @@ -2652,12 +2651,12 @@ paths: type: object examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rbac/role_assignments/{id}: get: operationId: st2api.controllers.v1.rbac:role_assignments_controller.get_one @@ -2674,18 +2673,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Role assignment object. schema: type: object examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rules: get: @@ -2747,20 +2746,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of rules schema: type: array items: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.rules:rule_controller.post description: | @@ -2770,25 +2769,25 @@ paths: in: body description: Rule content schema: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Single action being created schema: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rules/{ref_or_id}: get: x-requirements: @@ -2812,18 +2811,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Rule requested schema: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rules/{rule_ref_or_id}: put: operationId: st2api.controllers.v1.rules:rule_controller.put @@ -2839,25 +2838,25 @@ paths: in: body description: Rule content schema: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action updated schema: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.rules:rule_controller.delete description: | @@ -2874,12 +2873,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '204': + "204": description: Rule deleted default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rules/views: get: operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_all @@ -2936,20 +2935,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of rules schema: type: array items: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/rules/views/{ref_or_id}: get: operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_one @@ -2967,37 +2966,37 @@ paths: x-as: requester_user description: User running the action responses: - '200': + "200": description: Rule requested schema: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/ruletypes: get: operationId: st2api.controllers.v1.ruletypes:rule_types_controller.get_all description: Returns a list of all rule types. responses: - '200': + "200": description: List of rules schema: type: array items: - $ref: '#/definitions/RuleType' + $ref: "#/definitions/RuleType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/ruletypes/{id}: get: operationId: st2api.controllers.v1.ruletypes:rule_types_controller.get_one @@ -3009,18 +3008,18 @@ paths: type: string required: true responses: - '200': + "200": description: RuleType requested schema: - $ref: '#/definitions/RuleType' + $ref: "#/definitions/RuleType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/runnertypes: get: operationId: st2api.controllers.v1.runnertypes:runner_types_controller.get_all @@ -3073,20 +3072,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of runner types schema: type: array items: - $ref: '#/definitions/RunnerType' + $ref: "#/definitions/RunnerType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/runnertypes/{name_or_id}: get: operationId: st2api.controllers.v1.runnertypes:runner_types_controller.get_one @@ -3104,18 +3103,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: RunnerType requested schema: - $ref: '#/definitions/RunnerType' + $ref: "#/definitions/RunnerType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.runnertypes:runner_types_controller.put description: | @@ -3130,25 +3129,25 @@ paths: in: body description: RunnerType content schema: - $ref: '#/definitions/RunnerType' + $ref: "#/definitions/RunnerType" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: RunnerType updated schema: - $ref: '#/definitions/RunnerType' + $ref: "#/definitions/RunnerType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/sensortypes: get: operationId: st2api.controllers.v1.sensors:sensor_type_controller.get_all @@ -3201,20 +3200,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of rules schema: type: array items: - $ref: '#/definitions/SensorType' + $ref: "#/definitions/SensorType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/sensortypes/{ref_or_id}: get: operationId: st2api.controllers.v1.sensors:sensor_type_controller.get_one @@ -3232,18 +3231,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: SensorType requested schema: - $ref: '#/definitions/SensorType' + $ref: "#/definitions/SensorType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.sensors:sensor_type_controller.put description: | @@ -3258,25 +3257,25 @@ paths: in: body description: SensorType content schema: - $ref: '#/definitions/SensorType' + $ref: "#/definitions/SensorType" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: SensorType updated schema: - $ref: '#/definitions/SensorType' + $ref: "#/definitions/SensorType" examples: application/json: - ref: 'core.webhook' + ref: "core.webhook" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/apikeys: get: operationId: st2api.controllers.v1.auth:api_key_controller.get_all @@ -3303,20 +3302,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of actions schema: type: array items: - $ref: '#/definitions/ApiKey' + $ref: "#/definitions/ApiKey" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.auth:api_key_controller.post description: | @@ -3326,25 +3325,25 @@ paths: in: body description: Action content schema: - $ref: '#/definitions/ApiKey' + $ref: "#/definitions/ApiKey" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '201': + "201": description: Single action being created schema: - $ref: '#/definitions/ApiKey' + $ref: "#/definitions/ApiKey" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/apikeys/{api_key_id_or_key}: get: operationId: st2api.controllers.v1.auth:api_key_controller.get_one @@ -3366,18 +3365,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action requested schema: - $ref: '#/definitions/ApiKey' + $ref: "#/definitions/ApiKey" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.auth:api_key_controller.put description: | @@ -3392,25 +3391,25 @@ paths: in: body description: Action content schema: - $ref: '#/definitions/ApiKey' + $ref: "#/definitions/ApiKey" x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action updated schema: - $ref: '#/definitions/ApiKey' + $ref: "#/definitions/ApiKey" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.auth:api_key_controller.delete description: | @@ -3427,12 +3426,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '204': + "204": description: Action deleted default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/ruleenforcements: get: operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_all @@ -3497,13 +3496,13 @@ paths: - name: enforced_at_gt in: query description: | - Only return enforcements with enforced_at greater than the one provided. Use time in the format. + Only return enforcements with enforced_at greater than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ - name: enforced_at_lt in: query description: | - Only return enforcements with enforced_at lower than the one provided. Use time in the format. + Only return enforcements with enforced_at lower than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ x-parameters: @@ -3512,20 +3511,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of rule enforcements schema: type: array items: - $ref: '#/definitions/RuleEnforcementsList' + $ref: "#/definitions/RuleEnforcementsList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/ruleenforcements/views: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_all @@ -3590,13 +3589,13 @@ paths: - name: enforced_at_gt in: query description: | - Only return enforcements with enforced_at greater than the one provided. Use time in the format. + Only return enforcements with enforced_at greater than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ - name: enforced_at_lt in: query description: | - Only return enforcements with enforced_at lower than the one provided. Use time in the format. + Only return enforcements with enforced_at lower than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ x-parameters: @@ -3605,20 +3604,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of rule enforcements schema: type: array items: - $ref: '#/definitions/RuleEnforcementsList' + $ref: "#/definitions/RuleEnforcementsList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/ruleenforcements/{id}: get: operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one @@ -3635,18 +3634,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Rule Enforcements based on ref or id schema: - $ref: '#/definitions/RuleEnforcementsList' + $ref: "#/definitions/RuleEnforcementsList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/ruleenforcements/views/{id}: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_one @@ -3663,18 +3662,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Rule Enforcements based on ref or id schema: - $ref: '#/definitions/RuleEnforcementsList' + $ref: "#/definitions/RuleEnforcementsList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/timers: get: @@ -3687,20 +3686,20 @@ paths: description: Type of timer type: string responses: - '200': + "200": description: List of timers schema: type: array items: - $ref: '#/definitions/TimersList' + $ref: "#/definitions/TimersList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/timers/{ref_or_id}: get: operationId: st2api.controllers.v1.timers:timers_controller.get_one @@ -3717,18 +3716,18 @@ paths: x-as: requester_user description: User performing the operation responses: - '200': + "200": description: Trace schema: - $ref: '#/definitions/TimersList' + $ref: "#/definitions/TimersList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/traces: get: operationId: st2api.controllers.v1.traces:traces_controller.get_all @@ -3793,12 +3792,12 @@ paths: - name: sort_asc in: query description: | - Sort in ascending order by start timestamp, asc/ascending (earliest first) + Sort in ascending order by start timestamp, asc/ascending (earliest first) type: string - name: sort_desc in: query description: | - Sort in descending order by start timestamp, desc/descending (latest first) + Sort in descending order by start timestamp, desc/descending (latest first) type: string x-parameters: - name: user @@ -3806,20 +3805,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of traces schema: type: array items: - $ref: '#/definitions/TracesList' + $ref: "#/definitions/TracesList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/traces/{id}: get: operationId: st2api.controllers.v1.traces:traces_controller.get_one @@ -3836,18 +3835,18 @@ paths: x-as: requester_user description: User performing the operation responses: - '200': + "200": description: Trace schema: - $ref: '#/definitions/TracesList' + $ref: "#/definitions/TracesList" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/triggertypes: get: operationId: st2api.controllers.v1.triggers:triggertype_controller.get_all @@ -3902,20 +3901,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of trigger types schema: type: array items: - $ref: '#/definitions/TriggerType' + $ref: "#/definitions/TriggerType" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.triggers:triggertype_controller.post description: | @@ -3925,20 +3924,20 @@ paths: in: body description: TriggerType content schema: - $ref: '#/definitions/TriggerTypeRequest' + $ref: "#/definitions/TriggerTypeRequest" responses: - '201': + "201": description: Single trigger type being created schema: - $ref: '#/definitions/TriggerType' + $ref: "#/definitions/TriggerType" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/triggertypes/{triggertype_ref_or_id}: get: operationId: st2api.controllers.v1.triggers:triggertype_controller.get_one @@ -3951,18 +3950,18 @@ paths: type: string required: true responses: - '200': + "200": description: TriggerType requested schema: - $ref: '#/definitions/TriggerType' + $ref: "#/definitions/TriggerType" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.triggers:triggertype_controller.put description: | @@ -3977,20 +3976,20 @@ paths: in: body description: TriggerType content schema: - $ref: '#/definitions/TriggerTypeRequest' + $ref: "#/definitions/TriggerTypeRequest" responses: - '200': + "200": description: TriggerType updated schema: - $ref: '#/definitions/TriggerType' + $ref: "#/definitions/TriggerType" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.triggers:triggertype_controller.delete description: | @@ -4002,12 +4001,12 @@ paths: type: string required: true responses: - '204': + "204": description: TriggerType deleted default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/triggers: get: operationId: st2api.controllers.v1.triggers:trigger_controller.get_all @@ -4018,20 +4017,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of triggers schema: type: array items: - $ref: '#/definitions/Trigger' + $ref: "#/definitions/Trigger" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" post: operationId: st2api.controllers.v1.triggers:trigger_controller.post description: | @@ -4041,20 +4040,20 @@ paths: in: body description: Trigger content schema: - $ref: '#/definitions/TriggerRequest' + $ref: "#/definitions/TriggerRequest" responses: - '201': + "201": description: Single trigger being created schema: - $ref: '#/definitions/Trigger' + $ref: "#/definitions/Trigger" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/triggers/{trigger_id}: get: operationId: st2api.controllers.v1.triggers:trigger_controller.get_one @@ -4067,18 +4066,18 @@ paths: type: string required: true responses: - '200': + "200": description: Trigger requested schema: - $ref: '#/definitions/Trigger' + $ref: "#/definitions/Trigger" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" put: operationId: st2api.controllers.v1.triggers:trigger_controller.put description: | @@ -4093,20 +4092,20 @@ paths: in: body description: Trigger content schema: - $ref: '#/definitions/TriggerRequest' + $ref: "#/definitions/TriggerRequest" responses: - '200': + "200": description: Trigger updated schema: - $ref: '#/definitions/Trigger' + $ref: "#/definitions/Trigger" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" delete: operationId: st2api.controllers.v1.triggers:trigger_controller.delete description: | @@ -4118,12 +4117,12 @@ paths: type: string required: true responses: - '204': + "204": description: Trigger deleted default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/triggerinstances: get: operationId: st2api.controllers.v1.triggers:triggerinstance_controller.get_all @@ -4192,20 +4191,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of trigger instances schema: type: array items: - $ref: '#/definitions/TriggerInstance' + $ref: "#/definitions/TriggerInstance" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/triggerinstances/{instance_id}: get: operationId: st2api.controllers.v1.triggers:triggerinstance_controller.get_one @@ -4218,18 +4217,18 @@ paths: type: string required: true responses: - '200': + "200": description: Trigger instance requested schema: - $ref: '#/definitions/TriggerInstance' + $ref: "#/definitions/TriggerInstance" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/triggerinstances/{trigger_instance_id}/re_emit: post: operationId: st2api.controllers.v1.triggers:triggerinstance_resend_controller.post @@ -4242,38 +4241,38 @@ paths: type: string required: true responses: - '200': + "200": description: Trigger instance being re-emitted schema: - $ref: '#/definitions/TriggerInstance' + $ref: "#/definitions/TriggerInstance" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/webhooks: get: operationId: st2api.controllers.v1.webhooks:webhooks_controller.get_all x-permissions: webhook_list description: Returns a list of all webhooks. responses: - '200': + "200": description: List of webhooks schema: type: array items: - $ref: '#/definitions/Webhook' + $ref: "#/definitions/Webhook" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/webhooks/{hook}: post: operationId: st2api.controllers.v1.webhooks:webhooks_controller.post @@ -4291,7 +4290,7 @@ paths: in: body description: Webhook payload schema: - $ref: '#/definitions/WebhookBody' + $ref: "#/definitions/WebhookBody" x-parameters: - name: headers in: request @@ -4301,18 +4300,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '202': + "202": description: Single action being created schema: - $ref: '#/definitions/WebhookBody' + $ref: "#/definitions/WebhookBody" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/webhooks/{url}: get: operationId: st2api.controllers.v1.webhooks:webhooks_controller.get_one @@ -4330,18 +4329,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: Action requested schema: - $ref: '#/definitions/Webhook' + $ref: "#/definitions/Webhook" examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/workflows/inspect: post: operationId: st2api.controllers.v1.workflow_inspection:workflow_inspection_controller.post @@ -4353,20 +4352,20 @@ paths: schema: type: string responses: - '200': + "200": description: List of inspection errors separated by categories. schema: - $ref: '#/definitions/WorkflowInspectionErrors' + $ref: "#/definitions/WorkflowInspectionErrors" examples: application/json: - type: "content" - message: "The action \"std.noop\" is not registered in the database." + message: 'The action "std.noop" is not registered in the database.' schema_path: "properties.tasks.patternProperties.^\\w+$.properties.action" spec_path: "tasks.task3.action" - type: "context" language: "yaql" expression: "<% ctx().foobar %>" - message: "Variable \"foobar\" is referenced before assignment." + message: 'Variable "foobar" is referenced before assignment.' schema_path: "properties.tasks.patternProperties.^\\w+$.properties.input" spec_path: "tasks.task1.input" - type: "expression" @@ -4382,7 +4381,7 @@ paths: default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/service_registry/groups: get: @@ -4394,14 +4393,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of available groups. schema: type: object default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /api/v1/service_registry/groups/{group_id}/members: get: operationId: st2api.controllers.v1.service_registry:members_controller.get_one @@ -4418,15 +4417,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: List of available members. schema: type: object default: description: Unexpected error schema: - $ref: '#/definitions/Error' - + $ref: "#/definitions/Error" /auth/v1/tokens: post: @@ -4448,7 +4446,7 @@ paths: in: body description: Lifespan of the token schema: - $ref: '#/definitions/TokenRequest' + $ref: "#/definitions/TokenRequest" x-parameters: - name: remote_addr in: environ @@ -4459,31 +4457,31 @@ paths: description: set externally to indicate user identity in case of proxy auth type: string responses: - '201': + "201": description: New token has been created schema: - $ref: '#/definitions/Token' + $ref: "#/definitions/Token" headers: x-api-url: type: string examples: application/json: user: st2admin - token: '5e86421776f946e98faea36c29e5a7c7' - expiry: '2016-05-28T12:39:28.650231Z' - id: '574840001878c10d0b6e8fbf' + token: "5e86421776f946e98faea36c29e5a7c7" + expiry: "2016-05-28T12:39:28.650231Z" + id: "574840001878c10d0b6e8fbf" metadata: {} - '401': + "401": description: Invalid or missing credentials has been provided schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" examples: application/json: faultstring: Invalid or missing credentials default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" security: [] /auth/v1/tokens/validate: post: @@ -4495,36 +4493,36 @@ paths: in: body description: Token to validate schema: - $ref: '#/definitions/TokenValidationRequest' + $ref: "#/definitions/TokenValidationRequest" responses: - '200': + "200": description: Validation results schema: - $ref: '#/definitions/TokenValidationResult' + $ref: "#/definitions/TokenValidationResult" examples: application/json: valid: false default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /auth/v1/sso: get: operationId: st2auth.controllers.v1.sso:sso_controller.get description: Returns a flag indicating if SSO is enabled or not. responses: - '200': + "200": description: Returns a flag indicating if SSO is enabled schema: - $ref: '#/definitions/SSOEnabledResult' + $ref: "#/definitions/SSOEnabledResult" examples: application/json: enabled: false default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" security: [] /auth/v1/sso/request/web: @@ -4537,7 +4535,7 @@ paths: description: set externally to indicate real source of the request type: string responses: - '307': + "307": description: Temporary redirect security: [] @@ -4551,17 +4549,18 @@ paths: description: SSO request with callback and key encryption schema: type: object + required: + - key + - callback_url properties: key: type: string description: The symmetric key to be used to encrypt contents of callback - required: true callback_url: type: string - description: What URL to be called back once the response from SSO is received - required: true + description: What URL to be called back once the response from SSO is received responses: - '200': + "200": description: SSO request valid security: [] /auth/v1/sso/callback: @@ -4575,21 +4574,21 @@ paths: schema: type: object responses: - '200': + "200": description: SSO response valid - '302': + "302": description: SSO response valid and callback URL returned - '401': + "401": description: Invalid or missing credentials has been provided schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" examples: application/json: faultstring: Invalid or missing credentials default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" security: [] /stream/v1/stream: @@ -4637,16 +4636,16 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: EventSource compatible stream of events examples: application/json: - ref: 'core.local' + ref: "core.local" # and stuff default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" /stream/v1/executions/{id}/output: get: operationId: st2stream.controllers.v1.executions:action_execution_output_controller.get_one @@ -4675,12 +4674,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - '200': + "200": description: EventSource compatible stream of events default: description: Unexpected error schema: - $ref: '#/definitions/Error' + $ref: "#/definitions/Error" definitions: Action: @@ -4715,12 +4714,12 @@ definitions: entry_point: type: string description: The entry point for the action. - default: '' + default: "" pack: type: string description: The content pack this action belongs to. parameters: - $ref: '#/definitions/ActionParametersSubSchema' + $ref: "#/definitions/ActionParametersSubSchema" tags: type: array description: User associated metadata assigned to this object. @@ -4730,9 +4729,9 @@ definitions: description: Notification settings for action. type: object properties: - on-complete: {$ref: '#/definitions/NotificationPropertySubSchema'} - on-failure: {$ref: '#/definitions/NotificationPropertySubSchema'} - on-success: {$ref: '#/definitions/NotificationPropertySubSchema'} + on-complete: { $ref: "#/definitions/NotificationPropertySubSchema" } + on-failure: { $ref: "#/definitions/NotificationPropertySubSchema" } + on-success: { $ref: "#/definitions/NotificationPropertySubSchema" } additionalProperties: False # additionalProperties: false ActionCreateRequest: @@ -4743,35 +4742,35 @@ definitions: pack: type: string description: The content pack this action belongs to. - default: 'default' - - $ref: '#/definitions/Action' - - $ref: '#/definitions/DataFilesSubSchema' + default: "default" + - $ref: "#/definitions/Action" + - $ref: "#/definitions/DataFilesSubSchema" ActionUpdateRequest: x-api-model: st2common.models.api.action:ActionUpdateAPI allOf: - - $ref: '#/definitions/Action' - - $ref: '#/definitions/DataFilesSubSchema' + - $ref: "#/definitions/Action" + - $ref: "#/definitions/DataFilesSubSchema" ActionDeleteRequest: allOf: - - $ref: '#/definitions/ActionDeleteSchema' + - $ref: "#/definitions/ActionDeleteSchema" ActionCloneRequest: allOf: - - $ref: '#/definitions/ActionCloneSchema' + - $ref: "#/definitions/ActionCloneSchema" ActionParameters: type: object properties: parameters: - $ref: '#/definitions/ActionParametersSubSchema' + $ref: "#/definitions/ActionParametersSubSchema" ActionAlias: x-api-model: st2common.models.api.action:ActionAliasAPI type: object ActionAliasRequest: type: object required: - - name - - ref - - pack - - action_ref + - name + - ref + - pack + - action_ref properties: name: description: Alias name. @@ -4795,15 +4794,15 @@ definitions: description: Enable or disable the action from invocation. default: True ActionAliasMatchRequest: - type: object - required: - - command - properties: - command: - description: Command string to try to match the aliases against. - type: string + type: object + required: + - command + properties: + command: + description: Command string to try to match the aliases against. + type: string ActionAliasMatch: - type: object + type: object ActionAliasHelp: type: object properties: @@ -4863,21 +4862,21 @@ definitions: description: Possible parameter format. items: oneOf: - - type: string - - type: object - description: Matched alias formats - properties: - representation: - type: array - description: Alias format representations - items: + - type: string + - type: object + description: Matched alias formats + properties: + representation: + type: array + description: Alias format representations + items: + type: string + match_multiple: + type: boolean + optional: true + display: type: string - match_multiple: - type: boolean - optional: true - display: - type: string - description: Display string of alias format + description: Display string of alias format ack: type: object description: Acknowledgement message format. @@ -4910,7 +4909,7 @@ definitions: type: object description: Execution result properties: - $ref: '#/definitions/Execution/properties' + $ref: "#/definitions/Execution/properties" message: type: string AliasMatchAndExecuteInputAPI: @@ -4946,19 +4945,19 @@ definitions: id: type: string trigger: - $ref: '#/definitions/Trigger' + $ref: "#/definitions/Trigger" trigger_type: - $ref: '#/definitions/TriggerType' + $ref: "#/definitions/TriggerType" trigger_instance: - $ref: '#/definitions/TriggerInstance' + $ref: "#/definitions/TriggerInstance" rule: - $ref: '#/definitions/Rule' + $ref: "#/definitions/Rule" action: - $ref: '#/definitions/Action' + $ref: "#/definitions/Action" runner: - $ref: '#/definitions/RunnerType' + $ref: "#/definitions/RunnerType" liveaction: - $ref: '#/definitions/LiveAction' + $ref: "#/definitions/LiveAction" task_execution: type: string workflow_execution: @@ -4966,7 +4965,23 @@ definitions: status: description: The current status of the action execution. type: string - enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] + enum: + [ + "requested", + "scheduled", + "delayed", + "running", + "succeeded", + "failed", + "timeout", + "abandoned", + "canceling", + "canceled", + "pending", + "pausing", + "paused", + "resuming", + ] start_timestamp: description: The start time when the action is executed. type: string @@ -4978,35 +4993,35 @@ definitions: elapsed_seconds: description: Time duration in seconds taken for completion of this execution. type: number -# required: False + # required: False web_url: description: History URL for this execution if you want to view in UI. type: string -# required: False + # required: False parameters: description: Input parameters for the action. type: object -# patternProperties: -# ^\w+$: -# anyOf: -# - type: array -# - type: boolean -# - type: integer -# - type: number -# - type: object -# - type: string -# additionalProperties: False + # patternProperties: + # ^\w+$: + # anyOf: + # - type: array + # - type: boolean + # - type: integer + # - type: number + # - type: object + # - type: string + # additionalProperties: False context: type: object result: type: object -# anyOf: -# - type: array -# - type: boolean -# - type: integer -# - type: number -# - type: object -# - type: string + # anyOf: + # - type: array + # - type: boolean + # - type: integer + # - type: number + # - type: object + # - type: string result_size: type: integer default: 0 @@ -5028,7 +5043,23 @@ definitions: pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ status: type: string - enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] + enum: + [ + "requested", + "scheduled", + "delayed", + "running", + "succeeded", + "failed", + "timeout", + "abandoned", + "canceling", + "canceled", + "pending", + "pausing", + "paused", + "resuming", + ] delay: description: How long (in milliseconds) to delay the execution before scheduling. type: integer @@ -5037,21 +5068,37 @@ definitions: additionalProperties: False ExecutionRequest: allOf: - - $ref: '#/definitions/LiveAction' + - $ref: "#/definitions/LiveAction" - type: object properties: user: type: string x-nullable: true description: User context under which action should run (admins only) - default: '' + default: "" ExecutionUpdateRequest: type: object properties: status: description: The current status of the action execution. type: string - enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] + enum: + [ + "requested", + "scheduled", + "delayed", + "running", + "succeeded", + "failed", + "timeout", + "abandoned", + "canceling", + "canceled", + "pending", + "pausing", + "paused", + "resuming", + ] result: type: object additionalProperties: False @@ -5225,33 +5272,36 @@ definitions: description: Types of content to register. type: array items: - $ref: '#/definitions/PacksContentRegisterType' + $ref: "#/definitions/PacksContentRegisterType" fail_on_failure: type: boolean description: True to fail on failure default: true PacksContentRegisterType: - type: string - enum: ['all', - 'runner', - 'action', - 'actions', - 'trigger', - 'triggers', - 'sensor', - 'sensors', - 'rule', - 'rules', - 'rule_type', - 'rule_types', - 'alias', - 'aliases', - 'policy_type', - 'policy_types', - 'policy', - 'policies', - 'config', - 'configs'] + type: string + enum: + [ + "all", + "runner", + "action", + "actions", + "trigger", + "triggers", + "sensor", + "sensors", + "rule", + "rules", + "rule_type", + "rule_types", + "alias", + "aliases", + "policy_type", + "policy_types", + "policy", + "policies", + "config", + "configs", + ] PacksSearchShow: type: object properties: @@ -5339,7 +5389,23 @@ definitions: status: description: The current status of the action execution. type: string - enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] + enum: + [ + "requested", + "scheduled", + "delayed", + "running", + "succeeded", + "failed", + "timeout", + "abandoned", + "canceling", + "canceled", + "pending", + "pausing", + "paused", + "resuming", + ] start_timestamp: description: The start time when the action is executed. type: string @@ -5387,9 +5453,9 @@ definitions: description: Notification settings for liveaction. type: object properties: - on-complete: {$ref: '#/definitions/NotificationPropertySubSchema'} - on-failure: {$ref: '#/definitions/NotificationPropertySubSchema'} - on-success: {$ref: '#/definitions/NotificationPropertySubSchema'} + on-complete: { $ref: "#/definitions/NotificationPropertySubSchema" } + on-failure: { $ref: "#/definitions/NotificationPropertySubSchema" } + on-success: { $ref: "#/definitions/NotificationPropertySubSchema" } additionalProperties: False delay: description: How long (in milliseconds) to delay the execution before scheduling. @@ -5473,7 +5539,7 @@ definitions: description: Channels to post notifications to. items: type: string - channels: # Deprecated. Only here for backward compatibility. + channels: # Deprecated. Only here for backward compatibility. type: array description: Channels to post notifications to. items: @@ -5493,7 +5559,7 @@ definitions: ttl: type: - integer - - 'null' + - "null" minimum: 1 Token: type: object @@ -5516,7 +5582,7 @@ definitions: token: type: - string - - 'null' + - "null" TokenValidationResult: type: object properties: @@ -5567,7 +5633,7 @@ definitions: WorkflowInspectionErrors: type: array items: - $ref: '#/definitions/WorkflowInspectionError' + $ref: "#/definitions/WorkflowInspectionError" ValidationError: type: object diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index 6131d106c4..9c2177bc41 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -4547,15 +4547,16 @@ paths: description: SSO request with callback and key encryption schema: type: object + required: + - key + - callback_url properties: key: type: string description: The symmetric key to be used to encrypt contents of callback - required: true callback_url: type: string - description: What URL to be called back once the response from SSO is received - required: true + description: What URL to be called back once the response from SSO is received responses: '200': description: SSO request valid From eb17b7dadb3c10af72228eeff7320ec2d005f83a Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 21 Sep 2022 14:26:26 -0300 Subject: [PATCH 43/49] regenerating openapi spec --- st2common/st2common/openapi.yaml | 1225 ++++++++++++++---------------- 1 file changed, 580 insertions(+), 645 deletions(-) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index a1fa29f29b..de2c693a27 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -2,13 +2,13 @@ # Edit st2common/st2common/openapi.yaml.j2 and then run # make .generate-api-spec # to generate the final spec file -swagger: "2.0" +swagger: '2.0' info: version: "1.0.0" title: StackStorm API description: | - + ## Welcome Welcome to the StackStorm API Reference documentation! You can use the StackStorm API to integrate StackStorm with 3rd-party systems and custom applications. Example integrations include writing your own self-service user portal, or integrating with other orquestation systems. @@ -197,6 +197,7 @@ info: Join our [Slack Community](https://stackstorm.com/community-signup) to get help from the engineering team and fellow users. You can also create issues against the main [StackStorm GitHub repo](https://github.com/StackStorm/st2/issues), or the [st2apidocs repo](https://github.com/StackStorm/st2apidocs) for documentation-specific issues. We also recommend reviewing the main [StackStorm documentation](https://docs.stackstorm.com/). + paths: /api/v1/: @@ -204,7 +205,7 @@ paths: operationId: st2api.controllers.root:root_controller.index description: General API info. responses: - "200": + '200': description: General API info. schema: type: object @@ -215,12 +216,12 @@ paths: type: string examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/user: get: operationId: st2api.controllers.v1.user:user_controller.get @@ -235,14 +236,14 @@ paths: x-as: auth_info description: Information on how user authenticated. responses: - "200": + '200': description: Metadata information about the authenticated user. schema: type: object default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actions: get: @@ -298,20 +299,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of actions schema: type: array items: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.actions:actions_controller.post description: | @@ -321,25 +322,25 @@ paths: in: body description: Action content schema: - $ref: "#/definitions/ActionCreateRequest" + $ref: '#/definitions/ActionCreateRequest' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Single action being created schema: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actions/{ref_or_id}: get: operationId: st2api.controllers.v1.actions:actions_controller.get_one @@ -357,18 +358,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action requested schema: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.actions:actions_controller.put description: | @@ -383,25 +384,25 @@ paths: in: body description: Action content schema: - $ref: "#/definitions/ActionUpdateRequest" + $ref: '#/definitions/ActionUpdateRequest' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action updated schema: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.actions:actions_controller.delete description: | @@ -416,19 +417,19 @@ paths: in: body description: Flag to remove action files from disk schema: - $ref: "#/definitions/ActionDeleteRequest" + $ref: '#/definitions/ActionDeleteRequest' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "204": + '204': description: Action deleted default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actions/{ref_or_id}/clone: post: operationId: st2api.controllers.v1.actions:actions_controller.clone @@ -444,7 +445,7 @@ paths: in: body description: Destination action content schema: - $ref: "#/definitions/ActionCloneRequest" + $ref: '#/definitions/ActionCloneRequest' required: true x-parameters: - name: user @@ -452,18 +453,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Single action being cloned schema: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actions/views/parameters/{ref_or_id}: get: operationId: st2api.controllers.v1.action_views:parameters_view_controller.get_one @@ -481,10 +482,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: An object containing action parameters schema: - $ref: "#/definitions/ActionParameters" + $ref: '#/definitions/ActionParameters' examples: application/json: parameters: @@ -494,7 +495,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actions/views/overview: get: operationId: st2api.controllers.v1.action_views:overview_controller.get_all @@ -543,20 +544,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of actions schema: type: array items: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actions/views/overview/{ref_or_id}: get: operationId: st2api.controllers.v1.action_views:overview_controller.get_one @@ -574,18 +575,18 @@ paths: x-as: requester_user description: User running the action responses: - "200": + '200': description: Action requested schema: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actions/views/entry_point/{ref_or_id}: get: operationId: st2api.controllers.v1.action_views:entry_point_controller.get_one @@ -603,7 +604,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Entry point code schema: type: string @@ -616,13 +617,13 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actionalias: get: operationId: st2api.controllers.v1.actionalias:action_alias_controller.get_all x-permissions: action_alias_list description: | - Get list of action-aliases. + Get list of action-aliases. parameters: - name: exclude_attributes in: query @@ -665,20 +666,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of action aliases. schema: type: array items: - $ref: "#/definitions/ActionAlias" + $ref: '#/definitions/ActionAlias' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.actionalias:action_alias_controller.post description: | @@ -688,25 +689,25 @@ paths: in: body description: Action alias file. schema: - $ref: "#/definitions/ActionAlias" + $ref: '#/definitions/ActionAlias' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Action alias created schema: - $ref: "#/definitions/ActionAlias" + $ref: '#/definitions/ActionAlias' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actionalias/{ref_or_id}: get: operationId: st2api.controllers.v1.actionalias:action_alias_controller.get_one @@ -726,18 +727,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action alias requested schema: - $ref: "#/definitions/ActionAlias" + $ref: '#/definitions/ActionAlias' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.actionalias:action_alias_controller.put description: Update action alias @@ -751,7 +752,7 @@ paths: in: body description: JSON/YAML file containing the action alias to update. schema: - $ref: "#/definitions/ActionAlias" + $ref: '#/definitions/ActionAlias' required: true x-parameters: - name: user @@ -759,18 +760,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action alias updated. schema: - $ref: "#/definitions/ActionAlias" + $ref: '#/definitions/ActionAlias' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.actionalias:action_alias_controller.delete description: | @@ -787,12 +788,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "204": + '204': description: Action alias deleted default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actionalias/match: post: operationId: st2api.controllers.v1.actionalias:action_alias_controller.match @@ -804,16 +805,16 @@ paths: in: body description: Object containing the format to be matched. schema: - $ref: "#/definitions/ActionAliasMatchRequest" + $ref: '#/definitions/ActionAliasMatchRequest' responses: - "200": + '200': description: Action alias match pattern schema: - $ref: "#/definitions/ActionAliasMatch" + $ref: '#/definitions/ActionAliasMatch' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/actionalias/help: get: operationId: st2api.controllers.v1.actionalias:action_alias_controller.help @@ -840,18 +841,18 @@ paths: type: integer default: 0 responses: - "200": + '200': description: Action alias match pattern schema: - $ref: "#/definitions/ActionAliasHelp" + $ref: '#/definitions/ActionAliasHelp' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/aliasexecution: post: operationId: st2api.controllers.v1.aliasexecution:action_alias_execution_controller.post @@ -862,7 +863,7 @@ paths: in: body description: Alias execution payload. schema: - $ref: "#/definitions/AliasExecution" + $ref: '#/definitions/AliasExecution' - name: show_secrets in: query description: Show secrets in plain text @@ -873,18 +874,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Action alias created schema: - $ref: "#/definitions/AliasExecution" + $ref: '#/definitions/AliasExecution' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/aliasexecution/match_and_execute: post: operationId: st2api.controllers.v1.aliasexecution:action_alias_execution_controller.match_and_execute @@ -895,7 +896,7 @@ paths: in: body description: Input data. schema: - $ref: "#/definitions/AliasMatchAndExecuteInputAPI" + $ref: '#/definitions/AliasMatchAndExecuteInputAPI' - name: show_secrets in: query description: Show secrets in plain text @@ -906,7 +907,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Action alias executions created schema: type: object @@ -916,15 +917,15 @@ paths: type: array items: type: object - $ref: "#/definitions/AliasExecution" + $ref: '#/definitions/AliasExecution' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions: get: @@ -1045,12 +1046,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of executions schema: type: array items: - $ref: "#/definitions/Execution" + $ref: '#/definitions/Execution' examples: application/json: - trigger: @@ -1061,7 +1062,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.post x-log-result: false @@ -1072,7 +1073,7 @@ paths: in: body description: Execution request schema: - $ref: "#/definitions/ExecutionRequest" + $ref: '#/definitions/ExecutionRequest' - name: show_secrets in: query description: Show secrets in plain text @@ -1088,10 +1089,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Execution being created schema: - $ref: "#/definitions/Execution" + $ref: '#/definitions/Execution' examples: application/json: trigger: @@ -1102,7 +1103,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions/{id}: get: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.get_one @@ -1144,10 +1145,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Execution requested schema: - $ref: "#/definitions/Execution" + $ref: '#/definitions/Execution' examples: application/json: trigger: @@ -1158,7 +1159,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.put description: | @@ -1173,7 +1174,7 @@ paths: in: body description: Execution update request schema: - $ref: "#/definitions/ExecutionUpdateRequest" + $ref: '#/definitions/ExecutionUpdateRequest' - name: show_secrets in: query description: Show secrets in plain text @@ -1184,14 +1185,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Execution that was updated schema: - $ref: "#/definitions/Execution" + $ref: '#/definitions/Execution' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.actionexecutions:action_executions_controller.delete description: | @@ -1212,12 +1213,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Execution cancelled default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions/{id}/output: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_output_controller.get_one @@ -1246,12 +1247,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Execution output. default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions/{id}/re_run: post: operationId: st2api.controllers.v1.actionexecutions:action_execution_rerun_controller.post @@ -1282,7 +1283,7 @@ paths: default: [] user: type: string - default: "" + default: '' delay: description: How long (in milliseconds) to delay the execution before scheduling. type: integer @@ -1296,10 +1297,10 @@ paths: x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Single action being created schema: - $ref: "#/definitions/Execution" + $ref: '#/definitions/Execution' examples: application/json: trigger: @@ -1310,7 +1311,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions/{id}/attribute/{attribute}: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_attribute_controller.get @@ -1335,7 +1336,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Execution attribute requested examples: application/json: @@ -1345,7 +1346,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions/{id}/children: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_children_controller.get_one @@ -1374,12 +1375,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Execution attribute requested schema: type: array items: - $ref: "#/definitions/Execution" + $ref: '#/definitions/Execution' examples: application/json: trigger: @@ -1388,7 +1389,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions/{id}/result: get: operationId: st2api.controllers.v1.actionexecutions:action_execution_raw_result_controller.get @@ -1423,7 +1424,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Execution result schema: type: string @@ -1432,7 +1433,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/executions/views/filters: get: operationId: st2api.controllers.v1.execution_views:filters_controller.get_all @@ -1447,10 +1448,10 @@ paths: items: type: string responses: - "200": + '200': description: A number of distinct values for the requested filters schema: - $ref: "#/definitions/ExecutionFilters" + $ref: '#/definitions/ExecutionFilters' examples: application/json: trigger: @@ -1461,7 +1462,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/inquiries: get: operationId: st2api.controllers.v1.inquiries:inquiries_controller.get_all @@ -1492,16 +1493,16 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of inquries schema: type: array items: - $ref: "#/definitions/Inquiry" + $ref: '#/definitions/Inquiry' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/inquiries/{inquiry_id}: get: operationId: st2api.controllers.v1.inquiries:inquiries_controller.get_one @@ -1519,14 +1520,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Inquiry requested. schema: - $ref: "#/definitions/Inquiry" + $ref: '#/definitions/Inquiry' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.inquiries:inquiries_controller.put description: | @@ -1542,25 +1543,25 @@ paths: required: true description: Inquiry response schema: - $ref: "#/definitions/InquiryResponseResult" + $ref: '#/definitions/InquiryResponseResult' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Response received schema: - $ref: "#/definitions/InquiryResponseResult" + $ref: '#/definitions/InquiryResponseResult' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/keys: get: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.get_all @@ -1570,7 +1571,7 @@ paths: - name: prefix in: query description: | - Only return values which name starts with the provided prefix. + Only return values which name starts with the provided prefix. type: string - name: scope in: query @@ -1612,19 +1613,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of key value pairs schema: type: array items: - $ref: "#/definitions/KeyValuePair" + $ref: '#/definitions/KeyValuePair' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/keys/{name}: get: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.get_one @@ -1654,18 +1655,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Key value pair requested. schema: - $ref: "#/definitions/KeyValuePair" + $ref: '#/definitions/KeyValuePair' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.put description: | @@ -1680,7 +1681,7 @@ paths: in: body description: Key Value pair content. schema: - $ref: "#/definitions/KeyValuePairRequest" + $ref: '#/definitions/KeyValuePairRequest' required: true x-parameters: - name: user @@ -1688,18 +1689,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Key set/updated. schema: - $ref: "#/definitions/KeyValuePair" + $ref: '#/definitions/KeyValuePair' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.keyvalue:key_value_pair_controller.delete description: Delete a Key. @@ -1723,12 +1724,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "204": + '204': description: Key deleted. default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs: get: operationId: st2api.controllers.v1.packs:packs_controller.get_all @@ -1782,19 +1783,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of installed packs. schema: type: array items: - $ref: "#/definitions/PacksList" + $ref: '#/definitions/PacksList' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/{ref_or_id}: get: operationId: st2api.controllers.v1.packs:packs_controller.get_one @@ -1813,17 +1814,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Information about a pack. schema: - $ref: "#/definitions/PackView" + $ref: '#/definitions/PackView' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/install: post: operationId: st2api.controllers.v1.packs:packs_controller.install.post @@ -1835,25 +1836,25 @@ paths: in: body description: Packs to be installed schema: - $ref: "#/definitions/PacksInstall" + $ref: '#/definitions/PacksInstall' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "202": + '202': description: Pack installation request has been accepted schema: - $ref: "#/definitions/AsyncRequest" + $ref: '#/definitions/AsyncRequest' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/uninstall: post: operationId: st2api.controllers.v1.packs:packs_controller.uninstall.post @@ -1865,25 +1866,25 @@ paths: in: body description: Packs to be uninstalled schema: - $ref: "#/definitions/PacksUninstall" + $ref: '#/definitions/PacksUninstall' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "202": + '202': description: Pack uninstallation request has been accepted schema: - $ref: "#/definitions/AsyncRequest" + $ref: '#/definitions/AsyncRequest' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/register: post: operationId: st2api.controllers.v1.packs:packs_controller.register.post @@ -1895,37 +1896,37 @@ paths: in: body description: Pack(s) to be Registered schema: - $ref: "#/definitions/PacksRegister" + $ref: '#/definitions/PacksRegister' responses: - "200": + '200': description: Pack(s) Registered. schema: - $ref: "#/definitions/PacksList" + $ref: '#/definitions/PacksList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/index: get: operationId: st2api.controllers.v1.packs:packs_controller.index.get_all x-permissions: pack_search description: To list all the packs of all indexes used by your StackStorm instance. responses: - "200": + '200': description: Pack index. schema: - $ref: "#/definitions/PackIndex" + $ref: '#/definitions/PackIndex' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/index/search: post: operationId: st2api.controllers.v1.packs:packs_controller.index.search.post @@ -1937,9 +1938,9 @@ paths: in: body description: A query to search a pack or a pack name to get its details schema: - $ref: "#/definitions/PacksSearchShow" + $ref: '#/definitions/PacksSearchShow' responses: - "200": + '200': description: Pack search results. schema: type: @@ -1951,12 +1952,12 @@ paths: # $ref: '#/definitions/PacksList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' # TODO: No unit test for /packs/index/health /api/v1/packs/index/health: get: @@ -1964,17 +1965,17 @@ paths: x-permissions: pack_views_index_health description: To get the state of all indexes used by your StackStorm instance. responses: - "200": + '200': description: Index health. schema: - $ref: "#/definitions/PackIndex" + $ref: '#/definitions/PackIndex' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/views/files/{ref_or_id}: get: operationId: st2api.controllers.v1.packs:packs_controller.views.files.get_one @@ -1992,19 +1993,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Information about a pack. schema: type: array items: - $ref: "#/definitions/DataFilesSubSchema" + $ref: '#/definitions/DataFilesSubSchema' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/packs/views/file/{ref_or_id}/{file_path}: get: operationId: st2api.controllers.v1.packs:packs_controller.views.file.get_one @@ -2040,14 +2041,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Content of the file. - "304": + '304': description: File has not been modified. default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/configs: get: operationId: st2api.controllers.v1.pack_configs:pack_configs_controller.get_all @@ -2090,19 +2091,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Get packs config. # schema: - # type: array - # items: - # $ref: '#/definitions/PackConfig' + # type: array + # items: + # $ref: '#/definitions/PackConfig' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/configs/{pack_ref}: get: operationId: st2api.controllers.v1.pack_configs:pack_configs_controller.get_one @@ -2123,17 +2124,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Config for a particular pack. schema: - $ref: "#/definitions/PackConfigView" + $ref: '#/definitions/PackConfigView' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.pack_configs:pack_configs_controller.put x-permissions: pack_config @@ -2148,7 +2149,7 @@ paths: in: body description: Pack config content schema: - $ref: "#/definitions/PackConfigContent" + $ref: '#/definitions/PackConfigContent' - name: show_secrets in: query description: Show secrets in plain text @@ -2159,17 +2160,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Config for a particular pack. schema: - $ref: "#/definitions/PackConfigCreate" + $ref: '#/definitions/PackConfigCreate' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/config_schemas: get: operationId: st2api.controllers.v1.pack_config_schemas:pack_config_schema_controller.get_all @@ -2208,19 +2209,19 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Get packs config schema. # schema: - # type: array - # items: - # $ref: '#/definitions/PackConfig' + # type: array + # items: + # $ref: '#/definitions/PackConfig' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/config_schemas/{pack_ref}: get: operationId: st2api.controllers.v1.pack_config_schemas:pack_config_schema_controller.get_one @@ -2237,17 +2238,17 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Config schema for a particular pack. schema: - $ref: "#/definitions/PackConfigView" + $ref: '#/definitions/PackConfigView' examples: application/json: - ref: "core.local" + ref: 'core.local' default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/policytypes: get: operationId: st2api.controllers.v1.policies:policy_type_controller.get_all @@ -2303,20 +2304,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of policy types schema: type: array items: - $ref: "#/definitions/PolicyTypeList" + $ref: '#/definitions/PolicyTypeList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/policytypes/{ref_or_id}: get: operationId: st2api.controllers.v1.policies:policy_type_controller.get_one @@ -2333,18 +2334,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Specific policy type. schema: - $ref: "#/definitions/PolicyTypeList" + $ref: '#/definitions/PolicyTypeList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/policies: get: operationId: st2api.controllers.v1.policies:policy_controller.get_all @@ -2389,20 +2390,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Policy list schema: type: array items: - - $ref: "#/definitions/PolicyList" + - $ref: '#/definitions/PolicyList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.policies:policy_controller.post description: | @@ -2412,7 +2413,7 @@ paths: in: body description: Policy details in yaml/json file schema: - $ref: "#/definitions/PolicyCreate" + $ref: '#/definitions/PolicyCreate' required: true x-parameters: - name: user @@ -2420,18 +2421,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Policy created successfully. schema: - $ref: "#/definitions/PolicyList" + $ref: '#/definitions/PolicyList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/policies/{ref_or_id}: get: operationId: st2api.controllers.v1.policies:policy_controller.get_one @@ -2449,18 +2450,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Policy found. schema: - $ref: "#/definitions/PolicyList" + $ref: '#/definitions/PolicyList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.policies:policy_controller.put description: | @@ -2475,7 +2476,7 @@ paths: in: body description: Policy details in yaml/json file schema: - $ref: "#/definitions/PolicyCreate" + $ref: '#/definitions/PolicyCreate' required: true x-parameters: - name: user @@ -2483,18 +2484,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Policy updated successfully. schema: - $ref: "#/definitions/PolicyList" + $ref: '#/definitions/PolicyList' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.policies:policy_controller.delete description: | @@ -2511,12 +2512,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "204": + '204': description: Policy deleted default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rbac/permission_types: get: operationId: st2api.controllers.v1.rbac:permission_types_controller.get_all @@ -2527,14 +2528,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Dictionary of permission types by resource types. schema: type: object default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rbac/permission_types/{resource_type}: get: operationId: st2api.controllers.v1.rbac:permission_types_controller.get_one @@ -2551,14 +2552,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of permission types. schema: type: object default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rbac/roles: get: operationId: st2api.controllers.v1.rbac:roles_controller.get_all @@ -2574,7 +2575,7 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of all roles. schema: type: array @@ -2582,12 +2583,12 @@ paths: type: object examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rbac/roles/{name_or_id}: get: operationId: st2api.controllers.v1.rbac:roles_controller.get_one @@ -2604,18 +2605,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of roles. schema: type: object examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rbac/role_assignments: get: operationId: st2api.controllers.v1.rbac:role_assignments_controller.get_all @@ -2643,7 +2644,7 @@ paths: description: Only include remote role assignments. type: boolean responses: - "200": + '200': description: List of all role assignments. schema: type: array @@ -2651,12 +2652,12 @@ paths: type: object examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rbac/role_assignments/{id}: get: operationId: st2api.controllers.v1.rbac:role_assignments_controller.get_one @@ -2673,18 +2674,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Role assignment object. schema: type: object examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rules: get: @@ -2746,20 +2747,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of rules schema: type: array items: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.rules:rule_controller.post description: | @@ -2769,25 +2770,25 @@ paths: in: body description: Rule content schema: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Single action being created schema: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rules/{ref_or_id}: get: x-requirements: @@ -2811,18 +2812,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Rule requested schema: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rules/{rule_ref_or_id}: put: operationId: st2api.controllers.v1.rules:rule_controller.put @@ -2838,25 +2839,25 @@ paths: in: body description: Rule content schema: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action updated schema: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.rules:rule_controller.delete description: | @@ -2873,12 +2874,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "204": + '204': description: Rule deleted default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rules/views: get: operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_all @@ -2935,20 +2936,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of rules schema: type: array items: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/rules/views/{ref_or_id}: get: operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_one @@ -2966,37 +2967,37 @@ paths: x-as: requester_user description: User running the action responses: - "200": + '200': description: Rule requested schema: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/ruletypes: get: operationId: st2api.controllers.v1.ruletypes:rule_types_controller.get_all description: Returns a list of all rule types. responses: - "200": + '200': description: List of rules schema: type: array items: - $ref: "#/definitions/RuleType" + $ref: '#/definitions/RuleType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/ruletypes/{id}: get: operationId: st2api.controllers.v1.ruletypes:rule_types_controller.get_one @@ -3008,18 +3009,18 @@ paths: type: string required: true responses: - "200": + '200': description: RuleType requested schema: - $ref: "#/definitions/RuleType" + $ref: '#/definitions/RuleType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/runnertypes: get: operationId: st2api.controllers.v1.runnertypes:runner_types_controller.get_all @@ -3072,20 +3073,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of runner types schema: type: array items: - $ref: "#/definitions/RunnerType" + $ref: '#/definitions/RunnerType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/runnertypes/{name_or_id}: get: operationId: st2api.controllers.v1.runnertypes:runner_types_controller.get_one @@ -3103,18 +3104,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: RunnerType requested schema: - $ref: "#/definitions/RunnerType" + $ref: '#/definitions/RunnerType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.runnertypes:runner_types_controller.put description: | @@ -3129,25 +3130,25 @@ paths: in: body description: RunnerType content schema: - $ref: "#/definitions/RunnerType" + $ref: '#/definitions/RunnerType' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: RunnerType updated schema: - $ref: "#/definitions/RunnerType" + $ref: '#/definitions/RunnerType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/sensortypes: get: operationId: st2api.controllers.v1.sensors:sensor_type_controller.get_all @@ -3200,20 +3201,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of rules schema: type: array items: - $ref: "#/definitions/SensorType" + $ref: '#/definitions/SensorType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/sensortypes/{ref_or_id}: get: operationId: st2api.controllers.v1.sensors:sensor_type_controller.get_one @@ -3231,18 +3232,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: SensorType requested schema: - $ref: "#/definitions/SensorType" + $ref: '#/definitions/SensorType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.sensors:sensor_type_controller.put description: | @@ -3257,25 +3258,25 @@ paths: in: body description: SensorType content schema: - $ref: "#/definitions/SensorType" + $ref: '#/definitions/SensorType' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: SensorType updated schema: - $ref: "#/definitions/SensorType" + $ref: '#/definitions/SensorType' examples: application/json: - ref: "core.webhook" + ref: 'core.webhook' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/apikeys: get: operationId: st2api.controllers.v1.auth:api_key_controller.get_all @@ -3302,20 +3303,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of actions schema: type: array items: - $ref: "#/definitions/ApiKey" + $ref: '#/definitions/ApiKey' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.auth:api_key_controller.post description: | @@ -3325,25 +3326,25 @@ paths: in: body description: Action content schema: - $ref: "#/definitions/ApiKey" + $ref: '#/definitions/ApiKey' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "201": + '201': description: Single action being created schema: - $ref: "#/definitions/ApiKey" + $ref: '#/definitions/ApiKey' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/apikeys/{api_key_id_or_key}: get: operationId: st2api.controllers.v1.auth:api_key_controller.get_one @@ -3365,18 +3366,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action requested schema: - $ref: "#/definitions/ApiKey" + $ref: '#/definitions/ApiKey' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.auth:api_key_controller.put description: | @@ -3391,25 +3392,25 @@ paths: in: body description: Action content schema: - $ref: "#/definitions/ApiKey" + $ref: '#/definitions/ApiKey' x-parameters: - name: user in: context x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action updated schema: - $ref: "#/definitions/ApiKey" + $ref: '#/definitions/ApiKey' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.auth:api_key_controller.delete description: | @@ -3426,12 +3427,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "204": + '204': description: Action deleted default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/ruleenforcements: get: operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_all @@ -3496,13 +3497,13 @@ paths: - name: enforced_at_gt in: query description: | - Only return enforcements with enforced_at greater than the one provided. Use time in the format. + Only return enforcements with enforced_at greater than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ - name: enforced_at_lt in: query description: | - Only return enforcements with enforced_at lower than the one provided. Use time in the format. + Only return enforcements with enforced_at lower than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ x-parameters: @@ -3511,20 +3512,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of rule enforcements schema: type: array items: - $ref: "#/definitions/RuleEnforcementsList" + $ref: '#/definitions/RuleEnforcementsList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/ruleenforcements/views: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_all @@ -3589,13 +3590,13 @@ paths: - name: enforced_at_gt in: query description: | - Only return enforcements with enforced_at greater than the one provided. Use time in the format. + Only return enforcements with enforced_at greater than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ - name: enforced_at_lt in: query description: | - Only return enforcements with enforced_at lower than the one provided. Use time in the format. + Only return enforcements with enforced_at lower than the one provided. Use time in the format. type: string pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ x-parameters: @@ -3604,20 +3605,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of rule enforcements schema: type: array items: - $ref: "#/definitions/RuleEnforcementsList" + $ref: '#/definitions/RuleEnforcementsList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/ruleenforcements/{id}: get: operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one @@ -3634,18 +3635,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Rule Enforcements based on ref or id schema: - $ref: "#/definitions/RuleEnforcementsList" + $ref: '#/definitions/RuleEnforcementsList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/ruleenforcements/views/{id}: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_one @@ -3662,18 +3663,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Rule Enforcements based on ref or id schema: - $ref: "#/definitions/RuleEnforcementsList" + $ref: '#/definitions/RuleEnforcementsList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/timers: get: @@ -3686,20 +3687,20 @@ paths: description: Type of timer type: string responses: - "200": + '200': description: List of timers schema: type: array items: - $ref: "#/definitions/TimersList" + $ref: '#/definitions/TimersList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/timers/{ref_or_id}: get: operationId: st2api.controllers.v1.timers:timers_controller.get_one @@ -3716,18 +3717,18 @@ paths: x-as: requester_user description: User performing the operation responses: - "200": + '200': description: Trace schema: - $ref: "#/definitions/TimersList" + $ref: '#/definitions/TimersList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/traces: get: operationId: st2api.controllers.v1.traces:traces_controller.get_all @@ -3792,12 +3793,12 @@ paths: - name: sort_asc in: query description: | - Sort in ascending order by start timestamp, asc/ascending (earliest first) + Sort in ascending order by start timestamp, asc/ascending (earliest first) type: string - name: sort_desc in: query description: | - Sort in descending order by start timestamp, desc/descending (latest first) + Sort in descending order by start timestamp, desc/descending (latest first) type: string x-parameters: - name: user @@ -3805,20 +3806,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of traces schema: type: array items: - $ref: "#/definitions/TracesList" + $ref: '#/definitions/TracesList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/traces/{id}: get: operationId: st2api.controllers.v1.traces:traces_controller.get_one @@ -3835,18 +3836,18 @@ paths: x-as: requester_user description: User performing the operation responses: - "200": + '200': description: Trace schema: - $ref: "#/definitions/TracesList" + $ref: '#/definitions/TracesList' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/triggertypes: get: operationId: st2api.controllers.v1.triggers:triggertype_controller.get_all @@ -3901,20 +3902,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of trigger types schema: type: array items: - $ref: "#/definitions/TriggerType" + $ref: '#/definitions/TriggerType' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.triggers:triggertype_controller.post description: | @@ -3924,20 +3925,20 @@ paths: in: body description: TriggerType content schema: - $ref: "#/definitions/TriggerTypeRequest" + $ref: '#/definitions/TriggerTypeRequest' responses: - "201": + '201': description: Single trigger type being created schema: - $ref: "#/definitions/TriggerType" + $ref: '#/definitions/TriggerType' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/triggertypes/{triggertype_ref_or_id}: get: operationId: st2api.controllers.v1.triggers:triggertype_controller.get_one @@ -3950,18 +3951,18 @@ paths: type: string required: true responses: - "200": + '200': description: TriggerType requested schema: - $ref: "#/definitions/TriggerType" + $ref: '#/definitions/TriggerType' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.triggers:triggertype_controller.put description: | @@ -3976,20 +3977,20 @@ paths: in: body description: TriggerType content schema: - $ref: "#/definitions/TriggerTypeRequest" + $ref: '#/definitions/TriggerTypeRequest' responses: - "200": + '200': description: TriggerType updated schema: - $ref: "#/definitions/TriggerType" + $ref: '#/definitions/TriggerType' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.triggers:triggertype_controller.delete description: | @@ -4001,12 +4002,12 @@ paths: type: string required: true responses: - "204": + '204': description: TriggerType deleted default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/triggers: get: operationId: st2api.controllers.v1.triggers:trigger_controller.get_all @@ -4017,20 +4018,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of triggers schema: type: array items: - $ref: "#/definitions/Trigger" + $ref: '#/definitions/Trigger' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' post: operationId: st2api.controllers.v1.triggers:trigger_controller.post description: | @@ -4040,20 +4041,20 @@ paths: in: body description: Trigger content schema: - $ref: "#/definitions/TriggerRequest" + $ref: '#/definitions/TriggerRequest' responses: - "201": + '201': description: Single trigger being created schema: - $ref: "#/definitions/Trigger" + $ref: '#/definitions/Trigger' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/triggers/{trigger_id}: get: operationId: st2api.controllers.v1.triggers:trigger_controller.get_one @@ -4066,18 +4067,18 @@ paths: type: string required: true responses: - "200": + '200': description: Trigger requested schema: - $ref: "#/definitions/Trigger" + $ref: '#/definitions/Trigger' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' put: operationId: st2api.controllers.v1.triggers:trigger_controller.put description: | @@ -4092,20 +4093,20 @@ paths: in: body description: Trigger content schema: - $ref: "#/definitions/TriggerRequest" + $ref: '#/definitions/TriggerRequest' responses: - "200": + '200': description: Trigger updated schema: - $ref: "#/definitions/Trigger" + $ref: '#/definitions/Trigger' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' delete: operationId: st2api.controllers.v1.triggers:trigger_controller.delete description: | @@ -4117,12 +4118,12 @@ paths: type: string required: true responses: - "204": + '204': description: Trigger deleted default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/triggerinstances: get: operationId: st2api.controllers.v1.triggers:triggerinstance_controller.get_all @@ -4191,20 +4192,20 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of trigger instances schema: type: array items: - $ref: "#/definitions/TriggerInstance" + $ref: '#/definitions/TriggerInstance' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/triggerinstances/{instance_id}: get: operationId: st2api.controllers.v1.triggers:triggerinstance_controller.get_one @@ -4217,18 +4218,18 @@ paths: type: string required: true responses: - "200": + '200': description: Trigger instance requested schema: - $ref: "#/definitions/TriggerInstance" + $ref: '#/definitions/TriggerInstance' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/triggerinstances/{trigger_instance_id}/re_emit: post: operationId: st2api.controllers.v1.triggers:triggerinstance_resend_controller.post @@ -4241,38 +4242,38 @@ paths: type: string required: true responses: - "200": + '200': description: Trigger instance being re-emitted schema: - $ref: "#/definitions/TriggerInstance" + $ref: '#/definitions/TriggerInstance' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/webhooks: get: operationId: st2api.controllers.v1.webhooks:webhooks_controller.get_all x-permissions: webhook_list description: Returns a list of all webhooks. responses: - "200": + '200': description: List of webhooks schema: type: array items: - $ref: "#/definitions/Webhook" + $ref: '#/definitions/Webhook' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/webhooks/{hook}: post: operationId: st2api.controllers.v1.webhooks:webhooks_controller.post @@ -4290,7 +4291,7 @@ paths: in: body description: Webhook payload schema: - $ref: "#/definitions/WebhookBody" + $ref: '#/definitions/WebhookBody' x-parameters: - name: headers in: request @@ -4300,18 +4301,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "202": + '202': description: Single action being created schema: - $ref: "#/definitions/WebhookBody" + $ref: '#/definitions/WebhookBody' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/webhooks/{url}: get: operationId: st2api.controllers.v1.webhooks:webhooks_controller.get_one @@ -4329,18 +4330,18 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: Action requested schema: - $ref: "#/definitions/Webhook" + $ref: '#/definitions/Webhook' examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/workflows/inspect: post: operationId: st2api.controllers.v1.workflow_inspection:workflow_inspection_controller.post @@ -4352,20 +4353,20 @@ paths: schema: type: string responses: - "200": + '200': description: List of inspection errors separated by categories. schema: - $ref: "#/definitions/WorkflowInspectionErrors" + $ref: '#/definitions/WorkflowInspectionErrors' examples: application/json: - type: "content" - message: 'The action "std.noop" is not registered in the database.' + message: "The action \"std.noop\" is not registered in the database." schema_path: "properties.tasks.patternProperties.^\\w+$.properties.action" spec_path: "tasks.task3.action" - type: "context" language: "yaql" expression: "<% ctx().foobar %>" - message: 'Variable "foobar" is referenced before assignment.' + message: "Variable \"foobar\" is referenced before assignment." schema_path: "properties.tasks.patternProperties.^\\w+$.properties.input" spec_path: "tasks.task1.input" - type: "expression" @@ -4381,7 +4382,7 @@ paths: default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/service_registry/groups: get: @@ -4393,14 +4394,14 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of available groups. schema: type: object default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /api/v1/service_registry/groups/{group_id}/members: get: operationId: st2api.controllers.v1.service_registry:members_controller.get_one @@ -4417,14 +4418,15 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: List of available members. schema: type: object default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' + /auth/v1/tokens: post: @@ -4446,7 +4448,7 @@ paths: in: body description: Lifespan of the token schema: - $ref: "#/definitions/TokenRequest" + $ref: '#/definitions/TokenRequest' x-parameters: - name: remote_addr in: environ @@ -4457,31 +4459,31 @@ paths: description: set externally to indicate user identity in case of proxy auth type: string responses: - "201": + '201': description: New token has been created schema: - $ref: "#/definitions/Token" + $ref: '#/definitions/Token' headers: x-api-url: type: string examples: application/json: user: st2admin - token: "5e86421776f946e98faea36c29e5a7c7" - expiry: "2016-05-28T12:39:28.650231Z" - id: "574840001878c10d0b6e8fbf" + token: '5e86421776f946e98faea36c29e5a7c7' + expiry: '2016-05-28T12:39:28.650231Z' + id: '574840001878c10d0b6e8fbf' metadata: {} - "401": + '401': description: Invalid or missing credentials has been provided schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' examples: application/json: faultstring: Invalid or missing credentials default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' security: [] /auth/v1/tokens/validate: post: @@ -4493,36 +4495,36 @@ paths: in: body description: Token to validate schema: - $ref: "#/definitions/TokenValidationRequest" + $ref: '#/definitions/TokenValidationRequest' responses: - "200": + '200': description: Validation results schema: - $ref: "#/definitions/TokenValidationResult" + $ref: '#/definitions/TokenValidationResult' examples: application/json: valid: false default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /auth/v1/sso: get: operationId: st2auth.controllers.v1.sso:sso_controller.get description: Returns a flag indicating if SSO is enabled or not. responses: - "200": + '200': description: Returns a flag indicating if SSO is enabled schema: - $ref: "#/definitions/SSOEnabledResult" + $ref: '#/definitions/SSOEnabledResult' examples: application/json: enabled: false default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' security: [] /auth/v1/sso/request/web: @@ -4535,7 +4537,7 @@ paths: description: set externally to indicate real source of the request type: string responses: - "307": + '307': description: Temporary redirect security: [] @@ -4560,7 +4562,7 @@ paths: type: string description: What URL to be called back once the response from SSO is received responses: - "200": + '200': description: SSO request valid security: [] /auth/v1/sso/callback: @@ -4574,21 +4576,21 @@ paths: schema: type: object responses: - "200": + '200': description: SSO response valid - "302": + '302': description: SSO response valid and callback URL returned - "401": + '401': description: Invalid or missing credentials has been provided schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' examples: application/json: faultstring: Invalid or missing credentials default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' security: [] /stream/v1/stream: @@ -4636,16 +4638,16 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: EventSource compatible stream of events examples: application/json: - ref: "core.local" + ref: 'core.local' # and stuff default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' /stream/v1/executions/{id}/output: get: operationId: st2stream.controllers.v1.executions:action_execution_output_controller.get_one @@ -4674,12 +4676,12 @@ paths: x-as: requester_user description: User performing the operation. responses: - "200": + '200': description: EventSource compatible stream of events default: description: Unexpected error schema: - $ref: "#/definitions/Error" + $ref: '#/definitions/Error' definitions: Action: @@ -4714,12 +4716,12 @@ definitions: entry_point: type: string description: The entry point for the action. - default: "" + default: '' pack: type: string description: The content pack this action belongs to. parameters: - $ref: "#/definitions/ActionParametersSubSchema" + $ref: '#/definitions/ActionParametersSubSchema' tags: type: array description: User associated metadata assigned to this object. @@ -4729,9 +4731,9 @@ definitions: description: Notification settings for action. type: object properties: - on-complete: { $ref: "#/definitions/NotificationPropertySubSchema" } - on-failure: { $ref: "#/definitions/NotificationPropertySubSchema" } - on-success: { $ref: "#/definitions/NotificationPropertySubSchema" } + on-complete: {$ref: '#/definitions/NotificationPropertySubSchema'} + on-failure: {$ref: '#/definitions/NotificationPropertySubSchema'} + on-success: {$ref: '#/definitions/NotificationPropertySubSchema'} additionalProperties: False # additionalProperties: false ActionCreateRequest: @@ -4742,35 +4744,35 @@ definitions: pack: type: string description: The content pack this action belongs to. - default: "default" - - $ref: "#/definitions/Action" - - $ref: "#/definitions/DataFilesSubSchema" + default: 'default' + - $ref: '#/definitions/Action' + - $ref: '#/definitions/DataFilesSubSchema' ActionUpdateRequest: x-api-model: st2common.models.api.action:ActionUpdateAPI allOf: - - $ref: "#/definitions/Action" - - $ref: "#/definitions/DataFilesSubSchema" + - $ref: '#/definitions/Action' + - $ref: '#/definitions/DataFilesSubSchema' ActionDeleteRequest: allOf: - - $ref: "#/definitions/ActionDeleteSchema" + - $ref: '#/definitions/ActionDeleteSchema' ActionCloneRequest: allOf: - - $ref: "#/definitions/ActionCloneSchema" + - $ref: '#/definitions/ActionCloneSchema' ActionParameters: type: object properties: parameters: - $ref: "#/definitions/ActionParametersSubSchema" + $ref: '#/definitions/ActionParametersSubSchema' ActionAlias: x-api-model: st2common.models.api.action:ActionAliasAPI type: object ActionAliasRequest: type: object required: - - name - - ref - - pack - - action_ref + - name + - ref + - pack + - action_ref properties: name: description: Alias name. @@ -4794,15 +4796,15 @@ definitions: description: Enable or disable the action from invocation. default: True ActionAliasMatchRequest: - type: object - required: - - command - properties: - command: - description: Command string to try to match the aliases against. - type: string + type: object + required: + - command + properties: + command: + description: Command string to try to match the aliases against. + type: string ActionAliasMatch: - type: object + type: object ActionAliasHelp: type: object properties: @@ -4862,21 +4864,21 @@ definitions: description: Possible parameter format. items: oneOf: - - type: string - - type: object - description: Matched alias formats - properties: - representation: - type: array - description: Alias format representations - items: - type: string - match_multiple: - type: boolean - optional: true - display: + - type: string + - type: object + description: Matched alias formats + properties: + representation: + type: array + description: Alias format representations + items: type: string - description: Display string of alias format + match_multiple: + type: boolean + optional: true + display: + type: string + description: Display string of alias format ack: type: object description: Acknowledgement message format. @@ -4909,7 +4911,7 @@ definitions: type: object description: Execution result properties: - $ref: "#/definitions/Execution/properties" + $ref: '#/definitions/Execution/properties' message: type: string AliasMatchAndExecuteInputAPI: @@ -4945,19 +4947,19 @@ definitions: id: type: string trigger: - $ref: "#/definitions/Trigger" + $ref: '#/definitions/Trigger' trigger_type: - $ref: "#/definitions/TriggerType" + $ref: '#/definitions/TriggerType' trigger_instance: - $ref: "#/definitions/TriggerInstance" + $ref: '#/definitions/TriggerInstance' rule: - $ref: "#/definitions/Rule" + $ref: '#/definitions/Rule' action: - $ref: "#/definitions/Action" + $ref: '#/definitions/Action' runner: - $ref: "#/definitions/RunnerType" + $ref: '#/definitions/RunnerType' liveaction: - $ref: "#/definitions/LiveAction" + $ref: '#/definitions/LiveAction' task_execution: type: string workflow_execution: @@ -4965,23 +4967,7 @@ definitions: status: description: The current status of the action execution. type: string - enum: - [ - "requested", - "scheduled", - "delayed", - "running", - "succeeded", - "failed", - "timeout", - "abandoned", - "canceling", - "canceled", - "pending", - "pausing", - "paused", - "resuming", - ] + enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] start_timestamp: description: The start time when the action is executed. type: string @@ -4993,35 +4979,35 @@ definitions: elapsed_seconds: description: Time duration in seconds taken for completion of this execution. type: number - # required: False +# required: False web_url: description: History URL for this execution if you want to view in UI. type: string - # required: False +# required: False parameters: description: Input parameters for the action. type: object - # patternProperties: - # ^\w+$: - # anyOf: - # - type: array - # - type: boolean - # - type: integer - # - type: number - # - type: object - # - type: string - # additionalProperties: False +# patternProperties: +# ^\w+$: +# anyOf: +# - type: array +# - type: boolean +# - type: integer +# - type: number +# - type: object +# - type: string +# additionalProperties: False context: type: object result: type: object - # anyOf: - # - type: array - # - type: boolean - # - type: integer - # - type: number - # - type: object - # - type: string +# anyOf: +# - type: array +# - type: boolean +# - type: integer +# - type: number +# - type: object +# - type: string result_size: type: integer default: 0 @@ -5043,23 +5029,7 @@ definitions: pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ status: type: string - enum: - [ - "requested", - "scheduled", - "delayed", - "running", - "succeeded", - "failed", - "timeout", - "abandoned", - "canceling", - "canceled", - "pending", - "pausing", - "paused", - "resuming", - ] + enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] delay: description: How long (in milliseconds) to delay the execution before scheduling. type: integer @@ -5068,37 +5038,21 @@ definitions: additionalProperties: False ExecutionRequest: allOf: - - $ref: "#/definitions/LiveAction" + - $ref: '#/definitions/LiveAction' - type: object properties: user: type: string x-nullable: true description: User context under which action should run (admins only) - default: "" + default: '' ExecutionUpdateRequest: type: object properties: status: description: The current status of the action execution. type: string - enum: - [ - "requested", - "scheduled", - "delayed", - "running", - "succeeded", - "failed", - "timeout", - "abandoned", - "canceling", - "canceled", - "pending", - "pausing", - "paused", - "resuming", - ] + enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] result: type: object additionalProperties: False @@ -5272,36 +5226,33 @@ definitions: description: Types of content to register. type: array items: - $ref: "#/definitions/PacksContentRegisterType" + $ref: '#/definitions/PacksContentRegisterType' fail_on_failure: type: boolean description: True to fail on failure default: true PacksContentRegisterType: - type: string - enum: - [ - "all", - "runner", - "action", - "actions", - "trigger", - "triggers", - "sensor", - "sensors", - "rule", - "rules", - "rule_type", - "rule_types", - "alias", - "aliases", - "policy_type", - "policy_types", - "policy", - "policies", - "config", - "configs", - ] + type: string + enum: ['all', + 'runner', + 'action', + 'actions', + 'trigger', + 'triggers', + 'sensor', + 'sensors', + 'rule', + 'rules', + 'rule_type', + 'rule_types', + 'alias', + 'aliases', + 'policy_type', + 'policy_types', + 'policy', + 'policies', + 'config', + 'configs'] PacksSearchShow: type: object properties: @@ -5389,23 +5340,7 @@ definitions: status: description: The current status of the action execution. type: string - enum: - [ - "requested", - "scheduled", - "delayed", - "running", - "succeeded", - "failed", - "timeout", - "abandoned", - "canceling", - "canceled", - "pending", - "pausing", - "paused", - "resuming", - ] + enum: ['requested', 'scheduled', 'delayed', 'running', 'succeeded', 'failed', 'timeout', 'abandoned', 'canceling', 'canceled', 'pending', 'pausing', 'paused', 'resuming'] start_timestamp: description: The start time when the action is executed. type: string @@ -5453,9 +5388,9 @@ definitions: description: Notification settings for liveaction. type: object properties: - on-complete: { $ref: "#/definitions/NotificationPropertySubSchema" } - on-failure: { $ref: "#/definitions/NotificationPropertySubSchema" } - on-success: { $ref: "#/definitions/NotificationPropertySubSchema" } + on-complete: {$ref: '#/definitions/NotificationPropertySubSchema'} + on-failure: {$ref: '#/definitions/NotificationPropertySubSchema'} + on-success: {$ref: '#/definitions/NotificationPropertySubSchema'} additionalProperties: False delay: description: How long (in milliseconds) to delay the execution before scheduling. @@ -5539,7 +5474,7 @@ definitions: description: Channels to post notifications to. items: type: string - channels: # Deprecated. Only here for backward compatibility. + channels: # Deprecated. Only here for backward compatibility. type: array description: Channels to post notifications to. items: @@ -5559,7 +5494,7 @@ definitions: ttl: type: - integer - - "null" + - 'null' minimum: 1 Token: type: object @@ -5582,7 +5517,7 @@ definitions: token: type: - string - - "null" + - 'null' TokenValidationResult: type: object properties: @@ -5633,7 +5568,7 @@ definitions: WorkflowInspectionErrors: type: array items: - $ref: "#/definitions/WorkflowInspectionError" + $ref: '#/definitions/WorkflowInspectionError' ValidationError: type: object From e01b7f3860e9e74ef65b9d17641d5411bdd77d4e Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 21 Sep 2022 16:50:32 -0300 Subject: [PATCH 44/49] fixing tests --- st2auth/st2auth/controllers/v1/sso.py | 1 + st2auth/tests/unit/controllers/v1/test_sso.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/st2auth/st2auth/controllers/v1/sso.py b/st2auth/st2auth/controllers/v1/sso.py index 8bbc250805..959d6081fa 100644 --- a/st2auth/st2auth/controllers/v1/sso.py +++ b/st2auth/st2auth/controllers/v1/sso.py @@ -183,6 +183,7 @@ def post_cli(self, response): 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!") diff --git a/st2auth/tests/unit/controllers/v1/test_sso.py b/st2auth/tests/unit/controllers/v1/test_sso.py index 0f3ab29177..55e9e8b4eb 100644 --- a/st2auth/tests/unit/controllers/v1/test_sso.py +++ b/st2auth/tests/unit/controllers/v1/test_sso.py @@ -252,7 +252,7 @@ def test_cli_default_backend_missing_key(self): { "callback_url": MOCK_CALLBACK_URL, }, - "Missing either key and/or callback_url!", + "'key' is a required property", ) def test_cli_default_backend_missing_callback_url(self): @@ -260,12 +260,12 @@ def test_cli_default_backend_missing_callback_url(self): { "key": MOCK_CLI_REQUEST_KEY_JSON, }, - "Missing either key and/or callback_url!", + "'callback_url' is a required property", ) def test_cli_default_backend_missing_key_and_callback_url(self): self._test_cli_request_bad_parameter_helper( - {"ops": "ops"}, "Missing either key and/or callback_url!" + {"ops": "ops"}, "'key' is a required property" ) def test_cli_default_backend_not_implemented(self): From 79f7ab8effcafed6f391417e7d8b5e432e704544 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 21 Sep 2022 18:44:41 -0300 Subject: [PATCH 45/49] adjusting importing dependencies --- st2client/st2client/utils/crypto.py | 4 +- st2client/st2client/utils/jsonify.py | 200 +++++++++++++++++++ st2client/st2client/utils/sso_interceptor.py | 2 +- st2client/tests/unit/test_auth.py | 25 ++- st2client/tests/unit/test_shell.py | 1 + 5 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 st2client/st2client/utils/jsonify.py diff --git a/st2client/st2client/utils/crypto.py b/st2client/st2client/utils/crypto.py index e6be862101..138e6b165f 100644 --- a/st2client/st2client/utils/crypto.py +++ b/st2client/st2client/utils/crypto.py @@ -49,8 +49,8 @@ from cryptography.hazmat.primitives import hmac from cryptography.hazmat.backends import default_backend -from st2common.util.jsonify import json_encode -from st2common.util.jsonify import json_decode +from st2client.utils.jsonify import json_encode +from st2client.utils.jsonify import json_decode __all__ = [ "KEYCZAR_HEADER_SIZE", diff --git a/st2client/st2client/utils/jsonify.py b/st2client/st2client/utils/jsonify.py new file mode 100644 index 0000000000..8c4a4984ce --- /dev/null +++ b/st2client/st2client/utils/jsonify.py @@ -0,0 +1,200 @@ +# Copyright 2020 The StackStorm Authors. +# Copyright 2019 Extreme Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +import logging + +LOG = logging.getLogger(__name__) + +try: + import simplejson as json + from simplejson import JSONEncoder +except ImportError: + import json + from json import JSONEncoder + +import six +import bson +import orjson + + +__all__ = [ + "json_encode", + "json_decode", + "json_loads", + "try_loads", + "get_json_type_for_python_value", +] + +# Which json library to use for data serialization and deserialization. +# We only expose this option so we can exercise code paths with different libraries inside the +# tests for compatibility reasons +DEFAULT_JSON_LIBRARY = "orjson" + + +class GenericJSON(JSONEncoder): + def default(self, obj): # pylint: disable=method-hidden + if hasattr(obj, "__json__") and six.callable(obj.__json__): + return obj.__json__() + elif isinstance(obj, bson.ObjectId): + return str(obj) + else: + return JSONEncoder.default(self, obj) + + +def default(obj): + if hasattr(obj, "__json__") and six.callable(obj.__json__): + return obj.__json__() + elif isinstance(obj, bytes): + # TODO: We should update the code which passes bytes to pass unicode to avoid this + # conversion here + return obj.decode("utf-8") + elif isinstance(obj, bson.ObjectId): + return str(obj) + raise TypeError + + +def json_encode_native_json(obj, indent=4, sort_keys=False): + if not indent: + separators = (",", ":") + else: + separators = None + return json.dumps( + obj, cls=GenericJSON, indent=indent, separators=separators, sort_keys=sort_keys + ) + + +def json_encode_orjson(obj, indent=None, sort_keys=False): + option = None + + if indent: + # NOTE: We don't use indent by default since it's quite a bit slower + option = orjson.OPT_INDENT_2 + + if sort_keys: + option = option | orjson.OPT_SORT_KEYS if option else orjson.OPT_SORT_KEYS + + if option: + return orjson.dumps(obj, default=default, option=option).decode("utf-8") + + return orjson.dumps(obj, default=default).decode("utf-8") + + +def json_decode_native_json(data): + return json.loads(data) + + +def json_decode_orjson(data): + return orjson.loads(data) + + +def json_encode(obj, indent=None, sort_keys=False): + """ + Wrapper function for encoding the provided object. + + This function automatically select appropriate JSON library based on the configuration value. + + This function should be used everywhere in the code base where json.dumps() behavior is desired. + """ + json_library = DEFAULT_JSON_LIBRARY + + if json_library == "json": + return json_encode_native_json(obj=obj, indent=indent, sort_keys=sort_keys) + elif json_library == "orjson": + return json_encode_orjson(obj=obj, indent=indent, sort_keys=sort_keys) + else: + raise ValueError("Unsupported json_library: %s" % (json_library)) + + +def json_decode(data): + """ + Wrapper function for decoding the provided JSON string. + + This function automatically select appropriate JSON library based on the configuration value. + + This function should be used everywhere in the code base where json.loads() behavior is desired. + """ + json_library = DEFAULT_JSON_LIBRARY + + if json_library == "json": + return json_decode_native_json(data=data) + elif json_library == "orjson": + return json_decode_orjson(data=data) + else: + raise ValueError("Unsupported json_library: %s" % (json_library)) + + +def load_file(path): + with open(path, "r") as fd: + return json.load(fd) + + +def json_loads(obj, keys=None): + """ + Given an object, this method tries to json.loads() the value of each of the keys. If json.loads + fails, the original value stays in the object. + + :param obj: Original object whose values should be converted to json. + :type obj: ``dict`` + + :param keys: Optional List of keys whose values should be transformed. + :type keys: ``list`` + + :rtype ``dict`` or ``None`` + """ + if not obj: + return None + + if not keys: + keys = list(obj.keys()) + + for key in keys: + try: + obj[key] = json_decode(obj[key]) + except Exception: + # NOTE: This exception is not fatal so we intentionally don't log anything. + # Method behaves in "best effort" manner and dictionary value not being JSON + # string is perfectly valid (and common) scenario so we should not log anything + pass + return obj + + +def try_loads(s): + try: + return json_decode(s) if s and isinstance(s, six.string_types) else s + except: + return s + + +def get_json_type_for_python_value(value): + """ + Return JSON type string for the provided Python value. + + :rtype: ``str`` + """ + if isinstance(value, six.text_type): + return "string" + elif isinstance(value, (int, float)): + return "number" + elif isinstance(value, dict): + return "object" + elif isinstance(value, (list, tuple)): + return "array" + elif isinstance(value, bool): + return "boolean" + elif value is None: + return "null" + else: + return "unknown" diff --git a/st2client/st2client/utils/sso_interceptor.py b/st2client/st2client/utils/sso_interceptor.py index a469d3cf5a..935a43394e 100644 --- a/st2client/st2client/utils/sso_interceptor.py +++ b/st2client/st2client/utils/sso_interceptor.py @@ -70,7 +70,7 @@ def callback_received(self, token): LOG.debug("Callback received and intercepted, token is provided :)") self.token = token - def get_token(self, timeout=8): + def get_token(self, timeout=90): LOG.debug( "Waiting for token to be received from SSO flow.. will timeout after [%s]s", timeout, diff --git a/st2client/tests/unit/test_auth.py b/st2client/tests/unit/test_auth.py index 22772c4e03..d43cfa9783 100644 --- a/st2client/tests/unit/test_auth.py +++ b/st2client/tests/unit/test_auth.py @@ -233,9 +233,6 @@ def runTest(self, mock_aeskey_generate, mock_post): "34000", ] - original_stdout = sys.stdout - out_buffer = io.StringIO() - def handle_sso_flow(): # Waiting for SSO link on the CLI LOG.debug("Waiting for SSO link") @@ -243,10 +240,10 @@ def handle_sso_flow(): timeout_at = time() + 5 while not match and timeout_at > time(): sleep(1) - LOG.debug("STDOUT buffer has: %s", out_buffer.getvalue()) - match = re.search( - r"http://localhost:34000/\S+", out_buffer.getvalue(), re.MULTILINE - ) + self.stdout.seek(0) + buffer = self.stdout.read() + LOG.debug("STDOUT buffer has: %s", buffer) + match = re.search(r"http://localhost:34000/\S+", buffer, re.MULTILINE) self.assertIsNotNone(match) # Hitting the localhost login url @@ -269,13 +266,15 @@ def handle_sso_flow(): self.assertEquals(response.headers["Location"], "/success") LOG.debug("Finished SSO flow") - # Calling the login proecss async - ssoFlowThread = Thread(target=handle_sso_flow, daemon=True) - ssoFlowThread.start() + def run_shell(): + self.shell.run(args) - sys.stdout = out_buffer - self.shell.run(args) - sys.stdout = original_stdout + shellThread = Thread(target=run_shell) + shellThread.start() + + handle_sso_flow() + + shellThread.join() with open(self.CONFIG_FILE, "r") as config_file: for line in config_file.readlines(): diff --git a/st2client/tests/unit/test_shell.py b/st2client/tests/unit/test_shell.py index 5eb27714ca..425347db97 100644 --- a/st2client/tests/unit/test_shell.py +++ b/st2client/tests/unit/test_shell.py @@ -37,6 +37,7 @@ from st2common.models.db.auth import TokenDB from tests import base + LOG = logging.getLogger(__name__) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) From 9cef2aa8fb716d54bf94bda9e93b41aa0898f381 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 21 Sep 2022 18:44:49 -0300 Subject: [PATCH 46/49] adding st2client build to ignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 555d276e69..b22c7e5f1c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 From 2657ac7b472c8954e8824fb1389f690fc915a4a7 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 21 Sep 2022 18:56:49 -0300 Subject: [PATCH 47/49] fixing lint --- st2client/tests/unit/test_auth.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/st2client/tests/unit/test_auth.py b/st2client/tests/unit/test_auth.py index d43cfa9783..bb1d951214 100644 --- a/st2client/tests/unit/test_auth.py +++ b/st2client/tests/unit/test_auth.py @@ -14,10 +14,8 @@ # limitations under the License. from __future__ import absolute_import -import io import os import re -import sys from time import sleep, time import uuid import json From d453d28b0ea21565c0fd24701ad3e90d534e2e5b Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Wed, 21 Sep 2022 21:53:54 -0300 Subject: [PATCH 48/49] removing st2client bson dependency --- st2client/st2client/utils/jsonify.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/st2client/st2client/utils/jsonify.py b/st2client/st2client/utils/jsonify.py index 8c4a4984ce..bedadf70cf 100644 --- a/st2client/st2client/utils/jsonify.py +++ b/st2client/st2client/utils/jsonify.py @@ -26,7 +26,6 @@ from json import JSONEncoder import six -import bson import orjson @@ -48,8 +47,6 @@ class GenericJSON(JSONEncoder): def default(self, obj): # pylint: disable=method-hidden if hasattr(obj, "__json__") and six.callable(obj.__json__): return obj.__json__() - elif isinstance(obj, bson.ObjectId): - return str(obj) else: return JSONEncoder.default(self, obj) @@ -61,8 +58,6 @@ def default(obj): # TODO: We should update the code which passes bytes to pass unicode to avoid this # conversion here return obj.decode("utf-8") - elif isinstance(obj, bson.ObjectId): - return str(obj) raise TypeError From dc0cc5f605646e3790cdc4369c93d3bee32d0786 Mon Sep 17 00:00:00 2001 From: Guilherme Pim Date: Fri, 7 Oct 2022 09:27:12 -0300 Subject: [PATCH 49/49] retrigger cis