Skip to content

Commit

Permalink
bounding box queries can use private coordinates #68; refactor filter…
Browse files Browse the repository at this point in the history
…s based on project trust
  • Loading branch information
pleary committed Jan 3, 2024
1 parent 3a81f41 commit f7cf9cc
Show file tree
Hide file tree
Showing 3 changed files with 768 additions and 154 deletions.
333 changes: 189 additions & 144 deletions lib/models/observation_query_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,145 +47,202 @@ ObservationQueryBuilder.applyLookupRules = async req => {
}
};

// Builds place filters for an authenticated user that is filtering by place.
// Massive complexity brought to you by trusting collection projects
ObservationQueryBuilder.placeFilterForUser = async ( req, params ) => {
const publicPlaceFilter = esClient.termFilter( "place_ids.keyword", params.place_id );
const privatePlaceFilter = esClient.termFilter( "private_place_ids.keyword", params.place_id );
// Current user should see obs that match the regular place filter OR obs
// that they created and have private coordinates that fall in the query
// place
const placeFilterForUser = {
// Given a public and private version of a filter, ensure that only users with
// permission to view private coordinates in the given context will use them,
// and all other user contexts will use the public filter
ObservationQueryBuilder.locationBasedFilterForUser = async ( req, pubicFilter, privateFilter ) => {
const {
usersTrustingForAny,
usersTrustingForTaxon
} = await ObservationQueryBuilder.contextTrustingUsers( req );

const filterConditions = [];
// there are users that trust the logged-in user to view private coordinates
// of all observations, so include include the private filter with those users
if ( !_.isEmpty( usersTrustingForAny ) ) {
filterConditions.push( {
bool: {
must: [
privateFilter,
{ terms: { "user.id.keyword": usersTrustingForAny } }
]
}
} );
}

// there are users that trust the logged-in users to view private coordinates
// of observations obscured due to taxon geoprivacy, so include the private
// filter with those users with the additional taxon_geoprivacy component
if ( !_.isEmpty( usersTrustingForTaxon ) ) {
filterConditions.push( {
bool: {
must: [
privateFilter,
// we are focusing only on obs obscured by taxon geoprivacy...
{ terms: { taxon_geoprivacy: ["obscured", "private"] } },
{ terms: { "user.id.keyword": usersTrustingForTaxon } }
],
// ... but not those obscured by personal geoprivacy
must_not: [
{ exists: { field: "geoprivacy" } }
]
}
} );
}

// no users trust the logged-in user in this context, so only use the public filter
if ( _.isEmpty( filterConditions ) ) {
return pubicFilter;
}

// include the public filter with the trusted private filters
filterConditions.push( pubicFilter );
return {
bool: {
should: [
publicPlaceFilter,
{
bool: {
must: [
privatePlaceFilter,
{ term: { "user.id.keyword": req.userSession.user_id } }
]
}
}
]
should: filterConditions
}
};
// req._collectionProject is a special attribute set when we are getting an
// obs query from collection project search params. See
// projectRulesQueryFilters
// If we're not doing complex project logic, just return that relatively
// simple filter for the signed in user
if ( !req._collectionProject ) {
return placeFilterForUser;
}
// We are building a query in the context of an umbrella project, but the umbrella
// hasn't enabled trusting, so skip the complex logic
if ( req._umbrellaProject && !req._umbrellaProject.prefers_user_trust ) {
return placeFilterForUser;
}
// We are building a query in the context of an collection project, but the collection
// hasn't enabled trusting, so skip the complex logic
if ( !req._collectionProject.prefers_user_trust && !req._umbrellaProject ) {
return placeFilterForUser;
}
// If we're filtering for a project, reset to the public filter and grant
// allowances based on curatorship and trusting member status
placeFilterForUser.bool.should = [publicPlaceFilter];
const usersTrustingProjectForAny = await ProjectUser.usersTrustingProjectFor(
req._collectionProject.id, "any"
};

// Builds a bounding box filter for the current user and context
ObservationQueryBuilder.boundsFilterForUser = async ( req, params ) => {
if ( !( params.nelat || params.nelng || params.swlat || params.swlng ) ) {
return null;
}
const bounds = {
nelat: params.nelat,
nelng: params.nelng,
swlat: params.swlat,
swlng: params.swlng
};
const publicEnvelopeFilter = esClient.envelopeFilter( {
envelope: {
geojson: bounds
}
} );
const privateEnvelopeFilter = esClient.envelopeFilter( {
envelope: {
private_geojson: bounds
}
} );
return ObservationQueryBuilder.locationBasedFilterForUser(
req, publicEnvelopeFilter, privateEnvelopeFilter
);
const usersTrustingProjectForTaxon = await ProjectUser.usersTrustingProjectFor(
req._collectionProject.id, "taxon"
};

// Builds a place filter fort the current user and context
ObservationQueryBuilder.placeFilterForUser = async ( req, params ) => {
if ( !params.place_id || params.place_id === "any" ) {
return null;
}

const publicPlaceFilter = esClient.termFilter( "place_ids.keyword", params.place_id );
const privatePlaceFilter = esClient.termFilter( "private_place_ids.keyword", params.place_id );
return ObservationQueryBuilder.locationBasedFilterForUser(
req, publicPlaceFilter, privatePlaceFilter
);
let usersTrustingForTaxon = usersTrustingProjectForTaxon;
let usersTrustingForAny = usersTrustingProjectForAny;
};

// The overall intent here is
// 1. To grant project curators access to obs by trusting users based on private
// coordinates
// 2. To allow project members to see which observations are "in" or "out" of
// the project based on their trusting status
// We are trying to avoid a situation where a non-member views a project and
// sees their own obscured obs included based on the private coordinates,
// which they might see in an ordinary place search b/c they have permission
// to see their own private stuff, but might be alarmed to see it in the
// context of a project they don't trust. We don't want them to think the
// project curators are seeing that too, b/c they're not.

// Returns users that trust the logged-in user to view private coordinates of
// their observations in the current query context. Users may trust viewers
// for all obscured coordinates, or only for coordinates obscured due to taxon
// geoprivacy. Massive complexity brought to you by trusting collection projects
ObservationQueryBuilder.contextTrustingUsers = async req => {
if ( !req?.userSession?.user_id ) {
return {
usersTrustingForAny: [],
usersTrustingForTaxon: []
};
}

// req._collectionProject and req._umbrellaProject are a special attributes
// set when we are getting an obs query from collection/umbrella project
// search params. See projectRulesQueryFilters.
// When we are building a query in the context of an umbrella project, add the
// users who trust the umbrella. So if I trust Umbrella 1 which contains
// Collection 1 but I don't trust Collection 1, show my obscured obs in
// queries for Umbrella 1 for curators of Umbrella 1
if ( req._umbrellaProject ) {
const usersTrustingUmbrellaProjectForTaxon = await ProjectUser.usersTrustingProjectFor(
req._umbrellaProject.id, "taxon"
);
usersTrustingForTaxon = usersTrustingProjectForTaxon.concat(
usersTrustingUmbrellaProjectForTaxon
);
const usersTrustingUmbrellaProjectForAny = await ProjectUser.usersTrustingProjectFor(
req._umbrellaProject.id, "any"
);
usersTrustingForAny = usersTrustingProjectForAny.concat(
usersTrustingUmbrellaProjectForAny
);
const projectContext = req._umbrellaProject || req._collectionProject;

// If we are not building in the context of a project, then ensure that
// the logged-in user trusts themselves in all cases, and trusts no one else
if ( !projectContext ) {
return {
usersTrustingForAny: [req.userSession.user_id],
usersTrustingForTaxon: []
};
}
const curatedProjectsIDs = await req.userSession.getCuratedProjectsIDs( );
const viewerCuratesProject = curatedProjectsIDs
&& curatedProjectsIDs.indexOf( req._collectionProject.id ) >= 0;
const viewerTrustsProjectForAny = usersTrustingForAny.includes(
req.userSession.user_id

// If we are not building in the context of a project that has enabled
// trusting, then no trusting should be allowed, even for the logged-in user.
// For example, if the user is viewing in the context of a project but has
// not allowed the project to access their private coordinates, that user
// should also not see their observations' private coordinates when viewing
// that project
if ( !projectContext.prefers_user_trust ) {
return {
usersTrustingForAny: [],
usersTrustingForTaxon: []
};
}

// Look up users that trust this project with all coordinates, and that trust
// the project for only coordinates obscured due to taxon geoprivacy
const usersTrustingProjectForAny = await ProjectUser.usersTrustingProjectFor(
projectContext.id, "any"
);
const viewerTrustsProjectForTaxon = usersTrustingForTaxon.includes(
req.userSession.user_id
const usersTrustingProjectForTaxon = await ProjectUser.usersTrustingProjectFor(
projectContext.id, "taxon"
);
// The overall intent here is
// 1. To grant project curators access to obs by trusting users based on private coordinates
// 2. To allow project members to see which observations are "in" or "out" of the project based
// on their trusting status
// We are trying to avoid a situation where a non-member views a project and
// sees their own obscured obs included based on the private coordinates,
// which they might see in an ordinary place search b/c they have permission
// to see their own private stuff, but might be alarmed to see it in the
// context of a project they don't trust. We don't want them to think the
// project curators are seeing that too, b/c they're not.
if ( usersTrustingForAny.length > 0 ) {
let userFilterForAny;
if ( viewerCuratesProject ) {
// If the current user curates the specified collection project, they
// should also see observations in that project by all project members who
// trust the project with all coordinates
userFilterForAny = { terms: { "user.id.keyword": usersTrustingForAny } };
} else if ( viewerTrustsProjectForAny ) {
// If the viewer trusts the project for any, they should see only their
// own obscured obs in the project
userFilterForAny = { term: { "user.id.keyword": req.userSession.user_id } };
}
if ( userFilterForAny ) {
placeFilterForUser.bool.should.push( {
bool: {
must: [
privatePlaceFilter,
userFilterForAny
]
}
} );
}

// Check to see if the logged-in user curates the project
const curatedProjectsIDs = await req.userSession.getCuratedProjectsIDs( );
const viewerCuratesProject = curatedProjectsIDs
&& curatedProjectsIDs.indexOf( projectContext.id ) >= 0;

if ( viewerCuratesProject ) {
// the logged-in user is a curator, so they are trusted by users who have
// opted-in to trusting this project with either all coordinates, or just
// those due to taxon geoprivacy
return {
usersTrustingForAny: usersTrustingProjectForAny,
usersTrustingForTaxon: usersTrustingProjectForTaxon
};
}
// Query logic for taxon-only trust is even more complicated...
if ( usersTrustingForTaxon.length > 0 ) {
let userFilterForTaxon;
// Viewer permissions largeley the same as above
if ( viewerCuratesProject ) {
userFilterForTaxon = { terms: { "user.id.keyword": usersTrustingForTaxon } };
} else if ( viewerTrustsProjectForTaxon ) {
userFilterForTaxon = { term: { "user.id.keyword": req.userSession.user_id } };
}
if ( userFilterForTaxon ) {
placeFilterForUser.bool.should.push( {
bool: {
must: [
privatePlaceFilter,
// taxon-only means we are focusing only on obs obscured by taxon geoprivacy...
{ terms: { taxon_geoprivacy: ["obscured", "private"] } },
userFilterForTaxon
],
// ... but not those obscured by personal geoprivacy
must_not: [
{ exists: { field: "geoprivacy" } }
]
}
} );
}

// the logged-in user is not a curator, and trusts the project with all coords
if ( usersTrustingProjectForAny.includes( req.userSession.user_id ) ) {
return {
usersTrustingForAny: [req.userSession.user_id],
usersTrustingForTaxon: []
};
}

// the logged-in user is not a curator, and trusts the project with taxon coords
if ( usersTrustingProjectForTaxon.includes( req.userSession.user_id ) ) {
return {
usersTrustingForAny: [],
usersTrustingForTaxon: [req.userSession.user_id]
};
}
return placeFilterForUser;

// the logged-in user is not a curator, and does not trust the project with any coords
return {
usersTrustingForAny: [],
usersTrustingForTaxon: []
};
};

ObservationQueryBuilder.reqToElasticQueryComponents = async req => {
Expand Down Expand Up @@ -692,16 +749,10 @@ ObservationQueryBuilder.reqToElasticQueryComponents = async req => {
}

if ( params.nelat || params.nelng || params.swlat || params.swlng ) {
searchFilters.push( {
envelope: {
geojson: {
nelat: params.nelat,
nelng: params.nelng,
swlat: params.swlat,
swlng: params.swlng
}
}
} );
const boundsFilterForUser = await ObservationQueryBuilder.boundsFilterForUser( req, params );
if ( !_.isEmpty( boundsFilterForUser ) ) {
searchFilters.push( boundsFilterForUser );
}
}

if ( params.lat && params.lng ) {
Expand Down Expand Up @@ -951,15 +1002,9 @@ ObservationQueryBuilder.reqToElasticQueryComponents = async req => {
// Set the place filters, which gets REALLY complicated when trying to decide
// when to search on private places or not
if ( params.place_id && params.place_id !== "any" ) {
// This is the basic filter of places everyone should see
const publicPlaceFilter = esClient.termFilter( "place_ids.keyword", params.place_id );
if ( req.userSession ) {
const placeFilterForUser = await ObservationQueryBuilder.placeFilterForUser( req, params );
if ( !_.isEmpty( placeFilterForUser ) ) {
searchFilters.push( placeFilterForUser );
}
} else {
searchFilters.push( publicPlaceFilter );
const placeFilterForUser = await ObservationQueryBuilder.placeFilterForUser( req, params );
if ( !_.isEmpty( placeFilterForUser ) ) {
searchFilters.push( placeFilterForUser );
}
}

Expand Down
Loading

0 comments on commit f7cf9cc

Please sign in to comment.