Skip to content

Commit ad5eeeb

Browse files
authored
feat: update access requirements for exemplar identifications, WEB-939 (#591)
1 parent cf906d3 commit ad5eeeb

6 files changed

Lines changed: 106 additions & 13 deletions

File tree

lib/controllers/v2/exemplar_identifications_controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const util = require( "../../util" );
99

1010
const ExemplarIdentificationsController = class ExemplarIdentificationsController {
1111
static async search( req ) {
12-
if ( !req.userSession || ( req.userSession && !req.userSession.isAdmin ) ) {
12+
if ( !req.userSession?.canViewExemplarIdentifications( ) ) {
1313
throw util.httpError( 401, "Unauthorized" );
1414
}
1515
let response = await ExemplarIdentificationsController.resultsForRequest( req );

lib/models/observation_preload.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ const ObservationPreload = class ObservationPreload {
495495
await ESModel.fetchBelongsTo( identifications, Identification, {
496496
foreignKey: "id", source: { excludes: ["observation", "taxon", "current_taxon"] }, forObs: true
497497
} );
498-
if ( !req.userSession?.isAdmin ) {
498+
if ( !req.userSession?.canViewExemplarIdentifications( ) ) {
499499
_.each( obs, o => {
500500
o.identifications = _.filter( _.map( o.identifications, "identification" ), _.identity );
501501
} );

lib/models/user.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ const User = class User extends Model {
393393
.field( "users.site_id" )
394394
.field( "users.confirmed_at" )
395395
.field( "users.created_at" )
396+
.field( "users.test_groups" )
396397
.field( "places.name", "place_name" )
397398
.field( "places.ancestry", "place_ancestry" )
398399
.field( "preferences.value", "prefers_common_names" )
@@ -418,7 +419,8 @@ const User = class User extends Model {
418419
defaultsLoaded: true,
419420
prefersCommonNames: row.prefers_common_names !== "f",
420421
site_id: row.site_id,
421-
created_at: moment( row.created_at )
422+
created_at: moment( row.created_at ),
423+
test_groups: []
422424
};
423425
if ( row.locale ) { defaults.locale = row.locale; }
424426
if ( row.is_admin ) { defaults.isAdmin = true; }
@@ -433,6 +435,7 @@ const User = class User extends Model {
433435
? row.place_ancestry.split( "/" ).map( Number ) : []
434436
};
435437
}
438+
if ( row.test_groups ) { defaults.test_groups = row.test_groups.split( "|" ); }
436439
defaults.taxonNamePriorities = await User.taxonNamePriorities( userID );
437440
return defaults;
438441
}

lib/user_session.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ const UserSession = class UserSession {
4343
this.siteID = await User.siteID( this.user_id );
4444
return this.siteID;
4545
}
46+
47+
canViewExemplarIdentifications( ) {
48+
return this.isAdmin || (
49+
!_.isEmpty( this.test_groups )
50+
&& _.includes( this.test_groups, "helpful-id-tips-reviewer" )
51+
&& _.includes( this.test_groups, "helpful-id-tips" )
52+
);
53+
}
4654
};
4755

4856
module.exports = UserSession;

test/integration/v2/exemplar_identifications.js

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,68 @@
11
const { expect } = require( "chai" );
22
const request = require( "supertest" );
33
const jwt = require( "jsonwebtoken" );
4+
const sinon = require( "sinon" );
5+
const UserSession = require( "../../../lib/user_session" );
46
const config = require( "../../../config" );
57

68
describe( "ExemplarIdentifications", ( ) => {
79
describe( "search", ( ) => {
8-
const token = jwt.sign(
10+
const adminToken = jwt.sign(
911
{ user_id: 1 },
1012
config.jwtSecret || "secret",
1113
{ algorithm: "HS512" }
1214
);
15+
16+
const nonAdminToken = jwt.sign(
17+
{ user_id: 123 },
18+
config.jwtSecret || "secret",
19+
{ algorithm: "HS512" }
20+
);
21+
22+
it( "does not allow unauthenticated requests", function ( done ) {
23+
request( this.app ).get( "/v2/exemplar_identifications" )
24+
.set( "Content-Type", "application/json" )
25+
.expect( 401, done );
26+
} );
27+
28+
it( "does not allow non-admins that are not in relevant test groups", function ( done ) {
29+
request( this.app ).get( "/v2/exemplar_identifications" )
30+
.set( "Authorization", nonAdminToken )
31+
.set( "Content-Type", "application/json" )
32+
.expect( 401, done );
33+
} );
34+
35+
it( "does not allow non-admins that not in all relevant test groups", function ( done ) {
36+
sinon.stub( UserSession.prototype, "extend" )
37+
.callsFake( function ( ) {
38+
this.test_groups = ["helpful-id-tips"];
39+
} );
40+
request( this.app ).get( "/v2/exemplar_identifications" )
41+
.set( "Authorization", nonAdminToken )
42+
.set( "Content-Type", "application/json" )
43+
.expect( 401, ( ) => {
44+
UserSession.prototype.extend.restore( );
45+
done( );
46+
} );
47+
} );
48+
49+
it( "allows non-admins that are in relevant test groups", function ( done ) {
50+
sinon.stub( UserSession.prototype, "extend" )
51+
.callsFake( function ( ) {
52+
this.test_groups = ["helpful-id-tips-reviewer", "helpful-id-tips"];
53+
} );
54+
request( this.app ).get( "/v2/exemplar_identifications" )
55+
.set( "Authorization", nonAdminToken )
56+
.set( "Content-Type", "application/json" )
57+
.expect( 200, ( ) => {
58+
UserSession.prototype.extend.restore( );
59+
done( );
60+
} );
61+
} );
62+
1363
it( "returns JSON", function ( done ) {
1464
request( this.app ).get( "/v2/exemplar_identifications" )
15-
.set( "Authorization", token )
65+
.set( "Authorization", adminToken )
1666
.set( "Content-Type", "application/json" )
1767
.expect( "Content-Type", /json/ )
1868
.expect( res => {
@@ -25,7 +75,7 @@ describe( "ExemplarIdentifications", ( ) => {
2575

2676
it( "can include include category_counts", function ( done ) {
2777
request( this.app ).get( "/v2/exemplar_identifications?direct_taxon_id=3&include_category_counts=true" )
28-
.set( "Authorization", token )
78+
.set( "Authorization", adminToken )
2979
.set( "Content-Type", "application/json" )
3080
.expect( "Content-Type", /json/ )
3181
.expect( res => {
@@ -36,7 +86,7 @@ describe( "ExemplarIdentifications", ( ) => {
3686

3787
it( "can include include category_controlled_terms", function ( done ) {
3888
request( this.app ).get( "/v2/exemplar_identifications?include_category_controlled_terms=true" )
39-
.set( "Authorization", token )
89+
.set( "Authorization", adminToken )
4090
.set( "Content-Type", "application/json" )
4191
.expect( "Content-Type", /json/ )
4292
.expect( res => {
@@ -47,7 +97,7 @@ describe( "ExemplarIdentifications", ( ) => {
4797

4898
it( "can filter by upvoted", function ( done ) {
4999
request( this.app ).get( "/v2/exemplar_identifications?direct_taxon_id=3&upvoted=true&fields=all" )
50-
.set( "Authorization", token )
100+
.set( "Authorization", adminToken )
51101
.set( "Content-Type", "application/json" )
52102
.expect( "Content-Type", /json/ )
53103
.expect( res => {
@@ -60,7 +110,7 @@ describe( "ExemplarIdentifications", ( ) => {
60110

61111
it( "can filter by downvoted", function ( done ) {
62112
request( this.app ).get( "/v2/exemplar_identifications?direct_taxon_id=3&downvoted=true&fields=all" )
63-
.set( "Authorization", token )
113+
.set( "Authorization", adminToken )
64114
.set( "Content-Type", "application/json" )
65115
.expect( "Content-Type", /json/ )
66116
.expect( res => {
@@ -73,7 +123,7 @@ describe( "ExemplarIdentifications", ( ) => {
73123

74124
it( "can filter by nominated", function ( done ) {
75125
request( this.app ).get( "/v2/exemplar_identifications?direct_taxon_id=7&nominated=true&fields=all" )
76-
.set( "Authorization", token )
126+
.set( "Authorization", adminToken )
77127
.set( "Content-Type", "application/json" )
78128
.expect( "Content-Type", /json/ )
79129
.expect( res => {
@@ -86,7 +136,7 @@ describe( "ExemplarIdentifications", ( ) => {
86136

87137
it( "can filter by not nominated", function ( done ) {
88138
request( this.app ).get( "/v2/exemplar_identifications?direct_taxon_id=7&nominated=false&fields=all" )
89-
.set( "Authorization", token )
139+
.set( "Authorization", adminToken )
90140
.set( "Content-Type", "application/json" )
91141
.expect( "Content-Type", /json/ )
92142
.expect( res => {
@@ -99,7 +149,7 @@ describe( "ExemplarIdentifications", ( ) => {
99149

100150
it( "can filter by query", function ( done ) {
101151
request( this.app ).get( "/v2/exemplar_identifications?direct_taxon_id=7&q=unnominated&fields=all" )
102-
.set( "Authorization", token )
152+
.set( "Authorization", adminToken )
103153
.set( "Content-Type", "application/json" )
104154
.expect( "Content-Type", /json/ )
105155
.expect( res => {
@@ -112,7 +162,7 @@ describe( "ExemplarIdentifications", ( ) => {
112162

113163
it( "can filter by term_value_id", function ( done ) {
114164
request( this.app ).get( "/v2/exemplar_identifications?direct_taxon_id=3&term_value_id=2&fields=all" )
115-
.set( "Authorization", token )
165+
.set( "Authorization", adminToken )
116166
.set( "Content-Type", "application/json" )
117167
.expect( "Content-Type", /json/ )
118168
.expect( res => {

test/user_session.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const { expect } = require( "chai" );
2+
const UserSession = require( "../lib/user_session" );
3+
4+
describe( "UserSession", ( ) => {
5+
describe( "canViewExemplarIdentifications", ( ) => {
6+
it( "returns true for sessions in relevant test groups", ( ) => {
7+
const userSession = new UserSession( {
8+
test_groups: ["helpful-id-tips-reviewer", "helpful-id-tips"]
9+
} );
10+
expect( userSession.canViewExemplarIdentifications( ) ).to.be.true;
11+
} );
12+
13+
it( "returns true for admin sessions", ( ) => {
14+
const userSession = new UserSession( {
15+
isAdmin: true
16+
} );
17+
expect( userSession.canViewExemplarIdentifications( ) ).to.be.true;
18+
} );
19+
20+
it( "returns false for sessions not in all relevant test groups", ( ) => {
21+
const userSession = new UserSession( {
22+
test_groups: ["helpful-id-tips"]
23+
} );
24+
expect( userSession.canViewExemplarIdentifications( ) ).to.be.false;
25+
} );
26+
27+
it( "returns false for sessions not in test groups", ( ) => {
28+
const userSession = new UserSession( );
29+
expect( userSession.canViewExemplarIdentifications( ) ).to.be.false;
30+
} );
31+
} );
32+
} );

0 commit comments

Comments
 (0)