GAE - Getter without Setter - How to prevent clien

2019-09-03 15:59发布

问题:

I am writing against Google App Engine using Java and Android Studio. Tools, Install Client Libraries creates a model for the frontend from backend classes. This works well.

Now, I have realized that getters and setters are always generated for the client as part of the class model, or at least whenever I use a getter, a setter is auto-generated for the same property. I understand that REST needs to have getters and setters exposed to serialize and deserialize on both sides.

But what happens if I don't want the client to be able to write a given property, such as for example a counter? In a connected scenario, as part of my business logic, I would leave out the setter for that property. But as it seems I am forced to implement it here.

Sure, on the server I could look at the returned object when it comes in and modify it before persisting it, but I think this doesn't make any sense. It can't be the solution to allow the client to set a property, only to later strip it off before saving.

  • Is there any way to prevent the client from accessing the setter? How is this correctly done?

I know I can write some code to fix that problem, but I am looking for best practices for this problem.

--

Something else just came to my mind, and this is even worse. Even the @Id (object identifier, speak "primary key") has getter and setter on the client side. What would happen if a client gets an object from datastore, modifies the identifier and sends it back to the backend?

  1. I cannot even identify that object anymore on the server-side.
  2. Malicious client code could mess up the datastore entirely by making the backend to update the wrong object.

I can't believe there is no proper solution to this.

回答1:

You can never totally trust client data. An attacker can do whatever they want to the payload of a REST call, so you have to implement security in some fashion. There's no real way to do this in an automated way because what a user can do to your data is up to your requirements. You need the getters and setters to translate the data from rest strings to your object but everything coming from a user has to be viewed with a weary eye.

Here's what I do.

1) verify that the user (if you authenticate users) has access to the object you are updating. Do they have permission to update that object? If it's a new object do they have rights to create a new one?

2) Assuming they have permission only let them update the parts that are updateable. i.e. they can't change the create/modify dates. That's for the server to decide. So generally I load the object up from the database using its id. Then I copy over all the fields that are relevant to the particular option. Often the best idea is to not let the client update the object directly at all.

Sometimes its far better to have dedicated actions on your api. For example let's say it's a bug tracking system. You don't let people wholesale update the object, but provide specific actions like "close bug." This cuts down the amount of data over the wire and makes the call very specific. It also means they don't get to screw around with all those internal fields they aren't supposed to change like when the bug was opened or the history.

As for changing the id that means they are sending you a different object. You'd validate against that object instead which if it was a randomly chosen number, will either not exist or most likely won't be an object belonging to that user and you'll reject the request.



回答2:

You don't need the Setter, much less for @Id - in fact, you shouldn't pass Datastore Ids to the clients at all, as this will unnecessarily reveal details of your Datastore to the clients.

1. Use Key instead of Id - The best way to pass information about an object is by using its Key instead of the Id. The Key can be calculated using:

Key<ObjClass> objKey = Key.create(this)

And you safely can pass on its String value to the client as:

String keyString = Key.create(this).getString()

Objectify refers to this as a webString as it is useful for transmission over the wire. Once you get it back, you can retrieve the object using:

Key<ObjClass> objKey = Key.create(keyString);
ObjClass obj = ofy().load().key(objKey).now();

Another reason why you always want to pass Key instead of Id is because the Key will contain hierarchy information, whereas Id is only valid in the context of the object's scope. If that sounds like a mouthful, simply mind that an Object's Id is only unique in the scope of it's Parent object, so if your object is a Child of some other object, you can't retrieve it directly form the Datastore with just the Id -- that would be sufficient: you'd need the Parent's Id as well, which the Key will contain. But I digress...

2. Don't trust Objects received from Clients

Treat objects received from Clients via REST/Endpoints as nothing more than data structures. Don't use them at all on your business logic. On an update scenario, load your existing Object from the Datastore and then apply only the values from the received Object as you see fit:

 @ApiMethod(...)
 public SomeRes updateChildObj(ChildObj childObj) {
   // Retrieve the ChildObj from the Datastore (see further below for code):
   ChildObj existingChildObj = ChildObj.getChildObj(childObj.getKey());

   // Copy the values between Objects, but otherwise keep them separate:
   existingChildObj.setSafeParam(childObj.getSafeParam());

   // Save the existing Object back to the Datastore:
   existingChildObj.save();
 }

3. Putting it all together

So here's an example of Parent and Child objects (or objects with a reference, for all that matters, or as likely in your scenario):

@Entity
class ParentObj {
  // This ensures that the client does not get to see Id:
  @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
  @Id Long id = null;

  // The client sees the Key, but it won't be saved on the Datastore:
  @Ignore String key = null;

  public Long getId() {
    return id;
  }

  public String getKey() {
    // Can only generate a Key if the object has been saved:
    return id != null ? Key.create(this).getString() : key;
  }

  public String setKey() {
    this.key = key;
  }

  public static ParentObj getParentObj(String keyStr) {
    Key<ParentObj> parentObjKey = Key.create(keyStr);
    return ofy().load().key(parentObjKey).now();
  }
}

@Entity
class ChildObj {
  // Same tricks as ParentObj:
  @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
  @Id Long id = null;
  @Ignore String key = null;

  // Same stuff again:
  public Long getId() { return id; }
  public String getKey() { return id != null ? Key.create(this).getString() : key; }
  public String setKey() { this.key = key; }

  // Now the Parent/Ref:
  @Parent @Index private Ref<ParentObj> parentObj = null;

  public Account getParentObj() {
    // Straight get() from the reference
    return parentObj.get();
  }

  public void setParentObj(ParentObj parentObj) {
    checkNotNull(account);
    checkNotNull(account.getKey()); // Check that we got a key

    if (account.getId() == null) {
      // We don't have an Id -- it's an empty shell, so we need to 
      // retrieve the actual object, otherwise you get the error:
      // You cannot create a Key for an object with a null @Id
      this.parentObj = Ref.create(ParentObj.getParentObj(parentObj.getKey()));
    } else {
      // We have an actual object form the Datastore, so we can just Ref to it:
      this.parentObj = Ref.create(parentObj);
    }
  }
}

4. One last note, regarding your comment

"The solution fails as soon as I have a reference between two entities. For that operation the setter must be available, otherwise I get this error: message: [...] JsonMappingException: You cannot create a Key for an object with a null @Id."

Seems like a GAE Endpoints + Objectify scenario, where the Endpoints JSON interpreter for an incoming payload is struggling to make objects out of the JSON provided via post as Objectify refuses to recreate them due to lack of info. Perhaps Endpoints is trying to reconstruct an object from a child object... which is precisely what example above tackles.