Code Monkey home page Code Monkey logo

targaryen's Introduction

targaryen

Build Status

Completely and thoroughly test your Firebase security rules without connecting to Firebase.

Usage

All you need to do is supply the security rules and some mock data, then write tests describing the expected behavior of the rules. Targaryen will interpret the rules and run the tests.

const assert = require('assert');
const targaryen = require('targaryen');

const rules = {
  rules: {
    foo: {
      '.write': 'true'
    }
  }
};
const data = {foo: 1};
const auth = {uid: 'someuid'};

const database = targaryen.database(rules, data).as(auth).with({debug: true});
const {allowed, newDatabase, info} = database.write('/foo', 2);

console.log('Rule evaluations:\n', info);
assert.ok(allowed);

assert.equal(newDatabase.rules, database.rules);
assert.equal(newDatabase.root.foo.$value(), 2);
assert.equal(newDatabase.auth, auth);

Targaryen provides three convenient ways to run tests:

  • as a standalone command-line utility:

    targaryen path/to/rules.json path/to/tests.json
  • as a set of custom matchers for Jasmine:

    const targaryen = require('targaryen/plugins/jasmine');
    const rules = targaryen.json.loadSync(RULES_PATH);
    
    describe('my security rules', function() {
    
      beforeEach(function() {
        jasmine.addMatchers(targaryen.matchers);
        targaryen.setFirebaseData(require(DATA_PATH));
        targaryen.setFirebaseRules(rules);
      });
    
      it('should allow authenticated user to read all data', function() {
        expect({uid: 'foo'}).canRead('/');
        expect(null).cannotRead('/');
      })
    
    });
  • as a plugin for Chai.

    const chai = require('chai');
    const targaryen = require('targaryen/plugins/chai');
    const expect = chai.expect;
    const rules = targaryen.json.loadSync(RULES_PATH);
    
    chai.use(targaryen);
    
    describe('my security rules', function() {
    
      before(function() {
        targaryen.setFirebaseData(require(DATA_PATH));
        targaryen.setFirebaseRules(rules);
      });
    
      it('should allow authenticated user to read all data', function() {
        expect({uid: 'foo'}).can.read.path('/');
        expect(null).cannot.read.path('/');
      })
    
    });
  • or as a set of custom matchers for Jest:

    const targaryen = require('targaryen/plugins/jest');
    const rules = targaryen.json.loadSync(RULES_PATH);
    
    expect.extend({
      toAllowRead: targaryen.toAllowRead,
      toAllowUpdate: targaryen.toAllowUpdate,
      toAllowWrite: targaryen.toAllowWrite
    });
    
    describe('my security rules', function() {
      const database = targaryen.getDatabase(rules, require(DATA_PATH));
    
      it('should allow authenticated user to read all data', function() {
        expect(database.as({uid: 'foo'})).toAllowRead('/');
        expect(database.as(null)).not.toAllowRead('/');
      })
    
    });

When a test fails, you get detailed debug information that explains why the read/write operation succeeded/failed.

See USAGE.md for more information.

How does Targaryen work?

Targaryen statically analyzes your security rules using esprima. It then conducts two passes over the abstract syntax tree. The first pass, during the parsing process, checks the types of variables and the syntax of the rules for correctness. The second pass, during the testing process, evaluates the expressions in the security rules given a set of state variables (the RuleDataSnapshots, auth data, the present time, and any wildchildren).

Install

npm install targaryen@3

API

  • targaryen.database(rules: object|Ruleset, data: object|DataNode, now: null|number): Database

    Creates a set of rules and initial data to simulate read, write and update of operations.

    The Database objects are immutable; to get an updated Database object with different the user auth data, rules, data or timestamp, use its with(options) method.

  • Database.prototype.with({rules: {rules: object}, data: any, auth: null|object, now: number, debug: boolean}): Database

    Extends the database object with new rules, data, auth data, or time stamp.

  • Database.prototype.as(auth: null|object): Database

    Extends the database object with auth data.

  • Database.prototype.read(path: string, options: {now: number, query: object}): Result

    Simulates a read operation.

  • Database.prototype.write(path: string, value: any, options: {now: number, priority: any}): Result

    Simulates a write operation.

  • Database.prototype.update(path: string, patch: object, options: {now: number}): Result

    Simulates an update operation (including multi-location update).

  • Result: {path: string, auth: any, allowed: boolean, info: string, database: Database, newDatabase: Database, newValue: any}

    It holds:

    • path: operation path;
    • auth: operation authentication data;
    • type: operation authentication type (read|write|patch);
    • allowed: success status;
    • info: rule evaluation info;
    • database: original database.

    For write and update operations, it also includes:

    • newDatabase: the resulting database;
    • newValue: the value written to the database.
  • targaryen.store(data: object|DataNode, options: {now: number|null, path: string|null, priority: string|number|null}): DataNode

    Can be used to create the database root ahead of time and check its validity.

    The path option defines the relative path of the data from the root; e.g. targaryen.store(1, {path: 'foo/bar/baz'}) is equivalent to targaryen.store({foo: {bar: {baz: 1}}}).

  • targaryen.store(rules: object|Ruleset): Ruleset

    Can be used to create the database rule set ahead of time and check its validity.

  • targaryen.util

    Set of helper functions used by the jasmine and chai plugins reference implementations.

Why is it named Targaryen?

There were trials. Of a sort. Lord Rickard demanded trial by combat, and the king granted the request. Stark armored himself as for battle, thinking to duel one of the Kingsguard. Me, perhaps. Instead they took him to the throne room and suspended him from the rafters while two of Aerys's pyromancers kindled a flame beneath him. The king told him that fire was the champion of House Targaryen. So all Lord Rickard needed to do to prove himself innocent of treason was... well, not burn.

George R.R. Martin, A Clash of Kings, chapter 55, New York: Bantam Spectra, 1999.

License

ISC.

targaryen's People

Contributors

a-xin avatar bijoutrouvaille avatar curtishumphrey avatar dinoboff avatar fredrikl avatar froelund avatar georgesboris avatar goldibex avatar ibash avatar jbearer avatar jtwebman avatar matijse avatar mhuebert avatar petrusek avatar pthrasher avatar willfarrell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

targaryen's Issues

newData as value instead of object

It appears that Targaryen evaluates newData as the value to be written, instead of an object with {key:value} of the data to be written (as Firebase does).

For example, if I have the collection:

/users/$uid
   - first_name
   - last_name
   - age

and I try to write "Ben" to /users/USER123/first_name and do the check: newData.val().length > 0, Targaryen will evaulate newData as "ben", while in Firebase, it will evaluate newData as {'first_name': "Ben"}, so to do the constraint, I actually need to write: newData.first_name.val().length > 0.

tryWrite does not merge root and newData

Given rules like this:

{
  "rules": {
    "foo": {
      "id": {
        ".validate": "newData.isString()"
      },
      "bar": {
        ".validate": "newData.isString()"
      }
    }
  }
}

Shouldn't the tryWrite pass? In a similar test I'm seeing a rejection at the level of /foo with newData.hasChildren[['id', 'bar'])

targaryen.setFirebaseData({foo: {id: 'hi', bar: 'blah'}})
root = targaryenHelpers.getFirebaseData()
rules = targaryenHelpers.getFirebaseRules()
rules.tryWrite('foo/bar', root, 'taco', ...)

newData should contain all the merged data

Hey man, thanks for developing this.

We've been trying to use targaryan for a while now with my team.
Although it helps a lot we're encountering some issues, specially since the updates made recently.

Take a look at this for instance.

In Firebase, when you update some paths, the newData holds all the information for the current tree, not only the information that is been sent.

Like so:

captura de tela 2016-12-22 as 4 49 46 pm

captura de tela 2016-12-22 as 4 45 41 pm

When trying to do the same with targaryan:

captura de tela de 2016-12-22 16-43-08

I will try to post here when we find any other issues.
And we will gladly contribute to their solutions, it's just that we're in a crazy end of year sprint.
We're kind of being forced to "let the tests slip for a while" instead of being allowed to fix this.

Can't run from the command line

Just tried to npm install on OSX El Capitan - targaryen won't run from the command line (command not found). Any tips on how to resolve?

Flag to log only the failing conditions

When using targaryen as the jasmine integration, when a test is failing, the output is really verbose for complex rules.

To debug the issue, we have to look for => false in all the logs.

I tried playing with setVerbose and setDebug, but none seems to help (either we have no logs or too much logs).

I'm suggesting adding an option to log only the failing conditions (the ones returning false or throwing an error).

Testing against .validate is not performed

Hi all,

I am trying to run the following test:

...
   before(() => {
    ...
    targaryen.setFirebaseData({
      technicians: {
        'technician-admin': {
          created,
          profile,
          type: 'admin',
        }
      }
    });
  });
...
const admin = { uid: 'technician-admin', email: '[email protected]' };
...
expect(admin).cannot.write({ index: 'firebase', query: '*' }).to.path('search/request/:id');
...

against the following rules:

{
  "rules": {
    "search": {
      "request": {
        "$request": {
          ".validate": "newData.hasChildren(['index', 'type', 'query'])",
          "index": {
            ".validate": "newData.isString() && newData.val().length > 0"
          },
          "type": {
            ".validate": "newData.isString() && newData.val().length > 0"
          },
          "query": {
            ".validate": "newData.isString() && newData.val().length > 0"
          },
          "$other": {
            ".validate": "false"
          },
          ".write": "data.val() == null && (auth != null && auth.uid != null) && (newData.child('type').val() == 'item' || newData.child('type').val() == 'practice' || root.child('technicians').child(auth.uid).child('type').val() == 'admin')"
        }
      }
    }
  }
}

but it fails with the following error:

1) database user:technician:admin /search /request /:id pass:
     AssertionError: Expected a user with credentials {"uid":"technician-admin","email":"[email protected]"} not to be able to write {"index":"firebase","query":"*"} to search/request/:id, but the rules allowed the write.
/search/request/:id:.write: "data.val() == null && (auth != null && auth.uid != null) && (newData.child('type').val() == 'item' || newData.child('type').val() == 'practice' || root.child('technicians').child(auth.uid).child('type').val() == 'admin')"
    => true
Write was allowed.
      at Assertion.<anonymous> (node_modules/targaryen/lib/chai.js:84:12)
      at Assertion.ctx.(anonymous function) [as path] (node_modules/chai/lib/chai/utils/addMethod.js:41:25)
      at Context.it (test/database/rules.targaryen.js:1234:76)

This test would not have failed if ".validate": "newData.hasChildren(['index', 'type', 'query'])" would have been evaluated, it seems that targaryen didn't pickup .validate

Any help would be appreciated. We were looking for a way to test Fireabase rules locally (we ran them against a Fireabase app until now) and a few days ago we have been recommended targaryen by Mike from the Firebase support team. We have rewritten 300 tests to run on targaryen but some things we are not able to test.

Thanks in advance,
Romans

Test borken, but console show all conditions true

The test is breaking, but all conditions than the console show are true. I'm using chai plugin.
It's my rules json:

{
  "rules": {
    "list": {
      "$listId": {
        ".write": "(newData.parent().parent().child('otherList1').child($listId).val() == true)
        	     && (newData.parent().parent().child('otherList2').child($listId).val() == false)"
      }
    }
  }
}

And it's is test file:

const chai = require('chai');
const targaryen = require('targaryen/plugins/chai');
const { expect } = chai;
const { users } = targaryen;

chai.use(targaryen);

describe('list', function() {

  before(function() {
    targaryen.setFirebaseData({});
    targaryen.setFirebaseRules(require('../../database.rules.json'));
  });

  it('', () => {

    const updates = {
      'list/_listId1/id': '_listId1',
      'otherList1/_listId1': true,
      'otherList2/_listId1': false
    };

    expect({ uid: '_uid1' }).can.patch(updates).to.path('/');

  });

});

The console:

captura de tela de 2016-12-21 16 18 35

Wrong result for array-like collections

Hey!

Great library! I was looking for something like that, BUT ... it doesn't work well for array-like collections. Example:
lets say we have collection like that:
(We're are using bolt in our project)

type SomeAttr {
  // some mandatory property
  itemId: String
  ...
  // some optional property
  someAttrProp: String | Null
  ...
}
type Item {
  ...
  someAttrsCollection: SomeAttr[]
  ...
}

This code after compile results with rules like

...
"someAttrsCollection": {
  "$key1": {
    ".validate": "newData.hasChildren() && newData.hasChild('itemId')",
    ...
    "someAttrProp": {
      ".validate": "newData.isString()"
    }
    ...
  }
}
...

so, if we make write operation like that: /item/someAttrsCollection/0/someAttrProp, 'some value' it should be successful (and it is in real database!!!), but unit test with targaryen fails (write is denied) complaining to this rule ".validate": "newData.hasChildren() && newData.hasChild('itemId')"

live testing for Firebase parity

I suggest we set up async tests that run against a live Firebase database. We currently have a number of known inconsistencies, and Firebase rule evaluation itself can change without notice.

CLI API

Expose an API for what the CLI does, so we can write programmatic tests without Chai or Jasmine.

Initial timestamp

Hi @goldibex thanks for writing and maintaning this lib, it is awesome.

I have a small issue that I was wondering if you might be able to help me solve.

I have a validation rule

type InitialTimestamp extends Number {
  validate() { initial(this, now) }
}
// Returns true if the value is intialized to init, or if it retains it's prior value, otherwise.
initial(value, init) { value == (prior(value) == null ? init : prior(value)) }

type UserProfile {
  email: String
  signupDate: InitialTimestamp
}

The data im using for the write test is like so:

userProfile =
  email: '[email protected]'
  splitType: 'auto'
  signupDate: { ".sv": "timestamp" }

expect(uid:userUID).can.write({userProfile}).to.path userProfilePath + "/#{userUID}"

When inforcing this inside of targaryen it seems like there is a timing issue because it rejects it.

 be able to write {"email":"[email protected]","splitType":"auto","signupDate":1463817931069} to /user/profiles/2d7fda85-6ef7-495f-a70b-2e05ccda6e9e, but the rules denied the write.
/user/profiles/2d7fda85-6ef7-495f-a70b-2e05ccda6e9e:.write: "auth != null && auth.uid == $uid"
    => true
/user/profiles/2d7fda85-6ef7-495f-a70b-2e05ccda6e9e:.validate: "newData.hasChildren(['email', 'signupDate'])"
    => true
/user/profiles/2d7fda85-6ef7-495f-a70b-2e05ccda6e9e/email:.validate: "newData.isString()"
    => true
/user/profiles/2d7fda85-6ef7-495f-a70b-2e05ccda6e9e/signupDate:.validate: "newData.isNumber() && newData.val() == (data.val() == null ? now : data.val())"
    => false
Validation failed.
Write was denied.

If I change the rule to (less than current time on server):

initial(value, init) { value <= (prior(value) == null ? init : prior(value)) }

It passes. So I can see in the source that targaryen converts { ".sv": "timestamp" } to Date.now() but I cannot find where it enforces that rule and if it is generating another time at a later point and trying to compare them.

Cheers for any help, Jake

Edit:
It must be comparing these two dates

'.value': Date.now(),

now: Date.now(),

Nested read/write rules not respected

Given the following rules:

{
  "rules": {
    ".read": "auth !== null",
    ".write": "auth !== null",
    "users": {
      "$user": {
        ".write": "auth.uid === $user"
      }
    }
  }
}

and the following data:

{
  "users": {
    "3403291b-fdc9-4995-9a54-9656241c835d": {
      "name": "User 1"
    },
    "500f6e96-92c6-4f60-ad5d-207253aee4d3": {
      "name": "User 2"
    }
  }
}

This test should pass but fails:

const user1 = { uid: '3403291b-fdc9-4995-9a54-9656241c835d' };
const user2 = { uid: '500f6e96-92c6-4f60-ad5d-207253aee4d3' };

expect(user1).can.write.to.path(`users/${user1.uid}`);
expect(user1).cannot.write.to.path(`users/${user2.uid}`);

If i remove the top-level ".write" rule, the test correctly passes as expected which leaves me to conclude that nested read/write rules may not be resolved correctly.

Subvalidations do not seem to be working

Firstly, thanks for this package, it will help me a lot in development.

I have a rule something like

     "invites": {
        "pending": {
          "$invite_id": {
            ".validate": "newData.hasChildren(['bookID', 'email', 'ownerID', 'status'])",
            ".write": "auth.uid === newData.child('ownerID').val()",
            "bookID": {
              ".validate": "newData.isString()"
            },
            "email": {
              ".validate": "newData.isString()"
            },
            "ownerID": {
              ".validate": "newData.isString()"
            },
            "status": {
              ".validate": "newData.isString()"
            },
            "$other": {
              ".validate": "false"
            }
          }
        }

When using the Firebase simulator, a write of { bookID: 'hey', email: 5, ownerID: 'simplelogin:3', status: 'hey' } (note the email field being a non-string) to invites/pending/invite_id as the simplelogin:3 user does not succeed.

However using the Targaryen test suite (using the CLI) a 'cannotWrite' fails (i.e. it does succeed, and the test fails).

  "tests": {
    "invites/pending/invite_id": {
      "cannotWrite": [
        { "auth": "John Brown", "data": { "ownerID": "simplelogin:3" } },
        { "auth": "John Brown", "data": {
            "ownerID": "simplelogin:3",
            "bookID": "hey",
            "email": 5,
            "status": "sent"
          }
        }
      ]
    }

I'm not sure if I'm doing something wrong, whether I should be using Jasmine instead because it's too complicated, or if this is an issue with the test suite. Let me know, thanks :)

Edit:

I should add that other tests succeed at this location, like cannotWrite for users which aren't simplelogin:3 and canWrite for users who are simplelogin:3.

Also, the '$other' validation doesn't seem to be recognized by the test suite either - when I add an additional field, the suite expects it to succeed even though it should fail (verified with simulator).

newData.parent().parent() not referencing the root

If I have the following rules:

{
  "rules": {
    "users": {},
    "groups": {
      "$groupId": {
        ".write": "newData.parent().parent().child('users').child(auth.uid).exists()"
      }  
    }
  }
}

The new newData.parent().parent() is not correcty pointing to the root, in my test logs I get:

newData.parent().parent() = {"path":"","exists":true}
newData.parent().parent().child('users') = {"path":"users","exists":false}

The rule is working fine using on Firebase.

Thanks!

--verbose is printing only one line

--verbose shows only one test. Ie: I have 8 tests. Running without --verbose I get 0 failures in 8 tests, Running with --verbose I get

│ Path                                        │ Type  │ Auth    │ Expect │ Got │
├─────────────────────────────────────────────┼───────┼─────────┼────────┼─────┤
│ users/db3993ee-4e1c-412e-a19c-e6f700def9c4  │ read  │ Marcel  │ ✓      │ ✓   │```

Document timestamp related api changes

Add documentation for:

  • setFirebaseData gains a now parameter: targaryen.setFirebaseData(data[, now]);.
  • jasmine matchers gain an optional now parameter.
  • chai plugin write and patch methods gain an optional now parameter.
  • chai plugin gains a readAt(now) method.

newData evaluation location not correctly computed?

I think this (rule,test) pair should work. It doesn't but if you delete .child('a)' from the validate rule it works. This suggests to me the evaluation of ".validate" is not aware of its logical position in relation to the written data.
{
"rules": {
".validate": "newData.child('a').val() != null",
"a": {
".read": "true",
".write": "true"
}
}

}

{
"root": {
"a": 1,
"b": 2
},
"tests": {
"a": {
"canRead": [1],
"canWrite": [{ "auth": 1, "data": { ".sv": "timestamp" } }]
}
}
}

Snapshot and string method should handle null

auth and its properties, or a snapshot val calls can evaluate to any primitive types, including null. Tests should be added and fix implemented to make sure snapshot and string methods, and binary operations behave like on firebase's; e.g., root.child(auth.foo).exists() and root.child(auth.foo).exists() == false should both evaluate to false if auth or auth.foo is null.

Null values are passed up the hierarchy

When writing or patching a null value, the null is passed up the hierarchy which causes tests to fail when they should not.

Example:

{
  "rules": {
    "A": {
      "B": {
        "C": {
          ".read": true,
          ".write": true
        }
      }
    }
  }
}

Chai code:

  1. expect().can.write({C: 1}).to.path('A/B'); - passes
  2. expect().can.write({C: null}).to.path('A/B'); - fails: AssertionError: Expected an unauthenticated user to be able to write `null` to A/B, but the rules denied the write. No .write rule allowed the operation. Write was denied.
  3. expect().can.patch({C: 1}).to.path('A/B'); - passes
  4. expect().can.patch({C: null}).to.path('A/B'); - passes
  5. expect().can.write({B: {C: 1}}).to.path('A'); - passes
  6. expect().can.write({B: {C: null}}).to.path('A'); - fails: AssertionError: Expected an unauthenticated user to be able to write `null` to A, but the rules denied the write. No .write rule allowed the operation. Write was denied.
  7. expect().can.patch({B: {C: 1}}).to.path('A'); - passes
  8. expect().can.patch({B: {C: null}}).to.path('A'); - fails: AssertionError: Expected an unauthenticated user to be able to write `null` to A/B, but the rules denied the write. No .write rule allowed the operation. Write was denied.

Invalid location in tests are not reported

".validate" rules are not respected when the auth uid has a period in it. For instance:

const targaryen = require('targaryen');
// Note that "firstSignIn" must be a number
const rules = {
  rules: {
    ".write": true,
    ".read": true,
    users: {
      '$uid': {
        activity: {
          firstSignIn: {
            '.validate': 'newData.isNumber()'
          }
        }
      }
    }
  }
};
const user = { uid: 'foo.bar' };
const data = { users: { } };
const path = `/users/${user.uid}/activity/firstSignIn`;
const database = targaryen.database(rules, data).as(user).with({debug: true});
const result = database.write(path, 'notAnumber,shouldFail');
console.log(result.validated) // -> true

However if you change the above user to be "foobar", result.validated will correctly return "false".

I encountered this as I was creating dummy users using Math.random() for the uid, which is easy enough to work around, but it was a headache to track down why this is happening.

Can't create unauthenticated user for tests

I'm trying to create a user in the users object that's unauthenticated via:

"users": {
      "unauthenticated": null
    }

Then I have a rule that checks auth != null:

".write": "auth != null"

I get this error when running tests:

/usr/local/lib/node_modules/targaryen/lib/test-jig.js:22
    auth.$description = desc;
                      ^
TypeError: Cannot set property '$description' of null
    at TestJig._lookupAuth (/usr/local/lib/node_modules/targaryen/lib/test-jig.js:22:23)
    at TestJig.<anonymous> (/usr/local/lib/node_modules/targaryen/lib/test-jig.js:50:19)
    at Array.map (native)
    at TestJig.<anonymous> (/usr/local/lib/node_modules/targaryen/lib/test-jig.js:48:42)
    at Array.forEach (native)
    at TestJig.run (/usr/local/lib/node_modules/targaryen/lib/test-jig.js:32:27)
    at Object.<anonymous> (/usr/local/lib/node_modules/targaryen/bin/targaryen.js:25:17)
    at Module._compile (module.js:460:26)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)

Leading "/" confuses path

"tests": {
"a": {
"canRead": [1]
}
}

is different to

"tests": {
"/a": {
"canRead": [1]
}
}

which can cause issues for people that naturally prepend / to path.

CanWrite passes if any child node can be written

Hi, I'm seeing tests pass that I don't think should actually be passing. I have a set of rules that look something like this (simplified a bit here):

{
  "rules":{
    ".write":"false",
    ".read":"false",
    "accounts": {
      ".write":"false",
      ".read":"true",
      "$accountId": {
        ".write":"false",
        ".read":"true",
        "flows": {
          ".write":"false",
          ".read":"true",
          "$flowId": {
            ".write":"false",
            "name": {
              ".write":"true",
              ".read":"true"
            },
            "createdAt": {
              ".write":"true",
              ".read":"true"
            },
            "steps": {
              ".write":"false",
              ".read":"true",
              "$stepId": {
                ".write":"true",
                ".read":"true",
                "stepType": {
                  ".write":"true",
                  ".read":"true"
                },
                "content": {
                  ".write":"true",
                  ".read":"true"
                }
              }
            }
          }
        }
      }
    }
  }
}

If I attempt to write the following data, it passes the write test (which it shouldn't according to my understanding of the rules):

  "tests": {
    "accounts/68/flows" : {
      "canWrite" : [{
        "auth": "User2000",
        "data": {
          "-Jkaaiir8329": {
            "name": "Test Flow",
            "createdAt": 123431411,
            "steps": {
              "-J9847saf7fhu": {
                "stepType": "image",
                "content": "test"
              }
            }
          }
        }
      }]
    }
  }

The rule should fail at this part I would think:

"$flowId": {
    ".write":"false",

But instead it looks like targaryen keeps evaluating child rules, finding one that passes. Here's the debug output, showing why the test passes:

/:.write: "false"
    => false
/accounts:.write: "false"
    => false
/accounts/68:.write: "false"
    => false
/accounts/68/flows:.write: "false"
    => false
/accounts/68/flows/-Jkaaiir8329:.write: "false"
    => false
/accounts/68/flows/-Jkaaiir8329/name:.write: "true"
    => true
Write was allowed.

┌───────────────────┬───────┬──────────┬────────┬─────┐
│ Path              │ Type  │ Auth     │ Expect │ Got │
├───────────────────┼───────┼──────────┼────────┼─────┤
│ accounts/68/flows │ write │ User2000 │ ✓      │ ✓   │
└───────────────────┴───────┴──────────┴────────┴─────┘

Maybe I'm using this incorrectly or have an incorrect assumption about how it should be working, but I think this may be a bug?

Thanks for the awesome tool by the way -- super helpful.

Jest support

Since there are jasmine matchers do you know if there is a way to use them with jest as well?

new lines and comments in rules.json

Hi all,

we are evaluating how to test firebase rules and this seems to be the only real solution. Our rules.json is getting big, and to keep things clear we add comments and new lines using the same syntax accepted by firebase. Nevertheless targaryen complains about the json format. It doesn't like new lines nor comments in the json.

(Of course I could clean up the file before using targaryen.)

Is there any plans to support new lines and comments ?

thanks

Fix type validations

Targaryen 3 will infer a node type to "any" or "primitive" when it cannot be inferred it and will allow the expression if type validation can be delayed till evaluation. Logical expression (including the unary one) do not allow it; other expression type validation can be delayed.

  • Loosen inferring #64
  • == and !== should should compare number, boolean, string, or null #84
  • >, >=, < and <= should compare number and string #85
  • Fix handling of arguments with wrong type in snapshot and string methods. #87
  • Stricter comparison type inferring #88
  • Strict boolean unary type inferring #89

Nested variables in rules

There is some undocumented problem with nested variables in Firebase rules recognized by targaryen. Firebase itself is handling them correctly.

Please see PR #23 where I demonstrate the problem in a (currently disabled) test suite.

Error: Expected an unauthenticated user to be able to read first/second/third, but the rules denied the read.
    /:<no rules>
    /first:<no rules>
    /first/second:<no rules>
    /first/second/third/: "$one == 'first' && $two == 'second' && $three == 'third'"
    $one: unknown variable $one
        => false
No .read rule allowed the operation.
Read was denied.

versus Firebase simulator

Attempt to read /first/second/third with auth=null
    /: "false"
        => false
    /first
    /first/second
    /first/second/third: "$one == 'first' && $two == 'second' && $three == 'third'"
        => true

Read was allowed.

Update operation result should include the new value of the node

For a write operation newValue holds the the value written to the node getting written; it is the value the user submitted and the value that will get written.

For multi-location write operations, having the patch data is important but having the resulting new value of the node would be useful; the alternative is something like result.newDatabase.root.path.to.node.$value().

I am not sure if the new attribute to the result object should be the resulting node value (minor change) or the patch data (major change if we use newValue for the resulting node value).

variables not propagated in path

Hi,

I am trying to test some rules in nested paths. Using the following code (testing using Jasmine)

var targaryen = require('targaryen');

targaryen.setFirebaseData(
    {
        "one": {
            "one": true,
            "two": true
        }
    }
);

targaryen.setFirebaseRules(
    {
        "rules": {
            "$first": {
                "$second": {
                    ".read": "$first == $second"
                }
            }
        }
    }
);


describe('A set of rules and data', function() {

    beforeEach(function() {
        jasmine.addMatchers(targaryen.jasmine.matchers);
    });

    it('can be tested', function() {
        expect(targaryen.users.unauthenticated).canRead("/one/one");
        expect(targaryen.users.unauthenticated).cannotRead("/one/two");
    });

});

I get a failure as:

Expected an unauthenticated user to be able to read /one/one, but the rules denied the read.
    /:<no rules>
    /one:<no rules>
    /one/one/: "$first == $second"
    $first: unknown variable $first
        => false
    No .read rule allowed the operation.
    Read was denied.

I think this should pass, and the test cases would suggest it should but I notice the relevant test case is 'pending'.

Patch does not allow deep updates

Targaryen Version: 3.0.1

Security Rules:

{
  "rules": {
    "A": {
      "B": {
        "C": {
          "D": {
            ".write": "true"
          }
        }
      },
      "X": {
        "Y": {
          "Z": {
            ".write": "true"
          }
        }
      }
    }
  }
}

The following tests are failing but should succceed:
1.) Update a single path
expect().can.patch({B: {C: {D: 10}}}).path('A');
2.) Update both paths
expect().can.patch({B: {C: {D: 10}}, X: {Y: {Z: 20}}}).path('A');

The following code succeeds using firebase with the same security rules:
1.) Update a single path

var data = {};
data["B/C/D"] = 10;
database.ref('A').update(data);

2.) Update both paths

var data = {};
data["B/C/D"] = 10;
data["X/Y/Z/"] = 20;
database.ref('A').update(data);

Summary:
Only the full path (e.g.: A/B/C/D ) is writeable. Firebase still allows an update on the node A with the data for B/C/D. Targaryen seems to be more strict and doesn't allow this kind of patch.

Nested objects within auth not working.

I've created a simple test that fails in targaryen, but passes on firebase simulator.

Test:
{
  "root": {
    "chats": {
      "chat-01": {
        "orgID": "org-01"
      }
    }
  },

  "users": {
    "user01": {
      "uid": "user-01",
      "orgs": {
        "org-01": true
      }
    }
  },

  "tests": {
    "chats/chat-01": {
      "canRead": [ "user01" ]
    }
  }
}
Rules:
{
  "rules": {
    "chats": {
      "$chatID": {
        ".read": "root.child('chats').child($chatID).child('orgID').val() != null && auth.orgs != null && auth.orgs[root.child('chats').child($chatID).child('orgID').val()] == true"
      }
    }
  }
}

For easier reading, here's the rule pretty-printed:

root.child('chats').child($chatID).child('orgID').val() != null
&&
auth.orgs != null
&&
auth.orgs[ root.child('chats').child($chatID).child('orgID').val() ] == true

Support firestore

Need a new security rule parser, a new runtime to evaluate rule condition and new data layer.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.