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

API authentication and security changes #635

Merged
merged 90 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
7d07e1f
style: Reorder functions in the module
Jun 25, 2024
60ab6a9
refactor: Moved non-view functions to viewer.utils
Jun 25, 2024
3cd3855
fix: Removed unused save_pdb_zip and minor refactoring
Jun 25, 2024
f0ba5c4
feat: Removed circular import
Jun 25, 2024
b8674dc
feat: Fix get_open_targets (also get_open_proposals now not _private_)
Jun 25, 2024
b71771c
feat: Fix get_open_proposals reference
Jun 25, 2024
ecc1c0a
refactor: ISpyB -> ISPyB
Jun 25, 2024
a3fe11c
docs: Updated for use of mixins
Jun 25, 2024
0ccbdc9
feat: More API security migrations
Jun 26, 2024
7c2487c
feat: More security migrations
Jun 26, 2024
21df3a4
feat: Security migrations for hotspots and hypothesis
Jun 26, 2024
cd6e19b
feat: More security fixes
Jun 26, 2024
0139073
feat: More security changes
Jun 27, 2024
0cc3380
feat: More security changes (and get_params -> get_img_from_smiles wi…
Jun 27, 2024
58738cd
fix: Attempt to fix calls to /xcdb/fragspect/ 500 errors
Jun 28, 2024
c021af2
feat: Another attempot to fix ISPyB
Jun 28, 2024
e89b2c7
feat: Use of new user_is_member_of_target()
Jun 28, 2024
956319a
feat: Experiment with validator
Jul 3, 2024
d4e8ced
feat: Better serializer log
Jul 3, 2024
97e32a5
feat: Even more work on the serializer
Jul 3, 2024
c0730fc
feat: Minor error message tweak
Jul 3, 2024
8bf2a8a
feat: Add support for TEST_RESTRICTED_TAS_LIST (#614)
alanbchristie Jul 4, 2024
e384f1a
target permission validation mixin pattern implemented for Pose
kaliif Jul 4, 2024
6756c47
Merge branch 'm2ms-1247-b-validate-mixin' into m2ms-1247-b
kaliif Jul 4, 2024
81f75ed
feat: Fix restricted logic
Jul 4, 2024
898bec9
most endpoints secured with VaildateTargetMixin
kaliif Jul 5, 2024
0c954ba
fix: Removed unused endpoint
Jul 8, 2024
ce8280f
fix: secure SessionActions serializer
kaliif Jul 8, 2024
1e27429
fix: Removed pset_download
Jul 8, 2024
4acd47a
fix: Design set upload now unsupported (404)
Jul 8, 2024
60689ac
fix: Snapshots now open again
Jul 8, 2024
5f067bf
fix: CompoundIdentifierTypeView & TagCategoryView now read-only views
Jul 8, 2024
d91bce8
fix: Discourse POST now requires login
Jul 8, 2024
5b1b63c
feat: User now needs to be a member of CSET target to download it
Jul 8, 2024
daa44f2
fix: secured TaskStatus endpoint
kaliif Jul 9, 2024
ecb42b4
feat: Removal of unsed xcdb app
Jul 9, 2024
f90b418
feat: Add log to use of dicttocsv
Jul 9, 2024
523ee4b
feat: More secure DictToCsv
Jul 9, 2024
d17fff1
feat: More consistent use of _ISPYB_SAFE_QUERY_SET
Jul 9, 2024
55dac2f
feat: Stricter UploadCSet class inheritance
Jul 9, 2024
d94ac6f
feat: Fix isort issues
Jul 9, 2024
2628927
feat: Fix ListAPIView
Jul 9, 2024
f93747b
feat: Remove references to xcdb
Jul 9, 2024
76fec3c
fix: secure UploadTaskView and ValidateTaskView
kaliif Jul 10, 2024
596e19f
Merge branch 'm2ms-1247-securing-tasks' into m2ms-1247-b
kaliif Jul 10, 2024
ca36c8d
Align 1247 with latest staging code (#616)
alanbchristie Jul 10, 2024
7a6ea19
Align 1247 from staging (#619)
alanbchristie Jul 10, 2024
699aa4a
Align 1247 with latest staging (#620)
alanbchristie Jul 10, 2024
47f2bc6
feat: Rstore CSetUpload post()
Jul 10, 2024
aa5c050
feat: Revert UploadCSet inheritance
Jul 10, 2024
3b2ab34
fix: Another attempt to fix UploadCSet
Jul 10, 2024
87c968d
fix: Another attmept to fix the view
Jul 10, 2024
05a3f12
fix: Anotehr attempt to get UploadCSet
Jul 10, 2024
35ea784
feat: Fix UploadCSet view
Jul 10, 2024
cdb8bc8
feat: Fix JobRequest GET (restrict to members of the project)
Jul 10, 2024
c2299d4
feat: Enhanced logging for membership check failures
Jul 11, 2024
c952183
docs: Improve docs relating to security
Jul 11, 2024
30f4e91
docs: Minor typo
Jul 11, 2024
f28b18a
fix: Remove TEST_RESTRICTED_TAS_LIST feature
Jul 12, 2024
508c38e
Align 1247 with staging (#621)
alanbchristie Jul 12, 2024
2c0c3bb
Align 1247 with latest staging (#623)
alanbchristie Jul 12, 2024
da938c0
fix: Attempt to fix PoseView (now Pose)
Jul 12, 2024
f402d1d
fix: Attempt to debug Pose failure
Jul 12, 2024
10aa1e9
fix: Another patch to Pose
Jul 12, 2024
248f638
fix: Fix log typo
Jul 12, 2024
a84aa6f
fix: Attempt to fix permission on create
Jul 12, 2024
7634d3d
fix: Fix for ValidateTargetMixin?
Jul 12, 2024
b18717d
fix: Better Mixin (renamed and copes with shortest filter string)
Jul 12, 2024
f2208e6
fix: Fix some project mixin views (includes some renaming)
Jul 15, 2024
556c9ef
refactor: View name consistency
Jul 15, 2024
fdd6bec
fix: Fix for targetdownload mixin (and extra log)
Jul 15, 2024
cc6ebb3
fix: Better file handling
Jul 15, 2024
663f2cc
fix: Now serches ExpUpload for first matching record
Jul 15, 2024
6edb2eb
fix: Better experiment download (use of only ExpUpload record)
Jul 15, 2024
43a6d83
fix: ExpDownload now inspects Project
Jul 15, 2024
f410b51
fix: More naming consistency changes
Jul 15, 2024
eebf7c9
fix: Attempt to fix 'ManyRelatedManager' is not iterable
Jul 15, 2024
bb67c17
fix: Use of correct download path
Jul 15, 2024
abec9ab
Align 1247 with staging (#624)
alanbchristie Jul 15, 2024
3d34254
Align 1247 with staging (#627)
alanbchristie Jul 22, 2024
a9f5e0f
docs: Tweak messages
Jul 22, 2024
af464ef
fix: Better file handling
Jul 22, 2024
fd8d90e
docs: Doc tweak
Jul 22, 2024
becb201
Merge compound fix to 1247 (#628)
alanbchristie Aug 1, 2024
b62f835
refactor: restrict_to_membership now restrict_pubic_to_membership
Aug 9, 2024
9ba331f
fix: ValidateProjectMixin does not insist on public proposal membersh…
Aug 9, 2024
96fb1cb
fix: Apply conflict from staging
Aug 9, 2024
509d4df
Align 1247 with staging (#631)
alanbchristie Aug 9, 2024
c35c695
fix: Fix project_id type (aligns with staging)
Aug 16, 2024
bf99cad
Align 1247 with staging (#633)
alanbchristie Aug 16, 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
11 changes: 5 additions & 6 deletions .github/workflows/build-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,11 @@ jobs:
with:
context: .
tags: ${{ steps.vars.outputs.BE_NAMESPACE }}/fragalysis-backend:${{ env.GITHUB_REF_SLUG }}
- name: Test
run: >
docker-compose -f docker-compose.test.yml up
--build
--exit-code-from tests
--abort-on-container-exit
- name: Test (docker compose)
uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: ./docker-compose.test.yml
up-flags: --build --exit-code-from tests --abort-on-container-exit
env:
BE_NAMESPACE: ${{ steps.vars.outputs.BE_NAMESPACE }}
BE_IMAGE_TAG: ${{ env.GITHUB_REF_SLUG }}
Expand Down
11 changes: 5 additions & 6 deletions .github/workflows/build-production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,11 @@ jobs:
tags: |
${{ steps.vars.outputs.BE_NAMESPACE }}/fragalysis-backend:${{ steps.vars.outputs.tag }}
${{ steps.vars.outputs.BE_NAMESPACE }}/fragalysis-backend:stable
- name: Test
run: >
docker-compose -f docker-compose.test.yml up
--build
--exit-code-from tests
--abort-on-container-exit
- name: Test (docker compose)
uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: ./docker-compose.test.yml
up-flags: --build --exit-code-from tests --abort-on-container-exit
env:
BE_NAMESPACE: ${{ steps.vars.outputs.BE_NAMESPACE }}
BE_IMAGE_TAG: ${{ steps.vars.outputs.tag }}
Expand Down
11 changes: 5 additions & 6 deletions .github/workflows/build-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,11 @@ jobs:
with:
context: .
tags: ${{ steps.vars.outputs.BE_NAMESPACE }}/fragalysis-backend:${{ steps.vars.outputs.tag }}
- name: Test
run: >
docker-compose -f docker-compose.test.yml up
--build
--exit-code-from tests
--abort-on-container-exit
- name: Test (docker compose)
uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: ./docker-compose.test.yml
up-flags: --build --exit-code-from tests --abort-on-container-exit
env:
BE_NAMESPACE: ${{ steps.vars.outputs.BE_NAMESPACE }}
BE_IMAGE_TAG: ${{ steps.vars.outputs.tag }}
Expand Down
70 changes: 51 additions & 19 deletions api/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def ping_configured_connector() -> bool:
return conn is not None


class ISpyBSafeQuerySet(viewsets.ReadOnlyModelViewSet):
class ISPyBSafeQuerySet(viewsets.ReadOnlyModelViewSet):
"""
This ISpyBSafeQuerySet, which inherits from the DRF viewsets.ReadOnlyModelViewSet,
is used for all views that need to yield (filter) view objects based on a
Expand Down Expand Up @@ -216,7 +216,7 @@ def get_queryset(self):
q_filter = self._get_q_filter(proposal_list)
return self.queryset.filter(q_filter).distinct()

def _get_open_proposals(self):
def get_open_proposals(self):
"""
Returns the set of proposals anybody can access.
These consist of any Projects that are marked "open_to_public"
Expand All @@ -226,6 +226,7 @@ def _get_open_proposals(self):
Project.objects.filter(open_to_public=True).values_list("title", flat=True)
)
open_proposals.update(settings.PUBLIC_TAS_LIST)
# End Temporary Test Code (1247)
return open_proposals

def _get_proposals_for_user_from_django(self, user):
Expand Down Expand Up @@ -341,36 +342,67 @@ def _get_proposals_from_connector(self, user, conn):
)
CachedContent.set_content(user.username, prop_id_set)

def user_is_member_of_any_given_proposals(self, user, proposals):
def user_is_member_of_target(
self, user, target, restrict_public_to_membership=True
):
"""
Returns true if the user has access to any proposal the target belongs to.
"""
target_proposals = [p.title for p in target.project_id.all()]
user_proposals = self.get_proposals_for_user(
user, restrict_public_to_membership=restrict_public_to_membership
)
is_member = any(proposal in user_proposals for proposal in target_proposals)
if not is_member:
logger.warning(
"Failed membership check user='%s' target='%s' target_proposals=%s",
user.username,
target.title,
target_proposals,
)
return is_member

def user_is_member_of_any_given_proposals(
self, user, proposals, restrict_public_to_membership=True
):
"""
Returns true if the user has access to any proposal in the given
proposals list.Only one needs to match for permission to be granted.
We 'restrict_to_membership' to only consider proposals the user
proposals list. Only one needs to match for permission to be granted.
We 'restrict_public_to_membership' to only consider proposals the user
has explicit membership.
"""
user_proposals = self.get_proposals_for_user(user, restrict_to_membership=True)
return any(proposal in user_proposals for proposal in proposals)
user_proposals = self.get_proposals_for_user(
user, restrict_public_to_membership=restrict_public_to_membership
)
is_member = any(proposal in user_proposals for proposal in proposals)
if not is_member:
logger.warning(
"Failed membership check user='%s' proposals=%s",
user.username,
proposals,
)
return is_member

def get_proposals_for_user(self, user, restrict_to_membership=False):
def get_proposals_for_user(self, user, restrict_public_to_membership=False):
"""
Returns a list of proposals that the user has access to.

If 'restrict_to_membership' is set only those proposals/visits where the user
If 'restrict_public_to_membership' is set only those proposals/visits where the user
is a member of the visit will be returned. Otherwise the 'public'
proposals/visits will also be returned. Typically 'restrict_to_membership' is
proposals/visits will also be returned. Typically 'restrict_public_to_membership' is
used for uploads/changes - this allows us to implement logic that (say)
only permits explicit members of public proposals to add/load data for that
project (restrict_to_membership=True), but everyone can 'see' public data
(restrict_to_membership=False).
project (restrict_public_to_membership=True), but everyone can 'see' public data
(restrict_public_to_membership=False).
"""
assert user

proposals = set()
ispyb_user = settings.ISPYB_USER
logger.debug(
"ispyb_user=%s restrict_to_membership=%s (DISABLE_RESTRICT_PROPOSALS_TO_MEMBERSHIP=%s)",
"ispyb_user=%s restrict_public_to_membership=%s (DISABLE_RESTRICT_PROPOSALS_TO_MEMBERSHIP=%s)",
ispyb_user,
restrict_to_membership,
restrict_public_to_membership,
settings.DISABLE_RESTRICT_PROPOSALS_TO_MEMBERSHIP,
)
if ispyb_user:
Expand All @@ -384,10 +416,10 @@ def get_proposals_for_user(self, user, restrict_to_membership=False):
# We have all the proposals where the user has authority.
# Add open/public proposals?
if (
not restrict_to_membership
not restrict_public_to_membership
or settings.DISABLE_RESTRICT_PROPOSALS_TO_MEMBERSHIP
):
proposals.update(self._get_open_proposals())
proposals.update(self.get_open_proposals())

# Return the set() as a list()
return list(proposals)
Expand All @@ -411,9 +443,9 @@ def _get_q_filter(self, proposal_list):
return Q(title__in=proposal_list) | Q(open_to_public=True)


class ISpyBSafeStaticFiles:
class ISPyBSafeStaticFiles:
def get_queryset(self):
query = ISpyBSafeQuerySet()
query = ISPyBSafeQuerySet()
query.request = self.request
query.filter_permissions = self.permission_string
query.queryset = self.model.objects.filter()
Expand Down Expand Up @@ -459,7 +491,7 @@ def get_response(self):
raise Http404 from exc


class ISpyBSafeStaticFiles2(ISpyBSafeStaticFiles):
class ISPyBSafeStaticFiles2(ISPyBSafeStaticFiles):
def get_response(self):
logger.info("+ get_response called with: %s", self.input_string)
# it wasn't working because found two objects with test file name
Expand Down
31 changes: 14 additions & 17 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@
from rest_framework.authtoken import views as drf_views
from rest_framework.routers import DefaultRouter

# from xcdb import views as xchem_views
from hotspots import views as hostpot_views
from hypothesis import views as hypo_views
from scoring import views as score_views
from viewer import views as viewer_views
from xcdb import views as xcdb_views

router = DefaultRouter()
# Register the basic data
router.register("compounds", viewer_views.CompoundView, "compounds")
router.register("targets", viewer_views.TargetView, "targets")
router.register("projects", viewer_views.ProjectView)
router.register("session-projects", viewer_views.SessionProjectsView)
router.register("snapshots", viewer_views.SnapshotsView)
router.register("session-projects", viewer_views.SessionProjectView)
router.register("snapshots", viewer_views.SnapshotView)
router.register("action-type", viewer_views.ActionTypeView)
router.register("session-actions", viewer_views.SessionActionsView)
router.register("snapshot-actions", viewer_views.SnapshotActionsView)
Expand All @@ -26,7 +24,7 @@
# Compounds sets
router.register("compound-sets", viewer_views.ComputedSetView)
router.register("compound-molecules", viewer_views.ComputedMoleculesView)
router.register("numerical-scores", viewer_views.NumericalScoresView)
router.register("numerical-scores", viewer_views.NumericalScoreValuesView)
router.register("text-scores", viewer_views.TextScoresView)
router.register("compound-scores", viewer_views.CompoundScoresView, "compound-scores")
router.register(
Expand Down Expand Up @@ -65,16 +63,13 @@
# Get the information
router.register("siteobservationannotation", score_views.SiteObservationAnnotationView)

# fragspect
router.register("fragspect", xcdb_views.FragspectCrystalView)

# discourse posts
router.register(
"discourse_post", viewer_views.DiscoursePostView, basename='discourse_post'
)

# Take a dictionary and return a csv
router.register("dicttocsv", viewer_views.DictToCsv, basename='dicttocsv')
router.register("dicttocsv", viewer_views.DictToCSVView, basename='dicttocsv')

# tags
router.register("tag_category", viewer_views.TagCategoryView, basename='tag_category')
Expand All @@ -92,36 +87,38 @@
# Download a zip file of the requested contents
router.register(
"download_structures",
viewer_views.DownloadStructures,
viewer_views.DownloadStructuresView,
basename='download_structures',
)

# Experiments and Experiment (XChemAlign) upload support
router.register(
"upload_target_experiments",
viewer_views.UploadTargetExperiments,
viewer_views.UploadExperimentUploadView,
basename='upload_target_experiments',
)
router.register(
"download_target_experiments",
viewer_views.DownloadTargetExperiments,
viewer_views.DownloadExperimentUploadView,
basename='download_target_experiments',
)


router.register(
"target_experiment_uploads",
viewer_views.TargetExperimentUploads,
viewer_views.ExperimentUploadView,
basename='target_experiment_uploads',
)
router.register(
"site_observations", viewer_views.SiteObservations, basename='site_observations'
"site_observations", viewer_views.SiteObservationView, basename='site_observations'
)
router.register("canon_sites", viewer_views.CanonSiteView, basename='canon_sites')
router.register(
"canon_site_confs", viewer_views.CanonSiteConfView, basename='canon_site_confs'
)
router.register("canon_sites", viewer_views.CanonSites, basename='canon_sites')
router.register(
"canon_site_confs", viewer_views.CanonSiteConfs, basename='canon_site_confs'
"xtalform_sites", viewer_views.XtalformSiteView, basename='xtalform_sites'
)
router.register("xtalform_sites", viewer_views.XtalformSites, basename='xtalform_sites')
router.register("poses", viewer_views.PoseView, basename='poses')

# Squonk Jobs
Expand Down
12 changes: 4 additions & 8 deletions api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,20 +305,16 @@ def parse_xenons(input_smi):
return bond_ids, bond_colours, e_mol.GetMol()


def get_params(smiles, request):
def get_img_from_smiles(smiles, request):
# try:
smiles = canon_input(smiles)
# except:
# smiles = ""
height = None
mol = None
bond_id_list = []
highlightBondColors = {}
if "height" in request.GET:
height = int(request.GET["height"])
width = None
if "width" in request.GET:
width = int(request.GET["width"])
height = int(request.GET.get("height", "128"))
width = int(request.GET.get("width", "128"))
if "atom_indices" in request.GET:
mol = Chem.MolFromSmiles(smiles)
bond_id_list, highlightBondColors, mol = parse_atom_ids(
Expand Down Expand Up @@ -360,7 +356,7 @@ def get_highlighted_diffs(request):
def mol_view(request):
if "smiles" in request.GET:
smiles = request.GET["smiles"].rstrip(".svg")
return get_params(smiles, request)
return get_img_from_smiles(smiles, request)
else:
return HttpResponse("Please insert SMILES")

Expand Down
Loading