def keys_dir(self, path): """Load the keys for the ``KeyStore`` from this directory. :param path: ``str``, keys directory path. :returns: :class:`FlaskSecurity`. """ if self.key_store: raise FlaskSecurityError('KeyStore is already defined.') self.key_store = KeyStore(dir_path=path) return self
def use_key(self, key_name, key_file): """Add mapped key to the ``KeyStore``. :param key_name: ``str``, the name (id) of the key. :param key_file: ``str``, path to the key file. :returns: :class:`FlaskSecurity`. """ if not self.key_store: self.key_store = KeyStore() self.key_store.add_key(key_name, key_file) return self
def test_acl_provider(self, get_header_mock, path_mock, method_mock): method_mock.return_value = 'GET' path_mock.return_value = '/todos' with TemporaryDirectory() as tmpdir: rsa_key = generate_rsa_keypair() priv_key = serialize_private_pem(rsa_key) pub_key = serialize_public_pem(rsa_key) with open(path_join(tmpdir, 'system'), 'wb') as keyfile: keyfile.write(priv_key) with open(path_join(tmpdir, 'system.pub'), 'wb') as keyfile: keyfile.write(pub_key) key_store = KeyStore(dir_path=tmpdir) header_token = get_jwt( key_store, { 'userId': 'test-user', 'username': '******', 'scopes': 'api:read', 'roles': 'user', 'organizations': 'test-organization', 'namespaces': 'test-namespace', }, ) get_header_mock.return_value = 'Bearer %s' % header_token jwt_provider = JWTProvider(key_store=key_store) local_context = local() # Thread-Local underlying local context context = SecurityContext(local_context=local_context) req_object = local() request = Request(req_object) response = Response() jwt_provider(context, request, response) acl_provider = ACLProvider(CONFIG_ACL) # Check SecurityException is not thrown acl_provider(context, request, response) assert context.has_auth() auth = context.get_auth() assert auth is not None assert auth.user_id == 'test-user' assert auth.username == 'ana' assert auth.scopes == ['api:read'] assert auth.roles == ['user'] assert auth.organizations == ['test-organization'] assert auth.namespaces == ['test-namespace']
def test_saml_sp(host_mock, form_mock, args_mock, register_sp_mock): """ Test SAML SP middleware """ register_sp_mock.return_value = 'OK' args_mock.return_value = ['arg1'] form_mock.return_value = {'RelayState': 'dsd67atdas6dad67ad67a'} host_mock = 'localhost:5000' with TemporaryDirectory() as tmpdir: rsa_key = generate_RSA_keypair() priv_key = serialize_private_pem(rsa_key) pub_key = serialize_public_pem(rsa_key) with open(path_join(tmpdir, 'service.key'), 'wb') as keyfile: keyfile.write(priv_key) with open(path_join(tmpdir, 'service.cert'), 'wb') as keyfile: keyfile.write(pub_key) key_store = KeyStore(dir_path=tmpdir) local_context = local() # Thread-Local underlying local context context = SecurityContext(local_context=local_context) req_object = local() request = Request(req_object) response = Response() sp = SAMLServiceProvider(key_store, configSAML) sp(context, request, response) assert response.redirect_url.startswith( 'http://*****:*****@example.com'], 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1': ['user'] } } sp = SAMLServiceProvider(key_store, configSAML, saml_session=saml_session) sp(context, request, response) auth = context.get_auth() assert context.has_auth() assert auth.user_id == 'test-user' assert auth.roles == ['user'] assert auth.username == '*****@*****.**'
def test_jwt_pass(get_header_mock): """Test the JWT provider in a successful vanila scenario. """ with TemporaryDirectory() as tmpdir: rsa_key = generate_RSA_keypair() priv_key = serialize_private_pem(rsa_key) pub_key = serialize_public_pem(rsa_key) with open(path_join(tmpdir, 'system'), 'wb') as keyfile: keyfile.write(priv_key) with open(path_join(tmpdir, 'system.pub'), 'wb') as keyfile: keyfile.write(pub_key) key_store = KeyStore(dir_path=tmpdir) header_token = get_jwt( key_store, { 'userId': 'test-user', 'username': '******', 'scopes': 'api:read', 'roles': 'user,test', 'organizations': 'org1,org2', 'namespaces': 'microkubes,special1' }) get_header_mock.return_value = 'Bearer %s' % header_token jwt_provider = JWTProvider(key_store=key_store) local_context = local() # Thread-Local underlying local context context = SecurityContext(local_context=local_context) req_object = local() request = Request(req_object) response = Response() jwt_provider(context, request, response) assert context.has_auth() auth = context.get_auth() assert auth is not None assert auth.user_id == 'test-user' assert auth.username == '*****@*****.**' assert auth.scopes == ['api:read'] assert auth.roles == ['user', 'test'] assert auth.organizations == ['org1', 'org2'] assert auth.namespaces == ['microkubes', 'special1']
def test_key_store_load_from_dir(): """Test KeyStore loading keys from a directory. """ with TemporaryDirectory() as tmpdir: with open(path_join(tmpdir, 'system'), 'wb') as kf: kf.write(b'SYSTEM PRIVATE KEY') with open(path_join(tmpdir, 'system.pub'), 'wb') as kf: kf.write(b'SYSTEM PUBLIC KEY') with open(path_join(tmpdir, 'service.crt'), 'wb') as kf: kf.write(b'service cert') with open(path_join(tmpdir, 'service.key'), 'wb') as kf: kf.write(b'service private key') with open(path_join(tmpdir, 'default.pub'), 'wb') as kf: kf.write(b'DEFAULT PUBLIC KEY') ks = KeyStore(dir_path=tmpdir) assert ks.keys.get('ignored') is None assert ks.keys.get('ignored.pub') is None assert ks.keys.get('service.crt') is not None assert ks.keys.get('service.key') is not None syskey = ks.get_key('system') assert syskey is not None assert syskey.public is True assert syskey.load() == b'SYSTEM PUBLIC KEY' syskey = ks.get_private_key('system') assert syskey is not None assert syskey.public is False assert syskey.load() == b'SYSTEM PRIVATE KEY' default = ks.get_key() assert default is not None public_keys = ks.public_keys() assert len(public_keys) == 3 assert public_keys.get('system') is not None assert public_keys.get('default') is not None
def test_key_store_load_keys_map(): """Test KeyStore loading keys from a given keys map. """ with TemporaryDirectory() as tmpdir: with open(path_join(tmpdir, 'system'), 'wb') as kf: kf.write(b'SYSTEM PRIVATE KEY') with open(path_join(tmpdir, 'default.pub'), 'wb') as kf: kf.write(b'DEFAULT PUBLIC KEY') with open(path_join(tmpdir, 'system.pub'), 'wb') as kf: kf.write(b'SYSTEM PUBLIC KEY') ks = KeyStore( keys={ 'default': path_join(tmpdir, 'default.pub'), 'system': path_join(tmpdir, 'system'), }) syskey = ks.get_private_key('system') assert syskey is not None assert syskey.public is False assert syskey.load() == b'SYSTEM PRIVATE KEY' default = ks.get_key() assert default is not None ks.add_key('system', path_join(tmpdir, 'system.pub')) assert ks.get_key('system') is not None assert ks.get_private_key('system') is not None assert ks.get_key('system') != ks.get_private_key('system') assert ks.get_key('system').load() != ks.get_private_key( 'system').load()
class FlaskSecurity: """Flask security builder. Builds new Microkubes enabled security for Flask applications. In the background it generates a ``SecurityChain`` and :class:`Security` that can be used to secure the Flask's endpoints. Example setup: .. code-block:: python from flask import Flask from microkubes.security import FlaskSecurity app = Flask(__name__) sec = (FlaskSecurity(). # new security with default secuity context keys_dir('./keys'). # the RSA keys are in this directory static_files(r'.*\\.js', r'.*\\.css', r'.*\\.png', r'.*\\.jpg', r'.*\\.jpeg'). # ignore these public_route('/public/.*'). # ignore this too jwt(). # Support JWT oauth2(). # Support OAuth2 build()) # Finally, build the security @app.route("/") @sec.secured def hello_world(): return 'hello world' :param context: :class:`microkubes.security.auth.SecurityContext` - the security context to be used. By default :class:`FlaskSecurityContext` is used. :param key_store: :class:`microkubes.security.keys.KeyStore`, ``KeyStore`` instance. """ def __init__(self, context=None, key_store=None): self.key_store = key_store context = context or _SECURITY_CONTEXT self._context = context self._chain = SecurityChain(security_context=context) self._public_routes = [] self._jwt_provider = None self._oauth_provider = None self._saml_sp = None self._acl_provider = None self._other_providers = [] self._prefer_json_respose = True def keys_dir(self, path): """Load the keys for the ``KeyStore`` from this directory. :param path: ``str``, keys directory path. :returns: :class:`FlaskSecurity`. """ if self.key_store: raise FlaskSecurityError('KeyStore is already defined.') self.key_store = KeyStore(dir_path=path) return self def use_key(self, key_name, key_file): """Add mapped key to the ``KeyStore``. :param key_name: ``str``, the name (id) of the key. :param key_file: ``str``, path to the key file. :returns: :class:`FlaskSecurity`. """ if not self.key_store: self.key_store = KeyStore() self.key_store.add_key(key_name, key_file) return self def jwt(self, header='Authorization', schema='Bearer', algs=None): """Setup JWT security provider. This provider tries to decode and create auth from a JWT in the HTTP request. :param header: ``str``, the name of the HTTP auth header. Default is ``Authorization``. :param schema: ``str``, the auth schema used for the auth HTTP header. By default this is ``Bearer`` token. :param algs: ``list``, list of accepted signing algorithms. If not specified assumes ``HS256`` and ``RS256``. :returns: :class:`FlaskSecurity`. """ if not self.key_store: raise FlaskSecurityError( 'KeyStore must be defined before setting up the JWT provider.') self._jwt_provider = JWTProvider(self.key_store, header=header, auth_schema=schema, algs=algs) return self def oauth2(self, algs=None): """Setup OAuth2 security provider. This provider will try to decode and validate an OAuth2 token. All tokens with Microkubes are self-contained and are JWTs. :param algs: ``list``, list of accepted signing algorithms. If not specified assumes ``HS256`` and ``RS256``. :returns: :class:`FlaskSecurity`. """ if not self.key_store: raise FlaskSecurityError( 'KeyStore must be defined before setting up the OAuth2 provider.' ) self._oauth_provider = OAuth2Provider(key_store=self.key_store, algs=algs) return self def saml(self, config=None): """Setup SAML SP :param config: ``dict``, the SAML SP config :returns: :class:`FlaskSecurity`. """ if not self.key_store: raise FlaskSecurityError( 'KeyStore must be defined before setting up the SAML service provider.' ) if not config: raise FlaskSecurityError('SAML config not provided') self._saml_sp = SAMLServiceProvider(self.key_store, config, saml_session=session) return self def acl(self, config=None): """Setup ACL provider :param app: :class:`flask.Flask`, current ``Flask`` instance. :param config: ``dict``, the ACL provider config :returns: :class:`FlaskSecurity`. """ if not config: raise FlaskSecurityError('ACL config not provided') self._acl_provider = ACLProvider(config) return self def public_route(self, *args): """Add public routes that will be ignored and not checked by the security. A public route is a HTTP request path that is publicly accessible and does not need authorization for access. :param args: variadic args, ``list`` of exact routes or regexp patterns to match against. All routes are treated as regular expression pattern, so: * ``/public/resource`` matches exactly this pattern * ``/public/.*`` matches both ``/public/a`` and ``/public/file.js`` but not ``/protected/file`` * ``.*\\.js`` matches all js files - both ``/public/file.js`` and ``/protected/file.js`` :returns: :class:`FlaskSecurity`. """ self._public_routes.append(public_routes_provider(*args)) return self def static_files(self, *args): """Alias for ``public_routes`` method. The only difference is a semantic one, to allow more readablity in the code. """ return self.public_route(*args) def add_provider(self, provider, position='last'): """Add custom security provider to the security chain. The chain executes multiple providers, in order, when processing a request. This method adds new provider to the security chain's providers list. Because there are some defult providers that are set up in a certain order, you can provide a ``position`` and place the custom provider anuwhere in the chain. If not given, the provider is appended near the end of the list (the provider that checks if the request is authenticated is always last). In general, these are the available positions, and corresponding placement for the providers: +-----+--------------------------+------------------------------+ | seq | POSITION | PROVIDERS | +-----+--------------------------+------------------------------+ | 1 | first | custom providers | +-----+--------------------------+------------------------------+ | 2 | N/A | public_routes | +-----+--------------------------+------------------------------+ | 3 | before_jwt, after_public | custom providers | +-----+--------------------------+------------------------------+ | 4 | N/A | jwt_provider | +-----+--------------------------+------------------------------+ | 5 | after_jwt, before_oauth2 | custom providers | +-----+--------------------------+------------------------------+ | 6 | N/A | oauth2_provider | +-----+--------------------------+------------------------------+ | 7 | last | custom providers | +-----+--------------------------+------------------------------+ | 8 | N/A | is_authenticated_provider | +-----+--------------------------+------------------------------+ | 9 | final | custom providers | +-----+--------------------------+------------------------------+ :param provider: ``function``, the security provider to add :param position: ``str``, at which position in the chain to add the provider. One of ``first``, ``before_jwt``, ``after_public``, ``after_jwt``, ``before_oauth2``, ``last`` and ``final`` is allowed. Default is ``last``. :returns: :class:`FlaskSecurity`. """ position = position or 'last' self._other_providers.append((provider, position)) return self def _merge_providers(self): providers = [] for provider, position in self._other_providers: if position == 'first': providers.append(provider) for provider in self._public_routes: providers.append(provider) for provider, position in self._other_providers: if position in ['before_jwt', 'after_public']: providers.append(provider) if self._jwt_provider: providers.append(self._jwt_provider) for provider, position in self._other_providers: if position in ['after_jwt', 'before_oauth', 'before_oauth2']: providers.append(provider) if self._oauth_provider: providers.append(self._oauth_provider) if self._saml_sp: providers.append(self._saml_sp) if self._acl_provider: providers.append(self._acl_provider) for provider, position in self._other_providers: if position in ['after_oauth', 'after_oauth2', 'last']: providers.append(provider) providers.append(is_authenticated_provider) for provider, position in self._other_providers: if position == 'final': providers.append(provider) return providers def build_chain(self): """Build a :class:`microkubes.security.chain.SecurityChain` from the configured values. :returns: the :class:`microkubes.security.chain.SecurityChain` """ if not self.key_store: raise FlaskSecurityError('Please define a KeyStore.') for provider in self._merge_providers(): self._chain.provider(provider) return self._chain def build(self): """Build a ``Security`` from the configured values. It builds a security chain and configures a new security to be used in flask apps. :returns: :class:`Security`. """ chain = self.build_chain() security = Security( security_chain=chain, context=self._context, json_response=self._prefer_json_respose, ) return security