From 3e4e956ff8ed68c4147f8b00926a6dab1b8f5257 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Thu, 24 Oct 2019 17:50:19 +0000 Subject: [PATCH] Regular expression support for CORS and OAuth ACLs Make it possible for allowed_origins and valid_oauth_clients to include regular expressions, for cases where part or all of the domain/URL cannot be predicted or easily enumerated. Change-Id: I9cfc729547560438e0fa1e47cc90cd5579168c73 --- doc/source/contribute/development.rst | 6 ++-- docker/storyboard.conf | 4 +-- etc/storyboard.conf.sample | 4 +-- storyboard/api/auth/openid_client.py | 11 ++++++- storyboard/api/middleware/cors_middleware.py | 33 +++++++++++--------- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/doc/source/contribute/development.rst b/doc/source/contribute/development.rst index 0388b218..a44bca74 100644 --- a/doc/source/contribute/development.rst +++ b/doc/source/contribute/development.rst @@ -277,7 +277,8 @@ whitespace at the start of the line. .. warning:: If you are running the API in a VM, and plan to access it remotely, ie. by its IP address or hostname, you also need to add that IP address or hostname to the ``valid_oauth_clients`` line in - the ``oauth`` section. Uncomment this line too. + the ``oauth`` section. Uncomment this line too. It can be a regular + expression as well if started with a ``^`` character. 5. Install tox @@ -446,7 +447,8 @@ running on the same machine. If your browser is on a different machine, the hostname or IP address of the machine running the API will need to be in the ``valid_oauth_clients`` key of -``./etc/storyboard.conf`` for the API in order to log in. +``./etc/storyboard.conf`` for the API in order to log in. It can be a regular +expression as well if started with a ``^`` character. By default, the API server uses port 8080, and so the API can be accessed at http://localhost:8080/. That will produce a 404 as the API doesn't diff --git a/docker/storyboard.conf b/docker/storyboard.conf index c0f1f1e7..1f6bb91f 100644 --- a/docker/storyboard.conf +++ b/docker/storyboard.conf @@ -61,7 +61,7 @@ enable_notifications = True # refresh_token_ttl = 604800 # A list of valid client id's that may connect to StoryBoard. -# valid_oauth_clients = storyboard.openstack.org, localhost +# valid_oauth_clients = ^.*\.openstack\.org, localhost [scheduler] # Storyboard's scheduled task management configuration @@ -73,7 +73,7 @@ enable_notifications = True # W3C CORS configuration. For more information, see http://www.w3.org/TR/cors/ # List of permitted CORS domains. -allowed_origins = https://storyboard.openstack.org, http://localhost:9000 +allowed_origins = ^https://.*\.openstack\.org, http://localhost:9000 # CORS browser options cache max age (in seconds) # max_age=3600 diff --git a/etc/storyboard.conf.sample b/etc/storyboard.conf.sample index f0f2453c..d99501af 100644 --- a/etc/storyboard.conf.sample +++ b/etc/storyboard.conf.sample @@ -61,7 +61,7 @@ lock_path = $state_path/lock # refresh_token_ttl = 604800 # A list of valid client id's that may connect to StoryBoard. -# valid_oauth_clients = storyboard.openstack.org, localhost +# valid_oauth_clients = ^.*\.openstack\.org, localhost [scheduler] # Storyboard's scheduled task management configuration @@ -73,7 +73,7 @@ lock_path = $state_path/lock # W3C CORS configuration. For more information, see http://www.w3.org/TR/cors/ # List of permitted CORS domains. -# allowed_origins = https://storyboard.openstack.org, http://localhost:9000 +# allowed_origins = ^https://.*\.openstack\.org, http://localhost:9000 # CORS browser options cache max age (in seconds) # max_age=3600 diff --git a/storyboard/api/auth/openid_client.py b/storyboard/api/auth/openid_client.py index a3366c34..714a4e5a 100644 --- a/storyboard/api/auth/openid_client.py +++ b/storyboard/api/auth/openid_client.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + from oslo_config import cfg from oslo_log import log import requests @@ -62,7 +64,14 @@ class OpenIdClient(object): if not client_id: raise InvalidClient(redirect_uri=redirect_uri, message=e_msg.NO_CLIENT_ID) - if client_id not in CONF.oauth.valid_oauth_clients: + oauth_client_is_invalid = True + for valid_oauth_client in CONF.oauth.valid_oauth_clients: + if ((valid_oauth_client == client_id) or + (valid_oauth_client.startswith('^') and + re.match(valid_oauth_client, client_id))): + oauth_client_is_invalid = False + break + if oauth_client_is_invalid: raise UnauthorizedClient(redirect_uri=redirect_uri, message=e_msg.INVALID_CLIENT_ID) diff --git a/storyboard/api/middleware/cors_middleware.py b/storyboard/api/middleware/cors_middleware.py index d5d371e7..deac94cd 100644 --- a/storyboard/api/middleware/cors_middleware.py +++ b/storyboard/api/middleware/cors_middleware.py @@ -12,6 +12,8 @@ # implied. See the License for the specific language governing permissions and # limitations under the License. +import re + # Default allowed headers ALLOWED_HEADERS = [ @@ -97,18 +99,21 @@ class CORSMiddleware(object): return start_response(status, headers, exc_info) # Does this request match one of our origin domains? - if origin in self.allowed_origins: + for allowed_origin in self.allowed_origins: + if ((allowed_origin == origin) or + (allowed_origin.startswith('^') and + re.match(allowed_origin, origin))): - # Is this an OPTIONS request? - if method == 'OPTIONS': - options_headers = [('Content-Length', '0')] - replacement_start_response('204 No Content', options_headers) - return '' - else: - # Handle the request. - return self.app(env, replacement_start_response) - else: - # This is not a request for a permitted CORS domain. Return - # the response without the appropriate headers and let the browser - # figure out the details. - return self.app(env, start_response) + # Is this an OPTIONS request? + if method == 'OPTIONS': + options_headers = [('Content-Length', '0')] + replacement_start_response('204 No Content', + options_headers) + return '' + else: + # Handle the request. + return self.app(env, replacement_start_response) + # This is not a request for a permitted CORS domain. Return + # the response without the appropriate headers and let the browser + # figure out the details. + return self.app(env, start_response)