Exemplo n.º 1
0
    def auto_auth_check(self):
        apikey = request.environ.get('HTTP_X_APIKEY', None)
        uname = request.environ.get('HTTP_X_USER', None)

        if apikey is not None and uname is not None:
            ip = request.headers.get("X-Forwarded-For", request.remote_addr)
            with elasticapm.capture_span(name="@api_login:auto_auth_check()",
                                         span_type="authentication"):
                try:
                    # TODO: apikey_handler is slow to verify the password (bcrypt's fault)
                    #       We could fix this by saving the hash of the combinaison of the
                    #       APIkey and the username in an ExpiringSet and looking it up for
                    #       sub-sequent calls...
                    validated_user, priv = validate_apikey(
                        uname, apikey, STORAGE)
                except AuthenticationException:
                    msg = "Invalid user or APIKey"
                    LOGGER.warning(
                        f"Authentication failure. (U:{uname} - IP:{ip}) [{msg}]"
                    )
                    abort(401, msg)
                    return

                if validated_user:
                    LOGGER.info(f"Login successful. (U:{uname} - IP:{ip})")
                    if not set(self.required_priv).intersection(set(priv)):
                        abort(
                            403,
                            "The method you've used to login does not give you access to this API"
                        )
                        return

                    return validated_user

        return None
Exemplo n.º 2
0
def check_submission_quota(user, num=1) -> Optional[str]:
    quota_user = user['uname']
    quota = user.get('submission_quota', 5)
    count = num + Hash('submissions-' + quota_user, **persistent).length()
    if count > quota:
        LOGGER.info("User %s exceeded their submission quota. [%s/%s]",
                    quota_user, count, quota)
        return "You've exceeded your maximum submission quota of %s " % quota
    return None
Exemplo n.º 3
0
def check_submission_quota(user) -> Optional[str]:
    quota_user = user['uname']
    max_quota = user.get('submission_quota', 5)

    if config.ui.enforce_quota and not SUBMISSION_TRACKER.begin(
            quota_user, max_quota):
        LOGGER.info("User %s exceeded their submission quota of %s.",
                    quota_user, max_quota)
        return "You've exceeded your maximum submission quota of %s " % max_quota
    return None
Exemplo n.º 4
0
        def base(*args, **kwargs):
            if 'user' in kwargs:
                if kwargs['user'].get('authenticated', False):
                    return func(*args, **kwargs)
                else:
                    abort(403, "Invalid pre-authenticated user")
                    return

            self.test_readonly("API")
            logged_in_uname = self.get_logged_in_user()
            impersonator = None

            # Impersonate
            authorization = request.environ.get("HTTP_AUTHORIZATION", None)
            if authorization:
                # noinspection PyBroadException
                try:
                    bearer_token = authorization.split(" ")[-1]
                    headers = jwt.get_unverified_header(bearer_token)
                    decoded = jwt.decode(
                        bearer_token,
                        hashlib.sha256(f"{SECRET_KEY}_{headers['token_id']}".
                                       encode()).hexdigest(),
                        algorithms=[headers.get('alg', "HS256")])
                except Exception:
                    abort(400, "Malformed bearer token")
                    return

                target_user = STORAGE.user.get(headers['user'], as_obj=False)
                if target_user:
                    target_token = target_user.get('apps', {}).get(
                        headers['token_id'], {})
                    if target_token == decoded and target_token[
                            'client_id'] == logged_in_uname:
                        impersonator = logged_in_uname
                        logged_in_uname = headers['user']
                        LOGGER.info(
                            f"{impersonator} is impersonating {logged_in_uname} for query: {request.path}"
                        )

                        if not set(self.required_priv).intersection(
                                set(SCOPES[decoded["scope"]])):
                            abort(
                                403,
                                "The method you've used to login does not give you access to this API"
                            )
                            return
                    else:
                        abort(403, "Invalid bearer token")
                        return
                else:
                    abort(404, "User not found")
                    return

            user = login(logged_in_uname)

            # Terms of Service
            if request.path not in ["/api/v4/help/tos/", "/api/v4/user/whoami/",
                                    f"/api/v4/user/tos/{logged_in_uname}/",
                                    "/api/v4/auth/logout/"] \
                    and not user.get('agrees_with_tos', False) and config.ui.tos is not None:
                abort(
                    403,
                    "Agree to Terms of Service before you can make any API calls"
                )
                return

            self.test_require_type(user, "API")

            #############################################
            # Special username api query validation
            #
            #    If an API call requests a username, the username as to match
            #    the logged in user or the user has to be ADMIN
            #
            #    API that needs this special validation need to make sure their
            #    variable name for the username is as an optional parameter
            #    inside 'username_key'. Default: 'username'
            if self.username_key in kwargs:
                if kwargs[self.username_key] != user['uname'] \
                        and not kwargs[self.username_key] == "__global__" \
                        and not kwargs[self.username_key] == "__workflow__" \
                        and not kwargs[self.username_key].lower() == "__current__" \
                        and 'admin' not in user['type']:
                    return make_api_response(
                        {}, "Your username does not match requested username",
                        403)

            self.audit_if_required(args,
                                   kwargs,
                                   logged_in_uname,
                                   user,
                                   func,
                                   impersonator=impersonator)

            # Save user credential in user kwarg for future reference
            kwargs['user'] = user

            if config.core.metrics.apm_server.server_url is not None:
                elasticapm.set_user_context(username=user.get('name', None),
                                            email=user.get('email', None),
                                            user_id=user.get('uname', None))

            # Check current user quota
            quota_user = user['uname']

            flsk_session['quota_user'] = quota_user
            flsk_session['quota_set'] = True

            quota = user.get('api_quota', 10)
            if not QUOTA_TRACKER.begin(quota_user, quota):
                if config.ui.enforce_quota:
                    LOGGER.info(
                        f"User {quota_user} was prevented from using the api due to exceeded quota."
                    )
                    return make_api_response(
                        "", f"You've exceeded your maximum quota of {quota}",
                        503)
                else:
                    LOGGER.debug(
                        f"Quota of {quota} exceeded for user {quota_user}.")
            else:
                LOGGER.debug(
                    f"{quota_user}'s quota is under or equal its limit of {quota}"
                )

            return func(*args, **kwargs)
Exemplo n.º 5
0
def login(**_):
    """
    Login the user onto the system
    
    Variables:
    None
    
    Arguments: 
    None
    
    Data Block:
    {
     "user": <UID>,
     "password": <ENCRYPTED_PASSWORD>,
     "otp": <OTP_TOKEN>,
     "apikey": <ENCRYPTED_APIKEY>,
     "webauthn_auth_resp": <RESPONSE_TO_CHALLENGE_FROM_WEBAUTHN>
    }

    Result example:
    {
     "username": <Logged in user>, # Username for the logged in user
     "privileges": ["R", "W"],     # Different privileges that the user will get for this session
     "session_duration": 60        # Time after which this session becomes invalid
                                   #   Note: The timer reset after each call
    }
    """
    data = request.json
    if not data:
        data = request.values

    user = data.get('user', None)
    password = data.get('password', None)
    apikey = data.get('apikey', None)
    webauthn_auth_resp = data.get('webauthn_auth_resp', None)
    oauth_provider = data.get('oauth_provider', None)
    oauth_token = data.get('oauth_token', None)

    if config.auth.oauth.enabled and oauth_provider:
        oauth = current_app.extensions.get('authlib.integrations.flask_client')
        provider = oauth.create_client(oauth_provider)

        if provider:
            redirect_uri = f'https://{request.host}/login.html?provider={oauth_provider}'
            return provider.authorize_redirect(redirect_uri=redirect_uri)

    try:
        otp = int(data.get('otp', 0) or 0)
    except Exception:
        raise AuthenticationException('Invalid OTP token')

    if (user and password) or (user and apikey) or (user and oauth_token):
        auth = {
            'username': user,
            'password': password,
            'otp': otp,
            'webauthn_auth_resp': webauthn_auth_resp,
            'apikey': apikey,
            'oauth_token': oauth_token
        }

        logged_in_uname = None
        ip = request.headers.get("X-Forwarded-For", request.remote_addr)
        try:
            logged_in_uname, priv = default_authenticator(
                auth, request, flsk_session, STORAGE)
            session_duration = config.ui.session_duration
            cur_time = now()
            xsrf_token = generate_random_secret()
            current_session = {
                'duration': session_duration,
                'ip': ip,
                'privileges': priv,
                'time': int(cur_time) - (int(cur_time) % session_duration),
                'user_agent': request.headers.get("User-Agent", None),
                'username': logged_in_uname,
                'xsrf_token': xsrf_token
            }
            session_id = hashlib.sha512(
                str(current_session).encode("UTF-8")).hexdigest()
            current_session['expire_at'] = cur_time + session_duration
            flsk_session['session_id'] = session_id
            KV_SESSION.add(session_id, current_session)
            return make_api_response(
                {
                    "username": logged_in_uname,
                    "privileges": priv,
                    "session_duration": session_duration
                },
                cookies={'XSRF-TOKEN': xsrf_token})
        except AuthenticationException as wpe:
            uname = auth.get('username', '(None)')
            LOGGER.warning(
                f"Authentication failure. (U:{uname} - IP:{ip}) [{wpe}]")
            return make_api_response("", err=str(wpe), status_code=401)
        finally:
            if logged_in_uname:
                LOGGER.info(
                    f"Login successful. (U:{logged_in_uname} - IP:{ip})")

    return make_api_response(
        "", "Not enough information to proceed with authentication", 401)
Exemplo n.º 6
0
        def base(*args, **kwargs):
            if 'user' in kwargs:
                if kwargs['user'].get('authenticated', False):
                    return func(*args, **kwargs)
                else:
                    abort(403, "Invalid pre-authenticated user")
                    return

            self.test_readonly("API")
            logged_in_uname = self.get_logged_in_user()

            user = login(logged_in_uname)

            # Terms of Service
            if request.path not in ["/api/v4/help/tos/", "/api/v4/user/whoami/",
                                    f"/api/v4/user/tos/{logged_in_uname}/",
                                    "/api/v4/auth/logout/"] \
                    and not user.get('agrees_with_tos', False) and config.ui.tos is not None:
                abort(
                    403,
                    "Agree to Terms of Service before you can make any API calls"
                )
                return

            self.test_require_type(user, "API")

            #############################################
            # Special username api query validation
            #
            #    If an API call requests a username, the username as to match
            #    the logged in user or the user has to be ADMIN
            #
            #    API that needs this special validation need to make sure their
            #    variable name for the username is as an optional parameter
            #    inside 'username_key'. Default: 'username'
            if self.username_key in kwargs:
                if kwargs[self.username_key] != user['uname'] \
                        and not kwargs[self.username_key] == "__global__" \
                        and not kwargs[self.username_key] == "__workflow__" \
                        and not kwargs[self.username_key].lower() == "__current__" \
                        and 'admin' not in user['type']:
                    return make_api_response(
                        {}, "Your username does not match requested username",
                        403)

            self.audit_if_required(args, kwargs, logged_in_uname, user, func)

            # Save user credential in user kwarg for future reference
            kwargs['user'] = user

            if config.core.metrics.apm_server.server_url is not None:
                elasticapm.set_user_context(username=user.get('name', None),
                                            email=user.get('email', None),
                                            user_id=user.get('uname', None))

            # Check current user quota
            quota_user = user['uname']

            flsk_session['quota_user'] = quota_user
            flsk_session['quota_set'] = True

            quota = user.get('api_quota', 10)
            if not QUOTA_TRACKER.begin(quota_user, quota):
                if config.ui.enforce_quota:
                    LOGGER.info(
                        f"User {quota_user} was prevented from using the api due to exceeded quota."
                    )
                    return make_api_response(
                        "", f"You've exceeded your maximum quota of {quota}",
                        503)
                else:
                    LOGGER.debug(
                        f"Quota of {quota} exceeded for user {quota_user}.")
            else:
                LOGGER.debug(
                    f"{quota_user}'s quota is under or equal its limit of {quota}"
                )

            return func(*args, **kwargs)