From 693f272609cc4a08140b42217215a739c1a91dc0 Mon Sep 17 00:00:00 2001 From: JasonWildMe Date: Wed, 24 Jun 2026 07:59:27 -0400 Subject: [PATCH] Enable GET /api/v3/individuals/{id} with proper view ACL The /api/v3/individuals/* path was already mapped to the BaseObject servlet and MarkedIndividual already overrode getById(), but the endpoint never worked: - Base.getByClassnameAndId() (the dispatch used by processGet) handled only encounters and annotations, so an individual GET fell through to return null -> 404. - MarkedIndividual did not override canUserView(User, Shepherd), so Base.jsonForApiGet()'s ACL gate used the Base default (false) and would have returned 401 for every caller even once the object resolved. Add the individuals dispatch case, add a MarkedIndividual.canUserView() that grants access to admins or users who can view at least one of the individual's encounters (no view grant for an encounter-less individual), and guard processGet against a missing id segment so /api/v3/individuals returns 404 rather than a generic 500. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/java/org/ecocean/Base.java | 3 +++ src/main/java/org/ecocean/MarkedIndividual.java | 14 ++++++++++++++ src/main/java/org/ecocean/api/BaseObject.java | 3 ++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/Base.java b/src/main/java/org/ecocean/Base.java index 648279c74c..e778996199 100644 --- a/src/main/java/org/ecocean/Base.java +++ b/src/main/java/org/ecocean/Base.java @@ -331,6 +331,9 @@ public static Base getByClassnameAndId(Shepherd myShepherd, String className, St case "annotations": tmp = new Annotation(); break; + case "individuals": + tmp = new MarkedIndividual(); + break; default: return null; } diff --git a/src/main/java/org/ecocean/MarkedIndividual.java b/src/main/java/org/ecocean/MarkedIndividual.java index 3e2e0d024b..129701043c 100644 --- a/src/main/java/org/ecocean/MarkedIndividual.java +++ b/src/main/java/org/ecocean/MarkedIndividual.java @@ -2122,6 +2122,20 @@ public boolean canUserAccess(HttpServletRequest request) { return Collaboration.canUserAccessMarkedIndividual(this, request); } + // view ACL for the generic /api/v3 GET path (see Base.jsonForApiGet). a user may view this + // individual if they are an admin or can view at least one of its encounters. an individual + // with no encounters grants no view access (rather than being visible to any logged-in user). + @Override public boolean canUserView(User user, Shepherd myShepherd) { + if (user == null) return false; + if (user.isAdmin(myShepherd)) return true; + Vector all = this.getEncounters(); + if ((all == null) || all.isEmpty()) return false; + for (Encounter enc : all) { + if (enc.canUserView(user, myShepherd)) return true; + } + return false; + } + // see note on Base class public List userIdsWithViewAccess(Shepherd myShepherd) { List ids = new ArrayList(); diff --git a/src/main/java/org/ecocean/api/BaseObject.java b/src/main/java/org/ecocean/api/BaseObject.java index bfda345ed9..81a2ffc916 100644 --- a/src/main/java/org/ecocean/api/BaseObject.java +++ b/src/main/java/org/ecocean/api/BaseObject.java @@ -250,7 +250,8 @@ protected JSONObject processGet(HttpServletRequest request, String[] args) try { User currentUser = myShepherd.getUser(request); Base obj = null; - if (args.length > 0) obj = Base.getByClassnameAndId(myShepherd, args[0], args[1]); + if ((args.length > 1) && Util.stringExists(args[1])) + obj = Base.getByClassnameAndId(myShepherd, args[0], args[1]); if (obj == null) { rtn.put("statusCode", 404); rtn.put("error", "not found");