diff --git a/kqueen/auth/base.py b/kqueen/auth/base.py index 69572cc5..c5d6b55f 100644 --- a/kqueen/auth/base.py +++ b/kqueen/auth/base.py @@ -26,3 +26,18 @@ def verify(self, user, password): """ raise NotImplementedError + + @classmethod + def get_parameter_schema(cls): + """Return parameters specific for a auth method implementation. + + These parameters are used to generate form for inviting user with fields, + specific for a particular authentication method. + + Returns: + dict: Returns ``self.parameter_schema`` by default, but can be overridden. + """ + if not hasattr(cls, 'parameter_schema'): + raise NotImplementedError('"parameter_schema" attribute should be provided in the ' + 'auth method implementation') + return cls.parameter_schema diff --git a/kqueen/auth/common.py b/kqueen/auth/common.py index 7f0fed04..7dbc3783 100644 --- a/kqueen/auth/common.py +++ b/kqueen/auth/common.py @@ -30,7 +30,7 @@ "parameters": { "uri": config.get('LDAP_URI'), "admin_dn": config.get('LDAP_DN'), - "password": config.get('LDAP_PASSWORD') + "_password": config.get('LDAP_PASSWORD') } }, "local": { @@ -51,7 +51,7 @@ def generate_auth_options(auth_list): if not auth_options: auth_options['local'] = {'engine': 'LocalAuth', 'parameters': {}} - logger.debug('Auth config generated {}'.format(auth_options)) + logger.debug('Auth configuration options are generated ') return auth_options diff --git a/kqueen/auth/ldap.py b/kqueen/auth/ldap.py index 7e66818b..091cae40 100644 --- a/kqueen/auth/ldap.py +++ b/kqueen/auth/ldap.py @@ -10,13 +10,29 @@ class LDAPAuth(BaseAuth): + verbose_name = 'LDAP' + parameter_schema = { + 'username': { + 'type': 'text', + 'label': 'User CN', + 'description': 'Enter user common name, resisted in the configured LDAP', + 'validators': { + 'required': True + }, + 'generate_password': False, + # TODO: add checkbox for notify if email is provided + 'notify': False, + # TODO: add optional email field for ldap user + }, + } + def __init__(self, *args, **kwargs): """ Implementation of :func:`~kqueen.auth.base.__init__` """ super(LDAPAuth, self).__init__(*args, **kwargs) - if not all(hasattr(self, attr) for attr in ['uri', 'admin_dn', 'password']): + if not all(hasattr(self, attr) for attr in ['uri', 'admin_dn', '_password']): msg = 'Failed to configure LDAP, please provide valid LDAP credentials' logger.error(msg) raise ImproperlyConfigured(msg) @@ -27,9 +43,9 @@ def __init__(self, *args, **kwargs): self.kqueen_dc = ','.join(dc_list) # Bind connection for Kqueen Read-only user - if self._bind(self.admin_dn, self.password): + if self._bind(self.admin_dn, self._password): self.connection = ldap.initialize(self.uri) - self.connection.simple_bind_s(self.admin_dn, self.password) + self.connection.simple_bind_s(self.admin_dn, self._password) self.connection.protocol_version = ldap.VERSION3 else: msg = 'Failed to bind connection for Kqueen Read-only user' diff --git a/kqueen/auth/local.py b/kqueen/auth/local.py index 7f36cd99..63743411 100644 --- a/kqueen/auth/local.py +++ b/kqueen/auth/local.py @@ -8,6 +8,20 @@ class LocalAuth(BaseAuth): + verbose_name = 'Local' + parameter_schema = { + 'username': { + 'type': 'email', + 'label': 'User Email', + 'description': 'Provide valid email of the user you want to invite to the organization', + 'validators': { + 'required': True + }, + 'active': False, + 'notify': True + } + } + def verify(self, user, password): """Implementation of :func:`~kqueen.auth.base.__init__` diff --git a/kqueen/auth/test_ldap.py b/kqueen/auth/test_ldap.py index a541d597..b1d26322 100644 --- a/kqueen/auth/test_ldap.py +++ b/kqueen/auth/test_ldap.py @@ -14,7 +14,7 @@ def setup(self, user): self.user.password = '' self.user.save() - self.auth_class = LDAPAuth(uri='ldap://127.0.0.1', admin_dn='cn=admin,dc=example,dc=org', password='heslo123') + self.auth_class = LDAPAuth(uri='ldap://127.0.0.1', admin_dn='cn=admin,dc=example,dc=org', _password='heslo123') def test_raise_on_missing_creds(self): with pytest.raises(Exception, msg='Failed to configure LDAP, please provide valid LDAP credentials'): @@ -29,8 +29,8 @@ def test_login_pass(self): assert error is None def test_login_bad_pass(self): - password = 'abc' - user, error = self.auth_class.verify(self.user, password) + _password = 'abc' + user, error = self.auth_class.verify(self.user, _password) assert not user assert error == 'Failed to validate full-DN. Check CN name and defined password of invited user' diff --git a/kqueen/blueprints/api/api3_0.yml b/kqueen/blueprints/api/api3_0.yml index 5a220f44..1bd4fa9c 100644 --- a/kqueen/blueprints/api/api3_0.yml +++ b/kqueen/blueprints/api/api3_0.yml @@ -344,6 +344,24 @@ components: $ref: "https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json#/definitions/io.k8s.api.core.v1.ServiceList" version: $ref: "https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json#/definitions/io.k8s.apimachinery.pkg.version.Info" + getAuthConfig: + type: "object" + auth_method_name: + type: "object": + properties: + engine: + type: "string" + description: "Name of the authentication class" + name: + type: "string" + description: "Verbose name of the authentication method" + ui_parameters: + type: "object" + description: "Auth-method specific parameters" + parameters: + type: "object" + description: "Authentication class initialization parameters" + responses: Unauthorized: description: "Authorization information is missing or invalid." @@ -1041,3 +1059,19 @@ paths: $ref: "#/components/schemas/Engine" "401": $ref: "#/components/responses/Unauthorized" + /configurations/auth: + get: + summary: "Get information about auth classes configuration" + tags: + - "Auth" + responses: + "200": + description: "Successful operation" + content: + application/json: + schema: + type: "array" + $ref: "#/components/schemas/getUser" + "401": + $ref: "#/components/responses/Unauthorized" + diff --git a/kqueen/blueprints/api/views.py b/kqueen/blueprints/api/views.py index 1ca56efe..c6a15ec2 100644 --- a/kqueen/blueprints/api/views.py +++ b/kqueen/blueprints/api/views.py @@ -13,6 +13,7 @@ from flask_jwt import jwt_required from importlib import import_module from kqueen.auth import encrypt_password +from kqueen.auth.common import generate_auth_options from kqueen.models import Cluster from kqueen.models import Organization from kqueen.models import Provisioner @@ -301,6 +302,7 @@ def provisioner_engine_list(): 'parameters': parameters }) except NotImplementedError: + logger.exception('UI parameters is not set for engine: {}'.format(engine)) engine_cls.append({ 'name': engine, 'verbose_name': engine, @@ -310,7 +312,7 @@ def provisioner_engine_list(): } }) except Exception: - logger.exception('Unable to read parameters for engine: ') + logger.exception('Unable to read parameters for engine: {}'.format(engine)) return jsonify(engine_cls) @@ -470,3 +472,33 @@ def swagger_json(): abort(500) return jsonify(data) + + +@api.route('/configurations/auth', methods=['GET']) +@jwt_required() +def auth_params_configuration(): + + auth_opts = generate_auth_options(config.get("AUTH_MODULES")) + try: + for name, configuration in auth_opts.items(): + auth_cls_name = configuration['engine'] + module = import_module('kqueen.auth') + _class = getattr(module, auth_cls_name) + + # Add UI fields description, verbose name and hide secure parameters + secure_params = {} + for k, v in configuration['parameters'].items(): + if k.startswith('_'): + v = '*****' + secure_params[k] = v + + auth_opts[name].update( + {'ui_parameters': _class.get_parameter_schema(), + 'name': getattr(_class, 'verbose_name', name), + 'parameters': secure_params}) + + except NotImplementedError: + logger.exception('UI parameters is not specified for "{}" auth type'.format(name)) + except Exception: + logger.exception('Unable to read UI parameters for "{}" auth type'.format(name)) + return jsonify(auth_opts) diff --git a/kqueen/engines/base.py b/kqueen/engines/base.py index 76287c63..5a6b822d 100644 --- a/kqueen/engines/base.py +++ b/kqueen/engines/base.py @@ -187,6 +187,9 @@ def get_parameter_schema(cls): Returns: dict: Returns ``self.parameter_schema`` in default, but can be overridden. """ + if not hasattr(cls, 'parameter_schema'): + raise NotImplementedError('"parameter_schema" attribute should be provided in the ' + 'Provisioner class implementation') return cls.parameter_schema def get_progress(self):