Skip to content

Commit

Permalink
Migrate social_auth to authlib
Browse files Browse the repository at this point in the history
  • Loading branch information
LenkaSeg authored and Zlopez committed Nov 22, 2024
1 parent d34f306 commit 6702b3c
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 46 deletions.
54 changes: 26 additions & 28 deletions anitya/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@

import flask
from flask_login import LoginManager, current_user, user_logged_in
from social_core.backends.utils import load_backends
from social_core.exceptions import AuthException
from social_flask.routes import social_auth
from social_flask_sqlalchemy import models as social_models
from flask import url_for, render_template
from authlib.integrations.flask_client import OAuth
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import NoResultFound

<<<<<<< HEAD

Check failure

Code scanning / CodeQL

Syntax error Error

Syntax Error (in Python 3).
=======
from anitya.config import config as anitya_config
from anitya.db import Session, initialize as initialize_db, models
from anitya.lib import utilities
from . import ui, admin, api, api_v2, authentication, auth
>>>>>>> ffbba79 (Migrate social_auth to authlib)
import anitya.lib
import anitya.mail_logging
from anitya import __version__
Expand Down Expand Up @@ -54,19 +59,6 @@ def create(config=None):
app.config.update(config)
initialize_db(config)

app.register_blueprint(social_auth)
if len(social_models.UserSocialAuth.__table_args__) == 0:
# This is a bit of a hack - this initialization call sets up the SQLAlchemy
# models with our own models and multiple calls to this function will cause
# SQLAlchemy to fail with sqlalchemy.exc.InvalidRequestError. Only calling it
# when there are no table arguments should ensure we only call it one time.
#
# Be aware that altering the configuration values this function uses, namely
# the SOCIAL_AUTH_USER_MODEL, after the first time ``create`` has been called
# will *not* cause the new configuration to be used for subsequent calls to
# ``create``.
social_models.init_social(app, Session)

login_manager = LoginManager()
login_manager.user_loader(authentication.load_user_from_session)
login_manager.request_loader(authentication.load_user_from_request)
Expand All @@ -91,10 +83,17 @@ def create(config=None):
app.register_blueprint(ui.ui_blueprint)
app.register_blueprint(api.api_blueprint)

oauth = OAuth(app)
for auth_backend in app.config.get("AUTHLIB_ENABLED_BACKENDS", []):
oauth.register(auth_backend.lower())

app.register_blueprint(auth.create_oauth_blueprint(oauth))

app.before_request(global_user)
app.teardown_request(shutdown_session)
app.register_error_handler(IntegrityError, integrity_error_handler)
app.register_error_handler(AuthException, auth_error_handler)
# TODO: Need to change for authlib
#app.register_error_handler(AuthException, auth_error_handler)

app.context_processor(inject_variable)

Expand Down Expand Up @@ -141,9 +140,7 @@ def inject_variable():
justedit=justedit,
cron_status=cron_status,
user=current_user,
available_backends=load_backends(
anitya_config["SOCIAL_AUTH_AUTHENTICATION_BACKENDS"]
),
available_backends=anitya_config["AUTHLIB_ENABLED_BACKENDS"],
)


Expand All @@ -164,7 +161,7 @@ def integrity_error_handler(error):
Session.rollback()
other_user = models.User.query.filter_by(email=error.params["email"]).one()
try:
social_auth_user = other_user.social_auth.filter_by(
social_auth_user = other_user.oauth.filter_by(
user_id=other_user.id
).one()
msg = (
Expand Down Expand Up @@ -219,9 +216,10 @@ def when_user_log_in(sender, user):
sqlalchemy.exc.IntegrityError: When user_social_auth table entry is
missing.
"""
if user.social_auth.count() == 0:
raise IntegrityError(
"Missing social_auth table",
{"social_auth": None, "email": user.email},
None,
)
# TODO: new social table need to be added
#if user.oauth.count() == 0:
# raise IntegrityError(
# "Missing authlib table",
# {"authlib": None, "email": user.email},
# None,

Check notice

Code scanning / CodeQL

Commented-out code Note

This comment appears to contain commented-out code.
# )
59 changes: 59 additions & 0 deletions anitya/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import flask
import flask_login

from anitya.db import User, Session


def create_oauth_blueprint(oauth):
oauth_blueprint = flask.Blueprint('oauth', __name__)

@oauth_blueprint.route("/oauthlogin/<name>")
def oauthlogin(name):
client = oauth.create_client(name)
if client is None:
flask.abort(400)
redirect_uri = flask.url_for('.auth', name=name, _external=True)
return client.authorize_redirect(redirect_uri)


@oauth_blueprint.route("/auth/<name>")
def auth(name):
client = oauth.create_client(name)
if client is None:
flask.abort(400)
id_token = flask.request.values.get('id_token')
if flask.request.values.get('code'):
token = client.authorize_access_token()
if id_token:
token['id_token'] = id_token
elif id_token:
token = {'id_token': id_token}

# for Google
if 'id_token' in token:
user_info = client.parse_id_token(token)
# for Github
else:
client.response = client.get('user', token=token)
user_info = client.response.json()
if user_info['email'] is None:
resp = client.get('user/emails', token=token)
resp.raise_for_status()
data = resp.json()
user_info['email'] = next(email['email'] for email in data if email['primary'])

# Check if the user exists
user = User.query.filter(User.email == user_info['email']).first()
if not user:

# TODO: Should create the user (and openid connections) if it does not exist
new_user = User(email=user_info['email'], username=user_info['email'])
Session.add(new_user)
Session.commit()
user = new_user
flask_login.login_user(user)

# TODO: Process next not to just redirect with the main page
return flask.redirect('/')

return oauth_blueprint
5 changes: 2 additions & 3 deletions anitya/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@
Anitya uses `Flask-Login`_ for user session management. It handles logging in,
logging out, and remembering users’ sessions over extended periods of time.
In addition, Anitya uses `Python Social Auth`_ to authenticate users from various
In addition, Anitya uses `Authlib`_ to authenticate users from various
third-party identity providers. It handles logging the user in and creating
:class:`anitya.db.models.User` objects as necessary.
.. _Flask-Login: https://flask-login.readthedocs.io/en/latest/
.. _Python Social Auth:
https://python-social-auth.readthedocs.io/en/latest/
.. _Authlib: https://docs.authlib.org/en/latest/
"""
import logging
import uuid
Expand Down
11 changes: 11 additions & 0 deletions anitya/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@
# project will be automatically removed, if no version was retrieved yet
CHECK_ERROR_THRESHOLD=100,
DISTRO_MAPPING_LINKS={},
# Enabled authentication backends
AUTHLIB_ENABLED_BACKENDS=['Fedora', 'GitHub', 'Google'],
# Token for GitHub API
GITHUB_ACCESS_TOKEN_URL='https://github.com/login/oauth/access_token',
GITHUB_AUTHORIZE_URL='https://github.com/login/oauth/authorize',
GITHUB_API_BASE_URL='https://api.github.com/',
GITHUB_CLIENT_KWARGS={'scope': 'user:email'},
FEDORA_CLIENT_KWARGS={'scope': 'openid email profile'},
FEDORA_SERVER_METADATA_URL='https://id.fedoraproject.org/.well-known/openid-configuration',
GOOGLE_CLIENT_KWARGS={'scope': 'openid email profile'},
GOOGLE_SERVER_METADATA_URL='https://accounts.google.com/.well-known/openid-configuration',
)

# Start with a basic logging configuration, which will be replaced by any user-
Expand Down
19 changes: 4 additions & 15 deletions anitya/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,10 @@
{% block title %}Login · Anitya{% endblock %}

{% block body %}
<div class="d-flex align-itmes-start gap-3">
{% for name, backend in available_backends.items() %}
{% if name == 'openid'%}
<form role="form" action="{{url_for('social.auth', backend=name)}}" method="post" class="d-flex">
<input name="openid_identifier" placeholder="https://id.openid.server" class="form-control me-2">
<button type="submit" class="btn btn-success">
Login</button>
</form>
{% elif name == 'google-oauth2' %}
<a title="{{ Google }}" class="btn btn-primary" role="button"
href="{{url_for('social.auth', backend=name)}}">Google</a>
{% else %}
<a title="{{ name.capitalize() }}" class="btn btn-primary" role="button"
href="{{url_for('social.auth', backend=name)}}">{{ name.capitalize() }}</a>
{% endif %}
<div class="socialaccount_ballot">
<ul class="socialaccount_providers list-inline">
{% for backend in available_backends %}
<li><a title="{{ backend }}" class="socialaccount_provider openid btn btn-default" href="{{ url_for('oauth.oauthlogin', name=backend.lower()) }}">{{ backend }}</a></li>
{% endfor %}
</div>
{% endblock %}
1 change: 1 addition & 0 deletions ansible/roles/anitya-dev/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
fedora-messaging,
python3-alembic,
python3-arrow,
python-authlib,
python3-beautifulsoup4,
python3-dateutil,
python3-defusedxml,
Expand Down

0 comments on commit 6702b3c

Please sign in to comment.