Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make anitya use authlib instead of social_auth #1220

Merged
merged 7 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
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
Fixed Show fixed Hide fixed
=======
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 @@
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 @@
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 @@
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 @@
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 @@
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,
Fixed Show fixed Hide fixed
# )
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
Copy link
Contributor

Choose a reason for hiding this comment

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

This file deserves some comments. :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I saw that the social_auth stuff was registered as flask blueprint in the app.py, so I was trying to make it similar.
The rest is from authlib examples. The def oauthlogin is usually named login in the authlib documentation. I renamed it because there already is login function in ui.py.
Then I created the tokens in github and google for the anitya app and since both parties send a different user_info, so it is processed differently.
I will add docstrings and comments.

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
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be also done in the example config and config for vagrant and container setup.

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,
Copy link
Contributor

Choose a reason for hiding this comment

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

It's nice that the authlib is available in Fedora, one less package to download from PyPi.

python3-beautifulsoup4,
python3-dateutil,
python3-defusedxml,
Expand Down
Loading