├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── firewall-map.rst ├── firewall.rst ├── index.rst ├── make.bat └── middleware.rst ├── lib ├── errors.js ├── firewall.js ├── index.js ├── map.js ├── strategy.js └── utils.js ├── package.json └── test ├── firewall.test.js ├── map.test.js └── testHelper.js /.gitignore: -------------------------------------------------------------------------------- 1 | # build/reports 2 | build/ 3 | reports/ 4 | 5 | # Node.js 6 | node_modules/ 7 | npm-debug.log 8 | 9 | # Mac OS X 10 | .DS_Store 11 | 12 | # IDE 13 | .idea 14 | 15 | # Documentation 16 | docs/_build -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | README.md 2 | build/ 3 | docs/ 4 | examples/ 5 | reports/ 6 | test/ 7 | 8 | # Node.js 9 | .npmignore 10 | node_modules/ 11 | npm-debug.log 12 | 13 | # Mac OS X 14 | .DS_Store 15 | 16 | # IDE 17 | .idea 18 | 19 | # Git 20 | .git* 21 | 22 | # Utilities 23 | .jshintrc 24 | .travis.yml 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "node_js" 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | # - "0.6" 6 | 7 | before_install: 8 | - "npm install istanbul -g" 9 | - "npm install coveralls -g" 10 | 11 | #after_success: 12 | # - "cat reports/coverage/lcov.info | coveralls" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Raphaël Benitte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nodejs Firewall 2 | 3 | [![Build](https://travis-ci.org/plouc/node-firewall.png)](https://travis-ci.org/plouc/node-firewall) 4 | [![Coverage](https://coveralls.io/repos/plouc/node-firewall/badge.png?branch=master)](https://coveralls.io/r/plouc/node-firewall?branch=master) 5 | [![Dependency Status](https://david-dm.org/plouc/node-firewall.png)](https://david-dm.org/plouc/node-firewall) 6 | [![NPM version](https://badge.fury.io/js/node-firewall.png)](http://badge.fury.io/js/node-firewall) 7 | 8 | Unobtrusively handles security based on roles (authorization) plus authentication initialization. 9 | 10 | [Documentation](http://node-firewall.readthedocs.org/en/latest/) is available on read the docs. 11 | 12 | ## Installation 13 | 14 | ### Node 15 | 16 | ``` 17 | npm install node-firewall 18 | ``` 19 | 20 | ## Usage 21 | 22 | Configuring the firewall. 23 | 24 | ```javascript 25 | var firewall = require('node-firewall'); 26 | 27 | var fw = new firewall.Firewall('fw.main', '^/'); 28 | 29 | // allow non authenticated users to access the login page 30 | fw.add('^/login', null); 31 | 32 | // secure admin area 33 | fw.add('^/admin', ['role', 'admin']); 34 | 35 | // all other urls require user role 36 | fw.add('^/', ['role', 'user']); 37 | 38 | // add our new firewall to the map 39 | firewall.map.add(fw); 40 | 41 | ``` 42 | 43 | Enabling the middleware 44 | 45 | ```javascript 46 | // init firewall middleware 47 | firewall.use(app); 48 | ``` 49 | 50 | ## Changelog 51 | 52 | * 0.1.2 53 | - Improve firewall log system 54 | * 0.1.3 55 | - Fix FirewallMap.get when trying to retrieve a non existent firewall 56 | - Improve doc blocks 57 | * 0.1.4 58 | - Improve documentation 59 | - Add ability to filter firewall rules based on request http method 60 | - Add Firewall.dump() method 61 | * 0.1.5 62 | - Add authentication handler 63 | - Add default handlers to firewall 64 | * 0.1.6 65 | - Fix problem with middleware 66 | * 0.2.0 67 | - Add strategies on firewall to ease addition of custom rules 68 | - Removed Firewall.dump because of strategy support 69 | * 0.2.1 70 | - Add a way to configure map from json object with custom strategies 71 | * 0.2.2 72 | - Fix issue #1 73 | - Add debug on namespace node-firewall:map and node-firewall:firewall 74 | - debug method and flags was removed 75 | - No default success handler 76 | - Callback next is called by default if no success handler is configured 77 | - 'use strict' on all modules 78 | 79 | ## Credits 80 | 81 | [Raphaël Benitte](http://github.com/plouc) 82 | 83 | ## License 84 | 85 | [The MIT License](http://opensource.org/licenses/MIT) 86 | 87 | Copyright (c) 2014 Raphaël Benitte <[http://rbenitte.com/](http://rbenitte.com/)> 88 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/node-firewall.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/node-firewall.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/node-firewall" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/node-firewall" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # node-firewall documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Mar 30 01:18:21 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'node-firewall' 47 | copyright = u'2014, Raphaël Benitte' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.1.13' 55 | # The full version, including alpha/beta/rc tags. 56 | release = '0.1.13' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all 73 | # documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | # If true, keep warnings as "system message" paragraphs in the built documents. 94 | #keep_warnings = False 95 | 96 | 97 | # -- Options for HTML output ---------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'default' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | #html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # Add any extra paths that contain custom files (such as robots.txt or 133 | # .htaccess) here, relative to this directory. These files are copied 134 | # directly to the root of the documentation. 135 | #html_extra_path = [] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | #html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | #html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | #html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | #html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | #html_domain_indices = True 154 | 155 | # If false, no index is generated. 156 | #html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | #html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | #html_show_sourcelink = True 163 | 164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 165 | #html_show_sphinx = True 166 | 167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 168 | #html_show_copyright = True 169 | 170 | # If true, an OpenSearch description file will be output, and all pages will 171 | # contain a tag referring to it. The value of this option must be the 172 | # base URL from which the finished HTML is served. 173 | #html_use_opensearch = '' 174 | 175 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 176 | #html_file_suffix = None 177 | 178 | # Output file base name for HTML help builder. 179 | htmlhelp_basename = 'node-firewalldoc' 180 | 181 | 182 | # -- Options for LaTeX output --------------------------------------------- 183 | 184 | latex_elements = { 185 | # The paper size ('letterpaper' or 'a4paper'). 186 | #'papersize': 'letterpaper', 187 | 188 | # The font size ('10pt', '11pt' or '12pt'). 189 | #'pointsize': '10pt', 190 | 191 | # Additional stuff for the LaTeX preamble. 192 | #'preamble': '', 193 | } 194 | 195 | # Grouping the document tree into LaTeX files. List of tuples 196 | # (source start file, target name, title, 197 | # author, documentclass [howto, manual, or own class]). 198 | latex_documents = [ 199 | ('index', 'node-firewall.tex', u'node-firewall Documentation', 200 | u'Raphaël Benitte', 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | #latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | #latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | #latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | #latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | #latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | #latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output --------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'node-firewall', u'node-firewall Documentation', 230 | [u'Raphaël Benitte'], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | #man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ------------------------------------------- 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'node-firewall', u'node-firewall Documentation', 244 | u'Raphaël Benitte', 'node-firewall', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | #texinfo_show_urls = 'footnote' 256 | 257 | # If true, do not generate a @detailmenu in the "Top" node's menu. 258 | #texinfo_no_detailmenu = False 259 | -------------------------------------------------------------------------------- /docs/firewall-map.rst: -------------------------------------------------------------------------------- 1 | FirewallMap 2 | =========== 3 | 4 | A FirewallMap contains **one or more firewalls**, you'll often have a single firewall in your application, 5 | but if you have more specific needs, you have the ability to add another one. 6 | 7 | Usage 8 | ----- 9 | 10 | You can configure a map by using the API. 11 | 12 | .. code-block:: javascript 13 | 14 | var FirewallMap = require('node-firewall').FirewallMap, 15 | Firewall = require('node-firewall').Firewall; 16 | 17 | var map = new FirewallMap(); 18 | 19 | // create a new firewall 20 | var fw = new Firewall('fw.main', '^/'); 21 | fw.add('^/login', null); // allow unauthenticated access on /login 22 | fw.add('^/', 'user'); // all other resources require `user` role 23 | 24 | // add it to the map 25 | map.add(fw); 26 | 27 | // ... 28 | 29 | map.check(req, res, next); 30 | 31 | You can also configure the FirewallMap using a plain old javascript object. 32 | 33 | .. code-block:: javascript 34 | 35 | var FirewallMap = require('node-firewall').FirewallMap; 36 | 37 | var map = new FirewallMap(); 38 | map.fromConfig({ 39 | 'fw.main': { 40 | path: '^/', 41 | rules: [ 42 | [ '^/login', null ], 43 | [ '^/', ['role', 'user'] ] 44 | ] 45 | } 46 | }); 47 | 48 | // ... -------------------------------------------------------------------------------- /docs/firewall.rst: -------------------------------------------------------------------------------- 1 | Firewall 2 | ======== 3 | 4 | A firewall have a path, this path determine if it should apply 5 | on an incoming request by checking that the request url match it. 6 | 7 | Then, if the firewall handle the request, it checks if it have 8 | an available rule to apply to it by checking its url and optionally 9 | its http method. 10 | 11 | A rule is composed of a **path** (a RegExp), a **list of roles** (an array) 12 | authorized to access the resource, and, as seen previously an optional 13 | **http method**. 14 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. node-firewall documentation master file, created by 2 | sphinx-quickstart on Sun Mar 30 01:18:21 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to node-firewall's documentation! 7 | ========================================= 8 | 9 | .. image:: https://travis-ci.org/plouc/node-firewall.png 10 | .. image:: https://coveralls.io/repos/plouc/node-firewall/badge.png?branch=master 11 | .. image:: https://david-dm.org/plouc/node-firewall.png 12 | .. image:: https://badge.fury.io/js/node-firewall.png 13 | 14 | The firewall module helps to handle **authorization** based on **roles** and init **authentication process**. 15 | 16 | It exposes three main components: 17 | 18 | * A **FirewallMap** 19 | * A **Firewall** 20 | * A **middleware** to easily plug it to express 21 | 22 | This module was build to work in conjunction with `passport `_ which 23 | is in charge of user **authentication**. 24 | 25 | Contents: 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | 30 | firewall 31 | firewall-map 32 | middleware 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\node-firewall.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\node-firewall.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/middleware.rst: -------------------------------------------------------------------------------- 1 | The middleware 2 | ============== 3 | 4 | The module provides an easy way to integrate it to an existing express based application. 5 | 6 | Usage 7 | ----- 8 | 9 | .. code-block:: javascript 10 | 11 | var firewall = require('node-firewall'); 12 | 13 | // configure the firewalls 14 | firewall.map.fromConfig({ 15 | 'fw.main': { 16 | path: '^/', 17 | rules: [ 18 | [ '^/login', null ], 19 | [ '^/', ['role', 'user'] ] 20 | ] 21 | } 22 | }); 23 | 24 | // plug it to the application 25 | firewall.use(app); 26 | 27 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Custom error to handle non authenticated users. 4 | * 5 | * @param {string} message 6 | * @constructor 7 | */ 8 | function NotAuthenticatedError(message) { 9 | this.name = 'NotAuthenticatedError'; 10 | this.message = (message || ''); 11 | } 12 | NotAuthenticatedError.prototype = new Error(); 13 | NotAuthenticatedError.prototype.constructor = NotAuthenticatedError; 14 | module.exports.NotAuthenticatedError = NotAuthenticatedError; 15 | 16 | /** 17 | * Custom error to handle access denied. 18 | * 19 | * @param {string} message 20 | * @constructor 21 | */ 22 | function AccessDeniedError(message) { 23 | this.name = 'AccessDeniedError'; 24 | this.message = (message || ''); 25 | } 26 | AccessDeniedError.prototype = new Error(); 27 | AccessDeniedError.prototype.constructor = AccessDeniedError; 28 | module.exports.AccessDeniedError = AccessDeniedError; -------------------------------------------------------------------------------- /lib/firewall.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'), 3 | utils = require('./utils'), 4 | strategy = require('./strategy'), 5 | errors = require('./errors'); 6 | var debug = require('debug')('node-firewall:firewall'); 7 | 8 | /** 9 | * The firewall is a simple container for multiple url based authorization rules. 10 | * 11 | * @param {string} name The firewall name 12 | * @param {string|RegExp} path The firewall will only apply on request url matching this value 13 | * @param {function|null} authenticationHandler Function to call when login is required 14 | * @param {function|null} successHandler Function to call when access is granted 15 | * @param {function|null} failureHandler Function to call when access is denied 16 | * @constructor 17 | */ 18 | var Firewall = function Firewall(name, path, authenticationHandler, successHandler, failureHandler) { 19 | this.name = name; 20 | this.path = utils.ensureRegexp(path); 21 | this.rules = []; 22 | 23 | // configure handlers 24 | this.authenticationHandler = authenticationHandler || function (req, res, next) { 25 | res.status(401); 26 | return res.redirect('/login'); 27 | }; 28 | this.successHandler = successHandler; 29 | this.failureHandler = failureHandler || function (req, res, next) { 30 | return res.send(403, 'forbidden'); 31 | }; 32 | 33 | this.logger = debug; 34 | 35 | this.strategies = { 36 | role: strategy.role 37 | }; 38 | }; 39 | 40 | /** 41 | * Add a new strategy factory to the firewall. 42 | * 43 | * @param {string} strategyName 44 | * @param {function} stategyFn 45 | * @returns {Firewall} 46 | * @api public 47 | */ 48 | Firewall.prototype.addStrategy = function (strategyName, stategyFn) { 49 | this.strategies[strategyName] = stategyFn; 50 | 51 | return this; 52 | }; 53 | 54 | /** 55 | * Check if the firewall has the given strategy factory. 56 | * 57 | * @param {string} strategyName 58 | * @returns {boolean} 59 | * @api public 60 | */ 61 | Firewall.prototype.hasStrategy = function (strategyName) { 62 | return this.strategies.hasOwnProperty(strategyName); 63 | } 64 | 65 | /** 66 | * @param {object} req 67 | * @param {object} res 68 | * @param {function} next 69 | * @api private 70 | */ 71 | Firewall.prototype._handleSuccess = function (req, res, next) { 72 | if (this.successHandler) { 73 | this._log('calling success handler'); 74 | this.successHandler(req, res, next); 75 | } else { 76 | this._log('no success handler'); 77 | } 78 | }; 79 | 80 | /** 81 | * @param {object} req 82 | * @param {object} res 83 | * @param {function} next 84 | * @api private 85 | */ 86 | Firewall.prototype._handleFailure = function (req, res, next) { 87 | if (this.failureHandler) { 88 | this._log('calling failure handler'); 89 | this.failureHandler(req, res, next); 90 | } else { 91 | this._log('no failure handler'); 92 | } 93 | }; 94 | 95 | /** 96 | * 97 | * @param {object} req 98 | * @param {object} res 99 | * @param {function} next 100 | * @private 101 | */ 102 | Firewall.prototype._handleAuthentication = function (req, res, next) { 103 | if (this.authenticationHandler) { 104 | this._log('calling authentication handler') 105 | this.authenticationHandler(req, res, next); 106 | } else { 107 | this._log('no authentication handler') 108 | } 109 | }; 110 | 111 | /** 112 | * (DEPRRECATED) Enable/disable debug. 113 | * 114 | * @param {boolean} flag 115 | * @api public 116 | */ 117 | Firewall.prototype.debug = function() { 118 | console.warn('DEPRECATED, node-firewall use debug package on namespace node-firewall:firewall'); 119 | // only for compat 120 | process.env.DEBUG = 'node-firewall:firewall'; 121 | }; 122 | 123 | /** 124 | * Log provided arguments, if arguments contain strings, 125 | * they will be prefixed with '[firewall] FIREWALL_NAME '. 126 | * Logging is only enabled exporting env variable like DEBUG=node-firewall:firewall 127 | * 128 | * @see Firewall.prototype.debug 129 | * @api private 130 | */ 131 | Firewall.prototype._log = function () { 132 | if (this.logger) { 133 | var args = [].slice.call(arguments); 134 | 135 | // prefix string arguments with firewall name 136 | _.forOwn(args, function (arg, index) { 137 | if (toString.call(arg) == '[object String]') { 138 | args[index] = '[firewall] "' + this.name + '" ' + arg; 139 | } 140 | }.bind(this)); 141 | 142 | this.logger.apply(this.logger, args); 143 | } 144 | }; 145 | 146 | /** 147 | * Check if the given request url match this firewall. 148 | * 149 | * @param req 150 | * @returns {boolean} 151 | * @api public 152 | */ 153 | Firewall.prototype.match = function (req) { 154 | var matches = req.url.match(this.path); 155 | if (matches !== null) { 156 | this._log('match request url: ' + req.url); 157 | 158 | return true; 159 | } 160 | 161 | return false; 162 | }; 163 | 164 | /** 165 | * Iterates over each rules to find one matching given request. 166 | * 167 | * @param {object} req 168 | * @param {object} res 169 | * @param {function} next 170 | * @param {bollean} handleNext 171 | * @returns {null|boolean} Returns 'null' if no rule matched the request, 172 | * 'true' for granted access and 'false' if denied 173 | * @api public 174 | */ 175 | Firewall.prototype.check = function (req, res, next, handleNext) { 176 | next = next || _.noop; 177 | handleNext = handleNext || false; 178 | var ruleCount = this.rules.length; 179 | var rule, i; 180 | 181 | // use traditional for loop to be able to break it with return 182 | for (i = 0; i < ruleCount; i++) { 183 | rule = this.rules[i]; 184 | if (req.url.match(rule.path)) { 185 | // no specific method defined on rule or method defined and req.method matching 186 | if (rule.method === '*' || rule.method === req.method) { 187 | 188 | this._log('rule match: ' + rule.path + ' [' + req.method + ' ' + req.url + ']'); 189 | 190 | if (rule.strategy) { 191 | try { 192 | rule.strategy(req); 193 | } catch (strategyErr) { 194 | if (strategyErr instanceof errors.NotAuthenticatedError) { 195 | this._log('denied access (user is not authenticated)'); 196 | this._handleAuthentication(req, res, next); 197 | 198 | return false; 199 | } else if (strategyErr instanceof errors.AccessDeniedError) { 200 | this._log('denied access (user has no allowed role)'); 201 | this._handleFailure(req, res, next); 202 | 203 | return false; 204 | } 205 | } 206 | } 207 | 208 | this._log('granted access'); 209 | this._handleSuccess(req, res, next); 210 | return true; 211 | } 212 | } 213 | } 214 | 215 | if (!handleNext) { 216 | next(); 217 | } 218 | return null; 219 | }; 220 | 221 | /** 222 | * Normalize given parameters to form a valid rule. 223 | * 224 | * @param {RegExp|string} path Regexp used to check if rule should be applied 225 | * @param {Array} strategyConf The strategy configuration 226 | * @param {string|null} method Rule will only be applied for this http method 227 | * @api private 228 | */ 229 | Firewall.prototype._normalizeRule = function (path, strategyConf, method) { 230 | var strategy; 231 | 232 | if (strategyConf === null ) { 233 | strategy = null; 234 | } else { 235 | var strategyArgIsArray = Object.prototype.toString.call(strategyConf) === '[object Array]'; 236 | if (!strategyArgIsArray || strategyConf.length < 1) { 237 | throw new Error('Invalid strategy given, strategy must be passed with the form [\'strategyName\', \'strategyArg0\', [\'strategyArg1\']]'); 238 | } 239 | 240 | if (!this.strategies[strategyConf[0]]) { 241 | throw new Error('Invalid strategy given, firewall "' + this.name + '" does not know how to build "' + strategyConf[0] + '" strategy'); 242 | } 243 | 244 | var strategyArgs = strategyConf.slice(1); 245 | strategyArgs.unshift(this); 246 | strategy = this.strategies[strategyConf[0]].apply(null, strategyArgs); 247 | } 248 | 249 | 250 | if (method !== undefined && method !== null) { 251 | method = utils.ensureValidHttpMethod(method); 252 | } else { 253 | method = '*'; 254 | } 255 | 256 | return { 257 | path: utils.ensureRegexp(path), 258 | strategy: strategy, 259 | method: method 260 | }; 261 | }; 262 | 263 | /** 264 | * Prepend a new rule. 265 | * 266 | * @param {RegExp|string} path Regexp used to check if rule should be applied 267 | * @param {Array.} roles Authorized roles 268 | * @param {string|null} method Rule will only be applied for this http method 269 | * 270 | * @returns {Firewall} 271 | */ 272 | Firewall.prototype.prepend = function (path, strategy, method) { 273 | this.rules.unshift(this._normalizeRule(path, strategy, method)); 274 | 275 | return this; 276 | }; 277 | 278 | /** 279 | * Append a new rule. 280 | * 281 | * @param {RegExp|string} path Regexp used to check if rule should be applied 282 | * @param {Array.} roles Authorized roles 283 | * @param {string|null} method Rule will only be applied for this http method 284 | * @returns {Firewall} 285 | * @api public 286 | */ 287 | Firewall.prototype.append = function (path, strategy, method) { 288 | this.rules.push(this._normalizeRule(path, strategy, method)); 289 | 290 | return this; 291 | }; 292 | 293 | /** 294 | * Alias for append. 295 | * 296 | * @see Firewall.prototype.append 297 | * @api public 298 | */ 299 | Firewall.prototype.add = Firewall.prototype.append; 300 | 301 | /** 302 | * expose `Firewall` 303 | */ 304 | module.exports = Firewall; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Firewall = require('./firewall'), 4 | FirewallMap = require('./map'), 5 | errors = require('./errors'); 6 | 7 | 8 | /** 9 | * expose `FirewallMap` 10 | */ 11 | module.exports.Map = FirewallMap; 12 | 13 | 14 | /** 15 | * expose `Firewall` 16 | */ 17 | module.exports.Firewall = Firewall; 18 | 19 | 20 | /** 21 | * expose custom errors 22 | */ 23 | module.exports.NotAuthenticatedError = errors.NotAuthenticatedError; 24 | module.exports.AccessDeniedError = errors.AccessDeniedError; 25 | 26 | 27 | /** 28 | * Creates a default map used for middleware. 29 | */ 30 | var map = new FirewallMap(); 31 | module.exports.map = map; 32 | 33 | 34 | /** 35 | * Firewall as a middleware. 36 | * 37 | * @param app The main application object 38 | */ 39 | module.exports.use = function (app) { 40 | app.use(function (req, res, next) { 41 | map.check(req, res, next); 42 | }); 43 | }; -------------------------------------------------------------------------------- /lib/map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var Firewall = require('./firewall'); 4 | var strategy = require('./strategy'); 5 | var debug = require('debug')('node-firewall:map'); 6 | /** 7 | * FirewallMap class act as a container for multiple firewalls. 8 | * 9 | * @constructor 10 | */ 11 | var FirewallMap = function FirewallMap() { 12 | this.firewalls = []; 13 | 14 | // we must be able to configure strategy factories from the map 15 | // for the FirewallMap.fromConfig() function. 16 | this.strategies = { 17 | role: strategy.role 18 | }; 19 | }; 20 | 21 | /** 22 | * Add a new strategy factory to the map. 23 | * 24 | * @param {string} strategyName 25 | * @param {function} stategyFn 26 | * @returns {FirewallMap} 27 | * @api public 28 | */ 29 | FirewallMap.prototype.addStrategy = function(strategyName, stategyFn) { 30 | this.strategies[strategyName] = stategyFn; 31 | 32 | return this; 33 | }; 34 | 35 | /** 36 | * Creates firewall based on given config object. 37 | * 38 | * @param {object} config 39 | * @returns {FirewallMap} 40 | * @api public 41 | */ 42 | FirewallMap.prototype.fromConfig = function(config) { 43 | for (var firewallName in config) { 44 | var firewallConfig = config[firewallName]; 45 | 46 | var firewall = new Firewall(firewallName, firewallConfig.path); 47 | 48 | _.forOwn(this.strategies, function(strategyFactory, strategyName) { 49 | if (!firewall.hasStrategy(strategyName)) { 50 | firewall.addStrategy(strategyName, strategyFactory); 51 | } 52 | }); 53 | 54 | firewallConfig.rules.forEach(function(rule) { 55 | firewall.add.apply(firewall, rule); 56 | }); 57 | 58 | this.firewalls.push(firewall); 59 | } 60 | 61 | return this; 62 | }; 63 | 64 | /** 65 | * Clear all previously declared firewalls. 66 | * 67 | * @returns {FirewallMap} 68 | * @api public 69 | */ 70 | FirewallMap.prototype.clear = function() { 71 | this.firewalls = []; 72 | 73 | return this; 74 | }; 75 | 76 | /** 77 | * Adds a firewall to the map. 78 | * 79 | * @param {Firewall} firewall 80 | * @throws Will throw an error if the given argument is note a Firewall. 81 | * @returns {FirewallMap} 82 | * @api public 83 | */ 84 | FirewallMap.prototype.add = function(firewall) { 85 | if (firewall instanceof Firewall === false) { 86 | throw new Error('Invalid firewall given'); 87 | } 88 | this.firewalls.push(firewall); 89 | 90 | return this; 91 | }; 92 | 93 | /** 94 | * Retrieve a firewall by its name. 95 | * 96 | * @param {string} firewallName 97 | * @throws Will throw an error if there is no firewall matching the name. 98 | * @returns {Firewall} 99 | * @api public 100 | */ 101 | FirewallMap.prototype.get = function(firewallName) { 102 | var firewall = _.find(this.firewalls, function(fw) { 103 | return fw.name === firewallName; 104 | }); 105 | 106 | if (firewall === undefined) { 107 | throw new Error('Unable to find a firewall by name "' + firewallName + '"'); 108 | } 109 | 110 | return firewall; 111 | }; 112 | 113 | 'use strict'; 114 | /** 115 | * Retrieve all firewall 116 | * 117 | * @returns {Firewall array} 118 | * @api public 119 | */ 120 | FirewallMap.prototype.getAll = function() { 121 | return this.firewalls; 122 | }; 123 | 124 | /** 125 | * Remove a firewall by name. 126 | * 127 | * @param {string} firewallName 128 | * @returns {FirewallMap} 129 | * @api public 130 | */ 131 | FirewallMap.prototype.remove = function(firewallName) { 132 | this.firewalls = _.remove(this.firewalls, function(fw) { 133 | return fw.name === firewallName; 134 | }); 135 | 136 | return this; 137 | }; 138 | 139 | /** 140 | * (DEPRRECATED) Enable/disable debug. 141 | * 142 | * @param {boolean} flag 143 | * @api public 144 | */ 145 | FirewallMap.prototype.debug = function(flag) { 146 | console.warn('DEPRECATED, node-firewall use debug package on namespace node-firewall:map'); 147 | // only for compat 148 | process.env.DEBUG = 'node-firewall:map'; 149 | }; 150 | 151 | /** 152 | * Iterates through all registered firewalls. 153 | * 154 | * @param {object} req The request object 155 | * @param {object} res The response object 156 | * @param {function} next Passthru function 157 | * @api public 158 | */ 159 | FirewallMap.prototype.check = function(req, res, next) { 160 | next = next || _.noop; 161 | var firewallsCount = this.firewalls.length; 162 | var i; 163 | var callbackDone = false; 164 | // this function protect for calling next() twice if multiple map is avalaible 165 | var callback = function() { 166 | if (callbackDone) { 167 | return; 168 | } 169 | debug('calling next'); 170 | next(); 171 | }; 172 | // use traditional for loop to be able to break it with return 173 | for (i = 0; i < firewallsCount; i++) { 174 | var firewall = this.firewalls[i]; 175 | if (firewall.match(req) === true) { 176 | var checkResult = firewall.check(req, res, callback, true); 177 | // we find something 178 | if (checkResult === true) { 179 | // safe try calling next if no success handler are configured 180 | return callback(); 181 | } 182 | if (checkResult === false) { 183 | // failure or aunthentification handler has been called 184 | return checkResult; 185 | } 186 | } 187 | } 188 | 189 | // nothing to do, continue 190 | debug('nothing to do, continue'); 191 | callback(); 192 | }; 193 | 194 | /** 195 | * expose `FirewallMap` 196 | */ 197 | module.exports = FirewallMap; 198 | -------------------------------------------------------------------------------- /lib/strategy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var errors = require('./errors'), 3 | _ = require('lodash'); 4 | 5 | module.exports.role = function (firewall, roles) { 6 | if (!roles) { 7 | throw new Error('RoleStrategy requires at least one role defined'); 8 | } 9 | 10 | if (Object.prototype.toString.call(roles) != '[object Array]') { 11 | roles = [roles]; 12 | } 13 | 14 | var allowedRoles = roles; 15 | 16 | return function (req) { 17 | if (!req.isAuthenticated()) { 18 | throw new errors.NotAuthenticatedError(); 19 | } else if (!req.user.role) { 20 | throw new errors.AccessDeniedError(); 21 | } 22 | 23 | firewall._log('user roles: "' + req.user.role.join('", "') + '"'); 24 | firewall._log('allowed roles: "' + allowedRoles.join('", "') + '"'); 25 | 26 | var matchingRoles = _.intersection(req.user.role, allowedRoles); 27 | if (matchingRoles.length === 0) { 28 | throw new errors.AccessDeniedError(); 29 | } 30 | 31 | firewall._log('matching roles: "' + matchingRoles.join('", "') + '"'); 32 | 33 | return true; 34 | }; 35 | }; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Ensure given object is a RegExp object. 4 | * 5 | * @param {RegExp|string} pattern 6 | * @returns {RegExp} 7 | */ 8 | exports.ensureRegexp = function (pattern) { 9 | if (!pattern instanceof RegExp) { 10 | pattern = new RegExp(pattern); 11 | } 12 | 13 | return pattern; 14 | }; 15 | 16 | /** 17 | * Check/Ensure given string is a valid http method. 18 | * 19 | * @param {string} method 20 | * @returns {string} normalized http method 21 | */ 22 | exports.httpMethods = ['GET', 'POST', 'PUT', 'DELETE']; 23 | exports.ensureValidHttpMethod = function (method) { 24 | var normMethod = method.toUpperCase(); 25 | if (exports.httpMethods.indexOf(normMethod) === -1) { 26 | throw new Error('"' + method + '" is not a valid http method, should be one of ' + exports.httpMethods.join(', ')); 27 | } 28 | 29 | return normMethod; 30 | }; 31 | 32 | /** 33 | * Repeat the given string n times. 34 | * 35 | * @param {string} str 36 | * @param {number} times 37 | * @returns {string} 38 | */ 39 | exports.repeatStr = function (str, times) { 40 | return (new Array(times + 1)).join(' '); 41 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-firewall", 3 | "version": "0.2.2", 4 | "description": "Unobtrusively handles security based on roles (authorization) plus authentication initialization", 5 | "keywords": [ 6 | "express", 7 | "connect", 8 | "authorization", 9 | "security", 10 | "roles" 11 | ], 12 | "main": "./lib", 13 | "scripts": { 14 | "test": "istanbul cover --dir reports/coverage --report lcovonly node_modules/.bin/_mocha -- --reporter spec --no-colors test/*.test.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/plouc/node-firewall.git" 19 | }, 20 | "author": "Raphaël Benitte (http://www.rbenitte.com/)", 21 | "licenses": [ 22 | { 23 | "type": "MIT", 24 | "url": "http://www.opensource.org/licenses/MIT" 25 | } 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/plouc/node-firewall/issues" 29 | }, 30 | "homepage": "https://github.com/plouc/node-firewall", 31 | "dependencies": { 32 | "debug": "~2.1.1", 33 | "lodash": "~2.4.1" 34 | }, 35 | "devDependencies": { 36 | "mocha": "1.x.x", 37 | "chai": "1.x.x", 38 | "chai-connect-middleware": "0.3.x" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/firewall.test.js: -------------------------------------------------------------------------------- 1 | var Firewall = require('../lib/firewall'), 2 | testHelper = require('./testHelper'), 3 | expect = require('chai').expect; 4 | 5 | describe('Firewall', function () { 6 | var fw; 7 | 8 | 9 | it('should have a name, pattern and rules when initialized', function () { 10 | fw = new Firewall('fw', /^\/$/); 11 | expect(fw.name).to.equal('fw'); 12 | expect(fw.path).to.deep.equal(/^\/$/); 13 | expect(fw.rules).to.be.a('array'); 14 | }); 15 | 16 | 17 | it('should ensure given path is a RegExp', function () { 18 | fw = new Firewall('fw', '^/$'); 19 | // weird failure 20 | //expect(fw.path).to.be.a('regexp'); 21 | }); 22 | 23 | 24 | it('should provide a way to easily add rules to it', function () { 25 | fw = new Firewall('fw', /^\/$/); 26 | expect(fw.rules).to.be.a('array'); 27 | expect(fw.rules.length).to.equal(0); 28 | 29 | fw.add(/$\/test$/, ['role', ['user']]); 30 | expect(fw.rules.length).to.equal(1); 31 | expect(fw.rules[0].strategy).to.be.a('function'); 32 | expect(fw.rules[0].method).to.equal('*'); 33 | }); 34 | 35 | 36 | it('should throw an error for rule with invalid http method', function () { 37 | fw = new Firewall('fw', /^\/$/); 38 | expect(function () { 39 | fw.add(/^\/test$/, ['role', ['user', 'admin']], 'invalid'); 40 | }).to.throw('"invalid" is not a valid http method, should be one of GET, POST, PUT, DELETE'); 41 | }); 42 | 43 | 44 | it('should provide a way to conditionally apply rules depending on request http method', function () { 45 | fw = new Firewall('fw', '^/'); 46 | fw.add('^/', ['role', 'user'], 'POST'); 47 | fw.add('^/', null, 'GET'); 48 | 49 | fw.check(testHelper.req('/', false, [], 'GET'), testHelper.res, testHelper.next()); 50 | fw.check(testHelper.req('/', false, [], 'POST'), testHelper.res, testHelper.next()); 51 | }); 52 | 53 | 54 | it('should only apply on request having an url matching its path', function () { 55 | fw = new Firewall('fw', '^/'); 56 | expect(fw.match(testHelper.req('/'))).to.equal(true); 57 | 58 | fw = new Firewall('fw', '^/admin'); 59 | expect(fw.match(testHelper.req('/'))).to.equal(false); 60 | }); 61 | 62 | 63 | it('should do nothing if no rule were defined for a given request', function (done) { 64 | fw = new Firewall('fw', '^/unreached'); 65 | 66 | var called = false; 67 | expect(fw.check(testHelper.req('/test', true), {}, function () { 68 | called = true; 69 | expect(called).to.equal(true); 70 | done(); 71 | })).to.equal(null); 72 | 73 | }) 74 | 75 | 76 | it('should apply rules in order they were defined', function () { 77 | fw = new Firewall('fw', '^/'); 78 | fw.add('^/test', ['role', 'user']).add('^/testing', null); 79 | expect(fw.check(testHelper.req('/test', true), testHelper.res, testHelper.next())).to.equal(false); 80 | 81 | fw = new Firewall('fw', '^/'); 82 | fw.add('^/test', null).add('^/testing', ['role', 'user']); 83 | expect(fw.check(testHelper.req('/test', true), testHelper.res, testHelper.next())).to.equal(true); 84 | }); 85 | 86 | 87 | it('should provide useful informations for debugging', function () { 88 | fw = new Firewall('fw', '^/'); 89 | fw 90 | .add('^/', null) 91 | .prepend('^/admin', ['role', 'admin']) 92 | 93 | var logs = []; 94 | fw.logger = function () { 95 | logs.push([].slice.call(arguments)[0]); 96 | }; 97 | 98 | expect(fw.match(testHelper.req('/', false))).to.equal(true); 99 | expect(logs).to.deep.equal([ 100 | '[firewall] "fw" match request url: /' 101 | ]); 102 | logs = []; 103 | 104 | expect(fw.check(testHelper.req('/', false), testHelper.res, testHelper.next())).to.equal(true); 105 | expect(logs).to.deep.equal([ 106 | '[firewall] "fw" rule match: ^/ [GET /]', 107 | '[firewall] "fw" granted access', 108 | '[firewall] "fw" no success handler' 109 | ]); 110 | logs = []; 111 | 112 | expect(fw.check(testHelper.req('/admin', false), testHelper.res, testHelper.next())).to.equal(false); 113 | expect(logs).to.deep.equal([ 114 | '[firewall] "fw" rule match: ^/admin [GET /admin]', 115 | '[firewall] "fw" denied access (user is not authenticated)', 116 | '[firewall] "fw" calling authentication handler' 117 | ]); 118 | logs = []; 119 | 120 | expect(fw.check(testHelper.req('/admin', true), testHelper.res, testHelper.next())).to.equal(false); 121 | expect(logs).to.deep.equal([ 122 | '[firewall] "fw" rule match: ^/admin [GET /admin]', 123 | '[firewall] "fw" denied access (user has no allowed role)', 124 | '[firewall] "fw" calling failure handler' 125 | ]); 126 | logs = []; 127 | 128 | expect(fw.check(testHelper.req('/admin', true, [ 'user' ]), testHelper.res, testHelper.next())).to.equal(false); 129 | expect(logs).to.deep.equal([ 130 | '[firewall] "fw" rule match: ^/admin [GET /admin]', 131 | '[firewall] "fw" user roles: "user"', 132 | '[firewall] "fw" allowed roles: "admin"', 133 | '[firewall] "fw" denied access (user has no allowed role)', 134 | '[firewall] "fw" calling failure handler' 135 | ]); 136 | logs = []; 137 | 138 | expect(fw.check(testHelper.req('/admin', true, [ 'admin' ]), testHelper.res, testHelper.next())).to.equal(true); 139 | expect(logs).to.deep.equal([ 140 | '[firewall] "fw" rule match: ^/admin [GET /admin]', 141 | '[firewall] "fw" user roles: "admin"', 142 | '[firewall] "fw" allowed roles: "admin"', 143 | '[firewall] "fw" matching roles: "admin"', 144 | '[firewall] "fw" granted access', 145 | '[firewall] "fw" no success handler' 146 | ]); 147 | logs = []; 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/map.test.js: -------------------------------------------------------------------------------- 1 | var FirewallMap = require('../lib/map'), 2 | Firewall = require('../lib/firewall'), 3 | testHelper = require('./testHelper'), 4 | expect = require('chai').expect; 5 | 6 | describe('FirewallMap', function () { 7 | var map = new FirewallMap(); 8 | 9 | 10 | it('should have an empty set of firewalls initialized', function () { 11 | expect(map.firewalls).to.be.a('array'); 12 | expect(map.firewalls).to.deep.equal([]); 13 | }); 14 | 15 | 16 | it('should provide a way to easily add/retrieve/remove firewalls', function () { 17 | expect(map.firewalls.length).to.equal(0); 18 | 19 | var testFirewallA = new Firewall('firewall.test.A', /^\/$/); 20 | map.add(testFirewallA); 21 | expect(map.firewalls.length).to.equal(1); 22 | expect(map.get('firewall.test.A')).to.deep.equal(testFirewallA); 23 | 24 | var testFirewallB = new Firewall('firewall.test.B', /^\/$/); 25 | map.add(testFirewallB); 26 | expect(map.firewalls.length).to.equal(2); 27 | expect(map.get('firewall.test.B')).to.deep.equal(testFirewallB); 28 | 29 | map.remove('firewall.test.B'); 30 | expect(map.firewalls.length).to.equal(1); 31 | }); 32 | 33 | 34 | it('should throw an error when trying to add a non firewall object', function () { 35 | expect(function () { 36 | map.add('invalid'); 37 | }).to.throw('Invalid firewall given'); 38 | }); 39 | 40 | 41 | it('should throw an error when trying to get a non-existent firewall', function () { 42 | expect(function () { 43 | map.get('invalid'); 44 | }).to.throw('Unable to find a firewall by name "invalid"'); 45 | }); 46 | 47 | 48 | it('should only apply on request having its url matching firewall path', function () { 49 | var granted = null; 50 | var authCb = function () { granted = 'auth'; } 51 | var grantedCb = function () { granted = true; } 52 | var deniedCb = function () { granted = false; } 53 | 54 | var fw0 = new Firewall('fw.0', '^/test0', authCb, grantedCb, deniedCb); 55 | fw0.add('^/', null); 56 | 57 | var fw1 = new Firewall('fw.0', '^/test1', authCb, grantedCb, deniedCb); 58 | fw1.add('^/', ['role', 'user']); 59 | 60 | map.clear().add(fw0).add(fw1); 61 | 62 | map.check(testHelper.req('/')); 63 | expect(granted).to.equal(null); // no match 64 | 65 | map.check(testHelper.req('/test0')); 66 | expect(granted).to.equal(true); // match fw0 67 | 68 | map.check(testHelper.req('/test1')); 69 | expect(granted).to.equal('auth'); // match fw1 70 | 71 | map.check(testHelper.req('/test1', true, ['user.partial'])); 72 | expect(granted).to.equal(false); // match fw1 73 | }); 74 | 75 | 76 | it('should configure firewalls with a config object', function () { 77 | map.clear().fromConfig({ 78 | 'fw.main': { 79 | path: '^/', 80 | rules: [ 81 | ['^/login', null], 82 | ['^/', ['role', ['user', 'admin']]], 83 | ['^/admin', ['role', 'admin']] 84 | ] 85 | } 86 | }); 87 | 88 | expect(map.firewalls.length).to.equal(1); 89 | expect(map.firewalls[0].name).to.equal('fw.main'); 90 | expect(map.firewalls[0].path).to.equal('^/'); 91 | expect(map.firewalls[0].rules.length).to.equal(3); 92 | }); 93 | 94 | 95 | it('should throw an error when trying to configure firewall with a non-existing strategy', function () { 96 | map.clear(); 97 | 98 | expect(function () { 99 | map.fromConfig({ 100 | 'fw.main': { 101 | path: '^/', 102 | rules: [ ['^/', ['custom']] ] 103 | } 104 | }); 105 | }).to.throw('Invalid strategy given, firewall "fw.main" does not know how to build "custom" strategy'); 106 | }); 107 | 108 | 109 | it('should provide a way to configure firewalls with non-default strategy', function () { 110 | map.clear(); 111 | map.addStrategy('custom', function () { }); 112 | expect(function () { 113 | map.fromConfig({ 114 | 'fw.main': { 115 | path: '^/', 116 | rules: [ ['^/', ['custom']] ] 117 | } 118 | }); 119 | }).not.to.throw('Invalid strategy given, firewall "fw.main" does not know how to build "invalid" strategy'); 120 | }); 121 | }); -------------------------------------------------------------------------------- /test/testHelper.js: -------------------------------------------------------------------------------- 1 | module.exports.req = function (url, authenticated, roles, method) { 2 | return { 3 | url: url, 4 | method: method || 'GET', 5 | isAuthenticated: function () { 6 | return authenticated; 7 | }, 8 | user: { 9 | role: roles 10 | } 11 | } 12 | }; 13 | 14 | var Response = function () { 15 | 16 | }; 17 | Response.prototype = { 18 | status: function (status) { 19 | 20 | }, 21 | redirect: function (url) { 22 | 23 | }, 24 | send: function (data) { 25 | 26 | } 27 | }; 28 | module.exports.res = new Response(); 29 | 30 | module.exports.next = function () { 31 | return function () { 32 | 33 | }; 34 | }; --------------------------------------------------------------------------------