Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
b243f16
Initialize api auth flow
ephraimbuddy Mar 25, 2021
f46b1e0
add oauth endpoint
ephraimbuddy Mar 25, 2021
c96319d
Add more auth flow and token refreshing
ephraimbuddy Mar 26, 2021
ed06c4a
Add tests for dblogin and ldaplogin
ephraimbuddy Mar 26, 2021
bf4f01f
test oauth login flow
ephraimbuddy Mar 26, 2021
0d09ec8
Fix test & minor fix in OpenApi yaml
kaxil Mar 26, 2021
746de47
add test for oauth authorize
ephraimbuddy Mar 26, 2021
165c618
fix ldap sign in
ephraimbuddy Mar 28, 2021
0151714
improve test for ldap
ephraimbuddy Mar 28, 2021
936e83f
More tests for authorize oauth
ephraimbuddy Mar 28, 2021
1fe3aea
add tests for remote user
ephraimbuddy Mar 29, 2021
c7d3435
Fix test and remove openid provision for now
ephraimbuddy Mar 29, 2021
b8caa9a
Add logout and fix incorrect user loading
ephraimbuddy Mar 29, 2021
73f5e36
add tests for logout
ephraimbuddy Mar 29, 2021
4871a38
remove jwt schema
ephraimbuddy Mar 29, 2021
0f69cd7
Apply suggestions from code review
ephraimbuddy Mar 30, 2021
5c74749
add token to response body
ephraimbuddy Mar 30, 2021
209ccfd
add token to response body
ephraimbuddy Apr 2, 2021
00e28e4
Return 401 for every auth failure
ephraimbuddy Apr 2, 2021
69a2c3e
Add blocklist model
ephraimbuddy Apr 2, 2021
041805c
add tokens table
ephraimbuddy Apr 6, 2021
0463660
Apply suggestion from review
ephraimbuddy Apr 6, 2021
4ab7cb0
add tests for token model
ephraimbuddy Apr 6, 2021
7a94db5
improve auth flow and tests
ephraimbuddy Apr 6, 2021
22a2d30
add token revoke endpoint and improve tests
ephraimbuddy Apr 6, 2021
21595c3
import from api-connnection utils
ephraimbuddy Apr 6, 2021
47509af
add object type to response object
ephraimbuddy Apr 6, 2021
b4eb910
Add bearer auth to security scheme, we can change to using cookie any…
ephraimbuddy Apr 7, 2021
9c85de1
Fix model doc and pylint
ephraimbuddy Apr 7, 2021
cb8a616
add words to spelling
ephraimbuddy Apr 7, 2021
0af79b3
fixup! add words to spelling
ephraimbuddy Apr 7, 2021
aa11314
Rename token table
ephraimbuddy Apr 7, 2021
88c30dd
fixup! Rename token table
ephraimbuddy Apr 7, 2021
f0f63c0
fix jwttoken model test
ephraimbuddy Apr 7, 2021
f4488f2
Fix migration
ephraimbuddy Apr 7, 2021
b5fe8aa
Do not save oauth_token in session
ephraimbuddy Apr 11, 2021
8009eea
Use token blocklist and add necessary endpoints
ephraimbuddy Apr 14, 2021
b6c2b26
Move some common code to security manager
ephraimbuddy Apr 17, 2021
54fc35b
change token table migration and fix table
ephraimbuddy Apr 17, 2021
924bea3
Apply suggestions from code review
ephraimbuddy Apr 21, 2021
a01cabd
Move auth_schema out of security manager and fix tests
ephraimbuddy Apr 21, 2021
92247fe
Removing check for if user already logged in, since we no longer set …
ephraimbuddy Apr 21, 2021
8ca6889
Allow storing token in header or cookie.
ephraimbuddy Apr 21, 2021
e71a685
fixup! Allow storing token in header or cookie.
ephraimbuddy Apr 21, 2021
fad8be2
fixup! fixup! Allow storing token in header or cookie.
ephraimbuddy Apr 21, 2021
09658c6
Make Oauth redirect happen inside the server
ephraimbuddy Apr 21, 2021
22223e4
fixup! Make Oauth redirect happen inside the server
ephraimbuddy Apr 22, 2021
9866f12
move some api related stuff from security manager
ephraimbuddy Apr 23, 2021
47e46cd
Drop support for token in cookie
ephraimbuddy Apr 23, 2021
da25fa1
fix test
ephraimbuddy Apr 23, 2021
7dcd433
fixup! fix test
ephraimbuddy Apr 23, 2021
a507911
add method to delete expired tokens
ephraimbuddy Apr 26, 2021
60988fb
Delete expired tokens at startup
ephraimbuddy Apr 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ repos:
pass_filenames: true
exclude: >
(?x)
^airflow/api_connexion/endpoints/auth_endpoint.py$|
^airflow/api_connexion/security.py$|
^airflow/providers/apache/cassandra/hooks/cassandra.py$|
^airflow/providers/apache/hive/operators/hive_stats.py$|
^airflow/providers/apache/hive/.*PROVIDER_CHANGES_*|
Expand Down
242 changes: 242 additions & 0 deletions airflow/api_connexion/endpoints/auth_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
import re

import jwt
from flask import current_app, jsonify, request
from flask_appbuilder.const import AUTH_DB, AUTH_LDAP, AUTH_OAUTH, AUTH_OID, AUTH_REMOTE_USER
from flask_jwt_extended import (
create_access_token,
decode_token,
get_jwt_identity,
get_raw_jwt,
jwt_refresh_token_required,
jwt_required,
)
from flask_login import login_user
from marshmallow import ValidationError
from sqlalchemy.exc import IntegrityError

from airflow.api_connexion.exceptions import BadRequest, Unauthenticated
from airflow.api_connexion.schemas.auth_schema import (
auth_schema,
info_schema,
login_form_schema,
token_schema,
)
from airflow.configuration import conf
from airflow.models.auth import TokenBlockList

log = logging.getLogger(__name__)


def get_auth_info():
"""Get site authentication info"""
security_manager = current_app.appbuilder.sm
config = current_app.config
auth_type = security_manager.auth_type
type_mapping = {
AUTH_DB: "auth_db",
AUTH_LDAP: "auth_ldap",
AUTH_OID: "auth_oid",
AUTH_OAUTH: "auth_oauth",
AUTH_REMOTE_USER: "auth_remote_user",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think type_mapping can be a constant & be out of this function.

oauth_providers = config.get("OAUTH_PROVIDERS", None)
openid_providers = config.get("OPENID_PROVIDERS", None)
return info_schema.dump(
{
"auth_type": type_mapping[auth_type],
"oauth_providers": oauth_providers,
"openid_providers": openid_providers,
}
)


def auth_login():
"""Handle DB login"""
appbuilder = current_app.appbuilder
body = request.json
try:
data = login_form_schema.load(body)
except ValidationError as err:
raise BadRequest(detail=str(err.messages))
security_manager = appbuilder.sm
if security_manager.auth_type not in [AUTH_DB, AUTH_LDAP]:
raise Unauthenticated(detail="Authentication type does not match")
user = security_manager.login_with_user_pass(data['username'], data['password'])
if not user:
raise Unauthenticated(detail="Invalid login")
login_user(user, remember=False)
return security_manager.create_tokens_and_dump(user, auth_schema)


def auth_oauthlogin(provider, register=None):
"""Authenticates user and redirect to callback endpoint"""
appbuilder = current_app.appbuilder
security_manager = appbuilder.sm
if security_manager.auth_type != AUTH_OAUTH:
raise Unauthenticated(detail="Authentication type does not match")
api_base_path = "/api/v1/"
base_url = conf.get("webserver", "base_url") + api_base_path
redirect_uri = base_url + "oauth-authorized?provider=" + provider

arg = {'register': [str(register)]} if register else {}
state = jwt.encode(
arg,
key=appbuilder.app.config['SECRET_KEY'],
algorithm='HS256',
)
auth_provider = security_manager.oauth_remotes[provider]
try:
if provider == "twitter":
redirect_uri = redirect_uri + f"&state={state}"
return auth_provider.authorize_redirect(redirect_uri=redirect_uri)
else:
state = state.decode("ascii") if isinstance(state, bytes) else state
return auth_provider.authorize_redirect(
redirect_uri=redirect_uri,
state=state,
)
except Exception as err: # pylint: disable=broad-except
log.error("Error on OAuth authorize: %s", err)
raise Unauthenticated(detail=str(err))


def authorize_oauth():
"""Callback to authorize Oauth."""
args = request.args
provider = args.get('provider', None)
state = args.get('state', None)
if not (provider and state):
raise BadRequest(detail="Missing required parameters: provider and state")
appbuilder = current_app.appbuilder
security_manager = appbuilder.sm
log.debug("Authorized init")
resp = security_manager.oauth_remotes[provider].authorize_access_token()
if resp is None:
raise Unauthenticated(detail="You denied the request to sign in")
# Verify state
try:
jwt.decode(
state,
key=appbuilder.app.config['SECRET_KEY'],
algorithms='HS256',
)
except jwt.InvalidTokenError:
raise BadRequest(detail="State signature is not valid!")
log.debug("OAUTH Authorized resp: %s", resp)
# Retrieves specific user info from the provider
try: # pylint: disable=too-many-nested-blocks
userinfo = security_manager.oauth_user_info(provider, resp)
except Exception as err: # pylint: disable=broad-except
log.debug("Error retrieving user info. Error =>%s", err)
user = None
else:
log.debug("User info retrieved from %s: %s", provider, userinfo)
# User email is not whitelisted
if provider in security_manager.oauth_whitelists:
whitelist = security_manager.oauth_whitelists[provider]
allow = False
for e in whitelist:
if re.search(e, userinfo["email"]):
allow = True
break
if not allow:
raise Unauthenticated(detail="You are not authorized.")
else:
log.debug("No allow list for OAuth provider")
user = security_manager.auth_user_oauth(userinfo)
log.debug("User is %s", user)
if user is None:
raise Unauthenticated(detail="Invalid login")
login_user(user, remember=False)
return security_manager.create_tokens_and_dump(user, auth_schema)


def auth_remoteuser():
"""Handle remote user auth"""
appbuilder = current_app.appbuilder
security_manager = appbuilder.sm
if security_manager.auth_type != AUTH_REMOTE_USER:
raise Unauthenticated(detail="Authentication type does not match")
username = request.environ.get("REMOTE_USER")
if not username:
raise Unauthenticated(detail="Invalid login")
user = security_manager.auth_user_remote_user(username)
if user is None:
raise Unauthenticated(detail="Invalid login")
login_user(user, remember=False)
return security_manager.create_tokens_and_dump(user, auth_schema)


@jwt_refresh_token_required
def refresh_token():
"""Refresh token"""
user = get_jwt_identity()
access_token = create_access_token(identity=user)
return {'access_token': access_token}


@jwt_required
def revoke_token():
"""
An endpoint for revoking both access and refresh token.

This is intended for a case where a logged in user want to revoke
another user's tokens
"""
body = request.json
try:
data = token_schema.load(body)
except ValidationError as err:
raise BadRequest(detail=str(err.messages))
token = decode_token(data['token'])

@msumit msumit Aug 4, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't understand the logic here or maybe I misinterpreted it. How is a user going to provide the token of another user?

try:
TokenBlockList.add_token(jti=token['jti'], expiry_delta=token['exp'])
except IntegrityError:
pass
return jsonify({"revoked": True})


@jwt_required
def revoke_current_user_token():
"""
An endpoint for revoking current user access token
This should be called during current user logout
"""
raw_jwt = get_raw_jwt()
try:
TokenBlockList.add_token(jti=raw_jwt['jti'], expiry_delta=raw_jwt['exp'])
except IntegrityError:
pass
return jsonify({"revoked": True})


@jwt_refresh_token_required
def revoke_current_user_refresh_token():
"""
An endpoint for revoking current user refresh token
This should be called during current user logout
"""
raw_jwt = get_raw_jwt()
try:
TokenBlockList.add_token(jti=raw_jwt['jti'], expiry_delta=raw_jwt['exp'])
except IntegrityError:
pass
return jsonify({"revoked": True})
Loading