Protected properties using Name objects

# Brandon Benvie (13 years ago)

Currently there's no way to have a data property that is writable by some but not all. This can't even really be achieved with accessors or proxies directly. Rather, the underlying data is mutated through some other avenue, and the value returned on access comes from this secondary source. Neither method of course is remotely as efficient as a real data property as either.

Using Name objects, it seems like a full fledged elegant "protected" property access API is possible to describe. For this context, I define a protected property as a property on an object who's description or value can only be modified after passing some access check. There's two levels of access covered by what I propose: the base level ability to write a property's data (writable descriptor property) and the ability to configure a property (configurability descriptor property).

For writing to an object, this concept allows for a property to be { writable: false } but be written through a Name object corresponding to the property.

For modifying the property's descriptor, a fourth boolean descriptor attribute is now defined named 'protected'. When a property is protected, the Name object associated with it is required in order to modify it using Object.defineProperty. A property which is protected acts the same as a non-configurable property when modification is attempted using the property's string name. A property can become unprotected later, unlike a non-configurable property, and can be modified internally to engine as well. Protected only acts like non-configurable for unauthorized access from code.

Code (also available at gist.github.com/2938186)

let x = { a: 10 };

// ############################# // ### Protecting a property ### // #############################

let writeA = Object.protectProperty(x, 'a'); // -- or -- let writeA = new Name; Object.protectProperty(x, 'a', writeA);

// ####################################### // ### Describing a protected property ### // #######################################

Object.isProtected(x, 'a'); // true

Object.getOwnPropertyDescriptor(x, 'a') // { writable: false, default protectProperty to set non-writable? // protected: true, // enumerable: true, // configurable: true }

// ##################################### // ### Writing to protected property ### // #####################################

x.a = 50 // silent fail normally, throws in strict "Cannot assign to protected read-only property 'x'"

x[writeA] = 50; // x is { a: 50 }

// ################################################## // ### Modifying a protected property description ### // ##################################################

Object.defineProperty(x, 'a', { writable: true }); // throw "Cannot modify protected property 'a'"

Object.defineProperty(x, writeA, { writable: true }); // { writable: true, // protected: true, // enumerable: true, // configurable: true }

// ############################### // ### Unprotecting a property ### // ###############################

Object.defineProperty(x, writeA, { protected: false }); // -- or -- Object.protectProperty(x, writeA);

// ######################################### // ### Changing a protected property key ### // #########################################

Object.defineProperty(x, writeA, { protected: false }); let newWriteA = Object.protectProperty(x, 'a'); // -- or -- let newWriteA = new Name(); Object.protectProperty(x, writeA, newWriteA);

x[writeA] = 100; // throw 'Invalid protected property assignment'

// from strawman:maximally_minimal_classes

const pHealth = new Name(); const wAlive = new Name();

class Monster { constructor(name, health) { this.name = name; this.alive = true; this[pHealth] = health; Object.protectProperty(this, 'alive', wAlive); }

attack(target) { log('The monster attacks ' + target); }

defend(roll) { let damage = Math.random() * roll; this[pHealth] -= damage; if (this[pHealth] <= 0) { this[wAlive] = false; } }

set health(value) { if (value < 0) { throw new Error('Health must be non-negative.'); } this[pHealth] = value; } }

# Brandon Benvie (13 years ago)

The description of Object.protectProperty:

Object.protectProperty(object : Object, key : String) -> Name

Object.protectProperty(object : Object, key : String, protector : Name) ->

true Object.protectProperty(object : Object, currentProtector : Name) -> true

Object.protectProperty(object : Object, currentProtector : Name, newProtector : Name) -> true

["object", "key"] returns new Name to be used as protector ["object", "key", "protector"] sets protector to existing Name ["object", "currentProtector"] sets protected to false ["object", "currentProtector", "newProtector"] change protector to newProtector

# Herby Vojčík (13 years ago)

I have hard time to put my objections well. Different words appear in my mind like "too monolithic", "too spaghetti", ... but none of them is very good at explaining the problem.

It seems to me that this make thing complex and brittle. Protected overides writable and configurable, so their meaning is compromised. Having fourth one added to the three increases the number of combinations. Having only one or none protected key brings problems with later unprotecting / reprotecting when there are more than one subset of consumers.

I include a counter-proposal, which defines aliases for existing properties, albeit with different access.

Brandon Benvie wrote:

Currently there's no way to have a data property that is writable by some but not all. This can't even really be achieved with accessors or proxies directly. Rather, the underlying data is mutated through some other avenue, and the value returned on access comes from this secondary source. Neither method of course is remotely as efficient as a real data property as either.

Using Name objects, it seems like a full fledged elegant "protected" property access API is possible to describe. For this context, I define a protected property as a property on an object who's description or value can only be modified after passing some access check. There's two levels of access covered by what I propose: the base level ability to write a property's data (writable descriptor property) and the ability to configure a property (configurability descriptor property).

For writing to an object, this concept allows for a property to be { writable: false } but be written through a Name object corresponding to the property.

For modifying the property's descriptor, a fourth boolean descriptor attribute is now defined named 'protected'. When a property is protected, the Name object associated with it is required in order to modify it using Object.defineProperty. A property which is protected acts the same as a non-configurable property when modification is attempted using the property's string name. A property can become unprotected later, unlike a non-configurable property, and can be modified internally to engine as well. Protected only acts like non-configurable for unauthorized access from code.

Code (also available at gist.github.com/2938186)

let x = { a: 10 };

// ############################# // ### Protecting a property ### // #############################

let writeA = Object.protectProperty(x, 'a');

let writeA = Object.aliasProperty(x, 'a', { enumerable: true, configurable: true, writable: true});

// -- or -- let writeA = new Name; Object.protectProperty(x, 'a', writeA);

let writeA = new Name; Object.aliasProperty(x, 'a', writeA, { enumerable: true, configurable: true, writable: true}); // or writeA as last

// ####################################### // ### Describing a protected property ### // #######################################

Object.isProtected(x, 'a'); // true

N/A, not needed. Instead maybe this, if needed: Object.isAlias(x, 'a') // false Object.isAlias(x, writeA); // true

Object.getOwnPropertyDescriptor(x, 'a') // { writable: false, default protectProperty to set non-writable? // protected: true, // enumerable: true, // configurable: true }

returns present state of property 'a' as usual. It can have false anywhere.

// ##################################### // ### Writing to protected property ### // #####################################

x.a = 50 // silent fail normally, throws in strict "Cannot assign to protected read-only property 'x'" // behaves as normal proerty (fails silently or not if non-writable)

x[writeA] = 50; // x is { a: 50 }

// ################################################## // ### Modifying a protected property description ### // ##################################################

Object.defineProperty(x, 'a', { writable: true }); // throw "Cannot modify protected property 'a'" // again, behaves normally, fails if nonconfigurable.

Object.defineProperty(x, writeA, { writable: true }); // { writable: true, // protected: true, // enumerable: true, // configurable: true } // behaves normally like on 'a' except it has access of writeA. // So if it 'a' was non-writable, configurable and enumerable, returns: // { writable: true, // enumerable: true. // configurable: true }

// ############################### // ### Unprotecting a property ### // ###############################

Object.defineProperty(x, writeA, { protected: false }); // -- or -- Object.protectProperty(x, writeA);

Object.removeAlias(x, writeA); // true // removes writeA alias // It is different than delete x.writeA, which // deletes 'a' if writeA has enough access.

// ######################################### // ### Changing a protected property key ### // #########################################

Object.defineProperty(x, writeA, { protected: false }); let newWriteA = Object.protectProperty(x, 'a'); // -- or -- let newWriteA = new Name(); Object.protectProperty(x, writeA, newWriteA);

x[writeA] = 100; // throw 'Invalid protected property assignment'

not applicable

// from strawman:maximally_minimal_classes

const pHealth = new Name(); const wAlive = new Name();

class Monster { constructor(name, health) { this.name, this.name = name; this.alive = true; this[pHealth] = health; Object.protectProperty(this, 'alive', wAlive); Object.aliasProperty(this, 'alive', wAlive,{writable:true,...}); Object.defineProperty(this, 'alive', {writable:false, ...}); // above line is needed - you restrict the access manually, // it is not automatic }

attack(target) { log('The monster attacks ' + target); }

defend(roll) { let damage = Math.random() * roll; this[pHealth] -= damage; if (this[pHealth] <= 0) { this[wAlive] = false; } }

set health(value) { if (value < 0) { throw new Error('Health must be non-negative.'); } this[pHealth] = value; } }

As the last thing, I introduce the twist which could be possible with this API, and provide shorter variant of constructor. The twist is, you can define property and alias in reversed order:

 constructor(name, health) {
this.name = name;
this[wAlive] = true;
this[pHealth] = health;
Object.aliasProperty(this,wAlive,'alive',{writable:false,...});
 }

That is, you could define public restricted alias on private full-access property.

# Herby Vojčík (13 years ago)

Addition to previous post on aliasProperty: you can only create alias on configurable property, of course.

# Brandon Benvie (13 years ago)

I addressed some of these as well but didn't put it up here yet. Semantics (this is all on the gist as well in more readable form)

  • Objects keep an internal map of protector Name objects to keys.
  • A protector can only protect one key per object.
  • Each key on an object can only have one protector (this isn't necessary but seems preferable).
  • The key a protector protects can vary between objects.
  • The protector for an object's key can be changed.
  • A non-writable property's value can always be changed via the protector (but not direct assignment).
  • Setting an object to non-configurable makes it non-protected (they are mutually exclusive and configurability trumps).
  • By extension, a non-configurable property cannot become protected.
  • For Accessors, the public property of the protector is passed to the setter (matches how private Names work with Proxies).
# Brandon Benvie (13 years ago)

Note: Given the above constraints on configurability, no norms are violated in this. A non-writable property is writable currently using Object.defineProperty. It's just not writable directly, which is upheld here.

# David Bruant (13 years ago)

Le 15/06/2012 21:27, Brandon Benvie a écrit :

Currently there's no way to have a data property that is writable by some but not all.

My heartbeat reaction to this first sentence is: is it a use case anyway?

This can't even really be achieved with accessors or proxies directly. Rather, the underlying data is mutated through some other avenue, and the value returned on access comes from this secondary source. Neither method of course is remotely as efficient as a real data property as either.

I disagree with that statement (but until direct proxies are efficiently implemented, it's hard to tell who is right :-) ).

var o = {a:1, b:2}; var o2 = makeProxyWithSomeReadOnlyProperties(o, ['a']);

The makeProxyWithSomeReadOnlyProperties creates a proxy with o as target. Only property-mutation-operation traps need to be implemented ("read" traps are the forward-to-target default ones). In this configuration, at least the reads on o2 can be made as efficient as the read in o (since the engine knows the target and knows that "read" traps are the default ones, so it can delegate the operation to the target with no overhead)

There is certainly an overhead for write operations (since you need to check whether the property is readOnly or not). It is unclear whether it's so much more than it wouldbe natively to justify a direct native implementation in my opinion. The reason why it's unclear is also that a new property descriptor attirbute like the one your propose may deteriorate the performance of all write operations on all objects (since a new check may be necessary).

It seems to me that your proposal could be implemented in pure ES.next (not on existing objects, though). I'd be interested in seeing it implemented and used in real world examples. Only then will we be able to tell whether there are use cases and whether, on these use cases, the performance is bad enough and not improvable to justify native implementations.

A nit on your API: "Object.protectProperty(object : Object, currentProtector : Name) -> true

unprotects the key on object which currentProtector protects" => I don't think Object.protectProperty should ever "unprotect" something.

# Brandon Benvie (13 years ago)

Actually I'm working on implementing it now. I previously partially implemented Names which is half the equation (though only in V8) and this is just an extension to that.

There shouldn't need to be any additional checks using what I've proposed. A non-writable property is no more writable than it currently is, and in no different a way. Right now you can write to a non-writable property by doing Object.defineProperty(o, k, { value: v }). This proposal uses Name objects to make that look cleaner. It also actually adds increased protection that doesn't exist, since it closes the defineProperty route of modifying a read-only property entirely unless you have the key.

# Brandon Benvie (13 years ago)

The gist now includes a rough implementation of this ( gist.github.com/2938186 ).

# Tom Van Cutsem (13 years ago)

I agree with David: is this use case sufficiently common/needed to require such deep extensions to Javascript's object model? Especially the interplay between writable & configurable is already complex. Adding more knobs should not be done lightly.

Accessors seem like a fine abstraction for "protected" attributes. I can't imagine their overhead to be prohibitive?