Compare commits
61 Commits
cb216393a5
...
4.40.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d557ef875 | |||
| 6e56ea4489 | |||
| def0de643b | |||
| 9e7cb2c7dd | |||
| f1110506c0 | |||
| f5bce7d7ff | |||
| 75f45d9365 | |||
| ead425e0c2 | |||
| 6c910d62c5 | |||
| 99ffd1ec0c | |||
| eda940f8b2 | |||
| 1dad582523 | |||
| e516266a27 | |||
| 850fc95477 | |||
| d172825900 | |||
| 026865e5bf | |||
| add94ef2a2 | |||
| 1081400948 | |||
| 5776128905 | |||
| d661860f4c | |||
| 0a52e32972 | |||
| 703dcbd0eb | |||
| ce7ed69547 | |||
| 4f5564df16 | |||
| 2fee569131 | |||
| 7ea45d6f5d | |||
| 6d24db50bd | |||
| 88f270c6a1 | |||
| 0962b1cf29 | |||
| 6051d72691 | |||
| c31a75a9ef | |||
| ef289385ff | |||
| 9b12a2ad33 | |||
| 8eb19d88f3 | |||
| e36e9d3077 | |||
| b2430cbc5b | |||
| 1258115397 | |||
| 38c134d903 | |||
| cd77e4cc2d | |||
| 87aedf3207 | |||
| 3523c9fc15 | |||
| a6f4995cb5 | |||
| 727f61a35e | |||
| ce5124605a | |||
| 2c82b03f8d | |||
| 1b7a6223ac | |||
| 75331c62a4 | |||
| 3f68a3e640 | |||
| 8ee4f9462e | |||
| 822855d584 | |||
| 1a6a7e079b | |||
| 5210cb6515 | |||
| b643f0644b | |||
| 5d093db4f6 | |||
| 0b16fcac67 | |||
| a0d294da53 | |||
| c3f755aede | |||
| 0aea62c222 | |||
| 92f4ad2237 | |||
| 20da343c54 | |||
| 02776e8478 |
@ -5,7 +5,7 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CONTAINER_NAME: git.mrmeeb.stream/mrmeeb/simple-login
|
CONTAINER_NAME: git.mrmeeb.stream/mrmeeb/simple-login-dev
|
||||||
TEA_VERSION: 0.9.2
|
TEA_VERSION: 0.9.2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@ -16,7 +16,6 @@ from .views import (
|
|||||||
social,
|
social,
|
||||||
recovery,
|
recovery,
|
||||||
api_to_cookie,
|
api_to_cookie,
|
||||||
oidc,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -37,5 +36,4 @@ __all__ = [
|
|||||||
"social",
|
"social",
|
||||||
"recovery",
|
"recovery",
|
||||||
"api_to_cookie",
|
"api_to_cookie",
|
||||||
"oidc",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from wtforms import StringField, validators
|
|||||||
|
|
||||||
from app.auth.base import auth_bp
|
from app.auth.base import auth_bp
|
||||||
from app.auth.views.login_utils import after_login
|
from app.auth.views.login_utils import after_login
|
||||||
from app.config import CONNECT_WITH_PROTON, CONNECT_WITH_OIDC_ICON, OIDC_CLIENT_ID
|
from app.config import CONNECT_WITH_PROTON
|
||||||
from app.events.auth_event import LoginEvent
|
from app.events.auth_event import LoginEvent
|
||||||
from app.extensions import limiter
|
from app.extensions import limiter
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
@ -77,6 +77,4 @@ def login():
|
|||||||
next_url=next_url,
|
next_url=next_url,
|
||||||
show_resend_activation=show_resend_activation,
|
show_resend_activation=show_resend_activation,
|
||||||
connect_with_proton=CONNECT_WITH_PROTON,
|
connect_with_proton=CONNECT_WITH_PROTON,
|
||||||
connect_with_oidc=OIDC_CLIENT_ID is not None,
|
|
||||||
connect_with_oidc_icon=CONNECT_WITH_OIDC_ICON,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
from flask import request, session, redirect, flash, url_for
|
|
||||||
from requests_oauthlib import OAuth2Session
|
|
||||||
|
|
||||||
from app import config
|
|
||||||
from app.auth.base import auth_bp
|
|
||||||
from app.auth.views.login_utils import after_login
|
|
||||||
from app.config import (
|
|
||||||
URL,
|
|
||||||
OIDC_AUTHORIZATION_URL,
|
|
||||||
OIDC_USER_INFO_URL,
|
|
||||||
OIDC_TOKEN_URL,
|
|
||||||
OIDC_SCOPES,
|
|
||||||
OIDC_NAME_FIELD,
|
|
||||||
)
|
|
||||||
from app.db import Session
|
|
||||||
from app.email_utils import send_welcome_email
|
|
||||||
from app.log import LOG
|
|
||||||
from app.models import User, SocialAuth
|
|
||||||
from app.utils import encode_url, sanitize_email, sanitize_next_url
|
|
||||||
|
|
||||||
|
|
||||||
# need to set explicitly redirect_uri instead of leaving the lib to pre-fill redirect_uri
|
|
||||||
# when served behind nginx, the redirect_uri is localhost... and not the real url
|
|
||||||
_redirect_uri = URL + "/auth/oidc/callback"
|
|
||||||
|
|
||||||
SESSION_STATE_KEY = "oauth_state"
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/oidc/login")
|
|
||||||
def oidc_login():
|
|
||||||
if config.OIDC_CLIENT_ID is None or config.OIDC_CLIENT_SECRET is None:
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
|
|
||||||
next_url = sanitize_next_url(request.args.get("next"))
|
|
||||||
if next_url:
|
|
||||||
redirect_uri = _redirect_uri + "?next=" + encode_url(next_url)
|
|
||||||
else:
|
|
||||||
redirect_uri = _redirect_uri
|
|
||||||
|
|
||||||
oidc = OAuth2Session(
|
|
||||||
config.OIDC_CLIENT_ID, scope=[OIDC_SCOPES], redirect_uri=redirect_uri
|
|
||||||
)
|
|
||||||
authorization_url, state = oidc.authorization_url(OIDC_AUTHORIZATION_URL)
|
|
||||||
|
|
||||||
# State is used to prevent CSRF, keep this for later.
|
|
||||||
session[SESSION_STATE_KEY] = state
|
|
||||||
return redirect(authorization_url)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/oidc/callback")
|
|
||||||
def oidc_callback():
|
|
||||||
if SESSION_STATE_KEY not in session:
|
|
||||||
flash("Invalid state, please retry", "error")
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
if config.OIDC_CLIENT_ID is None or config.OIDC_CLIENT_SECRET is None:
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
|
|
||||||
# user clicks on cancel
|
|
||||||
if "error" in request.args:
|
|
||||||
flash("Please use another sign in method then", "warning")
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
oidc = OAuth2Session(
|
|
||||||
config.OIDC_CLIENT_ID,
|
|
||||||
state=session[SESSION_STATE_KEY],
|
|
||||||
scope=[OIDC_SCOPES],
|
|
||||||
redirect_uri=_redirect_uri,
|
|
||||||
)
|
|
||||||
oidc.fetch_token(
|
|
||||||
OIDC_TOKEN_URL,
|
|
||||||
client_secret=config.OIDC_CLIENT_SECRET,
|
|
||||||
authorization_response=request.url,
|
|
||||||
)
|
|
||||||
|
|
||||||
oidc_user_data = oidc.get(OIDC_USER_INFO_URL)
|
|
||||||
if oidc_user_data.status_code != 200:
|
|
||||||
LOG.e(
|
|
||||||
f"cannot get oidc user data {oidc_user_data.status_code} {oidc_user_data.text}"
|
|
||||||
)
|
|
||||||
flash(
|
|
||||||
"Cannot get user data from OIDC, please use another way to login/sign up",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
oidc_user_data = oidc_user_data.json()
|
|
||||||
|
|
||||||
email = oidc_user_data.get("email")
|
|
||||||
|
|
||||||
if not email:
|
|
||||||
LOG.e(f"cannot get email for OIDC user {oidc_user_data} {email}")
|
|
||||||
flash(
|
|
||||||
"Cannot get a valid email from OIDC, please another way to login/sign up",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
|
|
||||||
email = sanitize_email(email)
|
|
||||||
user = User.get_by(email=email)
|
|
||||||
|
|
||||||
if not user and config.DISABLE_REGISTRATION:
|
|
||||||
flash(
|
|
||||||
"Sorry you cannot sign up via the OIDC provider. Please sign-up first with your email.",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
return redirect(url_for("auth.register"))
|
|
||||||
elif not user:
|
|
||||||
user = create_user(email, oidc_user_data)
|
|
||||||
|
|
||||||
if not SocialAuth.get_by(user_id=user.id, social="oidc"):
|
|
||||||
SocialAuth.create(user_id=user.id, social="oidc")
|
|
||||||
Session.commit()
|
|
||||||
|
|
||||||
# The activation link contains the original page, for ex authorize page
|
|
||||||
next_url = sanitize_next_url(request.args.get("next")) if request.args else None
|
|
||||||
|
|
||||||
return after_login(user, next_url)
|
|
||||||
|
|
||||||
|
|
||||||
def create_user(email, oidc_user_data):
|
|
||||||
new_user = User.create(
|
|
||||||
email=email,
|
|
||||||
name=oidc_user_data.get(OIDC_NAME_FIELD),
|
|
||||||
password="",
|
|
||||||
activated=True,
|
|
||||||
)
|
|
||||||
LOG.i(f"Created new user for login request from OIDC. New user {new_user.id}")
|
|
||||||
Session.commit()
|
|
||||||
|
|
||||||
send_welcome_email(new_user)
|
|
||||||
|
|
||||||
return new_user
|
|
||||||
@ -6,7 +6,7 @@ from wtforms import StringField, validators
|
|||||||
|
|
||||||
from app import email_utils, config
|
from app import email_utils, config
|
||||||
from app.auth.base import auth_bp
|
from app.auth.base import auth_bp
|
||||||
from app.config import CONNECT_WITH_PROTON, CONNECT_WITH_OIDC_ICON
|
from app.config import CONNECT_WITH_PROTON
|
||||||
from app.auth.views.login_utils import get_referral
|
from app.auth.views.login_utils import get_referral
|
||||||
from app.config import URL, HCAPTCHA_SECRET, HCAPTCHA_SITEKEY
|
from app.config import URL, HCAPTCHA_SECRET, HCAPTCHA_SITEKEY
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
@ -109,8 +109,6 @@ def register():
|
|||||||
next_url=next_url,
|
next_url=next_url,
|
||||||
HCAPTCHA_SITEKEY=HCAPTCHA_SITEKEY,
|
HCAPTCHA_SITEKEY=HCAPTCHA_SITEKEY,
|
||||||
connect_with_proton=CONNECT_WITH_PROTON,
|
connect_with_proton=CONNECT_WITH_PROTON,
|
||||||
connect_with_oidc=config.OIDC_CLIENT_ID is not None,
|
|
||||||
connect_with_oidc_icon=CONNECT_WITH_OIDC_ICON,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -234,7 +234,7 @@ else:
|
|||||||
|
|
||||||
print("WARNING: Use a temp directory for GNUPGHOME", GNUPGHOME)
|
print("WARNING: Use a temp directory for GNUPGHOME", GNUPGHOME)
|
||||||
|
|
||||||
# Github, Google, Facebook, OIDC client id and secrets
|
# Github, Google, Facebook client id and secrets
|
||||||
GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID")
|
GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID")
|
||||||
GITHUB_CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
|
GITHUB_CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
|
||||||
|
|
||||||
@ -244,15 +244,6 @@ GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET")
|
|||||||
FACEBOOK_CLIENT_ID = os.environ.get("FACEBOOK_CLIENT_ID")
|
FACEBOOK_CLIENT_ID = os.environ.get("FACEBOOK_CLIENT_ID")
|
||||||
FACEBOOK_CLIENT_SECRET = os.environ.get("FACEBOOK_CLIENT_SECRET")
|
FACEBOOK_CLIENT_SECRET = os.environ.get("FACEBOOK_CLIENT_SECRET")
|
||||||
|
|
||||||
CONNECT_WITH_OIDC_ICON = os.environ.get("CONNECT_WITH_OIDC_ICON")
|
|
||||||
OIDC_AUTHORIZATION_URL = os.environ.get("OIDC_AUTHORIZATION_URL")
|
|
||||||
OIDC_USER_INFO_URL = os.environ.get("OIDC_USER_INFO_URL")
|
|
||||||
OIDC_TOKEN_URL = os.environ.get("OIDC_TOKEN_URL")
|
|
||||||
OIDC_CLIENT_ID = os.environ.get("OIDC_CLIENT_ID")
|
|
||||||
OIDC_CLIENT_SECRET = os.environ.get("OIDC_CLIENT_SECRET")
|
|
||||||
OIDC_SCOPES = os.environ.get("OIDC_SCOPES")
|
|
||||||
OIDC_NAME_FIELD = os.environ.get("OIDC_NAME_FIELD", "name")
|
|
||||||
|
|
||||||
PROTON_CLIENT_ID = os.environ.get("PROTON_CLIENT_ID")
|
PROTON_CLIENT_ID = os.environ.get("PROTON_CLIENT_ID")
|
||||||
PROTON_CLIENT_SECRET = os.environ.get("PROTON_CLIENT_SECRET")
|
PROTON_CLIENT_SECRET = os.environ.get("PROTON_CLIENT_SECRET")
|
||||||
PROTON_BASE_URL = os.environ.get(
|
PROTON_BASE_URL = os.environ.get(
|
||||||
@ -432,7 +423,6 @@ except Exception:
|
|||||||
HIBP_API_KEYS = sl_getenv("HIBP_API_KEYS", list) or []
|
HIBP_API_KEYS = sl_getenv("HIBP_API_KEYS", list) or []
|
||||||
HIBP_MAX_ALIAS_CHECK = 10_000
|
HIBP_MAX_ALIAS_CHECK = 10_000
|
||||||
HIBP_RPM = 100
|
HIBP_RPM = 100
|
||||||
HIBP_SKIP_PARTNER_ALIAS = os.environ.get("HIBP_SKIP_PARTNER_ALIAS")
|
|
||||||
|
|
||||||
POSTMASTER = os.environ.get("POSTMASTER")
|
POSTMASTER = os.environ.get("POSTMASTER")
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,11 @@ from flask_login import login_required, current_user
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import PasswordField, validators
|
from wtforms import PasswordField, validators
|
||||||
|
|
||||||
from app.config import CONNECT_WITH_PROTON, OIDC_CLIENT_ID, CONNECT_WITH_OIDC_ICON
|
from app.config import CONNECT_WITH_PROTON
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import limiter
|
from app.extensions import limiter
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import PartnerUser, SocialAuth
|
from app.models import PartnerUser
|
||||||
from app.proton.utils import get_proton_partner
|
from app.proton.utils import get_proton_partner
|
||||||
from app.utils import sanitize_next_url
|
from app.utils import sanitize_next_url
|
||||||
|
|
||||||
@ -51,19 +51,11 @@ def enter_sudo():
|
|||||||
if not partner_user or partner_user.partner_id != get_proton_partner().id:
|
if not partner_user or partner_user.partner_id != get_proton_partner().id:
|
||||||
proton_enabled = False
|
proton_enabled = False
|
||||||
|
|
||||||
oidc_enabled = OIDC_CLIENT_ID is not None
|
|
||||||
if oidc_enabled:
|
|
||||||
oidc_enabled = (
|
|
||||||
SocialAuth.get_by(user_id=current_user.id, social="oidc") is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/enter_sudo.html",
|
"dashboard/enter_sudo.html",
|
||||||
password_check_form=password_check_form,
|
password_check_form=password_check_form,
|
||||||
next=request.args.get("next"),
|
next=request.args.get("next"),
|
||||||
connect_with_proton=proton_enabled,
|
connect_with_proton=proton_enabled,
|
||||||
connect_with_oidc=oidc_enabled,
|
|
||||||
connect_with_oidc_icon=CONNECT_WITH_OIDC_ICON,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ from email.header import Header
|
|||||||
from email.message import Message
|
from email.message import Message
|
||||||
|
|
||||||
from app.email import headers
|
from app.email import headers
|
||||||
from app import config
|
|
||||||
from app.email_utils import add_or_replace_header, delete_header
|
from app.email_utils import add_or_replace_header, delete_header
|
||||||
from app.handler.unsubscribe_encoder import (
|
from app.handler.unsubscribe_encoder import (
|
||||||
UnsubscribeEncoder,
|
UnsubscribeEncoder,
|
||||||
@ -48,11 +47,6 @@ class UnsubscribeGenerator:
|
|||||||
method = raw_method[start + 1 : end]
|
method = raw_method[start + 1 : end]
|
||||||
url_data = urllib.parse.urlparse(method)
|
url_data = urllib.parse.urlparse(method)
|
||||||
if url_data.scheme == "mailto":
|
if url_data.scheme == "mailto":
|
||||||
if url_data.path == config.UNSUBSCRIBER:
|
|
||||||
LOG.debug(
|
|
||||||
f"Skipping replacing unsubscribe since the original email already points to {config.UNSUBSCRIBER}"
|
|
||||||
)
|
|
||||||
return message
|
|
||||||
query_data = urllib.parse.parse_qs(url_data.query)
|
query_data = urllib.parse.parse_qs(url_data.query)
|
||||||
mailto_unsubs = (url_data.path, query_data.get("subject", [""])[0])
|
mailto_unsubs = (url_data.path, query_data.get("subject", [""])[0])
|
||||||
LOG.debug(f"Unsub is mailto to {mailto_unsubs}")
|
LOG.debug(f"Unsub is mailto to {mailto_unsubs}")
|
||||||
|
|||||||
@ -1422,9 +1422,6 @@ def generate_random_alias_email(
|
|||||||
|
|
||||||
class Alias(Base, ModelMixin):
|
class Alias(Base, ModelMixin):
|
||||||
__tablename__ = "alias"
|
__tablename__ = "alias"
|
||||||
|
|
||||||
FLAG_PARTNER_CREATED = 1 << 0
|
|
||||||
|
|
||||||
user_id = sa.Column(
|
user_id = sa.Column(
|
||||||
sa.ForeignKey(User.id, ondelete="cascade"), nullable=False, index=True
|
sa.ForeignKey(User.id, ondelete="cascade"), nullable=False, index=True
|
||||||
)
|
)
|
||||||
@ -1434,9 +1431,6 @@ class Alias(Base, ModelMixin):
|
|||||||
name = sa.Column(sa.String(128), nullable=True, default=None)
|
name = sa.Column(sa.String(128), nullable=True, default=None)
|
||||||
|
|
||||||
enabled = sa.Column(sa.Boolean(), default=True, nullable=False)
|
enabled = sa.Column(sa.Boolean(), default=True, nullable=False)
|
||||||
flags = sa.Column(
|
|
||||||
sa.BigInteger(), default=0, server_default="0", nullable=False, index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
custom_domain_id = sa.Column(
|
custom_domain_id = sa.Column(
|
||||||
sa.ForeignKey("custom_domain.id", ondelete="cascade"), nullable=True, index=True
|
sa.ForeignKey("custom_domain.id", ondelete="cascade"), nullable=True, index=True
|
||||||
@ -2592,13 +2586,10 @@ class Job(Base, ModelMixin):
|
|||||||
nullable=False,
|
nullable=False,
|
||||||
server_default=str(JobState.ready.value),
|
server_default=str(JobState.ready.value),
|
||||||
default=JobState.ready.value,
|
default=JobState.ready.value,
|
||||||
index=True,
|
|
||||||
)
|
)
|
||||||
attempts = sa.Column(sa.Integer, nullable=False, server_default="0", default=0)
|
attempts = sa.Column(sa.Integer, nullable=False, server_default="0", default=0)
|
||||||
taken_at = sa.Column(ArrowType, nullable=True)
|
taken_at = sa.Column(ArrowType, nullable=True)
|
||||||
|
|
||||||
__table_args__ = (Index("ix_state_run_at_taken_at", state, run_at, taken_at),)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Job {self.id} {self.name} {self.payload}>"
|
return f"<Job {self.id} {self.name} {self.payload}>"
|
||||||
|
|
||||||
@ -2946,9 +2937,7 @@ class RecoveryCode(Base, ModelMixin):
|
|||||||
|
|
||||||
class Notification(Base, ModelMixin):
|
class Notification(Base, ModelMixin):
|
||||||
__tablename__ = "notification"
|
__tablename__ = "notification"
|
||||||
user_id = sa.Column(
|
user_id = sa.Column(sa.ForeignKey(User.id, ondelete="cascade"), nullable=False)
|
||||||
sa.ForeignKey(User.id, ondelete="cascade"), nullable=False, index=True
|
|
||||||
)
|
|
||||||
message = sa.Column(sa.Text, nullable=False)
|
message = sa.Column(sa.Text, nullable=False)
|
||||||
title = sa.Column(sa.String(512))
|
title = sa.Column(sa.String(512))
|
||||||
|
|
||||||
|
|||||||
14
app/cron.py
14
app/cron.py
@ -1059,15 +1059,13 @@ async def check_hibp():
|
|||||||
LOG.d("Preparing list of aliases to check")
|
LOG.d("Preparing list of aliases to check")
|
||||||
queue = asyncio.Queue()
|
queue = asyncio.Queue()
|
||||||
max_date = arrow.now().shift(days=-config.HIBP_SCAN_INTERVAL_DAYS)
|
max_date = arrow.now().shift(days=-config.HIBP_SCAN_INTERVAL_DAYS)
|
||||||
alias_query = Alias.filter(
|
|
||||||
or_(Alias.hibp_last_check.is_(None), Alias.hibp_last_check < max_date),
|
|
||||||
Alias.user_id.notin_(user_ids),
|
|
||||||
Alias.enabled,
|
|
||||||
)
|
|
||||||
if config.HIBP_SKIP_PARTNER_ALIAS:
|
|
||||||
alias_query = alias_query(Alias.flags.op("&")(Alias.FLAG_PARTNER_CREATED) == 0)
|
|
||||||
for alias in (
|
for alias in (
|
||||||
alias_query.order_by(nullsfirst(Alias.hibp_last_check.asc()), Alias.id.asc())
|
Alias.filter(
|
||||||
|
or_(Alias.hibp_last_check.is_(None), Alias.hibp_last_check < max_date),
|
||||||
|
Alias.user_id.notin_(user_ids),
|
||||||
|
)
|
||||||
|
.filter(Alias.enabled)
|
||||||
|
.order_by(nullsfirst(Alias.hibp_last_check.asc()), Alias.id.asc())
|
||||||
.yield_per(500)
|
.yield_per(500)
|
||||||
.enable_eagerloads(False)
|
.enable_eagerloads(False)
|
||||||
):
|
):
|
||||||
|
|||||||
@ -116,16 +116,6 @@ WORDS_FILE_PATH=local_data/test_words.txt
|
|||||||
# CONNECT_WITH_PROTON=true
|
# CONNECT_WITH_PROTON=true
|
||||||
# CONNECT_WITH_PROTON_COOKIE_NAME=to_fill
|
# CONNECT_WITH_PROTON_COOKIE_NAME=to_fill
|
||||||
|
|
||||||
# Login with OIDC
|
|
||||||
# CONNECT_WITH_OIDC_ICON=fa-github
|
|
||||||
# OIDC_AUTHORIZATION_URL=to_fill
|
|
||||||
# OIDC_USER_INFO_URL=to_fill
|
|
||||||
# OIDC_TOKEN_URL=to_fill
|
|
||||||
# OIDC_SCOPES=openid email profile
|
|
||||||
# OIDC_NAME_FIELD=name
|
|
||||||
# OIDC_CLIENT_ID=to_fill
|
|
||||||
# OIDC_CLIENT_SECRET=to_fill
|
|
||||||
|
|
||||||
# Flask profiler
|
# Flask profiler
|
||||||
# FLASK_PROFILER_PATH=/tmp/flask-profiler.sql
|
# FLASK_PROFILER_PATH=/tmp/flask-profiler.sql
|
||||||
# FLASK_PROFILER_PASSWORD=password
|
# FLASK_PROFILER_PASSWORD=password
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 52510a633d6f
|
|
||||||
Revises: 818b0a956205
|
|
||||||
Create Date: 2024-03-12 12:46:24.161644
|
|
||||||
|
|
||||||
"""
|
|
||||||
import sqlalchemy_utils
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "52510a633d6f"
|
|
||||||
down_revision = "818b0a956205"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column(
|
|
||||||
"alias", sa.Column("flags", sa.BigInteger(), server_default="0", nullable=False)
|
|
||||||
)
|
|
||||||
with op.get_context().autocommit_block():
|
|
||||||
op.create_index(op.f("ix_alias_flags"), "alias", ["flags"], unique=False)
|
|
||||||
op.create_index(op.f("ix_job_state"), "job", ["state"], unique=False)
|
|
||||||
op.create_index(
|
|
||||||
"ix_state_run_at_taken_at",
|
|
||||||
"job",
|
|
||||||
["state", "run_at", "taken_at"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_notification_user_id"), "notification", ["user_id"], unique=False
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.get_context().autocommit_block():
|
|
||||||
op.drop_index(op.f("ix_notification_user_id"), table_name="notification")
|
|
||||||
op.drop_index("ix_state_run_at_taken_at", table_name="job")
|
|
||||||
op.drop_index(op.f("ix_job_state"), table_name="job")
|
|
||||||
op.drop_index(op.f("ix_alias_flags"), table_name="alias")
|
|
||||||
op.drop_column("alias", "flags")
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
|
|
||||||
from app.log import LOG
|
|
||||||
from app.models import Alias, SLDomain
|
|
||||||
from app.db import Session
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
prog="Mark partner created aliases with the PARTNER_CREATED flag",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
domains = SLDomain.filter(SLDomain.partner_id.isnot(None)).all()
|
|
||||||
|
|
||||||
for domain in domains:
|
|
||||||
LOG.i(f"Checking aliases for domain {domain.domain}")
|
|
||||||
for alias in (
|
|
||||||
Alias.filter(
|
|
||||||
Alias.email.like(f"%{domain.domain}"),
|
|
||||||
Alias.flags.op("&")(Alias.FLAG_PARTNER_CREATED) == 0,
|
|
||||||
)
|
|
||||||
.enable_eagerloads(False)
|
|
||||||
.yield_per(100)
|
|
||||||
.all()
|
|
||||||
):
|
|
||||||
alias.flags = alias.flags | Alias.FLAG_PARTNER_CREATED
|
|
||||||
LOG.i(f" * Updating {alias.email} to {alias.flags}")
|
|
||||||
Session.commit()
|
|
||||||
@ -38,21 +38,11 @@
|
|||||||
<span>or</span>
|
<span>or</span>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary btn-block mt-2 proton-button"
|
<a class="btn btn-primary btn-block mt-2 proton-button"
|
||||||
href="{{ url_for('auth.proton_login', next=next_url) }}">
|
href="{{ url_for("auth.proton_login", next=next_url) }}">
|
||||||
<img class="mr-2" src="/static/images/proton.svg" />
|
<img class="mr-2" src="/static/images/proton.svg" />
|
||||||
Log in with Proton
|
Log in with Proton
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if connect_with_oidc %}
|
|
||||||
|
|
||||||
<div class="text-center my-2 text-gray">
|
|
||||||
<span>or</span>
|
|
||||||
</div>
|
|
||||||
<a class="btn btn-primary btn-block mt-2 btn-social"
|
|
||||||
href="{{ url_for('auth.oidc_login', next=next_url) }}">
|
|
||||||
<i class="fa {{ connect_with_oidc_icon }}"></i> Log in with SSO
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-muted mt-2">
|
<div class="text-center text-muted mt-2">
|
||||||
|
|||||||
@ -50,21 +50,11 @@
|
|||||||
<span>or</span>
|
<span>or</span>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary btn-block mt-2 proton-button"
|
<a class="btn btn-primary btn-block mt-2 proton-button"
|
||||||
href="{{ url_for('auth.proton_login', next=next_url) }}">
|
href="{{ url_for("auth.proton_login", next=next_url) }}">
|
||||||
<img class="mr-2" src="/static/images/proton.svg" />
|
<img class="mr-2" src="/static/images/proton.svg" />
|
||||||
Sign up with Proton
|
Sign up with Proton
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if connect_with_oidc %}
|
|
||||||
|
|
||||||
<div class="text-center my-2 text-gray">
|
|
||||||
<span>or</span>
|
|
||||||
</div>
|
|
||||||
<a class="btn btn-primary btn-block mt-2 btn-social"
|
|
||||||
href="{{ url_for('auth.oidc_login', next=next_url) }}">
|
|
||||||
<i class="fa {{ connect_with_oidc_icon }}"></i> Sign up with SSO
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="text-center text-muted mb-6">
|
<div class="text-center text-muted mb-6">
|
||||||
|
|||||||
@ -22,20 +22,11 @@
|
|||||||
<p>Alternatively you can use your Proton credentials to ensure it's you.</p>
|
<p>Alternatively you can use your Proton credentials to ensure it's you.</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary btn-block mt-2 proton-button w-25"
|
<a class="btn btn-primary btn-block mt-2 proton-button w-25"
|
||||||
href="{{ url_for('auth.proton_login', next=next) }}">
|
href="{{ url_for("auth.proton_login", next=next) }}">
|
||||||
<img class="mr-2" src="/static/images/proton.svg" />
|
<img class="mr-2" src="/static/images/proton.svg" />
|
||||||
Authenticate with Proton
|
Authenticate with Proton
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if connect_with_oidc %}
|
|
||||||
|
|
||||||
<div class="my-3">
|
|
||||||
<p>Alternatively you can use your SSO credentials to ensure it's you.</p>
|
|
||||||
<a class="btn btn-primary btn-block mt-2 btn-social w-25"
|
|
||||||
href="{{ url_for('auth.oidc_login', next=next) }}">
|
|
||||||
<i class="fa {{ connect_with_oidc_icon }}"></i> Authenticate with SSO
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
{{ render_text("Hi") }}
|
{{ render_text("Hi") }}
|
||||||
{{ render_text("If you use Safari on a MacBook or iMac, you should check out our new Safari extension.") }}
|
{{ render_text("If you use Safari on a MacBook or iMac, you should check out our new Safari extension.") }}
|
||||||
{{ render_text('It can be installed on
|
{{ render_text('It can be installed on
|
||||||
<a href="https://apps.apple.com/app/id6475835429">App Store</a>
|
<a href="https://apps.apple.com/app/id1494051017">App Store</a>
|
||||||
. Its code is available on
|
. Its code is available on
|
||||||
<a href="https://github.com/simple-login/mac-app">GitHub</a>
|
<a href="https://github.com/simple-login/mac-app">GitHub</a>
|
||||||
.') }}
|
.') }}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ If you use Safari on a MacBook or iMac, you should check out our new Safari exte
|
|||||||
|
|
||||||
It can be installed on:
|
It can be installed on:
|
||||||
|
|
||||||
https://apps.apple.com/app/id6475835429
|
https://apps.apple.com/app/id1494051017
|
||||||
|
|
||||||
As usual, let me know if you have any question by replying to this email.
|
As usual, let me know if you have any question by replying to this email.
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ If you want to quickly create aliases <b>without</b> going to SimpleLogin websit
|
|||||||
(or other Chromium-based browsers like Brave or Vivaldi),
|
(or other Chromium-based browsers like Brave or Vivaldi),
|
||||||
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox</a>
|
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox</a>
|
||||||
and
|
and
|
||||||
<a href="https://apps.apple.com/app/id6475835429 ">Safari</a>
|
<a href="https://apps.apple.com/app/id1494051017 ">Safari</a>
|
||||||
extension.
|
extension.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ Chrome: https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmg
|
|||||||
|
|
||||||
Firefox: https://addons.mozilla.org/firefox/addon/simplelogin/
|
Firefox: https://addons.mozilla.org/firefox/addon/simplelogin/
|
||||||
|
|
||||||
Safari: https://apps.apple.com/app/id6475835429
|
Safari: https://apps.apple.com/app/id1494051017
|
||||||
|
|
||||||
You can also manage your aliases using SimpleLogin mobile apps, available at
|
You can also manage your aliases using SimpleLogin mobile apps, available at
|
||||||
- Play Store https://play.google.com/store/apps/details?id=io.simplelogin.android
|
- Play Store https://play.google.com/store/apps/details?id=io.simplelogin.android
|
||||||
|
|||||||
@ -124,7 +124,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="https://apps.apple.com/app/id6475835429">
|
href="https://apps.apple.com/app/id1494051017">
|
||||||
Safari
|
Safari
|
||||||
Extension
|
Extension
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -1,304 +0,0 @@
|
|||||||
from app import config
|
|
||||||
from flask import url_for
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
from urllib3.util import parse_url
|
|
||||||
from app.auth.views.oidc import create_user
|
|
||||||
from app.utils import random_string
|
|
||||||
from unittest.mock import patch
|
|
||||||
from app.models import User
|
|
||||||
|
|
||||||
from app.config import URL, OIDC_CLIENT_ID
|
|
||||||
|
|
||||||
|
|
||||||
def test_oidc_login(flask_client):
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_login"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
query = parse_qs(parsed.query)
|
|
||||||
|
|
||||||
expected_redirect_url = f"{URL}/auth/oidc/callback"
|
|
||||||
|
|
||||||
assert "code" == query["response_type"][0]
|
|
||||||
assert OIDC_CLIENT_ID == query["client_id"][0]
|
|
||||||
assert expected_redirect_url == query["redirect_uri"][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_oidc_login_no_client_id(flask_client):
|
|
||||||
config.OIDC_CLIENT_ID = None
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_login"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/auth/login"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
|
|
||||||
config.OIDC_CLIENT_ID = "to_fill"
|
|
||||||
|
|
||||||
|
|
||||||
def test_oidc_login_no_client_secret(flask_client):
|
|
||||||
config.OIDC_CLIENT_SECRET = None
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_login"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/auth/login"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
|
|
||||||
config.OIDC_CLIENT_SECRET = "to_fill"
|
|
||||||
|
|
||||||
|
|
||||||
def test_oidc_callback_no_oauth_state(flask_client):
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_oidc_callback_no_client_id(flask_client):
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = "state"
|
|
||||||
config.OIDC_CLIENT_ID = None
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/auth/login"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
|
|
||||||
config.OIDC_CLIENT_ID = "to_fill"
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
|
|
||||||
def test_oidc_callback_no_client_secret(flask_client):
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = "state"
|
|
||||||
config.OIDC_CLIENT_SECRET = None
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/auth/login"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
|
|
||||||
config.OIDC_CLIENT_SECRET = "to_fill"
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.fetch_token")
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.get")
|
|
||||||
def test_oidc_callback_invalid_user(mock_get, mock_fetch_token, flask_client):
|
|
||||||
mock_get.return_value = MockResponse(400, {})
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = "state"
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/auth/login"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
assert mock_get.called
|
|
||||||
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.fetch_token")
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.get")
|
|
||||||
def test_oidc_callback_no_email(mock_get, mock_fetch_token, flask_client):
|
|
||||||
mock_get.return_value = MockResponse(200, {})
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = "state"
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/auth/login"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
assert mock_get.called
|
|
||||||
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.fetch_token")
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.get")
|
|
||||||
def test_oidc_callback_disabled_registration(mock_get, mock_fetch_token, flask_client):
|
|
||||||
config.DISABLE_REGISTRATION = True
|
|
||||||
email = random_string()
|
|
||||||
mock_get.return_value = MockResponse(200, {"email": email})
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = "state"
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/auth/register"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
assert mock_get.called
|
|
||||||
|
|
||||||
config.DISABLE_REGISTRATION = False
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.fetch_token")
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.get")
|
|
||||||
def test_oidc_callback_registration(mock_get, mock_fetch_token, flask_client):
|
|
||||||
email = random_string()
|
|
||||||
mock_get.return_value = MockResponse(
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
"email": email,
|
|
||||||
config.OIDC_NAME_FIELD: "name",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = "state"
|
|
||||||
|
|
||||||
user = User.get_by(email=email)
|
|
||||||
assert user is None
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/dashboard/"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
assert mock_get.called
|
|
||||||
|
|
||||||
user = User.get_by(email=email)
|
|
||||||
assert user is not None
|
|
||||||
assert user.email == email
|
|
||||||
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.fetch_token")
|
|
||||||
@patch("requests_oauthlib.OAuth2Session.get")
|
|
||||||
def test_oidc_callback_login(mock_get, mock_fetch_token, flask_client):
|
|
||||||
email = random_string()
|
|
||||||
mock_get.return_value = MockResponse(
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
"email": email,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = "state"
|
|
||||||
|
|
||||||
user = User.create(
|
|
||||||
email=email,
|
|
||||||
name="name",
|
|
||||||
password="",
|
|
||||||
activated=True,
|
|
||||||
)
|
|
||||||
user = User.get_by(email=email)
|
|
||||||
assert user is not None
|
|
||||||
|
|
||||||
r = flask_client.get(
|
|
||||||
url_for("auth.oidc_callback"),
|
|
||||||
follow_redirects=False,
|
|
||||||
)
|
|
||||||
location = r.headers.get("Location")
|
|
||||||
assert location is not None
|
|
||||||
|
|
||||||
parsed = parse_url(location)
|
|
||||||
|
|
||||||
expected_redirect_url = "/dashboard/"
|
|
||||||
|
|
||||||
assert expected_redirect_url == parsed.path
|
|
||||||
assert mock_get.called
|
|
||||||
|
|
||||||
with flask_client.session_transaction() as session:
|
|
||||||
session["oauth_state"] = None
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_user():
|
|
||||||
email = random_string()
|
|
||||||
user = create_user(
|
|
||||||
email,
|
|
||||||
{
|
|
||||||
config.OIDC_NAME_FIELD: "name",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert user.email == email
|
|
||||||
assert user.name == "name"
|
|
||||||
assert user.activated
|
|
||||||
|
|
||||||
|
|
||||||
class MockResponse:
|
|
||||||
def __init__(self, status_code, json_data):
|
|
||||||
self.status_code = status_code
|
|
||||||
self.json_data = json_data
|
|
||||||
self.text = "error"
|
|
||||||
|
|
||||||
def json(self):
|
|
||||||
return self.json_data
|
|
||||||
@ -13,7 +13,8 @@ from app.handler.unsubscribe_encoder import (
|
|||||||
)
|
)
|
||||||
from app.handler.unsubscribe_generator import UnsubscribeGenerator
|
from app.handler.unsubscribe_generator import UnsubscribeGenerator
|
||||||
from app.models import Alias, Contact, UnsubscribeBehaviourEnum
|
from app.models import Alias, Contact, UnsubscribeBehaviourEnum
|
||||||
from tests.utils import create_new_user, random_email
|
from tests.utils import create_new_user
|
||||||
|
|
||||||
|
|
||||||
TEST_UNSUB_EMAIL = "unsub@sl.com"
|
TEST_UNSUB_EMAIL = "unsub@sl.com"
|
||||||
|
|
||||||
@ -203,23 +204,3 @@ def test_unsub_preserve_original(
|
|||||||
assert message[headers.LIST_UNSUBSCRIBE_POST] is None
|
assert message[headers.LIST_UNSUBSCRIBE_POST] is None
|
||||||
else:
|
else:
|
||||||
assert "List-Unsubscribe=One-Click" == message[headers.LIST_UNSUBSCRIBE_POST]
|
assert "List-Unsubscribe=One-Click" == message[headers.LIST_UNSUBSCRIBE_POST]
|
||||||
|
|
||||||
|
|
||||||
def test_unsub_preserves_sl_unsubscriber():
|
|
||||||
user = create_new_user()
|
|
||||||
user.unsub_behaviour = UnsubscribeBehaviourEnum.PreserveOriginal
|
|
||||||
alias = Alias.create_new_random(user)
|
|
||||||
Session.commit()
|
|
||||||
config.UNSUBSCRIBER = random_email()
|
|
||||||
contact = Contact.create(
|
|
||||||
user_id=user.id,
|
|
||||||
alias_id=alias.id,
|
|
||||||
website_email="contact@example.com",
|
|
||||||
reply_email="rep@sl.local",
|
|
||||||
commit=True,
|
|
||||||
)
|
|
||||||
message = Message()
|
|
||||||
original_header = f"<mailto:{config.UNSUBSCRIBER}?subject=dummysubject>"
|
|
||||||
message[headers.LIST_UNSUBSCRIBE] = original_header
|
|
||||||
message = UnsubscribeGenerator().add_header_to_message(alias, contact, message)
|
|
||||||
assert original_header == message[headers.LIST_UNSUBSCRIBE]
|
|
||||||
|
|||||||
@ -49,16 +49,6 @@ GOOGLE_CLIENT_SECRET=to_fill
|
|||||||
FACEBOOK_CLIENT_ID=to_fill
|
FACEBOOK_CLIENT_ID=to_fill
|
||||||
FACEBOOK_CLIENT_SECRET=to_fill
|
FACEBOOK_CLIENT_SECRET=to_fill
|
||||||
|
|
||||||
# Login with OIDC
|
|
||||||
CONNECT_WITH_OIDC_ICON=fa-github
|
|
||||||
OIDC_AUTHORIZATION_URL=to_fill
|
|
||||||
OIDC_USER_INFO_URL=to_fill
|
|
||||||
OIDC_TOKEN_URL=to_fill
|
|
||||||
OIDC_SCOPES=openid email profile
|
|
||||||
OIDC_NAME_FIELD=name
|
|
||||||
OIDC_CLIENT_ID=to_fill
|
|
||||||
OIDC_CLIENT_SECRET=to_fill
|
|
||||||
|
|
||||||
PGP_SENDER_PRIVATE_KEY_PATH=local_data/private-pgp.asc
|
PGP_SENDER_PRIVATE_KEY_PATH=local_data/private-pgp.asc
|
||||||
|
|
||||||
ALIAS_AUTOMATIC_DISABLE=true
|
ALIAS_AUTOMATIC_DISABLE=true
|
||||||
|
|||||||
Reference in New Issue
Block a user