tower-archive / tower

UNMAINTAINED - Small components for building apps, manipulating data, and automating a distributed infrastructure.
http://tower.github.io
MIT License
1.79k stars 120 forks source link

Add hash to a field #355

Closed norman784 closed 10 years ago

norman784 commented 11 years ago

how can I assign an default dynamic hash to a field?

for example:

class App.Application extends Tower.Model
  # something like
  @field 'secret', type: 'Hash', , default: -> if @isNew() then doHash() else @get("secret")
  @field 'name', type: 'String'

  @timestamps()

  # my custom function
  doHash: ->
    return crypto('md5').update(Time.now, 'utf-8', 'hex')
lancejpollard commented 11 years ago

The defaults work by first checking if the field is undefined, and if so, it will call the function:

packages/tower-model/shared/attribute.coffee#L117-L119, specifically:

# when you call `record.get('x')`, and it's undefined and you've defined a default value...
# ...
value = @getAttribute(key)
value = field.defaultValue(@) if value == undefined
field.decode(value, @)
# ...

The way you defined the default is pretty much correct, the only thing is that type: 'Hash' is a ruby-like hash, or simple JavaScript object, such as {key: 'value', key2: 'value2'} (for more info on JavaScript hash vs. object, see this: underscore/test/objects.js#L421-L449). So I would just change it to type: 'String'. Also, that crypto module only works on the server, so you might also need to check if Tower.isServer first.

Also, Tower is adding many useful/rails-like helpers to underscore, such as date helpers, so you can use those too (Time.now doesn't exist in Tower, but _.now does, more here: [packages/tower-support/shared/format.coffee#L69-L121](https://github.com/viatropos/tower/blob/00c4527004b0015a228640467245ea8d929f87b0/packages/tower-support/shared/format.coffee#L69-L121, we've only implemented a few so far but the main date helpers exist). Sidenote: currently, _.now is just new Date, so why not just use new Date? The reason is I'm thinking of adding functionality so that, if you wanted to run your app in the past (maybe you want to generate fake data where createdAt etc. is in the past), whenever you called _.now() it will be "now" in the past's frame of reference. You can't easily do that if you have new Date all through your code.

Finally, instead of checking @isNew, if all you care about is if secret is undefined, then you can simplify the default function.

So this is the end result:

class App.Application extends Tower.Model
  @field 'secret', type: 'String', default: -> @doHash()
  @field 'name', type: 'String'

  @timestamps()

  doHash: ->
    crypto('md5').update(_.now(), 'utf-8', 'hex') if Tower.isServer

Let me know if that works.

norman784 commented 11 years ago

Let me check it... how I load external libs? just put under the vendors folder? or towerjs autoloads all the node_modules?

lancejpollard commented 11 years ago

To load external libs, you can just install the node module through npm and require it:

doHash: ->
  require('crypto')('md5').update(_.now(), 'utf-8', 'hex') if Tower.isServer

Node uses CommonJS which just asks that you require(moduleName) any module you want.

Also, Tower lazy-loads a couple common modules (packages/tower/server.coffee#L24-L41), which you can access like this

crypto = Tower.module('crypto')

There are two main reasons for this approach:

  1. So you don't have to manually require the module at the top of your model/controller/etc files, which will slow down server startup the more modules your whole app requires at once.
  2. So the browser can also require modules (until we integrate a more robust solution).

So, in your specific case, you can just do this:

doHash: ->
  Tower.module('crypto')('md5').update(_.now(), 'utf-8', 'hex') if Tower.isServer

One final note

Since this code of yours is dealing specifically with server-side functionality, and since it is relating to security, it's up to you to prevent this code from being compiled to the ./public directory - where anyone can view it.

Here's the recommended approach for doing that.

  1. Put any server-side code into the server folder for your specific app/* folder. If you're creating a server-side-only model, then you can put it in app/models/server/myModel.coffee. You can also put in model "mixins" or helpers in that folder too, but, similar to how you have in Rails app/helpers which you can include into app/controllers, I like the idea of having "mixins" in app/concerns which you include in app/models. Maybe there is a better approach to this, open to ideas. The reason you'd want to do this is, the files in app/concerns are loaded before app/models, so it just works. If you put mixins into app/models, you would have to make sure your mixins are manually required before the model.
  2. Include that mixin in your model code, which in your case is app/models/shared/application.coffee (or could also just be app/models/application.coffee).
# app/concerns/server/application/secretsMixin.coffee
# 
# or, a perhaps simpler approach, 
# but you have to make sure the files are loaded in the right order:
# 
# app/models/server/applicationSecretsMixin.coffee
App.ApplicationSecretsMixin =
  # `included` is a special method that is called when the mixin is included in a class. 
  included: ->
    @field 'secret', type: 'String', default: -> @doHash()

  doHash: ->
    Tower.module('crypto')('md5').update(_.now(), 'utf-8', 'hex')
# app/models/application.coffee
class App.Application extends Tower.Model
  @include App.ApplicationSecretsMixin if Tower.isServer

  @field 'name', type: 'String'

  @timestamps()

By separating your code like this, your crypto code won't be compiled to the ./public folder, so it's secure. FYI, anything in a app/*/server folder will not be compiled to ./public.

Also one last note FYI. An additional benefit of placing "model mixin code" into app/concerns/<modelName>/someMixin.coffee is once you get into that pattern, you start really being able to modularize your code well, and minimize the amount of code the client gets. For example, if you are building an app that publishes to Facebook/Twitter/etc., you may want to create a few functions for each server on your model (maybe it's an App.User model, and they've authenticated via OAuth to these providers). So you could do this:

# app/models/user.coffee
class App.User extends Tower.Model
  if Tower.isServer
    @include App.UserFacebookMixin
    @include App.UserTwitterMixin
    @include App.UserGitHubMixin
    # etc.

  # other client/server shared code below
# app/concerns/user/facebookMixin.coffee
App.UserFacebookMixin =
  postToFacebook: (post, callback) ->
    # do some facebook api thing here

# app/concerns/user/twitterMixin.coffee
App.UserTwitterMixin =
  postToTwitter: (post, callback) ->
    # do some facebook api thing here

Hope that helps.

lancejpollard commented 11 years ago

Oh one other note in terms of security. Make sure to add that secret property to the set of protected attributes, so people can't pass a value in through a request. Not sure if you're familiar with this so this is just for reference (see the Rails Security Guide's section on mass assignment.

This is how GitHub got hacked.

Basically, people can submit anything they want to your app, so they might send params that get parsed into the @params object, maybe they send this:

console.log(@params)
#=> {"application":{"name":"My App","secret":"hacked-secret"}}
App.Application.create(@params)

So they were able to create a record with their own secret.

To avoid, this, all you need is:

class App.Application extends Tower.Model
  @field 'secret', type: 'String', default: -> @doHash()

  @protected 'secret'

Now, if that attribute is passed into App.Application.create(secret: 'asdf') (or App.Application.build or record.updateAttributes), it will just get deleted from the hash/object.

To set protected attributes, you just have to set it manually:

class App.ApplicationsController extends Tower.Controller
  create: ->
    record = App.Application.build(@params.application)
    # and if you wanted to set the secret manually:
    record.set('secret', record.doSecret())

Finally, you can assign "roles" to protected attributes, which allows you to override them. So say any admin can include secret in their params hash, but by default nobody can. You can do that with this:

App.Application.build({secret: 'asdf'}, as: 'admin')

See that mass assignment section in the Rails Security Guide for more details if you're interested, it's all mostly implemented in Tower. Also the tests show more examples:

test/cases/model/shared/massAssignmentTest.coffee