Skip to content

Commit f232449

Browse files
feat: migrate sso auth to c-idp
1 parent ccdf57f commit f232449

File tree

5 files changed

+82
-34
lines changed

5 files changed

+82
-34
lines changed

.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@ FLASK_TRINO_SF_PRIVATE_KEY=trino_private_key
3030
# FLASK_DISABLE_AUTH_FOR_TESTS=1
3131

3232

33+
# C-IDP Creds
34+
FLASK_SSO_CLIENT_ID=client-id
35+
FLASK_SSO_CLIENT_SECRET=client-secret
36+
FLASK_SSO_PROVIDER=oidc-provider-url

charm/charmcraft.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ config:
3838
default: false
3939
trino-sf:
4040
type: secret
41-
description: Trino credentials, must contain (project-id, private-key-id, client-email, client-id, private-key)
41+
description: Trino credentials, must contain (project-id, private-key-id, client-email, client-id, private-key)
42+
sso:
43+
type: secret
44+
description: SSO credentials, must contain (client-id, client-secret, provider)

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ google-auth==2.40.3
2222
cryptography==46.0.1
2323
setuptools==79.0.1
2424
black==25.1.0
25-
flake8==7.1.1
25+
flake8==7.1.1
26+
Authlib==1.6.6

webapp/sso.py

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,74 @@
22
import os
33

44
import flask
5-
from django_openid_auth.teams import TeamsRequest, TeamsResponse
6-
from flask_openid import OpenID
5+
from authlib.integrations.flask_client import OAuth
6+
import requests
77

8+
from webapp.views import get_users_data
89

9-
SSO_LOGIN_URL = "https://login.ubuntu.com"
1010
SSO_TEAM = "canonical-content-people"
11+
LAUNCHPAD_API_URL = "https://api.launchpad.net/1.0"
1112

1213

1314
def init_sso(app):
14-
open_id = OpenID(
15-
store_factory=lambda: None,
16-
safe_roots=[],
17-
extension_responses=[TeamsResponse],
15+
16+
oauth = OAuth(app)
17+
18+
oauth.register(
19+
"canonical",
20+
client_id=os.getenv("SSO_CLIENT_ID"),
21+
client_secret=os.getenv("SSO_CLIENT_SECRET"),
22+
server_metadata_url=os.getenv("SSO_PROVIDER"),
23+
client_kwargs={
24+
"token_endpoint_auth_method": "client_secret_post",
25+
"scope": "openid profile email",
26+
},
1827
)
1928

20-
@app.route("/login", methods=["GET", "POST"])
21-
@open_id.loginhandler
29+
@app.route("/login")
2230
def login():
2331
if "openid" in flask.session:
24-
return flask.redirect(open_id.get_next_url())
32+
return flask.redirect(flask.request.args.get("next") or "/manager")
33+
34+
redirect_uri = flask.url_for("oauth_callback", _external=True)
35+
return oauth.canonical.authorize_redirect(redirect_uri)
2536

26-
teams_request = TeamsRequest(query_membership=[SSO_TEAM])
27-
return open_id.try_login(
28-
SSO_LOGIN_URL, ask_for=["email"], extensions=[teams_request]
37+
@app.route("/auth/callback")
38+
def oauth_callback():
39+
token = oauth.canonical.authorize_access_token()
40+
users, status_code = get_users_data(token["userinfo"]["name"])
41+
42+
if status_code != 200 or not users:
43+
flask.abort(
44+
403, description="Failed to fetch user data from directory."
45+
)
46+
47+
response = requests.get(
48+
f"{LAUNCHPAD_API_URL}/~{users[0]['launchpadId']}/super_teams",
2949
)
3050

31-
@open_id.after_login
32-
def after_login(resp):
33-
if SSO_TEAM not in resp.extensions["lp"].is_member:
34-
flask.abort(403)
51+
if response.status_code != 200:
52+
flask.abort(
53+
403, description="Failed to fetch Launchpad team memberships."
54+
)
55+
56+
memberships = response.json().get("entries", [])
57+
if SSO_TEAM not in [team["name"] for team in memberships]:
58+
flask.abort(
59+
403,
60+
description=(
61+
"Please make sure you are a member of the "
62+
"canonical-content-people team on Launchpad."
63+
),
64+
)
3565

3666
flask.session["openid"] = {
37-
"identity_url": resp.identity_url,
38-
"email": resp.email,
67+
"identity_url": token["userinfo"]["iss"],
68+
"email": token["userinfo"]["email"],
69+
"fullname": token["userinfo"]["name"],
3970
}
4071

41-
return flask.redirect(open_id.get_next_url())
72+
return flask.redirect(flask.request.args.get("next") or "/manager")
4273

4374

4475
def login_required(func):

webapp/views.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -474,20 +474,21 @@ def delete_redirect(redirect_path):
474474
return jsonify({}), 204
475475

476476

477-
def get_users(username: str):
477+
def get_users_data(username: str):
478478
query = """
479-
query($name: String!) {
480-
employees(filter: { contains: { name: $name }}) {
481-
id
482-
firstName
483-
surname
484-
email
485-
team
486-
department
487-
jobTitle
479+
query($name: String!) {
480+
employees(filter: { contains: { name: $name }}) {
481+
id
482+
firstName
483+
surname
484+
email
485+
team
486+
department
487+
jobTitle
488+
launchpadId
489+
}
488490
}
489-
}
490-
"""
491+
"""
491492

492493
headers = {
493494
"Authorization": "token "
@@ -506,6 +507,14 @@ def get_users(username: str):
506507

507508
if response.status_code == 200:
508509
users = response.json().get("data", {}).get("employees", [])
510+
return users, 200
511+
512+
return None, response.status_code
513+
514+
515+
def get_users(username: str):
516+
users, status_code = get_users_data(username)
517+
if status_code == 200:
509518
return jsonify(list(users))
510519
return jsonify({"error": "Failed to fetch users"}), 500
511520

0 commit comments

Comments
 (0)