Example #1
0
def import_remote_user(uid, description, provider_type, provider_id):
    """
    Import and return user.

    Raises:
        EntityExists: user with that uid already exists.
    """
    log.debug('Attempt to create remote user with uid `%s`', uid)

    try:
        user = User(
            uid=uid,
            utype=UserType.regular,
            description=description,
            provider_type=provider_type,
            provider_id=provider_id,
        )
    except bouncer.app.exceptions.UidValidationError:
        _raise_cannot_import_user('Cannot import user: invalid uid `%s`' %
                                  (uid, ))

    # This might raise `bouncer.app.exceptions.EntityExists`. Let the caller of
    # this function handle it. Ususally, it has just checked before if the user
    # exists or not.
    User.add(user)
    return user
Example #2
0
    def on_patch(self, req, resp, uid):

        pw_given = req.context['idata'].get('password', None)
        update_description = req.context['idata'].get('description', None)

        if pw_given is None and update_description is None:
            raise falcon.HTTPBadRequest(
                description=
                'One of `description` and `password` must be provided.',
                code='ERR_INVALID_DATA',
            )

        user = User.get_or_terminate_request(uid, self.log)

        if pw_given is not None:
            if user.is_service:
                raise falcon.HTTPBadRequest(
                    description=
                    'Password update is not available for service user accounts.',
                    code='ERR_INVALID_DATA',
                )

            update_pw_hashed = self._validate_hash_password(pw_given)
            user.passwordhash = update_pw_hashed

        if update_description is not None:
            user.description = update_description

        dbsession.commit()

        resp.status = falcon.HTTP_NO_CONTENT
Example #3
0
    def on_put(self, req, resp, uid):

        pw_given = req.context['idata'].get('password', None)
        key_given = req.context['idata'].get('public_key', None)
        description = req.context['idata'].get('description', '')
        provider_type_str = req.context['idata'].get('provider_type', None)
        # Note(JP): why is the default of this not None?
        provider_id = req.context['idata'].get('provider_id', '')

        # Check if this is upstream Bouncer.
        if hasattr(oidcidtokenlogin, 'ISSUER_WHITELIST'):
            # Yes, it is. Implement a fallback to support inserting users with a
            # uid that looks like an email address, and where the HTTP request
            # lacks meaningful data in the request body. This is what the DC/OS
            # UI actually does up to version 1.12, and what other external
            # tooling might also do. What such an HTTP request actually means
            # is: insert user record with 'provider_type': 'oidc',
            # 'provider_id': 'https://dcos.auth0.com/'
            if not provider_id and provider_type_str is None:
                if not pw_given and not key_given:
                    if re.match(r'[^@]+@[^@]+\.[^@]+', uid):
                        # The email check really is meant to be very liberal.
                        # See https://stackoverflow.com/a/8022584/145400
                        provider_type_str = 'oidc'
                        # Expect a single key in the whitelist dict, get it.
                        provider_id = 'https://dcos.auth0.com/'

        # Basic validation: do not allow empty strings for password and public
        # key, under no circumstances.
        if pw_given == '':
            self.raise_invalid_data(
                '`password` must not be empty when provided')

        if key_given == '':
            self.raise_invalid_data(
                '`public_key` must not be empty when provided')

        ptype = self._parse_provider_type_str(provider_type_str)

        # Basic validation for non-internal providers:no password or public key
        # must be provided.
        if ptype is not ProviderType.internal:

            if pw_given:
                self.raise_invalid_data(
                    'external provider: `password` is unexpected')

            if key_given:
                self.raise_invalid_data(
                    'external provider: `public_key` is unexpected')

        # Assume that the user is a regular user account.
        utype = UserType.regular

        # Set defaults.
        publickey = None
        pw_hashed = None

        if ptype is ProviderType.internal:

            if len([_ for _ in (pw_given, key_given) if _]) != 1:
                self.raise_invalid_data(
                    'One of `password` or `public_key` must be provided')

            if pw_given:
                pw_hashed = self._validate_hash_password(pw_given)

            else:
                # Service user account. As long as there is no distinct notion
                # of service accounts in our data model, piggyback `pw_hashed`
                # for storing its public key. Note(JP): clean this up:
                # https://jira.mesosphere.com/browse/DCOS-43663
                self._validate_public_key(key_given)
                publickey = key_given
                # This is an internal user that is using key-based
                # authentication. This implies that the it is a service account.
                utype = UserType.service

        # Todo(jp): add validation logic to the User object creation.
        try:
            user = User(
                uid=uid,
                passwordhash=pw_hashed,
                publickey=publickey,
                utype=utype,
                description=description,
                provider_type=ptype,
                provider_id=provider_id,
            )
        except UidValidationError:
            raise falcon.HTTPBadRequest(description='Invalid user ID: %s' %
                                        uid,
                                        code='ERR_INVALID_USER_ID')
        except ProviderTypeValidationError as exc:
            self.raise_invalid_data('Invalid provider_type: %s' % str(exc))
        except ProviderIdValidationError as exc:
            self.raise_invalid_data('Invalid provider_id: %s' % str(exc))

        try:
            User.add(user)
        except EntityExists:
            raise falcon.HTTPConflict(
                description='User with id `%s` already exists.' % uid,
                code='ERR_USER_EXISTS')

        self.log.info('User with uid `%s` added to database.', uid)
        resp.status = falcon.HTTP_201
Example #4
0
 def on_get(self, req, resp):
     show_services = req.params.get('type', None) == 'service'
     utype = UserType.service if show_services else UserType.regular
     users = User.get_all(utype)
     req.context['odata'] = {'array': [u.jsonobj() for u in users]}
Example #5
0
 def on_delete(self, req, resp, uid):
     user = User.get_or_terminate_request(uid, self.log)
     ensure_at_least_one_superuser_will_be_left(user)
     dbsession.delete(user)
     dbsession.commit()
     resp.status = falcon.HTTP_NO_CONTENT
Example #6
0
 def on_get(self, req, resp, uid):
     user = User.get_or_terminate_request(uid, self.log)
     user_json_obj = user.jsonobj()
     req.context['odata'] = user_json_obj
Example #7
0
    def on_post(self, req, resp):
        """Authenticate against cluster-local user DB or against
        directory back-end (via LDAP, if configured).

        Create internal representation for remote user if it does not yet
        exist.

        This auto-populate concept is also used in:

            - http://www.roundup-tracker.org/cgi-bin/moin.cgi/LDAPLogin2
            - https://pythonhosted.org/django-auth-ldap/users.html

        Treat different combinations of local/remote user/service account
        and error out early.
        """
        login_params = self._validate_req_params(req, resp)

        if login_params.uid is None:

            # Rely on login_params.oidc_id_token to be set (that's a guarantee
            # `_validate_req_params()` has to give. It means initiation of an
            # OIDC ID token-based login (legacy for (open) DC/OS).
            assert login_params.oidc_id_token

            self._oidc_id_token_login(req, resp, login_params.oidc_id_token)

            # Make it explicit that the request handling must terminate here.
            return

        self.log.info('Trigger login procedure for uid `%s`', login_params.uid)

        try:
            user = User.get(login_params.uid)
        except bouncer.app.exceptions.EntityNotFound:
            if login_params.service_login_token is not None:
                # Do not fall back to an external username/password login system
                # for an attempted service account login.
                self._raise_local_nonauth_error()
            try:
                self._unknown_user_external_login_fallback(
                    req, resp, login_params)
            except AttributeError:
                # The `uid` is not known, and an AttributeError means that there
                # is no login fallback to an external system. Emit a 401
                # response. Note(JP): a cleaner plugin interface between this
                # auth module and and an external username/password login system
                # is required.
                self._raise_local_nonauth_error()
            # Terminate request processing after external username/password
            # login system fallback.
            return

        # Prepare expressive booleans to mitigate logic bugs.
        is_service = user.utype is UserType.service
        is_remote = user.is_remote
        local_regular_user = not is_service and not is_remote
        remote_user = not is_service and is_remote

        if is_service and login_params.service_login_token is None:
            # We know for a fact that this is a service user account, but the
            # request did not send a service login token along. Treat as bad
            # credentials.
            self._raise_local_nonauth_error()

        if remote_user:
            # POSTing credentials to the login endpoint plus known regular user
            # with `is_remote` set means: delegate the login to the external
            # login system.
            self.log.info(
                'User login: uid `%s` refers to a known remote user.',
                login_params.uid)
            self._external_login_user(req, resp, user, login_params)
            return

        elif local_regular_user:
            # Regular user account login.
            self.log.info('User login: uid `%s` refers to a known local user.',
                          login_params.uid)
            self._login_local_regular_user(req, resp, user, login_params)
            return

        elif is_service:
            self.log.info(
                'Service login: uid `%s` refers to a known service account.',
                login_params.uid)
            self._login_service(req, resp, user, login_params)
            return

        raise BouncerException("Unexpected account setup")
Example #8
0
    def _oidc_id_token_login(self, req, resp, oidc_id_token):

        # Hand off the ID Token validation business logic to the
        # `oidcidtokenlogin` module.
        issuer, email = oidcidtokenlogin.verify_id_token_or_terminate(
            req, resp, oidc_id_token)

        uid = sanitize_remote_uid(email)

        regular_user_count = dbsession.query(User).filter_by(
            utype=UserType.regular).count()

        if regular_user_count == 0:

            log.info('There is no regular user account yet. Create one.')

            # Add user to database. Rely on that we have just checked that no
            # user is there, i.e. a conflict is unexpected. Technically, there
            # is race condition and if a separate party was faster adding the
            # same user, `import_remote_user()` below could raise
            # `bouncer.app.exceptions.EntityExists`. In practice, that requires
            # the same user to log in multiple times via the external login
            # method on a sub-second timescale through different Bouncer
            # instances. Leave this unhandled (one request will succeed, the
            # others will see a 500 Internal Server Error response). Store
            # issuer as provider_id so that we keep record of which identity
            # provider precisely emitted the data.
            descr = f'User added through OIDC ID Token login. Issuer: {issuer}'
            user = import_remote_user(uid=uid,
                                      description=descr,
                                      provider_type=ProviderType.oidc,
                                      provider_id=issuer)

        else:
            try:
                user = User.get(uid)
            except bouncer.app.exceptions.EntityNotFound:
                log.info(
                    "I know %s user(s), but `%s` ain't one of them. Emit 401.",
                    regular_user_count, uid)
                # Note(JP): 403 is more appropriate because this is effectively
                # our coarse-grained authorization mechanism hitting in, but 401
                # I think should be maintained for legacy reasons.
                raise falcon.HTTPUnauthorized(
                    description='ID Token login failed: user unknown', )

            # Make sure that provider ID and type are matching. That is if a
            # user is known in the database with the same uid as presented by
            # the current ID Token but stemming from a different provider type
            # or from a different issuer than recorded in the database then
            # reject the login request.
            if user.provider_type != ProviderType.oidc:
                raise falcon.HTTPUnauthorized(
                    description='ID Token login failed: provider type mismatch',
                )
            if user.provider_id != issuer:
                raise falcon.HTTPUnauthorized(
                    description='ID Token login failed: provider ID mismatch',
                )

        generate_authtoken_json_response(crypt.generate_auth_token(user.uid),
                                         req, resp, user.uid, user.description)