Skip to content

Commit

Permalink
#117 and #125 support portal provider and remove support for password…
Browse files Browse the repository at this point in the history
… authentication

Completed support for portal / agol. Admin and front end limit what's shown based on the logged in users current portal/agol unless superuser.

Password auth removed and login form reformatted to show both social auth providers nicely with not username/password fields available.
  • Loading branch information
rdmccann committed Feb 23, 2023
1 parent 0ce0bf1 commit 22d88c1
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 87 deletions.
16 changes: 11 additions & 5 deletions AGOLAccountRequestor/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,22 @@
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'oauth2_provider.RefreshToken'
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken"

DISABLE_PASSWORD_AUTH = getattr(local_settings, 'DISABLE_PASSWORD_AUTH', False)

SOCIAL_AUTH_GEOPLATFORM_DOMAIN = local_settings.SOCIAL_AUTH_GEOPLATFORM_DOMAIN
SOCIAL_AUTH_GEOPLATFORM_KEY = local_settings.SOCIAL_AUTH_GEOPLATFORM_KEY
SOCIAL_AUTH_GEOPLATFORM_SECRET = local_settings.SOCIAL_AUTH_GEOPLATFORM_SECRET
SOCIAL_AUTH_GEOPLATFORM_PREAPPROVED_DOMAINS = getattr(local_settings, 'SOCIAL_AUTH_GEOPLATFORM_PREAPPROVED_DOMAINS', [])
SOCIAL_AUTH_GEOPLATFORM_UNKNOWN_REQUESTER_GROUP_ID = getattr(local_settings, 'SOCIAL_AUTH_GEOPLATFORM_UNKNOWN_REQUESTER_GROUP_ID', 0)

SOCIAL_AUTH_GEOSECURE_DOMAIN = local_settings.SOCIAL_AUTH_GEOSECURE_DOMAIN
SOCIAL_AUTH_GEOSECURE_KEY = local_settings.SOCIAL_AUTH_GEOSECURE_KEY
SOCIAL_AUTH_GEOSECURE_SECRET = local_settings.SOCIAL_AUTH_GEOSECURE_SECRET
SOCIAL_AUTH_REDIRECT_IS_HTTPS = getattr(local_settings, 'SOCIAL_AUTH_REDIRECT_IS_HTTPS', True)
SOCIAL_AUTH_PIPELINE = local_settings.SOCIAL_AUTH_PIPELINE
SOCIAL_AUTH_GEOPLATFORM_PREAPPROVED_DOMAINS = getattr(local_settings, 'SOCIAL_AUTH_GEOPLATFORM_PREAPPROVED_DOMAINS', [])
SOCIAL_AUTH_GEOPLATFORM_UNKNOWN_REQUESTER_GROUP_ID = getattr(local_settings, 'SOCIAL_AUTH_GEOPLATFORM_UNKNOWN_REQUESTER_GROUP_ID', 0)
SOCIAL_AUTH_GEOSECURE_PREAPPROVED_DOMAINS = getattr(local_settings, 'SOCIAL_AUTH_GEOPLATFORM_PREAPPROVED_DOMAINS', [])
SOCIAL_AUTH_GEOSECURE_UNKNOWN_REQUESTER_GROUP_ID = getattr(local_settings, 'SOCIAL_AUTH_GEOPLATFORM_UNKNOWN_REQUESTER_GROUP_ID', 0)

SOCIAL_AUTH_REDIRECT_IS_HTTPS = getattr(local_settings, 'SOCIAL_AUTH_REDIRECT_IS_HTTPS', True)
SOCIAL_AUTH_PIPELINE = local_settings.SOCIAL_AUTH_PIPELINE
SOCIAL_AUTH_ALLOWED_REDIRECT_HOSTS = getattr(local_settings, 'SOCIAL_AUTH_ALLOWED_REDIRECT_HOSTS', [])

REST_FRAMEWORK = local_settings.REST_FRAMEWORK
Expand All @@ -161,7 +165,7 @@
CORS_ORIGIN_WHITELIST = local_settings.CORS_ORIGIN_WHITELIST
CORS_ALLOW_CREDENTIALS = getattr(local_settings, 'CORS_ALLOW_CREDENTIALS', False)

EMAIL_BACKEND = 'sendgrid_backend.SendgridBackend'
EMAIL_BACKEND = local_settings.EMAIL_BACKEND
SENDGRID_API_KEY = local_settings.SENDGRID_API_KEY
SENDGRID_SANDBOX_MODE_IN_DEBUG = local_settings.SENDGRID_SANDBOX_MODE_IN_DEBUG

Expand Down Expand Up @@ -203,3 +207,5 @@
COORDINATOR_ADMIN_GROUP_ID = getattr(local_settings, 'COORDINATOR_ADMIN_GROUP_ID', 0)

CSRF_COOKIE_NAME = 'requestcsrftoken'

CSRF_TRUSTED_ORIGINS = getattr(local_settings, 'CSRF_TRUSTED_ORIGINS', [])
3 changes: 2 additions & 1 deletion AGOLAccountRequestor/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

router = routers.DefaultRouter()

router.register('portals', account_views.PortalsViewSet)
router.register('account/request', account_views.AccountRequestViewSet)
router.register('account/approvals', account_views.AccountViewSet)
router.register('responses', account_views.ResponseProjectViewSet)
Expand All @@ -44,7 +45,7 @@
path(f'{settings.URL_PREFIX}api/v1/', include(router.urls)),
path(f'{settings.URL_PREFIX}api/v1/email_field_coordinator_request/', email_field_coordinator_request),
# path('api/rest-auth/', include('rest_auth.urls')),
path(f'{settings.URL_PREFIX}api/auth/', include('rest_framework.urls', namespace='rest_framework')),
# path(f'{settings.URL_PREFIX}api/auth/', include('rest_framework.urls', namespace='rest_framework')), #remove support for password based auth
path(f'{settings.URL_PREFIX}api/oauth2/', include('social_django.urls', namespace='social_django')),
path(f'{settings.URL_PREFIX}api/current_user/', current_user),

Expand Down
56 changes: 49 additions & 7 deletions accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@


# hack to make full name show up in autocomplete b/c nothing else worked
User.__str__ = lambda x: f"{x.first_name} {x.last_name} ({x.agol_info.auth_provider if hasattr(x, 'agol_info') else None})"
User.__str__ = lambda x: f"{x.first_name} {x.last_name} ({x.agol_info.portal if hasattr(x, 'agol_info') else None})"
# make admin panel show full name and portal of currently logged in user
User.get_short_name = lambda user_instance: f"{user_instance.first_name} {user_instance.last_name} ({user_instance.agol_info.portal if hasattr(user_instance, 'agol_info') else None})"


@admin.register(AGOL)
class AGOLAdmin(admin.ModelAdmin):
fields = ['portal_url', 'user']
fields = ['portal_name', 'portal_url', 'user']
list_display = ['portal_name', 'portal_url']


admin.site.unregister(User)
Expand Down Expand Up @@ -49,6 +52,14 @@ class AGOLUserFieldsInline(admin.StackedInline):
@admin.register(User)
class AGOLUserAdmin(UserAdmin):
inlines = (AGOLUserFieldsInline,)
# list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_superuser')

def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.is_superuser:
return queryset
return queryset.filter(agol_info__portal_id=request.user.agol_info.portal_id)


# hacky solution b/c of https://code.djangoproject.com/ticket/29707
def get_search_results(self, request, queryset, search_term):
Expand All @@ -69,9 +80,15 @@ def get_search_results(self, request, queryset, search_term):
class AGOLGroupAdmin(admin.ModelAdmin):
ordering = ['-is_auth_group', 'title']
search_fields = ['title']
list_display = ['title', 'is_auth_group']
fields = ['title', 'is_auth_group']
readonly_fields = ['title']
list_display = ['title', 'agol', 'is_auth_group']
fields = ['title', 'agol', 'is_auth_group']
readonly_fields = ['title', 'agol']

def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.is_superuser:
return queryset
return queryset.filter(agol=request.user.agol_info.portal_id)

def has_add_permission(self, request):
return False
Expand Down Expand Up @@ -110,14 +127,26 @@ class RequestAdmin(admin.ModelAdmin):
list_display = ['last_name', 'first_name', 'email', 'username']
search_fields = list_display
ordering = ['-submitted']
list_filter = ['response', 'submitted', 'approved_by', 'approved', 'created']
#list_filter = ['response', 'submitted', 'approved_by', 'approved', 'created']
list_filter = (
('response', admin.RelatedOnlyFieldListFilter),
('submitted'),
('approved_by', admin.RelatedOnlyFieldListFilter),
('approved'),
('created'),
)
fields = ['first_name', 'last_name', 'email', 'possible_existing_account', 'existing_account_enabled', 'organization', 'username',
'username_valid', 'user_type', 'role', 'auth_group', 'sponsor', 'sponsor_notified', 'reason',
'approved', 'approved_by', 'created', 'response', 'is_existing_account']
readonly_fields = ['possible_existing_account', 'username_valid', 'sponsor_notified', 'approved', 'created',
'is_existing_account', 'existing_account_enabled', 'approved_by']
inlines = [GroupAdminInline, PendingNotificationInline]

def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.is_superuser:
return queryset
return queryset.filter(response__portal_id=request.user.agol_info.portal_id)

def set_system_default(modeladmin, request, queryset):
if queryset.count() > 1:
Expand Down Expand Up @@ -163,6 +192,7 @@ class ResponseProjectForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['users'].required = True
#self.fields['requester'].queryset = User.objects.filter(agol_info__portal_id=35)

class Meta:
model = ResponseProject
Expand All @@ -172,7 +202,7 @@ class Meta:
@admin.register(ResponseProject)
class ResponseProjectAdmin(admin.ModelAdmin):

list_display = ['name', 'requester', 'sponsors', 'approved', 'disabled']
list_display = ['name', 'portal', 'requester', 'sponsors', 'approved', 'disabled']
search_fields = ['name']
ordering = ['name']
fields = ['name', 'requester', 'users', 'assignable_groups', 'role', 'authoritative_group', 'default_reason',
Expand All @@ -184,6 +214,18 @@ class ResponseProjectAdmin(admin.ModelAdmin):
list_filter = ['disabled', 'approved']
form = ResponseProjectForm

def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.is_superuser:
return queryset
return queryset.filter(portal=request.user.agol_info.portal_id)

def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "requester":
if not request.user.is_superuser:
kwargs["queryset"] = User.objects.filter(agol_info__portal_id=request.user.agol_info.portal_id)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

def change_view(self, request, object_id, form_url='', extra_context=None):
response = super(ResponseProjectAdmin, self).change_view(request, object_id, form_url, extra_context)
if type(response) == HttpResponseRedirect:
Expand Down
12 changes: 6 additions & 6 deletions accounts/management/commands/refresh_agol_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@


class Command(BaseCommand):
help = 'Notify sponsors of new account requests'
help = 'Update groups and roles from all portals/agols'

def handle(self, *args, **options):
agol = AGOL.objects.get(portal_url='https://epa.maps.arcgis.com')
agol.get_all_groups()
agol.get_all_roles()
# todo: determine what happens if a group is removed but our tool has someone linked to it

#agol = AGOL.objects.get(portal_url='https://epa.maps.arcgis.com')
for agol in AGOL.objects.all():
agol.get_all_groups()
agol.get_all_roles()
# todo: determine what happens if a group is removed but our tool has someone linked to it
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.1.6 on 2023-02-08 20:16

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('accounts', '0021_add_pk_id_fields'),
]

operations = [
migrations.AddField(
model_name='agol',
name='portal_name',
field=models.CharField(choices=[('geosecure', 'Geosecure'), ('geoplatform', 'Geoplatform')], blank=True, null=True, max_length=50),
),
migrations.AddField(
model_name='agoluserfields',
name='portal',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='auth_provider', to='accounts.agol'),
),
migrations.AddField(
model_name='responseproject',
name='portal',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='accounts.agol'),
),
]
23 changes: 11 additions & 12 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ def clean(self):

class AGOL(models.Model):
id = models.AutoField(primary_key=True)
portal_name = models.CharField(max_length=50, blank=True, null=True , choices=[('geosecure', 'GeoSecure'),
('geoplatform', 'GeoPlatform')])
portal_url = models.URLField()
org_id = models.CharField(max_length=50, blank=True, null=True)
user = models.ForeignKey(User, on_delete=models.PROTECT)
Expand All @@ -160,10 +162,10 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)

def __str__(self):
return self.portal_url
return self.get_portal_name_display()

def get_token(self):
social = self.user.social_auth.get(provider=self.user.agol_info.auth_provider)
social = self.user.social_auth.get()
return social.get_access_token(load_strategy())

def get_org_id(self):
Expand All @@ -174,8 +176,7 @@ def get_org_id(self):

def get_list(self, url, results_key='results'):
try:

sys.stdout.write(f'\nGetting All {url}... \n')
sys.stdout.write(f'\nGetting All from {self.portal_url}... \n')

next_record, total_records, update_total = 0, 1, True
all_records = []
Expand Down Expand Up @@ -206,7 +207,6 @@ def get_list(self, url, results_key='results'):

return all_records


except:
sys.stdout.write('\nError encountered. Stopping update.\n')
# logger.exception('Error in refreshAllAGOL refresh_catalog')
Expand All @@ -215,7 +215,7 @@ def get_list(self, url, results_key='results'):
def get_all_groups(self):
try:
all_groups = self.get_list('community/groups')
sys.stdout.write('\nCreating/updating groups...\n')
sys.stdout.write(f'\nCreating/updating groups from {self.portal_url}...\n')

for group in all_groups:
AGOLGroup.objects.update_or_create(id=group['id'], defaults={'title': group['title'], 'agol': self})
Expand All @@ -226,7 +226,7 @@ def get_all_groups(self):

def get_all_roles(self):
all_roles = self.get_list('portals/self/roles', 'roles')
sys.stdout.write('\nCreating/updating roles...\n')
sys.stdout.write(f'\nCreating/updating roles from {self.portal_url}...\n')
for role in all_roles:
AGOLRole.objects.update_or_create(id=role['id'], defaults={'name': role['name'],
'description': role['description'],
Expand Down Expand Up @@ -403,11 +403,9 @@ def update_user_account(self, username, data):
class AGOLUserFields(models.Model):
id = models.AutoField(primary_key=True)
agol_username = models.CharField(max_length=200, null=True, blank=True)
auth_provider = models.CharField(max_length=50, choices=[('geosecure', 'Geosecure'),
('geoplatform', 'Geoplatform')])
portal = models.ForeignKey(AGOL, models.PROTECT, default=1)
sponsor = models.BooleanField(default=False)


user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='agol_info')
delegates = models.ManyToManyField(User, related_name='delegate_for', blank=True)

Expand Down Expand Up @@ -435,6 +433,7 @@ def __init__(self, *args, **kwargs):
users = models.ManyToManyField(User, related_name='response', verbose_name='Sponsors',
limit_choices_to={'agol_info__sponsor': True}, blank=True)
name = models.CharField('Name', max_length=500)
portal = models.ForeignKey(AGOL, models.PROTECT, default=1)
assignable_groups = models.ManyToManyField('AGOLGroup', related_name='response',
verbose_name='GeoPlatform Assignable Groups')
role = models.ForeignKey('AGOLRole', on_delete=models.PROTECT, verbose_name='GeoPlatform Role',
Expand All @@ -461,8 +460,8 @@ def sponsors(self):

@property
def disable_users_link(self):
# define link to relevant user accounts
agol = AGOL.objects.first()
# define link to relevant user accounts using AGOL/portal of response/project being modified
agol = self.portal
url = f'{agol.portal_url}/home/organization.html?'
query_params = {'showFilters': 'false', 'view': 'table', 'sortOrder': 'asc', 'sortField': 'fullname'}
# get account request AGOL IDs to define in link
Expand Down
8 changes: 6 additions & 2 deletions accounts/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from rest_framework.serializers import ModelSerializer, CharField, PrimaryKeyRelatedField, ChoiceField, \
JSONField, BooleanField
from .models import *
from rest_framework.decorators import api_view
from drf_recaptcha.fields import ReCaptchaV2Field
from .func import has_outstanding_request

Expand Down Expand Up @@ -68,7 +67,7 @@ class Meta:
class FullResponseProjectSerializer(ModelSerializer):
class Meta:
model = ResponseProject
fields = ['id', 'users', 'name', 'assignable_groups', 'authoritative_group', 'default_reason', 'role', 'requester']
fields = ['id', 'users', 'name', 'portal', 'assignable_groups', 'authoritative_group', 'default_reason', 'role', 'requester']


class AccountWithNestedDataSerializer(AccountSerializer):
Expand All @@ -91,3 +90,8 @@ class PendingNotificationSerializer(ModelSerializer):
class Meta:
model = Notification
fields = ['id', 'subject', 'content', 'to_emails']

class PortalsSerializer(ModelSerializer):
class Meta:
model = AGOL
fields = ['id', 'portal_name', 'portal_url']
Loading

0 comments on commit 22d88c1

Please sign in to comment.