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

Authentication and projects #60

Merged
merged 51 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b60591c
feat: server-side changes for authentication
annehaley Aug 8, 2024
a3bd8dc
feat: client-side changes for authentication
annehaley Aug 8, 2024
003f88f
feat: replace Context model with Project model
annehaley Aug 8, 2024
093fcb3
Switch from `@girder/oauth-client` to `@resonant/oauth-client`
annehaley Aug 9, 2024
ffbd515
fix: remove `SESSION_COOKIE_AGE` value override
annehaley Aug 12, 2024
245a04f
feat: Add access control check functions on each model
annehaley Aug 12, 2024
1754ac3
feat: Update API to use AccessControl filter backend
annehaley Aug 12, 2024
de74292
feat: Override signup form to include name fields
annehaley Aug 12, 2024
6943cb4
fix: address some server-side API bugs
annehaley Aug 14, 2024
7372b16
fix: Add homepage redirect url
annehaley Aug 14, 2024
10330c3
style: Lint fixes
annehaley Aug 15, 2024
4b3a0bd
test: pass homepage url env var through tox environments
annehaley Aug 15, 2024
f9a3221
fix: Include constraint removal step in migration file
annehaley Aug 15, 2024
faac2fd
fix: Add missing django env var to Celery config in `docker-compose.o…
annehaley Aug 21, 2024
c3b4ac2
docs: Add `makeclient` step to Setup Guide
annehaley Aug 21, 2024
b7f1cca
fix: Resolve merge conflicts after rebase
annehaley Aug 21, 2024
4906e9f
fix: Add authentication to maplibre tile requests
annehaley Sep 16, 2024
318f695
refactor: update `ingest_projects` function
annehaley Sep 27, 2024
88af424
refactor: update `makeclient` management command
annehaley Sep 27, 2024
d78a494
refactor: update `Dataset.is_in_project` function
annehaley Sep 27, 2024
df03bad
fix: Add watcher for `showMapBaseLayer`
annehaley Sep 27, 2024
9f6c9ab
refactor: add explanatory comment to Map's `transformRequest` function
annehaley Sep 27, 2024
af567f1
refactor: Add comment to `urls.py`
annehaley Sep 27, 2024
b40dd60
wip: Consolidate access control logic with `django-guardian`
annehaley Sep 27, 2024
f831cf3
refactor: apply suggestions to `guardian.py`
annehaley Sep 29, 2024
e317c6d
fix: roll back change to region viewset inheritance
annehaley Sep 29, 2024
3178461
fix: add `VectorFeature` clause to `get_object_queryset`
annehaley Sep 29, 2024
5935f62
refactor: remove permission fields from Project model and add `update…
annehaley Sep 29, 2024
bc1a27e
test: Add a test for API permissions
annehaley Sep 30, 2024
934f729
refactor: update `guardian.py` to get passing permissions tests
annehaley Sep 30, 2024
61f9767
style: fix lint errors
annehaley Sep 30, 2024
9989e6f
refactor: update `guardian.py`
annehaley Sep 30, 2024
52c9ffe
style: remove change to flake8 rules in `tox.ini`
annehaley Sep 30, 2024
ea5ff03
Generalize project filtering function
jjnesbitt Sep 30, 2024
4b10b27
Rename pk to id in get_vector_tile
jjnesbitt Sep 30, 2024
c5deb96
test: refactor API tests, split up by viewset
annehaley Sep 30, 2024
3315a4d
refactor: rename `guardian.py` -> `access_control.py`
annehaley Sep 30, 2024
7bb8976
style: remove unused imports and variables
annehaley Sep 30, 2024
61196f8
Refactor Project.set_permissions
jjnesbitt Sep 30, 2024
7d75b37
Fix bug when initially setting map center/zoom
jjnesbitt Sep 30, 2024
33eba30
Rename clearMap to setMapCenter
jjnesbitt Sep 30, 2024
fadc263
fix: allow filtering by `project_id` in request params
annehaley Sep 30, 2024
2e66d31
fix: update permissions fields on `ProjectSerializer`
annehaley Sep 30, 2024
305f0b2
refactor: move `project_id` query param filtering to `get_queryset` o…
annehaley Sep 30, 2024
448d070
Enforce single owner per project
jjnesbitt Oct 1, 2024
61e7a29
fix: update `set_permissions` call in Project `perform_create`
annehaley Oct 1, 2024
e157327
fix: update `set_permissions` call in `test_api`
annehaley Oct 1, 2024
de9725c
fix: enforce an owner on server-created projects
annehaley Oct 1, 2024
9453764
Append authentication settings to existing configuration
jjnesbitt Oct 1, 2024
8d1d9db
fix: add import to `ingest_use_case`
annehaley Oct 1, 2024
b614f13
test: ensure a superuser exists at beginning of populate test
annehaley Oct 1, 2024
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
7 changes: 7 additions & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ services:
# Log printing via Rich is enhanced by a TTY
tty: true
env_file: ./dev/.env.docker-compose
environment:
# ensure these match the web container
- DJANGO_HOMEPAGE_REDIRECT_URL=http://localhost:8080/
- VUE_APP_BASE_URL=http://localhost:8080/
- VUE_APP_OAUTH_CLIENT_ID=cBmD6D6F2YAmMWHNQZFPUr4OpaXVpW5w4Thod6Kj
volumes:
- .:/opt/uvdat-server
ports:
Expand All @@ -33,6 +38,8 @@ services:
]
# Docker Compose does not set the TTY width, which causes Celery errors
tty: false
environment:
- DJANGO_HOMEPAGE_REDIRECT_URL=http://localhost:8080/
env_file: ./dev/.env.docker-compose
volumes:
- .:/opt/uvdat-server
Expand Down
5 changes: 4 additions & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
b. Run `docker-compose run --rm django ./manage.py createsuperuser`
and follow the prompts to create your own user.

c. Run `docker-compose run --rm django ./manage.py populate` to use sample data.
c. Run `docker-compose run --rm django ./manage.py makeclient` to create a client Application object for authentication.

d. Run `docker-compose run --rm django ./manage.py populate` to use sample data.


### Run Application
1. Run `docker-compose up`.
Expand Down
47 changes: 25 additions & 22 deletions sample_data/ingest_use_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import os
from pathlib import Path

from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from django.core.files.base import ContentFile
import requests

from uvdat.core.models import Chart, Context, Dataset, FileItem
from uvdat.core.models import Chart, Project, Dataset, FileItem

from .use_cases.boston_floods import ingest as boston_floods_ingest
from .use_cases.new_york_energy import ingest as new_york_energy_ingest
Expand Down Expand Up @@ -54,26 +55,28 @@ def ingest_file(file_info, index=0, dataset=None, chart=None):
new_file_item.file.save(file_path, ContentFile(f.read()))


def ingest_contexts(use_case):
context_file_path = USE_CASE_FOLDER / use_case / 'contexts.json'
if context_file_path.exists():
print('Creating Context objects...')
with open(context_file_path) as contexts_json:
data = json.load(contexts_json)
for context in data:
print('\t- ', context['name'])
existing = Context.objects.filter(name=context['name'])
if existing.count():
context_for_setting = existing.first()
else:
context_for_setting = Context.objects.create(
name=context['name'],
default_map_center=Point(*context['default_map_center']),
default_map_zoom=context['default_map_zoom'],
)
print('\t', f'Context {context_for_setting.name} created.')
def ingest_projects(use_case):
project_file_path = USE_CASE_FOLDER / use_case / 'projects.json'
if not project_file_path.exists():
return

print('Creating Project objects...')
with open(project_file_path) as projects_json:
data = json.load(projects_json)
for project in data:
print('\t- ', project['name'])
project_for_setting, created = Project.objects.get_or_create(
name=project['name'],
defaults={
'default_map_center': Point(*project['default_map_center']),
'default_map_zoom': project['default_map_zoom'],
},
)
if created:
print('\t', f'Project {project_for_setting.name} created.')

context_for_setting.datasets.set(Dataset.objects.filter(name__in=context['datasets']))
project_for_setting.datasets.set(Dataset.objects.filter(name__in=project['datasets']))
project_for_setting.set_permissions(owner=User.objects.filter(is_superuser=True).first())


def ingest_charts(use_case):
Expand All @@ -91,7 +94,7 @@ def ingest_charts(use_case):
new_chart = Chart.objects.create(
name=chart['name'],
description=chart['description'],
context=Context.objects.get(name=chart['context']),
project=Project.objects.get(name=chart['project']),
chart_options=chart.get('chart_options'),
metadata=chart.get('metadata'),
editable=chart.get('editable', False),
Expand Down Expand Up @@ -162,5 +165,5 @@ def ingest_use_case(use_case_name, include_large=False, dataset_indexes=None):
include_large=include_large,
dataset_indexes=dataset_indexes,
)
ingest_contexts(use_case=use_case_name)
ingest_projects(use_case=use_case_name)
ingest_charts(use_case=use_case_name)
2 changes: 1 addition & 1 deletion sample_data/use_cases/boston_floods/charts.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "Boston Harbor Daily Tide Levels",
"description": "Raw data was obtained using the NOAA CO-OPS API for Data Retrieval and reformatted in tabular form",
"context": "Boston Transportation",
"project": "Boston Transportation",
"files": [
{
"url": "https://data.kitware.com/api/v1/item/64beb508b4d956782eee8cb1/download",
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
'django-configurations[database,email]==2.5.1',
'django-extensions==3.2.3',
'django-filter==24.3',
'django-guardian==2.4.0',
'django-oauth-toolkit==2.4.0',
'djangorestframework==3.15.2',
'django-large-image==0.10.0',
Expand Down
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ passenv =
DJANGO_MINIO_STORAGE_ACCESS_KEY
DJANGO_MINIO_STORAGE_ENDPOINT
DJANGO_MINIO_STORAGE_SECRET_KEY
DJANGO_HOMEPAGE_REDIRECT_URL
extras =
dev
deps =
Expand All @@ -66,6 +67,7 @@ passenv =
DJANGO_MINIO_STORAGE_ACCESS_KEY
DJANGO_MINIO_STORAGE_ENDPOINT
DJANGO_MINIO_STORAGE_SECRET_KEY
DJANGO_HOMEPAGE_REDIRECT_URL
extras =
dev
commands =
Expand Down
12 changes: 6 additions & 6 deletions uvdat/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

from uvdat.core.models import (
Chart,
Context,
Dataset,
DerivedRegion,
FileItem,
Network,
NetworkEdge,
NetworkNode,
Project,
RasterMapLayer,
SimulationResult,
SourceRegion,
Expand All @@ -17,7 +17,7 @@
)


class ContextAdmin(admin.ModelAdmin):
class ProjectAdmin(admin.ModelAdmin):
list_display = ['id', 'name']


Expand Down Expand Up @@ -72,10 +72,10 @@ def get_dataset_name(self, obj):


class DerivedRegionAdmin(admin.ModelAdmin):
list_display = ['id', 'name', 'get_context_name', 'operation', 'get_source_region_names']
list_display = ['id', 'name', 'get_project_name', 'operation', 'get_source_region_names']

def get_context_name(self, obj):
return obj.context.name
def get_project_name(self, obj):
return obj.project.name

def get_source_region_names(self, obj):
return ', '.join(r.name for r in obj.source_regions.all())
Expand Down Expand Up @@ -115,7 +115,7 @@ class SimulationResultAdmin(admin.ModelAdmin):
list_display = ['id', 'simulation_type', 'input_args']


admin.site.register(Context, ContextAdmin)
admin.site.register(Project, ProjectAdmin)
admin.site.register(Dataset, DatasetAdmin)
admin.site.register(FileItem, FileItemAdmin)
admin.site.register(Chart, ChartAdmin)
Expand Down
8 changes: 4 additions & 4 deletions uvdat/core/management/commands/load_roads.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ def add_arguments(self, parser):
type=str,
help='Target city to fetch roads from (e.g. "Boston, MA")',
)
parser.add_argument('--context_id', nargs='?', type=int, const=1)
parser.add_argument('--project_id', nargs='?', type=int, const=1)

def handle(self, *args, **kwargs):
city = kwargs['city']
context_id = kwargs['context_id']
print(f'Populating context {context_id} with roads for {city}...')
load_roads(context_id, city)
project_id = kwargs['project_id']
print(f'Populating project {project_id} with roads for {city}...')
load_roads(project_id, city)
38 changes: 38 additions & 0 deletions uvdat/core/management/commands/makeclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os

from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand, CommandError
from oauth2_provider.models import Application


class Command(BaseCommand):
help = 'Creates a client Application object for authentication purposes.'

def handle(self, **kwargs):
uri = os.environ.get('VUE_APP_BASE_URL')
client_id = os.environ.get('VUE_APP_OAUTH_CLIENT_ID')
if uri is None:
raise CommandError('Environment variable VUE_APP_BASE_URL is not set.')
if client_id is None:
raise CommandError('Environment variable VUE_APP_OAUTH_CLIENT_ID is not set.')

site = Site.objects.get_current() # type: ignore
site.domain = 'uvdat.demo'
site.name = 'UVDAT'
site.save()

_, created = Application.objects.get_or_create(
name='client-app',
defaults={
'redirect_uris': uri,
'client_id': client_id,
'client_type': 'public',
'authorization_grant_type': 'authorization-code',
'skip_authorization': True,
},
)
if not created:
raise CommandError(
'The client already exists. You can administer it from the admin console.'
)
self.stdout.write(self.style.SUCCESS('Client Application created.'))
80 changes: 80 additions & 0 deletions uvdat/core/migrations/0005_projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Generated by Django 5.0.7 on 2024-09-29 16:44

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


class Migration(migrations.Migration):

dependencies = [
('core', '0004_files_and_networks'),
]

operations = [
migrations.RenameModel(
old_name='Context',
new_name='Project',
),
migrations.AlterModelOptions(
name='project',
options={
'permissions': [
('owner', 'Can read, write, and delete'),
('collaborator', 'Can read and write'),
('follower', 'Can read'),
]
},
),
migrations.RemoveConstraint(
model_name='derivedregion',
name='unique-derived-region-name',
),
migrations.RemoveField(
model_name='chart',
name='context',
),
migrations.RemoveField(
model_name='derivedregion',
name='context',
),
migrations.RemoveField(
model_name='simulationresult',
name='context',
),
migrations.AddField(
model_name='chart',
name='project',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='charts',
to='core.project',
),
),
migrations.AddField(
model_name='derivedregion',
name='project',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='derived_regions',
to='core.project',
),
),
migrations.AddField(
model_name='simulationresult',
name='project',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='simulation_results',
to='core.project',
),
),
migrations.AddConstraint(
model_name='derivedregion',
constraint=models.UniqueConstraint(
fields=('project', 'name'), name='unique-derived-region-name'
),
),
]
4 changes: 2 additions & 2 deletions uvdat/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from .chart import Chart
from .context import Context
from .dataset import Dataset
from .file_item import FileItem
from .map_layers import RasterMapLayer, VectorFeature, VectorMapLayer
from .networks import Network, NetworkEdge, NetworkNode
from .project import Project
from .regions import DerivedRegion, SourceRegion
from .simulations import SimulationResult

__all__ = [
Chart,
Context,
Project,
Dataset,
FileItem,
RasterMapLayer,
Expand Down
7 changes: 2 additions & 5 deletions uvdat/core/models/chart.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
from django.db import models

from .context import Context
from .project import Project


class Chart(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True, blank=True)
context = models.ForeignKey(Context, on_delete=models.CASCADE, related_name='charts')
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='charts', null=True)
metadata = models.JSONField(blank=True, null=True)

chart_data = models.JSONField(blank=True, null=True)
chart_options = models.JSONField(blank=True, null=True)
editable = models.BooleanField(default=False)

def is_in_context(self, context_id):
return self.context.id == context_id

def spawn_conversion_task(
self,
conversion_options=None,
Expand Down
11 changes: 0 additions & 11 deletions uvdat/core/models/context.py

This file was deleted.

6 changes: 0 additions & 6 deletions uvdat/core/models/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ class DatasetType(models.TextChoices):
choices=DatasetType.choices,
)

def is_in_context(self, context_id):
from uvdat.core.models import Context

context = Context.objects.get(id=context_id)
return context.datasets.filter(id=self.id).exists()

def spawn_conversion_task(
self,
style_options=None,
Expand Down
Loading
Loading