Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    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 == '*****@*****.**'
Esempio n. 5
0
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']
Esempio n. 6
0
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
Esempio n. 7
0
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()
Esempio n. 8
0
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