Skip to content
Draft
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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ dependencies = [

[project.optional-dependencies]
ldap = ["python-ldap==3.4.5"] # optional for LDAP authentication, requires libldap (OpenLDAP) to build
allauth-mfa = [
"django-allauth[mfa]",
]
allauth-social = [
"django-allauth[socialaccount]",
]
docs = ["sphinx_rtd_theme>=2.0.0"]

[dependency-groups]
Expand Down
14 changes: 14 additions & 0 deletions python/nav/django/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
PUBLIC_URLS = [
'/api/', # No auth/different auth system
'/doc/', # No auth/different auth system
'/about/',
'/index/login/',
'/index/audit-logging-modal/',
'/refresh_session',
'/accounts/',
'/accounts/2fa/authenticate/',
]
NAV_LOGIN_URL = '/index/login/'
ALLAUTH_LOGIN_URL = '/accounts/login/'

LOGIN_URL = ALLAUTH_LOGIN_URL
26 changes: 25 additions & 1 deletion python/nav/django/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
'nav.django.legacy.LegacyCleanupMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django_htmx.middleware.HtmxMiddleware',
'allauth.account.middleware.AccountMiddleware',
)

SESSION_SERIALIZER = 'nav.web.session_serializer.PickleSerializer'
Expand Down Expand Up @@ -236,12 +237,21 @@
'nav.portadmin.napalm',
'nav.web.portadmin',
'django.contrib.postgres',
'allauth',
'allauth.account',
'allauth.mfa',
'allauth.socialaccount',
# noqa: Needs to be a setting
'allauth.socialaccount.providers.dataporten',
)

DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
AUTH_USER_MODEL = 'nav_models.Account'

AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
"allauth.account.auth_backends.AuthenticationBackend",
]
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/index/login/'

Expand Down Expand Up @@ -318,3 +328,17 @@
'JWT_ISSUERS': _issuers_setting,
'JWT_AUTH_HEADER_PREFIX': 'Bearer',
}

# Allauth settings

ACCOUNT_ADAPTER = "nav.web.auth.allauth.adapter.NAVAccountAdapter"
ACCOUNT_USER_MODEL_USERNAME_FIELD = 'login'
ACCOUNT_ALLOW_SIGNUPS = False
ACCOUNT_MAX_EMAIL_ADDRESSES = 1
LOGIN_URL = '/accounts/login/'
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = True # allow localhost
MFA_TOTP_ISSUER = 'NAV'
MFA_TOTP_TOLERANCE = 1
MFA_SUPPORTED_TYPES = ['totp', 'recovery_codes']
SOCIALACCOUNT_AUTO_SIGNUP = True
SOCIALACCOUNT_ADAPTER = 'nav.web.auth.allauth.adapter.NAVSocialAccountAdapter'
1 change: 1 addition & 0 deletions python/nav/django/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
path('refresh_session/', refresh_session, name='refresh-session'),
path('auditlog/', include('nav.auditlog.urls')),
path('interfaces/', include('nav.web.interface_browser.urls')),
path('accounts/', include('allauth.urls')),
path('500/', force_500),
]

Expand Down
247 changes: 247 additions & 0 deletions python/nav/models/sql/changes/sc.05.15.0503.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
-- Tables for django-allauth[mfa,socialaccount]

-- account_emailaddress

CREATE TABLE profiles.account_emailaddress (
id integer NOT NULL,
email character varying(254) NOT NULL,
verified boolean NOT NULL,
"primary" boolean NOT NULL,
user_id integer NOT NULL
);


ALTER TABLE profiles.account_emailaddress OWNER TO nav;


ALTER TABLE profiles.account_emailaddress ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
SEQUENCE NAME profiles.account_emailaddress_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);


ALTER TABLE ONLY profiles.account_emailaddress
ADD CONSTRAINT account_emailaddress_pkey PRIMARY KEY (id);


ALTER TABLE ONLY profiles.account_emailaddress
ADD CONSTRAINT account_emailaddress_user_id_email_987c8728_uniq UNIQUE (user_id, email);


CREATE INDEX account_emailaddress_email_03be32b2 ON profiles.account_emailaddress USING btree (email);


CREATE INDEX account_emailaddress_email_03be32b2_like ON profiles.account_emailaddress USING btree (email varchar_pattern_ops);


CREATE INDEX account_emailaddress_user_id_2c513194 ON profiles.account_emailaddress USING btree (user_id);


CREATE UNIQUE INDEX unique_primary_email ON profiles.account_emailaddress USING btree (user_id, "primary") WHERE "primary";


CREATE UNIQUE INDEX unique_verified_email ON profiles.account_emailaddress USING btree (email) WHERE verified;


ALTER TABLE ONLY profiles.account_emailaddress
ADD CONSTRAINT account_emailaddress_user_id_2c513194_fk_account_id FOREIGN KEY (user_id) REFERENCES profiles.account(id) DEFERRABLE INITIALLY DEFERRED;

-- account_emailconfirmation

CREATE TABLE profiles.account_emailconfirmation (
id integer NOT NULL,
created timestamp with time zone NOT NULL,
sent timestamp with time zone,
key character varying(64) NOT NULL,
email_address_id integer NOT NULL
);


ALTER TABLE profiles.account_emailconfirmation OWNER TO nav;


ALTER TABLE profiles.account_emailconfirmation ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
SEQUENCE NAME profiles.account_emailconfirmation_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);


ALTER TABLE ONLY profiles.account_emailconfirmation
ADD CONSTRAINT account_emailconfirmation_key_key UNIQUE (key);


ALTER TABLE ONLY profiles.account_emailconfirmation
ADD CONSTRAINT account_emailconfirmation_pkey PRIMARY KEY (id);


CREATE INDEX account_emailconfirmation_email_address_id_5b7f8c58 ON profiles.account_emailconfirmation USING btree (email_address_id);


CREATE INDEX account_emailconfirmation_key_f43612bd_like ON profiles.account_emailconfirmation USING btree (key varchar_pattern_ops);


ALTER TABLE ONLY profiles.account_emailconfirmation
ADD CONSTRAINT account_emailconfirm_email_address_id_5b7f8c58_fk_account_e FOREIGN KEY (email_address_id) REFERENCES profiles.account_emailaddress(id) DEFERRABLE INITIALLY DEFERRED;

-- mfa_authenticator

CREATE TABLE profiles.mfa_authenticator (
id bigint NOT NULL,
type character varying(20) NOT NULL,
data jsonb NOT NULL,
user_id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
last_used_at timestamp with time zone
);


ALTER TABLE profiles.mfa_authenticator OWNER TO nav;


ALTER TABLE profiles.mfa_authenticator ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
SEQUENCE NAME profiles.mfa_authenticator_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);


ALTER TABLE ONLY profiles.mfa_authenticator
ADD CONSTRAINT mfa_authenticator_pkey PRIMARY KEY (id);


CREATE INDEX mfa_authenticator_user_id_0c3a50c0 ON profiles.mfa_authenticator USING btree (user_id);


CREATE UNIQUE INDEX unique_authenticator_type ON profiles.mfa_authenticator USING btree (user_id, type) WHERE ((type)::text = ANY ((ARRAY['totp'::character varying, 'recovery_codes'::character varying])::text[]));


ALTER TABLE ONLY profiles.mfa_authenticator
ADD CONSTRAINT mfa_authenticator_user_id_0c3a50c0_fk_account_id FOREIGN KEY (user_id) REFERENCES profiles.account(id) DEFERRABLE INITIALLY DEFERRED;

-- socialaccount_socialaccount

CREATE TABLE profiles.socialaccount_socialaccount (
id integer NOT NULL,
provider character varying(200) NOT NULL,
uid character varying(191) NOT NULL,
last_login timestamp with time zone NOT NULL,
date_joined timestamp with time zone NOT NULL,
extra_data jsonb NOT NULL,
user_id integer NOT NULL
);


ALTER TABLE profiles.socialaccount_socialaccount OWNER TO nav;


ALTER TABLE profiles.socialaccount_socialaccount ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
SEQUENCE NAME profiles.socialaccount_socialaccount_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);


ALTER TABLE ONLY profiles.socialaccount_socialaccount
ADD CONSTRAINT socialaccount_socialaccount_pkey PRIMARY KEY (id);


ALTER TABLE ONLY profiles.socialaccount_socialaccount
ADD CONSTRAINT socialaccount_socialaccount_provider_uid_fc810c6e_uniq UNIQUE (provider, uid);


CREATE INDEX socialaccount_socialaccount_user_id_8146e70c ON profiles.socialaccount_socialaccount USING btree (user_id);


ALTER TABLE ONLY profiles.socialaccount_socialaccount
ADD CONSTRAINT socialaccount_social_user_id_8146e70c_fk_account FOREIGN KEY (user_id) REFERENCES profiles.account(id) DEFERRABLE INITIALLY DEFERRED;

-- socialaccount_socialapp

CREATE TABLE profiles.socialaccount_socialapp (
id integer NOT NULL,
provider character varying(30) NOT NULL,
name character varying(40) NOT NULL,
client_id character varying(191) NOT NULL,
secret character varying(191) NOT NULL,
key character varying(191) NOT NULL,
provider_id character varying(200) NOT NULL,
settings jsonb NOT NULL
);


ALTER TABLE profiles.socialaccount_socialapp OWNER TO nav;


ALTER TABLE profiles.socialaccount_socialapp ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
SEQUENCE NAME profiles.socialaccount_socialapp_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);


ALTER TABLE ONLY profiles.socialaccount_socialapp
ADD CONSTRAINT socialaccount_socialapp_pkey PRIMARY KEY (id);

-- socialaccount_socialapp


CREATE TABLE profiles.socialaccount_socialtoken (
id integer NOT NULL,
token text NOT NULL,
token_secret text NOT NULL,
expires_at timestamp with time zone,
account_id integer NOT NULL,
app_id integer
);


ALTER TABLE profiles.socialaccount_socialtoken OWNER TO nav;


ALTER TABLE profiles.socialaccount_socialtoken ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
SEQUENCE NAME profiles.socialaccount_socialtoken_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);


ALTER TABLE ONLY profiles.socialaccount_socialtoken
ADD CONSTRAINT socialaccount_socialtoken_app_id_account_id_fca4e0ac_uniq UNIQUE (app_id, account_id);


ALTER TABLE ONLY profiles.socialaccount_socialtoken
ADD CONSTRAINT socialaccount_socialtoken_pkey PRIMARY KEY (id);


CREATE INDEX socialaccount_socialtoken_account_id_951f210e ON profiles.socialaccount_socialtoken USING btree (account_id);


CREATE INDEX socialaccount_socialtoken_app_id_636a42d7 ON profiles.socialaccount_socialtoken USING btree (app_id);


ALTER TABLE ONLY profiles.socialaccount_socialtoken
ADD CONSTRAINT socialaccount_social_account_id_951f210e_fk_socialacc FOREIGN KEY (account_id) REFERENCES profiles.socialaccount_socialaccount(id) DEFERRABLE INITIALLY DEFERRED;


ALTER TABLE ONLY profiles.socialaccount_socialtoken
ADD CONSTRAINT socialaccount_social_app_id_636a42d7_fk_socialacc FOREIGN KEY (app_id) REFERENCES profiles.socialaccount_socialapp(id) DEFERRABLE INITIALLY DEFERRED;
3 changes: 2 additions & 1 deletion python/nav/web/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from django.http import HttpRequest
from django.urls import reverse

from nav.django.defaults import LOGIN_URL
from nav.auditlog.models import LogEntry
from nav.models.profiles import Account, AccountGroup
from nav.web.auth import ldap, remote_user
Expand All @@ -42,7 +43,7 @@
# This may seem like redundant information, but it seems django's reverse
# will hang under some usages of these middleware classes - so until we figure
# out what's going on, we'll hardcode this here.
LOGIN_URL = '/index/login/'
# LOGIN_URL = '/accounts/login/'
# The local logout url, redirects to '/' after logout
# If the entire site is protected via remote_user, this link must be outside
# that protection!
Expand Down
Empty file.
26 changes: 26 additions & 0 deletions python/nav/web/auth/allauth/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.conf import settings

from allauth.account.adapter import DefaultAccountAdapter
from allauth.account.adapter import get_adapter as get_account_adapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount import app_settings


class NAVAccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
"""
Whether to allow sign ups with username/password
"""
allow_signups = super().is_open_for_signup(request)
# Override with setting, otherwise default to super.
return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups)


class NAVSocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin):
"""
Whether to allow sign ups via social account
"""
if app_settings.AUTO_SIGNUP:
return True
return get_account_adapter(request).is_open_for_signup(request)
Loading
Loading