import hashlib
import hmac
import logging
import time
from urlparse import urlsplit, urlunsplit

from flask import jsonify, redirect, request, url_for
from flask_login import LoginManager, login_user, logout_user, user_logged_in
from redash import models, settings
from redash.authentication import jwt_auth
from redash.authentication.org_resolving import current_org
from redash.settings.organization import settings as org_settings
from redash.tasks import record_event
from sqlalchemy.orm.exc import NoResultFound
from werkzeug.exceptions import Unauthorized

login_manager = LoginManager()
logger = logging.getLogger('authentication')


def get_login_url(external=False, next="/"):
    if settings.MULTI_ORG and current_org == None:
        login_url = '/'
    elif settings.MULTI_ORG:
        login_url = url_for('redash.login', org_slug=current_org.slug, next=next, _external=external)
    else:
        login_url = url_for('redash.login', next=next, _external=external)

    return login_url


def sign(key, path, expires):
    if not key:
        return None

    h = hmac.new(str(key), msg=path, digestmod=hashlib.sha1)
    h.update(str(expires))

    return h.hexdigest()


@login_manager.user_loader
def load_user(user_id_with_identity):
    org = current_org._get_current_object()

    try:
        user_id, _ = user_id_with_identity.split("-")
        user = models.User.get_by_id_and_org(user_id, org)
        if user.is_disabled or user.get_id() != user_id_with_identity:
            return None

        return user
    except (models.NoResultFound, ValueError, AttributeError):
        return None


def request_loader(request):
    user = None
    if settings.AUTH_TYPE == 'hmac':
        user = hmac_load_user_from_request(request)
    elif settings.AUTH_TYPE == 'api_key':
        user = api_key_load_user_from_request(request)
    else:
        logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
        user = hmac_load_user_from_request(request)

    if org_settings['auth_jwt_login_enabled'] and user is None:
        user = jwt_token_load_user_from_request(request)
    return user


def hmac_load_user_from_request(request):
    signature = request.args.get('signature')
    expires = float(request.args.get('expires') or 0)
    query_id = request.view_args.get('query_id', None)
    user_id = request.args.get('user_id', None)

    # TODO: 3600 should be a setting
    if signature and time.time() < expires <= time.time() + 3600:
        if user_id:
            user = models.User.query.get(user_id)
            calculated_signature = sign(user.api_key, request.path, expires)

            if user.api_key and signature == calculated_signature:
                return user

        if query_id:
            query = models.Query.query.filter(models.Query.id == query_id).one()
            calculated_signature = sign(query.api_key, request.path, expires)

            if query.api_key and signature == calculated_signature:
                return models.ApiUser(query.api_key, query.org, query.groups.keys(), name="ApiKey: Query {}".format(query.id))

    return None


def get_user_from_api_key(api_key, query_id):
    if not api_key:
        return None

    user = None

    # TODO: once we switch all api key storage into the ApiKey model, this code will be much simplified
    org = current_org._get_current_object()
    try:
        user = models.User.get_by_api_key_and_org(api_key, org)
        if user.is_disabled:
            user = None
    except models.NoResultFound:
        try:
            api_key = models.ApiKey.get_by_api_key(api_key)
            user = models.ApiUser(api_key, api_key.org, [])
        except models.NoResultFound:
            if query_id:
                query = models.Query.get_by_id_and_org(query_id, org)
                if query and query.api_key == api_key:
                    user = models.ApiUser(api_key, query.org, query.groups.keys(), name="ApiKey: Query {}".format(query.id))

    return user


def get_api_key_from_request(request):
    api_key = request.args.get('api_key', None)

    if api_key is not None:
        return api_key

    if request.headers.get('Authorization'):
        auth_header = request.headers.get('Authorization')
        api_key = auth_header.replace('Key ', '', 1)
    elif request.view_args is not None and request.view_args.get('token'):
        api_key = request.view_args['token']

    return api_key


def api_key_load_user_from_request(request):
    api_key = get_api_key_from_request(request)
    if request.view_args is not None:
        query_id = request.view_args.get('query_id', None)
        user = get_user_from_api_key(api_key, query_id)
    else:
        user = None

    return user


def jwt_token_load_user_from_request(request):
    org = current_org._get_current_object()

    payload = None

    if org_settings['auth_jwt_auth_cookie_name']:
        jwt_token = request.cookies.get(org_settings['auth_jwt_auth_cookie_name'], None)
    elif org_settings['auth_jwt_auth_header_name']:
        jwt_token = request.headers.get(org_settings['auth_jwt_auth_header_name'], None)
    else:
        return None

    if jwt_token:
        payload, token_is_valid = jwt_auth.verify_jwt_token(
            jwt_token,
            expected_issuer=org_settings['auth_jwt_auth_issuer'],
            expected_audience=org_settings['auth_jwt_auth_audience'],
            algorithms=org_settings['auth_jwt_auth_algorithms'],
            public_certs_url=org_settings['auth_jwt_auth_public_certs_url'],
        )
        if not token_is_valid:
            raise Unauthorized('Invalid JWT token')

    if not payload:
        return

    try:
        user = models.User.get_by_email_and_org(payload['email'], org)
    except models.NoResultFound:
        user = create_and_login_user(current_org, payload['email'], payload['email'])

    return user


def log_user_logged_in(app, user):
    event = {
        'org_id': user.org_id,
        'user_id': user.id,
        'action': 'login',
        'object_type': 'redash',
        'timestamp': int(time.time()),
        'user_agent': request.user_agent.string,
        'ip': request.remote_addr
    }

    record_event.delay(event)


@login_manager.unauthorized_handler
def redirect_to_login():
    if request.is_xhr or '/api/' in request.path:
        response = jsonify({'message': "Couldn't find resource. Please login and try again."})
        response.status_code = 404
        return response

    login_url = get_login_url(next=request.url, external=False)

    return redirect(login_url)


def logout_and_redirect_to_index():
    logout_user()

    if settings.MULTI_ORG and current_org == None:
        index_url = '/'
    elif settings.MULTI_ORG:
        index_url = url_for('redash.index', org_slug=current_org.slug, _external=False)
    else:
        index_url = url_for('redash.index', _external=False)

    return redirect(index_url)


def init_app(app):
    from redash.authentication import google_oauth, saml_auth, remote_user_auth, ldap_auth

    login_manager.init_app(app)
    login_manager.anonymous_user = models.AnonymousUser

    app.register_blueprint(google_oauth.blueprint)
    app.register_blueprint(saml_auth.blueprint)
    app.register_blueprint(remote_user_auth.blueprint)
    app.register_blueprint(ldap_auth.blueprint)

    user_logged_in.connect(log_user_logged_in)
    login_manager.request_loader(request_loader)


def create_and_login_user(org, name, email, picture=None):
    try:
        user_object = models.User.get_by_email_and_org(email, org)
        if user_object.is_disabled:
            return None
        if user_object.is_invitation_pending:
            user_object.is_invitation_pending = False
            models.db.session.commit()
        if user_object.name != name:
            logger.debug("Updating user name (%r -> %r)", user_object.name, name)
            user_object.name = name
            models.db.session.commit()
    except NoResultFound:
        logger.debug("Creating user object (%r)", name)
        user_object = models.User(org=org, name=name, email=email, is_invitation_pending=False,
                                  _profile_image_url=picture, group_ids=[org.default_group.id])
        models.db.session.add(user_object)
        models.db.session.commit()

    login_user(user_object, remember=True)

    return user_object


def get_next_path(unsafe_next_path):
    if not unsafe_next_path:
        return ''

    # Preventing open redirection attacks
    parts = list(urlsplit(unsafe_next_path))
    parts[0] = ''  # clear scheme
    parts[1] = ''  # clear netloc
    safe_next_path = urlunsplit(parts)

    return safe_next_path

redash/authentication/__init__.py
import hashlib
import hmac
import logging
import time
from urlparse import urlsplit, urlunsplit

from flask import jsonify, redirect, request, url_for
from flask_login import LoginManager, login_user, logout_user, user_logged_in
from redash import models, settings
from redash.authentication import jwt_auth
from redash.authentication.org_resolving import current_org
from redash.settings.organization import settings as org_settings
from redash.tasks import record_event
from sqlalchemy.orm.exc import NoResultFound
from werkzeug.exceptions import Unauthorized

login_manager = LoginManager()
logger = logging.getLogger('authentication')


def get_login_url(external=False, next="/"):
    if settings.MULTI_ORG and current_org == None:
        login_url = '/'
    elif settings.MULTI_ORG:
        login_url = url_for('redash.login', org_slug=current_org.slug, next=next, _external=external)
    else:
        login_url = url_for('redash.login', next=next, _external=external)

    return login_url


def sign(key, path, expires):
    if not key:
        return None

    h = hmac.new(str(key), msg=path, digestmod=hashlib.sha1)
    h.update(str(expires))

    return h.hexdigest()


@login_manager.user_loader
def load_user(user_id_with_identity):
    user = api_key_load_user_from_request(request)
    if user:
        return user

    org = current_org._get_current_object()

    try:
        user_id, _ = user_id_with_identity.split("-")
        user = models.User.get_by_id_and_org(user_id, org)
        if user.is_disabled or user.get_id() != user_id_with_identity:
            return None

        return user
    except (models.NoResultFound, ValueError, AttributeError):
        return None


def request_loader(request):
    user = None
    if settings.AUTH_TYPE == 'hmac':
        user = hmac_load_user_from_request(request)
    elif settings.AUTH_TYPE == 'api_key':
        user = api_key_load_user_from_request(request)
    else:
        logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
        user = hmac_load_user_from_request(request)

    if org_settings['auth_jwt_login_enabled'] and user is None:
        user = jwt_token_load_user_from_request(request)
    return user


def hmac_load_user_from_request(request):
    signature = request.args.get('signature')
    expires = float(request.args.get('expires') or 0)
    query_id = request.view_args.get('query_id', None)
    user_id = request.args.get('user_id', None)

    # TODO: 3600 should be a setting
    if signature and time.time() < expires <= time.time() + 3600:
        if user_id:
            user = models.User.query.get(user_id)
            calculated_signature = sign(user.api_key, request.path, expires)

            if user.api_key and signature == calculated_signature:
                return user

        if query_id:
            query = models.Query.query.filter(models.Query.id == query_id).one()
            calculated_signature = sign(query.api_key, request.path, expires)

            if query.api_key and signature == calculated_signature:
                return models.ApiUser(query.api_key, query.org, query.groups.keys(), name="ApiKey: Query {}".format(query.id))

    return None


def get_user_from_api_key(api_key, query_id):
    if not api_key:
        return None

    user = None

    # TODO: once we switch all api key storage into the ApiKey model, this code will be much simplified
    org = current_org._get_current_object()
    try:
        user = models.User.get_by_api_key_and_org(api_key, org)
        if user.is_disabled:
            user = None
    except models.NoResultFound:
        try:
            api_key = models.ApiKey.get_by_api_key(api_key)
            user = models.ApiUser(api_key, api_key.org, [])
        except models.NoResultFound:
            if query_id:
                query = models.Query.get_by_id_and_org(query_id, org)
                if query and query.api_key == api_key:
                    user = models.ApiUser(api_key, query.org, query.groups.keys(), name="ApiKey: Query {}".format(query.id))

    return user


def get_api_key_from_request(request):
    api_key = request.args.get('api_key', None)

    if api_key is not None:
        return api_key

    if request.headers.get('Authorization'):
        auth_header = request.headers.get('Authorization')
        api_key = auth_header.replace('Key ', '', 1)
    elif request.view_args is not None and request.view_args.get('token'):
        api_key = request.view_args['token']

    return api_key


def api_key_load_user_from_request(request):
    api_key = get_api_key_from_request(request)
    if request.view_args is not None:
        query_id = request.view_args.get('query_id', None)
        user = get_user_from_api_key(api_key, query_id)
    else:
        user = None

    return user


def jwt_token_load_user_from_request(request):
    org = current_org._get_current_object()

    payload = None

    if org_settings['auth_jwt_auth_cookie_name']:
        jwt_token = request.cookies.get(org_settings['auth_jwt_auth_cookie_name'], None)
    elif org_settings['auth_jwt_auth_header_name']:
        jwt_token = request.headers.get(org_settings['auth_jwt_auth_header_name'], None)
    else:
        return None

    if jwt_token:
        payload, token_is_valid = jwt_auth.verify_jwt_token(
            jwt_token,
            expected_issuer=org_settings['auth_jwt_auth_issuer'],
            expected_audience=org_settings['auth_jwt_auth_audience'],
            algorithms=org_settings['auth_jwt_auth_algorithms'],
            public_certs_url=org_settings['auth_jwt_auth_public_certs_url'],
        )
        if not token_is_valid:
            raise Unauthorized('Invalid JWT token')

    if not payload:
        return

    try:
        user = models.User.get_by_email_and_org(payload['email'], org)
    except models.NoResultFound:
        user = create_and_login_user(current_org, payload['email'], payload['email'])

    return user


def log_user_logged_in(app, user):
    event = {
        'org_id': user.org_id,
        'user_id': user.id,
        'action': 'login',
        'object_type': 'redash',
        'timestamp': int(time.time()),
        'user_agent': request.user_agent.string,
        'ip': request.remote_addr
    }

    record_event.delay(event)


@login_manager.unauthorized_handler
def redirect_to_login():
    if request.is_xhr or '/api/' in request.path:
        response = jsonify({'message': "Couldn't find resource. Please login and try again."})
        response.status_code = 404
        return response

    login_url = get_login_url(next=request.url, external=False)

    return redirect(login_url)


def logout_and_redirect_to_index():
    logout_user()

    if settings.MULTI_ORG and current_org == None:
        index_url = '/'
    elif settings.MULTI_ORG:
        index_url = url_for('redash.index', org_slug=current_org.slug, _external=False)
    else:
        index_url = url_for('redash.index', _external=False)

    return redirect(index_url)


def init_app(app):
    from redash.authentication import google_oauth, saml_auth, remote_user_auth, ldap_auth

    login_manager.init_app(app)
    login_manager.anonymous_user = models.AnonymousUser

    app.register_blueprint(google_oauth.blueprint)
    app.register_blueprint(saml_auth.blueprint)
    app.register_blueprint(remote_user_auth.blueprint)
    app.register_blueprint(ldap_auth.blueprint)

    user_logged_in.connect(log_user_logged_in)
    login_manager.request_loader(request_loader)


def create_and_login_user(org, name, email, picture=None):
    try:
        user_object = models.User.get_by_email_and_org(email, org)
        if user_object.is_disabled:
            return None
        if user_object.is_invitation_pending:
            user_object.is_invitation_pending = False
            models.db.session.commit()
        if user_object.name != name:
            logger.debug("Updating user name (%r -> %r)", user_object.name, name)
            user_object.name = name
            models.db.session.commit()
    except NoResultFound:
        logger.debug("Creating user object (%r)", name)
        user_object = models.User(org=org, name=name, email=email, is_invitation_pending=False,
                                  _profile_image_url=picture, group_ids=[org.default_group.id])
        models.db.session.add(user_object)
        models.db.session.commit()

    login_user(user_object, remember=True)

    return user_object


def get_next_path(unsafe_next_path):
    if not unsafe_next_path:
        return ''

    # Preventing open redirection attacks
    parts = list(urlsplit(unsafe_next_path))
    parts[0] = ''  # clear scheme
    parts[1] = ''  # clear netloc
    safe_next_path = urlunsplit(parts)

    return safe_next_path

redash/authentication/__init__.py
diff --git a/redash/authentication/__init__.py b/redash/authentication/__init__.py
index 04af7a29ac..989ed52b11 100644
--- a/redash/authentication/__init__.py
+++ b/redash/authentication/__init__.py
@@ -41,6 +41,10 @@ def sign(key, path, expires):
 
 @login_manager.user_loader
 def load_user(user_id_with_identity):
+    user = api_key_load_user_from_request(request)
+    if user:
+        return user
+
     org = current_org._get_current_object()
 
     try:
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index 3cf1d8b1f2..192bb53316 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -127,6 +127,19 @@ def test_user_api_key(self):
             self.assertEqual(user.id, hmac_load_user_from_request(request).id)
 
 
+class TestSessionAuthentication(BaseTestCase):
+    def test_prefers_api_key_over_session_user_id(self):
+        user = self.factory.create_user()
+        query = self.factory.create_query(user=user)
+
+        other_org = self.factory.create_org()
+        other_user = self.factory.create_user(org=other_org)
+        models.db.session.flush()
+
+        rv = self.make_request('get', '/api/queries/{}?api_key={}'.format(query.id, query.api_key), user=other_user)
+        self.assertEqual(rv.status_code, 200)
+
+
 class TestCreateAndLoginUser(BaseTestCase):
     def test_logins_valid_user(self):
         user = self.factory.create_user(email=u'test@example.com')

API Key authentication should take precedence over cookies
Currently when you're logged in as a user, you can't use embeds or shared dashboards for which you do not have access -- even if you provide the API key. This is because the current user is being defined by the session cookie and the API key is being ignored.

We need to change this behavior so the API key takes precedence. From a quick look at [Flask-Login](https://flask-login.readthedocs.io)'s documentation, it seems that it calls `user_loader` first and only if this fails will try the `requets_loader`.

Unless this is configurable, we might need to change `user_loader` to always return `None` and implement our own logic in `request_loader`.
