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

Use DNS to list the IPA servers #1367

Merged
merged 1 commit into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion deployment/noggin.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
TEMPLATES_AUTO_RELOAD = True

# IPA settings
FREEIPA_SERVERS = ['ipa.noggin.test']
FREEIPA_DOMAIN = 'tinystage.test'
FREEIPA_CACERT = '/etc/ipa/ca.crt'

# Any user with admin privileges
Expand Down
2 changes: 1 addition & 1 deletion devel/ansible/roles/noggin/templates/noggin.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
TEMPLATES_AUTO_RELOAD = True

# IPA settings
FREEIPA_SERVERS = ['ipa.{{ ansible_domain }}']
FREEIPA_DOMAIN = '{{ ansible_domain }}'
FREEIPA_CACERT = '/etc/ipa/ca.crt'

# Any user with admin privileges
Expand Down
2 changes: 1 addition & 1 deletion devel/create-test-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
fake = Faker()
fake.seed_instance(0)

ipa_server = "ipa.noggin.test"
ipa_server = "ipa.tinystage.test"
ipa_user = "admin"
ipa_pw = "password"
ipa = Client(host=ipa_server, verify_ssl=False)
Expand Down
1 change: 1 addition & 0 deletions news/1357.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use DNS to list the IPA servers
4 changes: 3 additions & 1 deletion noggin/controller/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from flask_babel import _

from noggin.form.sync_token import SyncTokenForm
from noggin.security.ipa import maybe_ipa_login, untouched_ipa_client
from noggin.security.ipa import NoIPAServer, maybe_ipa_login, untouched_ipa_client
from noggin.utility.forms import FormError, handle_form_errors

from . import blueprint as bp
Expand Down Expand Up @@ -82,5 +82,7 @@ def otp_sync():
f'{form.username}: {e}'
)
raise FormError("non_field_errors", e.message)
except NoIPAServer:
raise FormError("non_field_errors", _("No IPA server available"))

return render_template('sync-token.html', sync_form=form)
23 changes: 15 additions & 8 deletions noggin/controller/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
PasswordResetForm,
)
from noggin.representation.user import User
from noggin.security.ipa import maybe_ipa_session, untouched_ipa_client
from noggin.security.ipa import NoIPAServer, maybe_ipa_session, untouched_ipa_client
from noggin.utility import messaging
from noggin.utility.controllers import require_self, user_or_404, with_ipa
from noggin.utility.forms import FormError, handle_form_errors
Expand All @@ -38,7 +38,10 @@

def _validate_change_pw_form(form, username, ipa=None):
if ipa is None:
ipa = untouched_ipa_client(current_app, session)
try:
ipa = untouched_ipa_client(current_app, session)
except NoIPAServer:
raise FormError("non_field_errors", _("No IPA server available"))

current_password = form.current_password.data
password = form.password.data
Expand Down Expand Up @@ -87,9 +90,10 @@ def password_reset():
form = PasswordResetForm()

if form.validate_on_submit():
res = _validate_change_pw_form(form, username)
if res and res.ok:
return redirect(url_for('.root'))
with handle_form_errors(form):
res = _validate_change_pw_form(form, username)
if res and res.ok:
return redirect(url_for('.root'))

return render_template(
'password-reset.html', password_reset_form=form, username=username
Expand All @@ -110,9 +114,10 @@ def user_settings_password(ipa, username):
form.current_password.description = ""

if form.validate_on_submit():
res = _validate_change_pw_form(form, username, ipa)
if res and res.ok:
return redirect(url_for('.root'))
with handle_form_errors(form):
res = _validate_change_pw_form(form, username, ipa)
if res and res.ok:
return redirect(url_for('.root'))

return render_template(
'user-settings-password.html',
Expand Down Expand Up @@ -281,6 +286,8 @@ def forgot_password_change():
form.non_field_errors.errors.append(
_('Could not change password, please try again.')
)
except NoIPAServer:
form.non_field_errors.errors.append(_("No IPA server available"))
else:
lock.delete()
flash(_('Your password has been changed.'), 'success')
Expand Down
4 changes: 3 additions & 1 deletion noggin/controller/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
)
from noggin.l10n import guess_locale
from noggin.representation.user import User
from noggin.security.ipa import maybe_ipa_login, untouched_ipa_client
from noggin.security.ipa import NoIPAServer, maybe_ipa_login, untouched_ipa_client
from noggin.signals import stageuser_created, user_registered
from noggin.utility.controllers import with_ipa
from noggin.utility.forms import FormError, handle_form_errors
Expand Down Expand Up @@ -300,6 +300,8 @@ def activate_account():
'warning',
)
return redirect(url_for(".root"))
except NoIPAServer:
raise FormError("non_field_errors", _("No IPA server available"))

# Try to log them in directly, so they don't have to type their password again.
try:
Expand Down
3 changes: 3 additions & 0 deletions noggin/defaults.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# This file contains the default configuration values
import socket


TEMPLATES_AUTO_RELOAD = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
FREEIPA_DOMAIN = ".".join(socket.getfqdn().split('.')[1:])
USER_DEFAULTS = {
"locale": "en-US",
"timezone": "UTC",
Expand Down
40 changes: 32 additions & 8 deletions noggin/security/ipa.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import random

import python_freeipa
import srvlookup
from cryptography.fernet import Fernet
from python_freeipa.client_meta import ClientMeta as IPAClient
from python_freeipa.exceptions import BadRequest, ValidationError
Expand Down Expand Up @@ -112,6 +111,12 @@ def fasagreement_disable(self, agreement, **kwargs):
self._request('fasagreement_disable', agreement, kwargs)


class NoIPAServer(Exception):
"""No IPA server available."""

pass


def raise_on_failed(result):
failed = result.get("failed", {})
num_failed = sum(
Expand All @@ -130,9 +135,22 @@ def choose_server(app, session=None):
server = None
if session is not None:
server = session.get('noggin_ipa_server_hostname', None)
available_servers = app.config['FREEIPA_SERVERS']
try:
available_servers = [
record.hostname
for record in srvlookup.lookup('ldap', domain=app.config["FREEIPA_DOMAIN"])
]
except srvlookup.SRVQueryFailure:
available_servers = []
if server is None or server not in available_servers:
server = random.choice(available_servers)
try:
server = available_servers[0]
except IndexError:
app.logger.warning(
"IPA server not found. Available servers: %s",
", ".join(available_servers),
)
raise NoIPAServer
if session is not None:
session['noggin_ipa_server_hostname'] = server
return server
Expand All @@ -157,7 +175,10 @@ def untouched_ipa_client(app, session):
# It will be None if no session was provided or was provided but invalid.
def maybe_ipa_session(app, session):
encrypted_session = session.get('noggin_session', None)
server_hostname = choose_server(app, session)
try:
server_hostname = choose_server(app, session)
except NoIPAServer:
return None
if encrypted_session and server_hostname:
fernet = Fernet(app.config['FERNET_SECRET'])
ipa_session = fernet.decrypt(encrypted_session)
Expand Down Expand Up @@ -189,9 +210,12 @@ def maybe_ipa_login(app, session, username, userpassword):
# in the session and just always use that. Flask sessions are signed, so we
# are safe in later assuming that the server hostname cookie has not been
# altered.
client = Client(
choose_server(app, session), verify_ssl=app.config['FREEIPA_CACERT']
)
try:
client = Client(
choose_server(app, session), verify_ssl=app.config['FREEIPA_CACERT']
)
except NoIPAServer:
return None

auth = client.login(username, userpassword)

Expand Down
18 changes: 17 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ translitcodec = "0.7.0"
unidecode = "^1.2.0"
flask-talisman = ">=0.8.1, <2.0"
pyotp = "^2.2.7"
srvlookup = "^2.0.0 || ^3.0.0"

[tool.poetry.group.dev.dependencies]
pytest = ">=7.1.0"
Expand Down
Loading