- auth 2017-03-19T16:58:15+01:00 https://fadeit.dk/blog/tag/auth.html Firebase advanced data modelling and role based authorization 2016-08-25T16:10:30+02:00 https://fadeit.dk/blog /2016/08/25/firebase-advanced-data-modelling-and-role-based-authentication-authorization <p>It’s been almost 5 years since Firebase was released. It looked cool from the beginning, but I never got to use it in a project. <br /> That’s going to change today, because I am set on using it and there’s no turning back.</p> <p>I’m not experienced with it, so this is just my idea of how to handle things. If you have a better idea, feel free to share so we can all become better at Firebasing!</p> <h2 id="project-requirements">Project requirements</h2> <p>Before diving deep into how I modelled my data, it’s important to understand what are the entities involved, how they relate to each other &amp; what kind of roles I have to deal with.</p> <p>I’ll try not to go into details; they shouldn’t affect my general approach (the stupid devil is <strong>*always*</strong> in the detail).</p> <h4 id="entities">1. Entities</h4> <p>I called the main objects in my real-time database entities: name them as you wish, they are just top-level creatures. The main rule I followed when designing these entities: avoid deep nesting (<a href="https://www.youtube.com/watch?v=9gnvC-xIDgs">related</a>, by <a href="http://twitter.com/chrisesplin">Chris Esplin</a>).</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">Auth</span><span class="err">:</span> <span class="p">{</span> <span class="cm">/* Model designed by Firebase, we can't touch this tanana */</span> <span class="p">}</span> <span class="nl">Users</span><span class="p">:</span> <span class="p">{</span> <span class="cm">/* $uid from Firebase Auth */</span> <span class="nl">$uid</span><span class="p">:</span> <span class="p">{</span> <span class="nl">role</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="c1">// i.e. 'admin', more on that later</span> <span class="p">}</span> <span class="p">}</span> <span class="nl">Projects</span><span class="p">:</span> <span class="p">{</span> <span class="nl">$pid</span><span class="p">:</span> <span class="p">{</span> <span class="cm">/* Metadata */</span> <span class="p">}</span> <span class="p">}</span> <span class="nl">Files</span><span class="p">:</span> <span class="p">{</span> <span class="nl">$pid</span><span class="p">:</span> <span class="p">{</span> <span class="nl">$fid</span><span class="p">:</span> <span class="p">{</span> <span class="cm">/* File */</span><span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="nl">Tasks</span><span class="p">:</span> <span class="p">{</span> <span class="nl">$pid</span><span class="p">:</span> <span class="p">{</span> <span class="nl">$tid</span><span class="p">:</span> <span class="p">{</span> <span class="cm">/* Task */</span><span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="nl">Messages</span><span class="p">:</span> <span class="p">{</span> <span class="nl">$pid</span><span class="p">:</span> <span class="p">{</span> <span class="nl">$mid</span><span class="p">:</span> <span class="p">{</span> <span class="cm">/* Message */</span><span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p><br /></p> <h4 id="user-roles--relationships">2. User roles &amp; relationships</h4> <p>Once more, I might be omitting some details here and there. Here are the main roles &amp; their permissions:</p> <ul> <li> <p>Admin</p> <ul> <li><strong>Everything</strong>: Create, Read, Update, Delete</li> </ul> </li> <li> <p>Manager</p> <ul> <li><strong>Projects</strong>, <strong>Tasks</strong>, <strong>Files</strong>: Create, Read, Update, Delete</li> <li><strong>Files</strong> Create, Read, Update, Delete</li> <li><strong>Messages</strong>: Create, Read, Update, Delete</li> <li><strong>Tasks</strong>: Create, Read, Update, Delete</li> <li><strong>Users</strong>: Create, Read</li> <li><strong>Self</strong> Read, Update, Delete (users/{$ownUid})</li> </ul> </li> <li> <p>User</p> <ul> <li><strong>Projects</strong>, <strong>Tasks</strong> Read</li> <li><strong>Files</strong> Create, Read, Update, Delete</li> <li><strong>Messages</strong> Read</li> <li><strong>Own Messages</strong> Create, Update, Delete</li> <li><strong>Tasks</strong> Read, Update</li> <li><strong>Self</strong> Read, Update, Delete (users/{$ownUid})</li> </ul> </li> <li> <p>External users / collaborators</p> <ul> <li><strong>Some Projects</strong> Read</li> <li><strong>Some Tasks</strong> Read</li> <li><strong>Files</strong> Create, Read</li> <li><strong>Own Files</strong> Update, Delete</li> <li><strong>Self</strong> Read, Update, Delete (users/{$ownUid})</li> </ul> </li> </ul> <h2 id="general-approach">General approach</h2> <p>As you can see, it’s quite messy. Expressing this with conditionals (i.e. <code class="highlighter-rouge">if(user.admin === true){...}</code>) would be a terrible idea. Here’s why:</p> <ul> <li>hard to document</li> <li>hard to maintain</li> <li>hard to change / scale</li> <li>code coupling</li> <li>a pain to express as Firebase’s JSON rules</li> </ul> <p>I’d like to avoid all this so I ditched the idea quickly. In fact, I can’t see how this approach would work with most tools / systems, not only with Firebase, but that’s another story. <br /> What I do want to go for instead is an activity-based authorization system.</p> <p>Here’s how that’ll work: By default no user has any permission. Then, I create a new entity in my real-time database, called <strong>Permissions</strong> where I can define roles (that give certain permissions). I’ll key by role name and write all permissions for a certain role. Example: <br /></p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">Permissions</span><span class="err">:</span> <span class="p">{</span> <span class="nl">user</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// User role</span> <span class="nl">users</span><span class="p">:</span> <span class="p">{</span> <span class="nl">own</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// can read, update, delete own data</span> <span class="nl">r</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">u</span><span class="err">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">d</span><span class="err">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">other</span><span class="err">:</span> <span class="p">{}</span> <span class="c1">// can't read/write 'other' (not owned) data; we can even omit this field</span> <span class="p">},</span> <span class="cm">/* * ... * skipped some entities because 'messages' are more interesting * ... */</span> <span class="nx">messages</span><span class="err">:</span> <span class="p">{</span> <span class="nl">c</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// can create messages</span> <span class="p">},</span> <span class="nx">message</span><span class="err">:</span> <span class="p">{</span> <span class="nl">own</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// can update &amp; delete own messages</span> <span class="nl">r</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">u</span><span class="err">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">d</span><span class="err">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">other</span><span class="err">:</span> <span class="p">{</span> <span class="c1">// can read other messages</span> <span class="nl">r</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>A small note here, my permissions mirror CRUD, but they can be anything really. I also grouped them in ‘own’ &amp; ‘other’ for readability, but it might as well be <code class="highlighter-rouge">canReadOwn</code> or <code class="highlighter-rouge">canReadOther</code>. For my use case the above is good enough, but you might choose to have more fine-grained control in yours.</p> <p>The way this will work is that upon creation, users are assigned a role. It’s important to note that only admins can add users to this app (there’s no open registration… kind of). Because I don’t want to use a back-end, there’s an immediate problem with this: there’s actually no way to restrict Firebase registration through the console. With some nifty scripts clever people could create an account for themselves, but the joke’s on them. <br /> Firstly, I’ll make sure a <code class="highlighter-rouge">changeRole</code> permission exists for writing to <code class="highlighter-rouge">/users/$uid/role</code>, which only admins have (they’ll have permission to do everything, including this). <br /> Secondly, if there’s no <code class="highlighter-rouge">/users/$uid/role</code> set, I restrict all read &amp; write access. That should keep those pesky clever users from messing with my database.</p> <p>Now that’s fine and dandy, but how to actually implement this?<br /> Don’t worry friend, we got this.</p> <h2 id="bolt-ftw">Bolt FTW</h2> <p>If you didn’t come across it yet, <a href="https://github.com/firebase/bolt">Firebase Bolt</a> is a compiler for Firebase rules (it might be illegal, but could we call it a Firebase rules transpiler?!). It’s supposedly experimental, so make sure to double-check the output after compiling (unit test!).</p> <p>Now, even with Bolt, I’ve noticed I’d be in trouble already. I want to have CRUD-like rules, but Firebase only knows about Read / Write. How to restrict deletion then? <br /> I had to choose between (I believe) two options:</p> <ul> <li><strong>A.</strong> build objects such that they have <code class="highlighter-rouge"><span class="p">{</span><span class="w"> </span><span class="err">public:</span><span class="w"> </span><span class="err">{/*</span><span class="w"> </span><span class="err">what</span><span class="w"> </span><span class="err">I</span><span class="w"> </span><span class="err">have</span><span class="w"> </span><span class="err">so</span><span class="w"> </span><span class="err">far</span><span class="w"> </span><span class="err">*/</span><span class="w"> </span><span class="p">}</span><span class="err">,</span><span class="w"> </span><span class="err">private:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">deleted:</span><span class="w"> </span><span class="err">Boolean</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="err">}</span></code> paths &amp; add a field named ‘deleted’ on <code class="highlighter-rouge">private</code>, for which I add stricter write rules, i.e. only admins will have permissions to write (NB: this way data will never actually be deleted). Then, I’d tweak the rules to prevent access for items with <code class="highlighter-rouge">private.deleted === true</code></li> <li><strong>B.</strong> use <code class="highlighter-rouge">newData.exists()</code> together with my permissions, which will prevent deletion for users that are not authorized to delete</li> </ul> <p>I went with <strong>B.</strong>, but some might find <strong>A.</strong> to be suitable: that way data can be restored later if needed. After I’ll deploy the first version of the app, the only way to change my decision is via (probably) a complicated migration script.</p> <p>I’ll have a similar issue with ‘create’. There’s no create rule in Firebase, but there are ways to make that work too.<br /> Long story short, Bolt supports splitting the <code class="highlighter-rouge">write</code> rule into <code class="highlighter-rouge">create</code>, <code class="highlighter-rouge">update</code>, <code class="highlighter-rouge">delete</code> (<a href="https://github.com/firebase/bolt/blob/master/docs/language.md#write-aliases">see here</a>). It’s a matter of preference in the end, but I’ll stick with it for the sake of simplicity. You can achieve the same in a more explicit manner.</p> <h2 id="bolt-implementation">Bolt implementation</h2> <p>Finally, the fun part. I’m going to sketch out how to implement this in Bolt, but take it with a pinch of salt. Most likely I’ll write up a quick new post on what changes I did to it before actually deploying. For now, here goes:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="cm">/* * Checks if a given action is allowed. * * @param {object} obj The object to check. * @param {string} entity The entity to check. * @param {string} permission The permission to check for. * @param {boolean} ownResources A flag that disables 'own' data checks if set (if false, users can perform an action if they own the resource) * */</span> <span class="nx">isAllowed</span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span> <span class="nx">entity</span><span class="p">,</span> <span class="nx">permission</span><span class="p">,</span> <span class="nx">ownResources</span><span class="p">)</span> <span class="p">{</span> <span class="nx">auth</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span> <span class="o">&amp;&amp;</span> <span class="c1">// has to be authenticated</span> <span class="nx">root</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">].</span><span class="nx">role</span> <span class="o">&amp;&amp;</span> <span class="c1">// has to have a role set</span> <span class="p">(</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span> <span class="o">!==</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">owner</span> <span class="c1">// user is not owner of the entity</span> <span class="p">?</span> <span class="nx">root</span><span class="p">.</span><span class="nx">permissions</span><span class="p">[</span><span class="nx">root</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">].</span><span class="nx">role</span><span class="p">][</span><span class="nx">entity</span><span class="p">].</span><span class="nx">all</span> <span class="o">===</span> <span class="kc">true</span> <span class="o">||</span> <span class="c1">// allow everything</span> <span class="cm">/* entity w/o owner: doesn't require extra nesting in the permissions obj (.other) */</span> <span class="nx">root</span><span class="p">.</span><span class="nx">permissions</span><span class="p">[</span><span class="nx">root</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">].</span><span class="nx">role</span><span class="p">][</span><span class="nx">entity</span><span class="p">][</span><span class="nx">permission</span><span class="p">]</span> <span class="o">===</span> <span class="kc">true</span> <span class="o">||</span> <span class="nx">root</span><span class="p">.</span><span class="nx">permissions</span><span class="p">[</span><span class="nx">root</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">].</span><span class="nx">role</span><span class="p">][</span><span class="nx">entity</span><span class="p">].</span><span class="nx">other</span><span class="p">[</span><span class="nx">permission</span><span class="p">]</span> <span class="o">===</span> <span class="kc">true</span> <span class="p">:</span> <span class="p">(</span><span class="o">!</span><span class="nx">ownResources</span> <span class="p">?</span> <span class="nx">root</span><span class="p">.</span><span class="nx">permissions</span><span class="p">[</span><span class="nx">root</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span><span class="p">].</span><span class="nx">role</span><span class="p">][</span><span class="nx">entity</span><span class="p">].</span><span class="nx">own</span><span class="p">[</span><span class="nx">permission</span><span class="p">]</span> <span class="o">===</span> <span class="kc">true</span> <span class="p">:</span> <span class="kc">false</span><span class="p">)</span> <span class="p">)</span> <span class="p">}</span> <span class="cm">/* Nobody can access anything by default */</span> <span class="nx">path</span> <span class="o">/</span> <span class="p">{</span> <span class="nx">read</span><span class="p">()</span> <span class="p">{</span> <span class="kc">false</span> <span class="p">}</span> <span class="nx">write</span><span class="p">()</span> <span class="p">{</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">}</span> <span class="cm">/* Example of permission check: Projects &amp; Project */</span> <span class="nx">type</span> <span class="nx">Projects</span> <span class="p">{</span> <span class="nx">create</span><span class="p">()</span> <span class="p">{</span> <span class="nx">isAllowed</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="s1">'projects'</span><span class="p">,</span> <span class="s1">'c'</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="nx">type</span> <span class="nx">Project</span> <span class="p">{</span> <span class="nx">read</span><span class="p">()</span> <span class="p">{</span> <span class="nx">isAllowed</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="s1">'project'</span><span class="p">,</span> <span class="s1">'r'</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> <span class="p">}</span> <span class="nx">update</span><span class="p">()</span> <span class="p">{</span> <span class="nx">isAllowed</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="s1">'project'</span><span class="p">,</span> <span class="s1">'u'</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> <span class="p">}</span> <span class="k">delete</span><span class="p">()</span> <span class="p">{</span> <span class="nx">isAllowed</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="s1">'project'</span><span class="p">,</span> <span class="s1">'d'</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="nx">path</span> <span class="o">/</span><span class="nx">projects</span> <span class="nx">is</span> <span class="nx">Projects</span> <span class="p">{}</span> <span class="nx">path</span> <span class="o">/</span><span class="nx">projects</span><span class="o">/</span><span class="p">{</span><span class="nx">$pid</span><span class="p">}</span> <span class="nx">is</span> <span class="nx">Project</span> <span class="p">{}</span> <span class="nx">path</span> <span class="o">/</span><span class="nx">users</span><span class="o">/</span><span class="p">{</span><span class="nx">$uid</span><span class="p">}</span><span class="sr">/role { /</span><span class="o">*</span> <span class="nx">Only</span> <span class="nx">admins</span> <span class="nx">should</span> <span class="nx">be</span> <span class="nx">able</span> <span class="nx">to</span> <span class="nx">write</span> <span class="nx">the</span> <span class="nx">role</span><span class="p">.</span> <span class="o">*</span><span class="sr">/</span><span class="err"> </span> <span class="nx">read</span><span class="p">()</span> <span class="p">{</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span> <span class="o">==</span> <span class="nx">$uid</span> <span class="p">}</span> <span class="cm">/* Users can read their own role */</span> <span class="nx">write</span><span class="p">()</span> <span class="p">{</span> <span class="nx">isAllowed</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="s1">'user'</span><span class="p">,</span> <span class="s1">'changeRole'</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="nx">path</span> <span class="o">/</span><span class="nx">projects</span><span class="o">/</span><span class="p">{</span><span class="nx">$uid</span><span class="p">}</span><span class="sr">/owner { /</span><span class="o">*</span> <span class="nx">Users</span> <span class="nx">can</span> <span class="nx">only</span> <span class="nx">write</span> <span class="nx">to</span> <span class="nx">their</span> <span class="nx">own</span> <span class="s1">'owner'</span> <span class="nx">fields</span><span class="p">.</span> <span class="o">*</span><span class="sr">/</span><span class="err"> </span> <span class="nx">write</span><span class="p">()</span> <span class="p">{</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">uid</span> <span class="o">==</span> <span class="nx">$uid</span> <span class="p">}</span> <span class="p">}</span> <span class="cm">/* * ... * Other entities omitted; they would look similar. * Each entity with an owner will have a `/owner` rule. * That prohibits anybody from changing it, like we have for `/projects/{$uid}/owner` */</span><span class="sr">/</span><span class="err"> </span></code></pre> </div> <p>That’s about it. It compiles via Bolt, so it should work… maybe with some tweaks here &amp; there. With proper unit tests, this baby’s gonna be able to handle whatever you throw at it.</p> <p>To give one more example, here’s how the admin role would look:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="nx">Permissions</span><span class="err">:</span> <span class="p">{</span> <span class="nl">admin</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// Admin role: allow everything</span> <span class="nl">users</span><span class="p">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">projects</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">project</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">tasks</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">task</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">files</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">file</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">messages</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">message</span><span class="err">:</span> <span class="p">{</span> <span class="nl">all</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> <span class="cm">/* ... */</span> <span class="p">}</span> </code></pre> </div> <h2 id="front-end-implementation">Front-end implementation</h2> <p>Firebase doesn’t require a back-end, but you can use one if you want to (or absolutely need it). For this project, I chose not to use a back-end. That means that I’ll handle the UI rendering 100% in the front-end. In turn, that will require that I know what a certain user is allowed to do upon authentication. This way I can show / hide relevant parts of the UI (i.e. a delete button).</p> <p>No matter if you are spinning up a React, Angular, etc. app or even cowboying your way through with a Vanilla JS app, the implementation shouldn’t differ much. <br /> One way to do it is load permissions when a resource is accessed (in the context of the current auth. user). Similarly to the Bolt implementation, a generic <code class="highlighter-rouge">isAllowed()</code> method should check if certain actions can be performed. Another way is to make use of the real-time database and keep an eye out for permission changes. Of course, that means all permissions will be loaded upon login.</p> <h2 id="caveats">Caveats</h2> <p><small>(yes, I used this word because I don’t like pitfalls. do you?!)</small></p> <ol> <li>Don’t take unit testing lightly, you need to do it properly (Bolt == experimental)</li> <li>It’s a bit hard to see the full picture; there might still be some security issues, but hey, it’s a complex app with many user roles (see 1.).</li> <li>Not being able to restrict registration makes me feel a bit uneasy, so perhaps a back-end would be necessary somewhere down the road.</li> </ol> <p>I want to believe that this strategy will work all the way &amp; I won’t need any server logic. Is that maybe too ambitious?<br /> Would love to get some feedback from experienced Firebasing guru-ninjas.</p>