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
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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)