/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.usergrid.rest.applications.collection.users;
import net.jcip.annotations.NotThreadSafe;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.usergrid.rest.test.resource.AbstractRestIT;
import org.apache.usergrid.rest.test.resource.model.*;
import org.apache.usergrid.utils.UUIDUtils;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.junit.Assert.*;
/**
* Tests permissions of adding and removing users from roles as well as groups.
*/
@NotThreadSafe
public class PermissionsResourceIT extends AbstractRestIT {
private static final Logger logger = LoggerFactory.getLogger(PermissionsResourceIT.class);
private static final String ROLE = "permtestrole";
private static final String USER = "edanuff";
private User user;
public PermissionsResourceIT() throws Exception {
}
/**
* Creates a user in the default org/app combo for use by all of the tests
*/
@Before
public void setup(){
user = new User(USER,USER,USER+"@apigee.com","password");
user = new User( this.app().collection("users").post(user));
waitForQueueDrainAndRefreshIndex();
}
/**
* Tests that we can delete a role that is in use by a user.
* @throws IOException
*/
@Test
public void deleteUserFromRole() throws IOException {
//Create a role and check the response
Entity data = new Entity().chainPut("name", ROLE);
Entity node = this.app().collection("roles").post(data);
assertNull(node.get("error"));
assertEquals( ROLE, node.get("name").toString() );
waitForQueueDrainAndRefreshIndex();
//Post the user with a specific role into the users collection
node = this.app().collection("roles").entity(node).collection("users").entity(USER).post();
assertNull( node.get( "error" ) );
waitForQueueDrainAndRefreshIndex();
// now check the user has the role
node = this.app().collection("users").entity(USER).collection("roles").entity(ROLE).get();
// check if the role was assigned
assertEquals( ROLE, node.get("name").toString() );
// now delete the role
this.app().collection("users").entity(USER).collection("roles").entity(ROLE).delete();
waitForQueueDrainAndRefreshIndex();
// check if the role was deleted
int status = 0;
try {
node = this.app().collection("users").entity(USER).collection("roles").entity(ROLE).get();
}catch (ClientErrorException e){
status = e.getResponse().getStatus();
}
// check if the role was assigned
assertEquals(404, status);
}
/**
* Deletes a user from the group.
* @throws IOException
*/
@Test
public void deleteUserGroup() throws IOException {
String groupPath = "groupPath" ;
//Creates and posts a group.
Entity data = new Entity().chainPut("name",groupPath).chainPut("type", "group").chainPut( "path", groupPath );
Entity node = this.app().collection("groups").post(data);
assertNull( node.get( "error" ) );
waitForQueueDrainAndRefreshIndex();
//Create a user that is in the group.
node = this.app().collection("groups").entity(groupPath).collection("users").entity(user).post();
assertNull( node.get( "error" ) );
waitForQueueDrainAndRefreshIndex();
//Get the user and make sure that they are part of the group
Collection groups = this.app().collection("users").entity(user).collection("groups").get();
assertEquals( groups.next().get( "name" ), groupPath );
// now delete the group
ApiResponse response = this.app()
.collection("groups").entity(groupPath).collection("users").entity(user).delete();
assertNull( response.getError() );
waitForQueueDrainAndRefreshIndex();
//Check that the user no longer exists in the group
int status = 0;
try {
this.app().collection("users").entity(user)
.collection("groups").entity( groupPath ).collection( "users" ).entity( user ).get();
fail("Should not have been able to retrieve the user as it was deleted");
}catch (ClientErrorException e){
status=e.getResponse().getStatus();
assertEquals( 404, status );
}
}
/**
* For the record, you should NEVER allow the guest role to add roles.
* This is a gaping security hole and a VERY BAD IDEA!
* That being said, this should technically work, and needs testing.
* Tests that you can allow a guest role to add additional roles.
*/
@Test
public void dictionaryPermissions() throws Exception {
// add the perms to the guest to allow users in the role to create roles
// themselves
addPermission( "guest", "get,put,post:/roles/**" );
Entity data = new Entity().chainPut("name", "usercreatedrole");
// create a role as the user
Entity entity = this.app().collection( "roles" ).post( data );
assertNull( entity.getError() );
waitForQueueDrainAndRefreshIndex();
// now try to add permission as the user, this should work
addPermission( "usercreatedrole", "get,put,post:/foo/**" );
}
@Test
public void getNonExistentEntityReturns404() throws Exception {
// Call a get on a existing entity with no access token and check if we get a 401
try {
this.app().collection( "roles" ).entity( "guest" ).get( false );
} catch(ClientErrorException uie){
assertEquals( 401,uie.getResponse().getStatus() );
}
// add the perms such that anybody can do a get call
addPermission( "guest", "get:/**" );
// Call a get on a non existing entity that doesn't need permissions and check it we get a 404.
try {
this.app().collection( "roles" ).entity( "banana" ).get( false );
} catch(ClientErrorException uie){
assertEquals( 404,uie.getResponse().getStatus() );
}
try {
this.app().collection( "roles" ).entity( UUIDUtils.newTimeUUID() ).get( false );
} catch(ClientErrorException uie){
assertEquals( 404,uie.getResponse().getStatus() );
}
}
/**
* Test application permissions by posting entities different users and making sure we work within
* the permissions given.
*/
@Test
public void applicationPermissions() throws Exception {
// Creates two new roles: reviewer1 and reviewer2
createRoleUser( "reviewer1", "reviewer1@usergrid.com" );
createRoleUser( "reviewer2", "reviewer2@usergrid.com" );
Entity data = new Entity().chainPut("name", "reviewer");
// Creates a new role "reviewer"
Entity node = this.app().collection("roles").post(data);
assertNull( node.getError() );
waitForQueueDrainAndRefreshIndex();
// delete the default role to test permissions later
ApiResponse response = this.app().collection("roles").entity("default").delete();
assertNull( response.getError() );
waitForQueueDrainAndRefreshIndex();
// Grants a permission to GET, POST, and PUT the reviews url for the reviewer role
addPermission( "reviewer", "get,put,post:/reviews/**" );
// Grants a permission GET on the guests for the reviews url
addPermission( "guest", "get:/reviews/**" );
// Creates a reviewer group
Entity group = new Entity().chainPut( "path", "reviewergroup" ).chainPut("name","reviewergroup");
this.app().collection("groups").post(group);
waitForQueueDrainAndRefreshIndex();
// Adds the reviewer to the reviewerGroup
this.app().collection("groups").entity("reviewergroup").collection("roles").entity("reviewer").post();
waitForQueueDrainAndRefreshIndex();
// Adds reviewer2 user to the reviewergroup
this.app().collection("users").entity("reviewer2").collection("groups").entity("reviewergroup").post();
waitForQueueDrainAndRefreshIndex();
// Adds reviewer1 to the reviewer role
this.app().collection("users").entity("reviewer1").collection("roles").entity("reviewer").post();
waitForQueueDrainAndRefreshIndex();
// Set the current context to reviewer1
this.app().token().post(new Token("reviewer1","password"));
// Post reviews to the reviews collection as reviewer1
Entity review = new Entity()
.chainPut("rating", "4").chainPut("name", "noca").chainPut("review", "Excellent service and food");
this.app().collection("reviews").post( review );
review = new Entity()
.chainPut ("rating", "4").chainPut( "name", "4peaks").chainPut("review", "Huge beer selection" );
this.app().collection("reviews").post(review);
waitForQueueDrainAndRefreshIndex();
// get the reviews and assert they were created
QueryParameters queryParameters = new QueryParameters();
queryParameters.setQuery( "select * ORDER BY created" );
Collection reviews = this.app().collection("reviews").get(queryParameters);
assertEquals( "noca",reviews.next().get("name") );
assertEquals("4peaks", reviews.next().get( "name" ));
// Try to delete the reviews, but it should fail due to have having delete permission in the grants.
int status = 0;
try {
this.app().collection("reviews").entity("noca").delete();
fail( "this should have failed due to having insufficient permissions" );
}
catch ( ClientErrorException uie ) {
status = uie.getResponse().getStatus();
}
assertEquals( Response.Status.UNAUTHORIZED.getStatusCode(), status );
status = 0;
// Try to delete the reviews, but it should fail due to have having delete permission in the grants.
try {
this.app().collection("reviews").entity("4peaks").delete();
fail( "this should have failed due to having insufficient permissions" );
}
catch ( ClientErrorException uie ) {
status = uie.getResponse().getStatus();
}
assertEquals( Response.Status.UNAUTHORIZED.getStatusCode(), status );
waitForQueueDrainAndRefreshIndex();
//TODO: maybe make this into two different tests?
//Change context to reviewer2
this.app().token().post(new Token("reviewer2", "password"));
// post 2 reviews as reviewer2
review = new Entity()
.chainPut("rating", "4").chainPut("name", "cowboyciao").chainPut("review", "Great atmosphoere");
this.app().collection("reviews").post(review);
review = new Entity()
.chainPut( "rating", "4" ).chainPut("name", "currycorner").chainPut( "review", "Authentic" );
this.app().collection("reviews").post(review);
waitForQueueDrainAndRefreshIndex();
// get all reviews as reviewer2
queryParameters = new QueryParameters();
queryParameters.setQuery( "select * ORDER BY created" );
reviews = this.app().collection("reviews").get(queryParameters);
assertEquals("noca", reviews.next().get("name").toString());
assertEquals("4peaks", reviews.next().get("name").toString());
assertEquals("cowboyciao", reviews.next().get("name").toString());
assertEquals("currycorner", reviews.next().get("name").toString());
// issue a delete, it shouldn't work, no permissions
status = 0;
try {
this.app().collection("reviews").entity("cowboyciao").delete();
fail( "this should have failed due to having insufficient permissions" );
}
catch ( ClientErrorException uie ) {
status = uie.getResponse().getStatus();
}
assertEquals( Response.Status.UNAUTHORIZED.getStatusCode(), status );
waitForQueueDrainAndRefreshIndex();
status = 0;
try {
this.app().collection("reviews").entity("currycorner").delete();
fail( "this should have failed due to having insufficient permissions" );
}
catch ( ClientErrorException uie ) {
status = uie.getResponse().getStatus();
}
assertEquals( Response.Status.UNAUTHORIZED.getStatusCode(), status );
}
/**
* Tests the scenario where we have roles declarations such as: <ul> <li>GET /users/[star]/reviews "any user can
* read any others book review"</li> <li>POST /users/[user1]/reviews "cannot post as user2 to user1's reviews"</li>
* <ii>POST /users/[star]/reviews/feedback/* "can post as user2 to user1's feedback/good or /bad</ii> </ul>
* <p/>
* Scenario is as follows: Create an application
* <p/>
* Add two application users - user1 - user2
* <p/>
* Create a book collection for user1
*/
@Test
public void wildcardMiddlePermission() throws Exception {
//Deletes the default role
this.app().collection("roles").entity("default").delete();
//Creates a reviewer role
Entity data = new Entity().chainPut("name", "reviewer");
this.app().collection("roles").post(data);
waitForQueueDrainAndRefreshIndex();
// allow access to reviews excluding delete
addPermission( "reviewer",
"get,put,post:/reviews/**" );
// allow access to all user's connections excluding delete
addPermission( "reviewer",
"get,put,post:/users/me/**" );
// allow access to the review relationship excluding delete
addPermission( "reviewer",
"get,put,post:/books/*/review/*" );
// create userOne
UUID userOneId =
createRoleUser( "wildcardpermuserone",
"wildcardpermuserone@apigee.com" );
assertNotNull( userOneId );
// create userTwo
UUID userTwoId =
createRoleUser( "wildcardpermusertwo",
"wildcardpermusertwo@apigee.com" );
assertNotNull( userTwoId );
waitForQueueDrainAndRefreshIndex();
//Add user1 to the reviewer role
this.app().collection("users").entity(userOneId).collection("roles").entity("reviewer").post();
waitForQueueDrainAndRefreshIndex();
//Add a book to the books collection
Entity book = new Entity().chainPut( "title", "Ready Player One" ).chainPut("author", "Earnest Cline");
book = this.app().collection("books").post(book);
assertEquals( "Ready Player One", book.get("title").toString() );
String bookId = book.get("uuid").toString();
waitForQueueDrainAndRefreshIndex();
//Switch the contex to be that of user1
this.app().token().post(new Token("wildcardpermuserone","password"));
// post a review of the book as user1
// POST https://api.usergrid.com/my-org/my-app/users/$user1/reviewed/books/$uuid
Entity review =
new Entity().chainPut( "heading", "Loved It" ).chainPut( "body", "80s Awesomeness set in the future" );
review = this.app().collection("reviews").post(review);
String reviewId = review.get("uuid").toString();
waitForQueueDrainAndRefreshIndex();
// POST https://api.usergrid.com/my-org/my-app/users/me/wrote/review/${reviewId}
this.app().collection("users").entity("me").connection("wrote").collection("review").entity(reviewId).post();
// POST https://api.usergrid.com/my-org/my-app/users/me/reviewed/review/${reviewId}
this.app().collection("users").entity("me").connection("reviewed").collection("books").entity(bookId).post();
waitForQueueDrainAndRefreshIndex();
// POST https://api.usergrid.com/my-org/my-app/books/${bookId}/review/${reviewId}
this.app().collection("books").entity(bookId).collection("review").entity(reviewId).post();
waitForQueueDrainAndRefreshIndex();
// now try to post the same thing to books to verify as userOne does not have correct permissions
int status = 0;
try {
this.app().collection("books").post(book);
}
catch ( ClientErrorException uie ) {
status = uie.getResponse().getStatus();
}
assertEquals( Response.Status.UNAUTHORIZED.getStatusCode(), status );
//Gets all books that user1 reviewed\
this.app().collection("users").entity("me").connection("reviewed").collection("books").get();
//Gets a specific review
this.app().collection("reviews").entity(reviewId).get();
//Gets all the reviews that user1 wrote
this.app().collection("users").entity("me").connection("wrote").get();
}
/**
* Tests the scenario where we have role declaration such as: <ul> <li>POST /users/[star]/following/users/${user}" a
* user can add himself to any other user following list"</li> </ul>
* <p/>
* Scenario is as follows: Create an application
* <p/>
* Add two application users - examplepatient - exampledoctor
* <p/>
* examplepatient add himself to exampledoctor following list
*/
//TODO: get this test working.
@Test
public void wildcardFollowingPermission() throws Exception {
//Delete default role
app().collection("roles").entity("default").delete();
//Create new role named patient
Entity data = new Entity().chainPut( "name", "patient" );
app().collection("roles").post(data);
//allow patients to add doctors as their followers
addPermission( "patient", "delete,post:/users/*/following/users/${user}" );
waitForQueueDrainAndRefreshIndex();
// create examplepatient
UUID patientId = createRoleUser( "examplepatient", "examplepatient@apigee.com" );
assertNotNull( patientId );
// create exampledoctor
UUID doctorId = createRoleUser( "exampledoctor", "exampledoctor@apigee.com" );
assertNotNull( doctorId );
waitForQueueDrainAndRefreshIndex();
// assign examplepatient the patient role
this.app().collection("users").entity(patientId).collection("roles").entity("patient").post();
waitForQueueDrainAndRefreshIndex();
this.app().token().post(new Token("examplepatient","password"));
waitForQueueDrainAndRefreshIndex();
//not working yet, used to be ignored
// this.app().collection("users").entity("exampledoctor").connection("following")
// .collection("users").entity("examplepatient").post();
}
/**
* Create the user, check there are no errors
*
* @return the userid
*/
private UUID createRoleUser(String username, String email)
throws Exception {
User props = new User(username, username, email, "password");
Entity entity = this.app().collection("users").post(props);
assertNotNull( entity );
return entity.getUuid();
}
/**
* Adds the permission in grant to the rolename role, and tests that they were added correctly
* @param rolename
* @param grant
* @throws IOException
*/
private void addPermission( String rolename, String grant ) throws IOException {
// Create and post the permissions
Entity props = new Entity().chainPut("permission", grant);
this.app().collection("roles").entity(rolename).collection("permissions").post(props);
// Checks that the permissions were added correctly
Collection node = this.app().collection("roles").entity(rolename).collection("permissions").get();
List<Object> data =(List) node.getResponse().getData();
for ( Object o : data ) {
if ( grant.equals(o.toString()) ) {
return;
}
}
fail( String.format( "didn't find grant %s in the results", grant ) );
}
@Test
public void testUsersMeAlwaysAvailable() {
// delete default roles/permissions from app
app().collection("roles").entity("default").delete();
// create an app user, get token (and switch context to that of user)
Token token = null;
try {
String password = "s3cr3t";
Entity newUser = app().collection( "users" ).post(
new User( "dave", "Dave Johnson", "dave@example.com", password ) );
token = app().token().post(
new Token( "password", (String)newUser.get("username") , password ));
} catch ( Exception e ) {
logger.error( "Error creating user and logging in: {}", e);
}
assertNotNull( token );
// user cannot post to a collection
try {
Map<String, Object> catMap = new HashMap<String, Object>() {{
put("name", "enzo");
put("color", "orange");
}};
app().collection( "cats" ).post( true, token, ApiResponse.class, catMap, null, false );
fail("Post should have failed");
} catch ( Exception expected ) {}
// but the /users/me end-point should work
Entity me = app().collection( "users" ).entity( "me" ).get();
assertNotNull( me );
try {
app().collection( "users" ).entity( "me" ).delete();
fail("Delete /users/me must fail");
} catch ( Exception expected ) {}
}
@Test
public void testAppUserNamedMeNotAllowed() {
// cannot create app user named me
try {
app().collection( "users" ).post( new User( "me", "it's me", "me@example.com", "me!me!" ) );
fail("Must not be able to create app user named me");
} catch ( BadRequestException expected ) {}
// cannot use update to rename app user to me
Entity user = app().collection( "users" ).post( new User( "dave", "Sneaky Me", "me@example.com", "me!me!" ) );
try {
app().collection( "users" ).entity( user ).put( new Entity().chainPut( "username", "me" ));
fail("Must not be able to update app user to name me");
} catch ( BadRequestException expected ) {}
}
@Test
public void testAdminUserNamedMeNotAllowed() {
// cannot create admin user named me
try {
Form form = new Form();
form.param( "username", "me" );
form.param( "email", "me@example.com");
form.param( "name", "me Me ME!");
form.param( "password", "me me 123" );
management().users().post( ApiResponse.class, form );
fail("Must not be able to create admin user named me");
} catch ( BadRequestException expected ) {}
// cannot use update to rename admin user to me
String randomString = RandomStringUtils.randomAlphanumeric( 10 );
String username = "user_" + randomString;
String password = "me me 123";
Form form = new Form();
form.param( "username", username );
form.param( "email", username + "@example.com");
form.param( "name", "Despicable me");
form.param( "password", password );
management().users().post( ApiResponse.class, form );
management().token().get( username, password );
try {
management().users().user( username ).put( true, new Entity().chainPut( "username", "me" ) );
fail("Must not be able to create admin user named me");
} catch ( BadRequestException e ) {}
}
}