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

Bounding box queries can use private coordinates #68 #419

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
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