Skip to content

Microkubes/microkubes-python

Repository files navigation

Microkubes Python library

Python tools and helpers for microservices written in Python on top of Microkubes.

It contains tools:

  • To register your serice with Microkubes API Gateway
  • Security library that works with Microkubes own security and user management:
  • Support for JWT based auth
  • Support for OAuth based auth
  • Integration with Flask, so you can secure your Flask API endpoints

Installation and setup

To install micrkubes-python run:

pip install "git+https://github.com/Microkubes/microkubes-python#egg=microkubes-python"

If you want to hack around, you can install it with pip:

pip install -e "git+https://github.com/Microkubes/microkubes-python#egg=microkubes-python"

or clone this repository directly:


git clone https://github.com/Microkubes/microkubes-python
cd microkubes-python
pip setup.py develop
pip install -r dev-requirements.txt

Use it with Flask

If you're developing with Flask, then you need to install it additionaly:

pip install Flask

Optionally, if you wat to add /healthcheck endpoint in your Flask microservice, you need to install the healthcheck library:

pip install healthcheck

Example service in Flask

As an example we can write small hello-world-like service in Flask (service.py):

import os
from flask import Flask
from microkubes.gateway import KongGatewayRegistrator


app = Flask(__name__)
registrator = KongGatewayRegistrator(os.environ.get("API_GATEWAY_URL", "http://localhost:8001"))  # Use the Kong registrator for Microkubes


# Self-registration on the API Gateway must be the first thing we do when running this service.
# If the registration fails, then the whole service must terminate.
registrator.register(name="hello-service",                  # the service name.
                     paths=["/"],                           # URL pattern that Kong will use to redirect requests to out service
                     host="hello-service.service.consul",   # The hostname of the service.
                     port=5000)                             # Flask default port. When redirecting, Kong will call us on this port.


@app.route("/hello")
def hello():
    return "Hello from Flask service on Microkubes"

Gateway registration

The library provides the basic function of registering your own service with the API Gateway. The tools are provided in package microkubes.gateway.

To register your service on a gateway, you need to use a Registrator. microkubes-python comes with support for Kong Gateway.

Example:

from microkubes.gateway import KongGatewayRegistrator


registrator = KongGatewayRegistrator(os.environ.get("API_GATEWAY_URL", "http://localhost:8001"))  # Use the Kong registrator for Microkubes


# Self-registration on the API Gateway must be the first thing we do when running this service.
# If the registration fails, then the whole service must terminate.
registrator.register(name="hello-service",                  # the service name.
                     paths=["/"],                           # URL pattern that Kong will use to redirect requests to out service
                     host="hello-service.service.consul",   # The hostname of the service.
                     port=5000)                             # Flask default port. When redirecting, Kong will call us on this port.

Note that the registration of our service to the gateway should be one of the first things that the service does. On Microkubes microservices do this check first and terminate if anything goes wrong.

Security

microkubes-python provides tools for implementing security in your services that works with the security infrastructure provided by Microkubes.

The security library offers python API for setting up security based on yor needs. It comes with couple of security providers out-of-the-box:

  • JWT provider - can decode and validate JWTs generated by Microkubes jwt-issuer service
  • OAuth2 provider -can decode and validate OAuth2 tokens generated by the OAuth2 Authorization Server in Microkubes.
  • SAML service provider - can decode session created from Identity Provider and populate auth object

The security is configured as a chain of providers, so you can have more than one type except SAML SP on any endpoint in your service.

The main entrypoint for this is the SecurityChain. When a new request is made by the client, the SecurityChain passes it to every provider in the chain. Every provider attempts to authenticate and authorize the request. If some of the providers is successful, then Auth object is generated and set i the SecurityContext.

An example of setting up security chain:

from microkubes.security import (SecurityChain,
                                 ThreadLocalSecurityContext,
                                 JWTSProvider,
                                 is_authenticated_provider,
                                 KeyStore)

store = KeyStore(dir_path='./keys')  # we need key store with the RSA keys so we can validate the JWT signature
context = ThreadLocalSecurityContext()  # Keeps the Auth in thread-local, which is fine for tests, but should be avoided for production

chain = (SecurityChain(context).
         provider(JWTSecurityProvider(key_store=store)).  # Security chain with JWT auth
         provider(is_authenticated_provider))  # check if the request has been authenticated


# assuming we receive an HTTP request, the processing of the request can be done like this:
    # try:
    #     chain.execute(context, request, response)
    # except SecurityException serr:
    #     response.status_code = serr.status_code
    #     response.write_json({"code": serr.status_code, "message": str(serr)})


# then we can access the context to get the Auth object:

auth = context.get_auth()

Security with Flask services

You can secure your Flask services by using the convinience security builder for Flask. You need to create new Security and then use Security.secured decorator on your endpoints.

By using the example in Flask above, we can secure the action by setting up a full security chain:

import os
from flask import Flask
from microkubes.gateway import KongGatewayRegistrator
from microkubes.security import FlaskSecurity


app = Flask(__name__)
registrator = KongGatewayRegistrator(os.environ.get("API_GATEWAY_URL", "http://localhost:8001"))  # Use the Kong registrator for Microkubes

# set up a security chain
sec = (FlaskSecurity().
        keys_dir("./keys").   # set up a key-store that has at least the public keys from the platform
        jwt().                # Add JWT support
        oauth2().             # Add OAuth2 support
        build())              # Build the security for Flask




# Self-registration on the API Gateway must be the first thing we do when running this service.
# If the registration fails, then the whole service must terminate.
registrator.register(name="hello-service",                  # the service name.
                     paths=["/"],                           # URL pattern that Kong will use to redirect requests to out service
                     host="hello-service.service.consul",   # The hostname of the service.
                     port=5000)                             # Flask default port. When redirecting, Kong will call us on this port.


@app.route("/hello")
@sec.secured   # this action is now secure
def hello():
    auth = sec.context.get_auth()  # get the Auth from the security context
    return "Hello %s from Flask service on Microkubes" % auth.username

Set up SAML service provider

SAML service provider can not be combined with others providers because if user is not authenticate then request is redirected to the Identity Provider. Setting up SAML SP is similar to the other providers, you need to create new Security and then use Security.secured decorator on your endpoints. The microkubes SAML SP is built on top of python3-saml library.

Below is the example that shows how to secure action in Flask:

import os
import logging

from flask import (Flask, request, session, redirect, make_response)

from microkubes.gateway import KongGatewayRegistrator
from microkubes.security import FlaskSecurity, SAMLSPUtils

logging.basicConfig(level=logging.DEBUG)

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or 'example-service'

configSAML ={
    "strict": True,
    "debug": False,
    "sp": {
        "entityId": "http://localhost:5000/metadata",
        "assertionConsumerService": {
            "url": "http://localhost:5000/acs",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        },
        "x509cert": "",
        "privateKey": ""
    },
    "idp": {
        "entityId": "http://localhost:8080/saml/idp/metadata",
        "singleSignOnService": {
            "url": "http://localhost:8080/saml/idp/sso",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
        "x509cert": ""
    },
    "security": {
        "nameIdEncrypted": False,
        "authnRequestsSigned": False,
        "signMetadata": True,
        "wantMessagesSigned": True,
        "wantAssertionsSigned": True,
        "wantNameId": False,
        "wantNameIdEncrypted": False,
        "wantAssertionsEncrypted": False,
    },
    "registration_url":"http://localhost:8080/saml/idp/services",
    "privateKeyName":"service.key",
    "certName": "service.cert"
}

# set up a security chain
sec = (FlaskSecurity().
        keys_dir("./keys").      # set up a key-store that has at least the public keys from the platform
        saml(config=configSAML). # Set SAML SP
        build())                # Build the security for Flask

@app.route("/")
@sec.secured   # this action is now secure
def hello():
    auth = sec.context.get_auth()  # get the Auth from the security context
    return "Hello %s from Flask service on Microkubes" % auth.username

# This method is executed before any request in Flask. In order to serve SP metadata and
# assertion consumer service endpoints define this method, do not forget decorator
@app.before_request
def serve_saml_ednpoints():
    if '/metadata' in request.path:
        metadata, errors = SAMLSPUtils.get_sp_metadata(configSAML)
        if len(errors) == 0:
            resp = make_response(metadata, 200)
            resp.headers['Content-Type'] = 'text/xml'
            return resp
        else:
            return make_response(', '.join(errors), 500)

    if '/acs' in request.path:
        return SAMLSPUtils.serve_acs(request, session, configSAML, redirect)

Set up ACL provider

ACL provider allows for integrating policies in a microservice as part of the security chain.

Policies are created as part of the ACL configuration, where you can specify fine-grained control over how each authenticated user has access to each resource. Each policy object has the following structure:

{
    'id': 'user-ana-allow-access',
    'description': 'Allow the user ana to read todos',
    'resources': ['/todos'],
    'actions': [
        'api:read'
    ],
    'subjects': ['ana'],
    'roles': ['user'],
    'organizations': ['test-organization'],
    'namespaces': ['test-namespace']
}

Where:

  • id is an id of the policy.
  • description is a description of what the policy does.
  • resources is a list of resource paths.
  • actions is a list of API actions. Currently supports "api:read" (GET) and "api:write" (POST, PUT, PATCH, DELETE).
  • subjects is a list of users that are allowed to access the resource in the policy.
  • roles is a list of roles.
  • organizations is a list of organizations that a user belongs to.
  • namespaces is a list of namespaces that a user belongs to.

Based on the currently authenticated user, all policy properties are checked against of those set in the JWT token.

The ACL provider is built on top of miracle-acl library.

Below is the example that shows how to configure the ACL provider and secure an action in Flask:

import logging

from flask import Flask

from microkubes.security import FlaskSecurity

logging.basicConfig(level=logging.DEBUG)

app = Flask(__name__)

config_acl = {
    'policies': [{
        'id': 'user-ana-allow-access',
        'description': 'Allow the user ana to read todos',
        'resources': ['/todos'],
        'actions': [
            'api:read'
        ],
        'subjects': ['ana'],
        'roles': ['user'],
        'organizations': ['test-organization'],
        'namespaces': ['test-namespace']
    },
    {
        'id': 'admin-allow-full-access',
        'descrition': 'Allow the admin to have full access.',
        'resources': ['/todos'],
        'actions': [
            'api:read',
            'api:write'
        ],
        'subjects': [],
        'roles': ['admin'],
        'organizations': ['test-organization', 'test-organization-1'],
        'namespaces': ['test-namespace', 'test-namespace-1']
    }]
}

sec = (FlaskSecurity().
        keys_dir('./keys').
        jwt().
        oauth2().
	    acl(config=config).
        build())

@app.route('/todos', methods=['GET'])
@sec.secured
def todos():
    auth = sec.context.get_auth()  # get the Auth from the security context
    return 'Hello %s from Flask service on Microkubes' % auth.username

Healthcheck

First make sure you have healthcheck installed:

pip install healthcheck

Then add healthcheck to your microservice by adding the following code:

from flask import Flask
from microkubes.tools import add_healthcheck

app = Flask(__name__)    
add_healthcheck(app)

Version endpoint

To add version endpoint to your microservice, first you need to create a configuration JSON file in the root of the microservice project, that looks like this:

{
    "version": "1.0.0"
}

After that, add the following code after you create the Flask instance:

import os 
from flask import Flask
from microkubes.tools import add_version_endpoint, read_config

app = Flask(__name__)    

current_dir_path = os.path.dirname(os.path.realpath(__file__))
config = read_config(current_dir_path + '/config.json')
add_version_endpoint(app, config)

To test it out, start the Flask server and do curl http://localhost:5000/version and it will return:

{
    "version": "1.0.0"
}

Contributing

For contributing to this repository or its documentation, see the Contributing guidelines.