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
This commit is contained in:
Jeremy Stanley 2019-10-24 17:50:19 +00:00
parent 4c1c7c0cfc
commit 3e4e956ff8
5 changed files with 37 additions and 21 deletions

View File

@ -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 .. 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 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 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 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 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 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 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 at http://localhost:8080/. That will produce a 404 as the API doesn't

View File

@ -61,7 +61,7 @@ enable_notifications = True
# refresh_token_ttl = 604800 # refresh_token_ttl = 604800
# A list of valid client id's that may connect to StoryBoard. # 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] [scheduler]
# Storyboard's scheduled task management configuration # 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/ # W3C CORS configuration. For more information, see http://www.w3.org/TR/cors/
# List of permitted CORS domains. # 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) # CORS browser options cache max age (in seconds)
# max_age=3600 # max_age=3600

View File

@ -61,7 +61,7 @@ lock_path = $state_path/lock
# refresh_token_ttl = 604800 # refresh_token_ttl = 604800
# A list of valid client id's that may connect to StoryBoard. # 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] [scheduler]
# Storyboard's scheduled task management configuration # 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/ # W3C CORS configuration. For more information, see http://www.w3.org/TR/cors/
# List of permitted CORS domains. # 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) # CORS browser options cache max age (in seconds)
# max_age=3600 # max_age=3600

View File

@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import re
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
import requests import requests
@ -62,7 +64,14 @@ class OpenIdClient(object):
if not client_id: if not client_id:
raise InvalidClient(redirect_uri=redirect_uri, raise InvalidClient(redirect_uri=redirect_uri,
message=e_msg.NO_CLIENT_ID) 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, raise UnauthorizedClient(redirect_uri=redirect_uri,
message=e_msg.INVALID_CLIENT_ID) message=e_msg.INVALID_CLIENT_ID)

View File

@ -12,6 +12,8 @@
# implied. See the License for the specific language governing permissions and # implied. See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import re
# Default allowed headers # Default allowed headers
ALLOWED_HEADERS = [ ALLOWED_HEADERS = [
@ -97,18 +99,21 @@ class CORSMiddleware(object):
return start_response(status, headers, exc_info) return start_response(status, headers, exc_info)
# Does this request match one of our origin domains? # 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? # Is this an OPTIONS request?
if method == 'OPTIONS': if method == 'OPTIONS':
options_headers = [('Content-Length', '0')] options_headers = [('Content-Length', '0')]
replacement_start_response('204 No Content', options_headers) replacement_start_response('204 No Content',
return '' options_headers)
else: return ''
# Handle the request. else:
return self.app(env, replacement_start_response) # Handle the request.
else: return self.app(env, replacement_start_response)
# This is not a request for a permitted CORS domain. Return # This is not a request for a permitted CORS domain. Return
# the response without the appropriate headers and let the browser # the response without the appropriate headers and let the browser
# figure out the details. # figure out the details.
return self.app(env, start_response) return self.app(env, start_response)