The Wikimedia Cloud VPS platform is powered by the OpenStack platform. In addition to the standard OpenStack services, we've needed to build our own services and APIs that add features specific to our installation.
Historically our OpenStack APIs have not been open to direct use and we've required everyone to use the OpenStack dashboard (Horizon) to manage their VPS project. Our custom APIs have been "secured" by just firewalling access from everywhere else except the OpenStack control hosts, which has worked fine for us in the past. However, we are planning on letting our users directly access the APIs to build their own integrations, and as a part of this work we needed to replace the firewall-based security model with something more flexible. The natural solution is to integrate our custom APIs with OpenStack's Identity service (Keystone).
Our services use the Flask framework. Thankfully this means that we can just use the code that Rackspace has written: the flask_keystone package does exactly what we want. The documentation is a bit lacking though.
flask_keystone makes heavy use of OpenStack's shared library Oslo and uses Rackspace's flask_oslolog too, so you need to install them. At Wikimedia we built our own Debian packages for both Rackspace libraries (Oslo is already packaged for on the official Debian repositories).
A minimal code example looks something like this:
import flask
from flask_keystone import FlaskKeystone
from flask_oslolog import OsloLog
from oslo_config import cfg
from oslo_context import context
key = FlaskKeystone()
log = OsloLog()
cfg.CONF(default_config_files=['/etc/path-to-your/config.ini'])
app = flask.Flask(__name__)
log.init_app(app)
key.init_app(app)
def get_oslo_context(rule, project_id):
# headers in a specific format that oslo.context wants
headers = {
f'HTTP_{name.upper().replace("-", "_")}': value
for name, value in flask.request.headers.items()
}
return context.RequestContext.from_environ(headers)
@app.route("/")
def hello():
ctx = get_oslo_context()
return f'Hello, {ctx.user_id} on project {ctx.project_id}!'
if __name__ == '__main__':
app.run()
You will also need a config file in the path you gave to oslo_config. A sample configuration looks something like this:
[DEFAULT]
auth_strategy=keystone
[keystone_authtoken]
identity_uri = https://openstack.example.org:25000
www_authenticate_uri = https://openstack.example.org:25000
http_connect_timeout = 5
interface = public
auth_version = 3.14
auth_type = password
auth_url = https://openstack.example.org:25000
user_domain_id = default
project_domain_id = default
username = some-username
password = some-password
project_name = some-project
service_type = some-keystone-service-type
delay_auth_decision = True
[flask_keystone]
roles = ""
In the config file, you need to add credentials for an OpenStack
account that has access to the identity:validate_token
rule. We
added a new role
for that purpose and granted our service account that role domain-wide.
Now that your service is secured with Keystone authentication, you probably want to make some requests to it. This can be done using the keystoneclient python package:
from keystoneauth1 import session as keystone_session
from keystoneauth1.identity import v3
from keystoneclient.v3 import client
auth = v3.Password(
auth_url='https://openstack.example.org:25000',
username='some-username',
password='some-password',
project_id='some-project',
user_domain_name='default',
project_domain_name='default',
)
session = keystone_session.Session(
auth=auth,
user_agent='some-app',
)
client = client.Client(
session=session(),
interface='public',
timeout=5,
)
service = client.services.list(type='some-keystone-service-type')[0]
endpoint = client.endpoints.list(service=service.id, interface='public', enabled=True)[0]
url = endpoint.url
print(session.get(f'{url}/test').json())
We can also use OpenStack oslo.policy for policy-based access control, similar to other OpenStack services.
First, create an Enforcer
object for your policy rules:
from oslo_config import cfg
from oslo_policy import policy
# you should already have this
cfg.CONF(default_config_files=['/etc/path-to-your/config.ini'])
enforcer = policy.Enforcer(cfg.CONF)
enforcer.register_defaults([
policy.RuleDefault('admin', 'role:admin'),
policy.RuleDefault('proxy:index', ''),
policy.RuleDefault('proxy:view', ''),
policy.RuleDefault('proxy:create', 'rule:admin'),
policy.RuleDefault('proxy:update', 'rule:admin'),
policy.RuleDefault('proxy:delete', 'rule:admin'),
])
Then you can enforce the policies in your route handlers:
@app.route('/v1/proxy')
def all_proxies():
ctx = get_oslo_context()
result = enforcer.authorize('proxy:index', {}, ctx)
if not result:
return flask.json({'error': 'forbidden'}), 403
return flask.json(get_proxies(ctx.project_id))