SoftInstigate / restheart

Rapid API Development with MongoDB
https://restheart.org
GNU Affero General Public License v3.0
805 stars 171 forks source link

How to prevent overwriting existing documents in PUT/POST (only creating new should be allowed) #162

Closed xavier268 closed 7 years ago

xavier268 commented 7 years ago

I want to allow unauthenticated users to register for authentication, so I let them POST to /db/coll/ Since I use the provided DB Identity Manager, the username they select ends up in the _id field.

When POSTing a new account to /db/col, I provide the _id in the json body. I am using security predicates to only open unautheticated access to POST and no other verbs, and I was planning on filtering away the password field in the returned data. That would have given me a both secure and simple architecture, where, anyone could POST to register a new account, but only the owner could change an existing account, using PUT, protected accordingly with a predicate.

But unfortunately, that does not work, since I realized that POSTing on the collection behaves exactly as PUTing on the document, and just overwrites the document if it exists ! Anyone can recreate an account with a different password, and hijack the previous account !

Is there a way to disable "updates" on POST, both allowing $unauthenticated users to create new accounts, but only authenticated users to modify existing ones ? I have been trying various combinations of PUT and PATCH but without any success. I am currently considering :

Am I missing a simpler way to achieve this use case in a secured way with Restheart out of the box ? Like a &update=false flag somewhere in the query string ?

Thanks for any hints or directions, and many thanks for the great product you've built !

Xavier

EDIT

Actually, I implemented a logic handler. I turned out to be much simpler that what I feared. Pasting the code below in case it can help someone else. (using Restheart 3.x snapshot)

import com.mongodb.MongoClient;
import io.undertow.server.HttpServerExchange;
import java.nio.charset.Charset;
import java.util.Map;
import org.bson.Document;
import org.restheart.db.MongoDBClientSingleton;
import org.restheart.handlers.PipedHttpHandler;
import org.restheart.handlers.RequestContext;
import org.restheart.handlers.RequestContext.METHOD;
import org.restheart.handlers.applicationlogic.ApplicationLogicHandler;
import org.restheart.utils.HttpStatus;

 /**
 * ApplicationLogicHandler to create a new user. Will only create user if
 * indexes are satisfying, will never update an existing previous user.
 *
 * @author xavier
 */
public class MyCreateUser extends ApplicationLogicHandler {

private static MongoClient CLIENT;
private final String db;
private final String coll;

public MyCreateUser(PipedHttpHandler next, Map<String, Object> args) {
    super(next, args);
    // Get client - mongo driver version 3.2
    CLIENT = MongoDBClientSingleton.getInstance().getClient();
    this.db = (String) args.get("db");
    this.coll = (String) args.get("coll");
}

@Override
public void handleRequest(HttpServerExchange exchange, RequestContext context) throws Exception {

    switch (context.getMethod()) {

        case OPTIONS: // cors handling
            exchange.getResponseHeaders().put(HttpString.tryFromString("Access-Control-Allow-Methods"), "POST");
            exchange.getResponseHeaders().put(HttpString.tryFromString("Access-Control-Allow-Headers"), "Accept, Accept-Encoding, Authorization, Content-Length, Content-Type, Host, Origin, X-Requested-With, User-Agent, No-Auth-Challenge, " + AUTH_TOKEN_HEADER + ", " + AUTH_TOKEN_VALID_HEADER + ", " + AUTH_TOKEN_LOCATION_HEADER);
            exchange.setStatusCode(HttpStatus.SC_OK);
            exchange.endExchange();
            break;

        case POST:
            exchange.getRequestReceiver().receiveFullString(
                    // Handler for the body content 
                    (exch, data) -> {
                        if (data != null && !data.isEmpty()) {
                            // Here, we handle the data from the body 
                            // We assume it is json, ignoring content-type.

                            // Parse content, assuming that is is clean json ...
                            // Nesting seems to work fine as well ..
                            Document doc = Document.parse(data);
                            System.out.printf("\n***Parsed content : %s***\n", doc);
                            // Try to insert the doc that was sent ...
                            if (doc.containsKey("_id")) {
                                try {
                                    CLIENT.getDatabase(db).getCollection(coll).insertOne(doc);
                                    exchange.setStatusCode(HttpStatus.SC_CREATED);
                                } catch (Exception ex) {
                                    exchange.setStatusCode(HttpStatus.SC_CONFLICT);
                                }
                            } else {
                                // Ignore if _id not provided, ignore request
                                exchange.setStatusCode(HttpStatus.SC_NO_CONTENT);
                            }
                            exchange.endExchange();
                        } else {
                            exchange.setStatusCode(HttpStatus.SC_NO_CONTENT);
                            exchange.endExchange();
                        }
                    },
                    Charset.forName("UTF-8")
            );
            break;

        default:
            exchange.setStatusCode(HttpStatus.SC_METHOD_NOT_ALLOWED);
            exchange.endExchange();

    }

}

}
xavier268 commented 7 years ago

Solved. See EDIT in the initial question.

ujibang commented 7 years ago

Hi glad you managed to find a solution.

There is a trick that could simplify it:

In RESTHeart both PUT and POST have upsert semantic. However there is a way to have them not creating a document if already existing using the checkEtag query parameter.

You can find in the ETag documentation section the following:

An interesting usage of the checkETag query parameter is avoiding the upsert semantic of RESTHeart. If the client wants to make sure that a write request only creates documents without updating them, it can send a random ETag; in case the resource does not exist, the ETag is not checked anyway.

PUT /db/coll/doc?checkEtag (passing the request header If-Match:x)

This request leads either to CREATED if the document with id doc was not existing or to PRECONDITION FAILED if it already exists.

Of course you need to define a security predicate that enforce the presence of the checkETag query parameter and the "wrong" If-Match header

xavier268 commented 7 years ago

Hi Andrea,

Thanks for taking the time for answering, and so fast.

I suspected you had a trick of that sort in you magic toolbox ... and your suggestion is spot on, and indeed solves exactly my point without a dedicated applicationhandler.

You are bringing a tremendous value to the community with the Restheart ! A big thank you for your efforts and your commitments !

Xavier