├── .gitignore ├── .pylintrc ├── .travis.yml ├── CHANGE.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── example.py ├── flask_perm ├── __init__.py ├── admin.py ├── api.py ├── app.py ├── core.py ├── models.py ├── script.py ├── services │ ├── __init__.py │ ├── permission.py │ ├── super_admin.py │ ├── user_group.py │ ├── user_group_member.py │ ├── user_group_permission.py │ ├── user_permission.py │ └── verification.py ├── static │ ├── admin.js │ ├── ng-admin.min.css │ └── ng-admin.min.js └── templates │ └── perm-admin │ ├── index.html │ └── login.html ├── migrate └── init.sql ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── setup.cfg ├── test_blueprint.py ├── test_init.py └── test_perm_app.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Mr Developer 45 | .mr.developer.cfg 46 | .project 47 | .pydevproject 48 | 49 | # Rope 50 | .ropeproject 51 | 52 | # Django stuff: 53 | *.log 54 | *.pot 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *,cover 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | 72 | 73 | ### https://raw.github.com/github/gitignore/master/Global/OSX.gitignore 74 | 75 | .DS_Store 76 | .AppleDouble 77 | .LSOverride 78 | 79 | # Icon must ends with two \r. 80 | Icon 81 | 82 | 83 | # Thumbnails 84 | ._* 85 | 86 | # Files that might appear on external disk 87 | .Spotlight-V100 88 | .Trashes 89 | 90 | 91 | ### https://raw.github.com/github/gitignore/master/Global/SublimeText.gitignore 92 | 93 | # workspace files are user-specific 94 | *.sublime-workspace 95 | .idea 96 | 97 | # project files should be checked into the repository, unless a significant 98 | # proportion of contributors will probably not be using SublimeText 99 | *.sublime-project 100 | demo 101 | 102 | # frontend 103 | 104 | # config 105 | 106 | venv/ 107 | ======= 108 | # PyBuilder 109 | target/ 110 | 111 | nasdaq/templates 112 | nasdaq/static 113 | nasdaq/settings/production.py 114 | nasdaq/settings/staging.py 115 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | # Deprecated. It was used to include message's id in output. Use --msg-template 25 | # instead. 26 | include-ids=no 27 | 28 | # Deprecated. It was used to include symbolic ids of messages in output. Use 29 | # --msg-template instead. 30 | symbols=no 31 | 32 | # Use multiple processes to speed up Pylint. 33 | jobs=1 34 | 35 | # Allow loading of arbitrary C extensions. Extensions are imported into the 36 | # active Python interpreter and may run arbitrary code. 37 | unsafe-load-any-extension=no 38 | 39 | # A comma-separated list of package or module names from where C extensions may 40 | # be loaded. Extensions are loading into the active Python interpreter and may 41 | # run arbitrary code 42 | extension-pkg-whitelist= 43 | 44 | # Allow optimization of some AST trees. This will activate a peephole AST 45 | # optimizer, which will apply various small optimizations. For instance, it can 46 | # be used to obtain the result of joining multiple strings with the addition 47 | # operator. Joining a lot of strings can lead to a maximum recursion error in 48 | # Pylint and this flag can prevent that. It has one side effect, the resulting 49 | # AST will be different than the one from reality. 50 | optimize-ast=no 51 | 52 | 53 | [MESSAGES CONTROL] 54 | 55 | # Only show warnings with the listed confidence levels. Leave empty to show 56 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 57 | confidence= 58 | 59 | # Enable the message, report, category or checker with the given id(s). You can 60 | # either give multiple identifier separated by comma (,) or put this option 61 | # multiple time. See also the "--disable" option for examples. 62 | #enable= 63 | 64 | # Disable the message, report, category or checker with the given id(s). You 65 | # can either give multiple identifiers separated by comma (,) or put this 66 | # option multiple times (only on the command line, not in the configuration 67 | # file where it should appear only once).You can also use "--disable=all" to 68 | # disable everything first and then reenable specific checks. For example, if 69 | # you want to run only the similarities checker, you can use "--disable=all 70 | # --enable=similarities". If you want to run only the classes checker, but have 71 | # no Warning level messages displayed, use"--disable=all --enable=classes 72 | # --disable=W" 73 | disable=E1608,W1627,E1601,E1603,E1602,E1605,E1604,E1607,E1606,W1621,W1620,W1623,W1622,W1625,W1624,W1609,W1608,W1607,W1606,W1605,W1604,W1603,W1602,W1601,W1639,W1640,I0021,W1638,I0020,W1618,W1619,W1630,W1626,W1637,W1634,W1635,W1610,W1611,W1612,W1613,W1614,W1615,W1616,W1617,W1632,W1633,W0704,W1628,W1629,W1636,W0232,W0622,C0111,W0141 74 | 75 | 76 | [REPORTS] 77 | 78 | # Set the output format. Available formats are text, parseable, colorized, msvs 79 | # (visual studio) and html. You can also give a reporter class, eg 80 | # mypackage.mymodule.MyReporterClass. 81 | output-format=text 82 | 83 | # Put messages in a separate file for each module / package specified on the 84 | # command line instead of printing them on stdout. Reports (if any) will be 85 | # written in a file name "pylint_global.[txt|html]". 86 | files-output=no 87 | 88 | # Tells whether to display a full report or only the messages 89 | reports=yes 90 | 91 | # Python expression which should return a note less than 10 (10 is the highest 92 | # note). You have access to the variables errors warning, statement which 93 | # respectively contain the number of errors / warnings messages and the total 94 | # number of statements analyzed. This is used by the global evaluation report 95 | # (RP0004). 96 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 97 | 98 | # Add a comment according to your evaluation note. This is used by the global 99 | # evaluation report (RP0004). 100 | comment=no 101 | 102 | # Template used to display messages. This is a python new-style format string 103 | # used to format the message information. See doc for all details 104 | #msg-template= 105 | 106 | 107 | [BASIC] 108 | 109 | # Required attributes for module, separated by a comma 110 | required-attributes= 111 | 112 | # List of builtins function names that should not be used, separated by a comma 113 | bad-functions=map,filter,input 114 | 115 | # Good variable names which should always be accepted, separated by a comma 116 | good-names=i,j,k,ex,Run,_ 117 | 118 | # Bad variable names which should always be refused, separated by a comma 119 | bad-names=foo,bar,baz,toto,tutu,tata 120 | 121 | # Colon-delimited sets of names that determine each other's naming style when 122 | # the name regexes allow several styles. 123 | name-group= 124 | 125 | # Include a hint for the correct naming format with invalid-name 126 | include-naming-hint=no 127 | 128 | # Regular expression matching correct function names 129 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 130 | 131 | # Naming hint for function names 132 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 133 | 134 | # Regular expression matching correct variable names 135 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 136 | 137 | # Naming hint for variable names 138 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 139 | 140 | # Regular expression matching correct constant names 141 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 142 | 143 | # Naming hint for constant names 144 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 145 | 146 | # Regular expression matching correct attribute names 147 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 148 | 149 | # Naming hint for attribute names 150 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 151 | 152 | # Regular expression matching correct argument names 153 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 154 | 155 | # Naming hint for argument names 156 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 157 | 158 | # Regular expression matching correct class attribute names 159 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 160 | 161 | # Naming hint for class attribute names 162 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 163 | 164 | # Regular expression matching correct inline iteration names 165 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 166 | 167 | # Naming hint for inline iteration names 168 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 169 | 170 | # Regular expression matching correct class names 171 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 172 | 173 | # Naming hint for class names 174 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 175 | 176 | # Regular expression matching correct module names 177 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 178 | 179 | # Naming hint for module names 180 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 181 | 182 | # Regular expression matching correct method names 183 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 184 | 185 | # Naming hint for method names 186 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 187 | 188 | # Regular expression which should only match function or class names that do 189 | # not require a docstring. 190 | no-docstring-rgx=__.*__ 191 | 192 | # Minimum line length for functions/classes that require docstrings, shorter 193 | # ones are exempt. 194 | docstring-min-length=0 195 | 196 | 197 | [FORMAT] 198 | 199 | # Maximum number of characters on a single line. 200 | max-line-length=100 201 | 202 | # Regexp for a line that is allowed to be longer than the limit. 203 | ignore-long-lines=^\s*(# )??$ 204 | 205 | # Allow the body of an if to be on the same line as the test if there is no 206 | # else. 207 | single-line-if-stmt=no 208 | 209 | # List of optional constructs for which whitespace checking is disabled 210 | no-space-check=trailing-comma,dict-separator 211 | 212 | # Maximum number of lines in a module 213 | max-module-lines=1000 214 | 215 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 216 | # tab). 217 | indent-string=' ' 218 | 219 | # Number of spaces of indent required inside a hanging or continued line. 220 | indent-after-paren=4 221 | 222 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 223 | expected-line-ending-format= 224 | 225 | 226 | [LOGGING] 227 | 228 | # Logging modules to check that the string format arguments are in logging 229 | # function parameter format 230 | logging-modules=logging 231 | 232 | 233 | [MISCELLANEOUS] 234 | 235 | # List of note tags to take in consideration, separated by a comma. 236 | notes=FIXME,XXX,TODO 237 | 238 | 239 | [SIMILARITIES] 240 | 241 | # Minimum lines number of a similarity. 242 | min-similarity-lines=4 243 | 244 | # Ignore comments when computing similarities. 245 | ignore-comments=yes 246 | 247 | # Ignore docstrings when computing similarities. 248 | ignore-docstrings=yes 249 | 250 | # Ignore imports when computing similarities. 251 | ignore-imports=no 252 | 253 | 254 | [SPELLING] 255 | 256 | # Spelling dictionary name. Available dictionaries: none. To make it working 257 | # install python-enchant package. 258 | spelling-dict= 259 | 260 | # List of comma separated words that should not be checked. 261 | spelling-ignore-words= 262 | 263 | # A path to a file that contains private dictionary; one word per line. 264 | spelling-private-dict-file= 265 | 266 | # Tells whether to store unknown words to indicated private dictionary in 267 | # --spelling-private-dict-file option instead of raising a message. 268 | spelling-store-unknown-words=no 269 | 270 | 271 | [TYPECHECK] 272 | 273 | # Tells whether missing members accessed in mixin class should be ignored. A 274 | # mixin class is detected if its name ends with "mixin" (case insensitive). 275 | ignore-mixin-members=yes 276 | 277 | # List of module names for which member attributes should not be checked 278 | # (useful for modules/projects where namespaces are manipulated during runtime 279 | # and thus existing member attributes cannot be deduced by static analysis 280 | ignored-modules= 281 | 282 | # List of classes names for which member attributes should not be checked 283 | # (useful for classes with attributes dynamically set). 284 | ignored-classes=SQLObject 285 | 286 | # When zope mode is activated, add a predefined set of Zope acquired attributes 287 | # to generated-members. 288 | zope=no 289 | 290 | # List of members which are set dynamically and missed by pylint inference 291 | # system, and so shouldn't trigger E0201 when accessed. Python regular 292 | # expressions are accepted. 293 | generated-members=REQUEST,acl_users,aq_parent,query 294 | 295 | 296 | [VARIABLES] 297 | 298 | # Tells whether we should check for unused import in __init__ files. 299 | init-import=no 300 | 301 | # A regular expression matching the name of dummy variables (i.e. expectedly 302 | # not used). 303 | dummy-variables-rgx=_$|dummy 304 | 305 | # List of additional names supposed to be defined in builtins. Remember that 306 | # you should avoid to define new builtins when possible. 307 | additional-builtins= 308 | 309 | # List of strings which can identify a callback function by name. A callback 310 | # name must start or end with one of those strings. 311 | callbacks=cb_,_cb 312 | 313 | 314 | [CLASSES] 315 | 316 | # List of interface methods to ignore, separated by a comma. This is used for 317 | # instance to not check methods defines in Zope's Interface base class. 318 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 319 | 320 | # List of method names used to declare (i.e. assign) instance attributes. 321 | defining-attr-methods=__init__,__new__,setUp 322 | 323 | # List of valid names for the first argument in a class method. 324 | valid-classmethod-first-arg=cls 325 | 326 | # List of valid names for the first argument in a metaclass class method. 327 | valid-metaclass-classmethod-first-arg=mcs 328 | 329 | # List of member names, which should be excluded from the protected access 330 | # warning. 331 | exclude-protected=_asdict,_fields,_replace,_source,_make 332 | 333 | 334 | [DESIGN] 335 | 336 | # Maximum number of arguments for function / method 337 | max-args=5 338 | 339 | # Argument names that match this expression will be ignored. Default to name 340 | # with leading underscore 341 | ignored-argument-names=_.* 342 | 343 | # Maximum number of locals for function / method body 344 | max-locals=15 345 | 346 | # Maximum number of return / yield for function / method body 347 | max-returns=6 348 | 349 | # Maximum number of branch for function / method body 350 | max-branches=12 351 | 352 | # Maximum number of statements in function / method body 353 | max-statements=50 354 | 355 | # Maximum number of parents for a class (see R0901). 356 | max-parents=7 357 | 358 | # Maximum number of attributes for a class (see R0902). 359 | max-attributes=7 360 | 361 | # Minimum number of public methods for a class (see R0903). 362 | min-public-methods=0 363 | 364 | # Maximum number of public methods for a class (see R0904). 365 | max-public-methods=20 366 | 367 | 368 | [IMPORTS] 369 | 370 | # Deprecated modules which should not be used, separated by a comma 371 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 372 | 373 | # Create a graph of every (i.e. internal and external) dependencies in the 374 | # given file (report RP0402 must not be disabled) 375 | import-graph= 376 | 377 | # Create a graph of external dependencies in the given file (report RP0402 must 378 | # not be disabled) 379 | ext-import-graph= 380 | 381 | # Create a graph of internal dependencies in the given file (report RP0402 must 382 | # not be disabled) 383 | int-import-graph= 384 | 385 | 386 | [EXCEPTIONS] 387 | 388 | # Exceptions that will emit a warning when being caught. Defaults to 389 | # "Exception" 390 | overgeneral-exceptions=Exception 391 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | 6 | install: "pip install -r requirements.txt" 7 | 8 | script: "py.test tests" 9 | -------------------------------------------------------------------------------- /CHANGE.rst: -------------------------------------------------------------------------------- 1 | Flask-Perm Changelog 2 | ==================== 3 | 4 | Here you can see the full list of changes between each release. 5 | 6 | Version 0.2 7 | ----------- 8 | 9 | * migrate superadmin configuration from app config to database. 10 | * add builtin flask-script commands. 11 | * register to app.extensions. 12 | 13 | Version 0.1 14 | ----------- 15 | 16 | First public preview release. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Soasme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include flask_perm *.py *.html *.css *.js *.pot *.mo *.po 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | distribute_package: 2 | python setup.py sdist bdist_wheel 3 | 4 | distribute_doc: 5 | python setup.py build_sphinx 6 | 7 | distribute: clean distribute_package distribute_doc 8 | 9 | upload_package: 10 | twine upload dist/*$(version)* 11 | 12 | upload_doc: 13 | python setup.py upload_sphinx 14 | 15 | upload: upload_package upload_doc 16 | 17 | patch: 18 | bumpversion --verbose patch 19 | 20 | clean: 21 | python setup.py clean --all 22 | rm -rf docs/_build/ 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flask-Perm[WIP] 2 | --------------- 3 | 4 | Flask-Perm is a flask permission management extension. 5 | 6 | ![Build Status](https://travis-ci.org/soasme/flask-perm.svg?branch=master) 7 | 8 | See example.py to have a quick look on Flask-Perm. 9 | 10 | For more information please refer to the online docs: 11 | 12 | https://pythonhosted.org/Flask-Perm/ 13 | -------------------------------------------------------------------------------- /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 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 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 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Perm.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Perm.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Perm" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Perm" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Perm documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jan 28 17:29:15 2016. 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 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.intersphinx', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'Flask-Perm' 52 | copyright = u'2016, Lin Ju' 53 | author = u'Lin Ju' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = u'0.2.8' 61 | # The full version, including alpha/beta/rc tags. 62 | release = u'0.2.8' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'alabaster' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | #html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | #html_sidebars = {} 159 | 160 | # Additional templates that should be rendered to pages, maps page names to 161 | # template names. 162 | #html_additional_pages = {} 163 | 164 | # If false, no module index is generated. 165 | #html_domain_indices = True 166 | 167 | # If false, no index is generated. 168 | #html_use_index = True 169 | 170 | # If true, the index is split into individual pages for each letter. 171 | #html_split_index = False 172 | 173 | # If true, links to the reST sources are added to the pages. 174 | #html_show_sourcelink = True 175 | 176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 177 | #html_show_sphinx = True 178 | 179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages will 183 | # contain a tag referring to it. The value of this option must be the 184 | # base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Language to be used for generating the HTML full-text search index. 191 | # Sphinx supports the following languages: 192 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 193 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 194 | #html_search_language = 'en' 195 | 196 | # A dictionary with options for the search language support, empty by default. 197 | # Now only 'ja' uses this config value 198 | #html_search_options = {'type': 'default'} 199 | 200 | # The name of a javascript file (relative to the configuration directory) that 201 | # implements a search results scorer. If empty, the default will be used. 202 | #html_search_scorer = 'scorer.js' 203 | 204 | # Output file base name for HTML help builder. 205 | htmlhelp_basename = 'Flask-Permdoc' 206 | 207 | # -- Options for LaTeX output --------------------------------------------- 208 | 209 | latex_elements = { 210 | # The paper size ('letterpaper' or 'a4paper'). 211 | #'papersize': 'letterpaper', 212 | 213 | # The font size ('10pt', '11pt' or '12pt'). 214 | #'pointsize': '10pt', 215 | 216 | # Additional stuff for the LaTeX preamble. 217 | #'preamble': '', 218 | 219 | # Latex figure (float) alignment 220 | #'figure_align': 'htbp', 221 | } 222 | 223 | # Grouping the document tree into LaTeX files. List of tuples 224 | # (source start file, target name, title, 225 | # author, documentclass [howto, manual, or own class]). 226 | latex_documents = [ 227 | (master_doc, 'Flask-Perm.tex', u'Flask-Perm Documentation', 228 | u'Lin Ju', 'manual'), 229 | ] 230 | 231 | # The name of an image file (relative to this directory) to place at the top of 232 | # the title page. 233 | #latex_logo = None 234 | 235 | # For "manual" documents, if this is true, then toplevel headings are parts, 236 | # not chapters. 237 | #latex_use_parts = False 238 | 239 | # If true, show page references after internal links. 240 | #latex_show_pagerefs = False 241 | 242 | # If true, show URL addresses after external links. 243 | #latex_show_urls = False 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #latex_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #latex_domain_indices = True 250 | 251 | 252 | # -- Options for manual page output --------------------------------------- 253 | 254 | # One entry per manual page. List of tuples 255 | # (source start file, name, description, authors, manual section). 256 | man_pages = [ 257 | (master_doc, 'flask-perm', u'Flask-Perm Documentation', 258 | [author], 1) 259 | ] 260 | 261 | # If true, show URL addresses after external links. 262 | #man_show_urls = False 263 | 264 | 265 | # -- Options for Texinfo output ------------------------------------------- 266 | 267 | # Grouping the document tree into Texinfo files. List of tuples 268 | # (source start file, target name, title, author, 269 | # dir menu entry, description, category) 270 | texinfo_documents = [ 271 | (master_doc, 'Flask-Perm', u'Flask-Perm Documentation', 272 | author, 'Flask-Perm', 'One line description of project.', 273 | 'Miscellaneous'), 274 | ] 275 | 276 | # Documents to append as an appendix to all manuals. 277 | #texinfo_appendices = [] 278 | 279 | # If false, no module index is generated. 280 | #texinfo_domain_indices = True 281 | 282 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 283 | #texinfo_show_urls = 'footnote' 284 | 285 | # If true, do not generate a @detailmenu in the "Top" node's menu. 286 | #texinfo_no_detailmenu = False 287 | 288 | 289 | # Example configuration for intersphinx: refer to the Python standard library. 290 | intersphinx_mapping = {'https://docs.python.org/': None} 291 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Flask-Perm documentation master file, created by 2 | sphinx-quickstart on Thu Jan 28 17:29:15 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Flask-Perm 7 | ========== 8 | 9 | .. module:: flask.ext.perm 10 | 11 | `Flask-Perm` is a Flask extension that can protect your view or function to be 12 | accessed by person who owns proper permission. There is a default dashboard 13 | to add/create/authorize/revoke permission to a person or a group, which is 14 | convenient for you to bootstrap your permission management.. 15 | 16 | Installation 17 | ------------ 18 | 19 | Install the extension with one of the following commands:: 20 | 21 | $ easy_install Flask-Perm 22 | 23 | or alternatively if you have pip installed:: 24 | 25 | $ pip install Flask-Perm 26 | 27 | Usage 28 | ----- 29 | 30 | Initialize 31 | `````````` 32 | 33 | To use the extension simply import the class wrapper and pass the Flask app 34 | object back to here. Do so like this:: 35 | 36 | from flask import Flask 37 | from flask.ext.perm import Perm 38 | 39 | app = Flask(__name__) 40 | perm = Perm(app) 41 | 42 | Or initialize perm in factory function:: 43 | 44 | perm = Perm(app) 45 | 46 | def create_app(): 47 | app = Flask(__name__) 48 | perm.init_app(app) 49 | return app 50 | 51 | Register 52 | ```````` 53 | 54 | To have Flask-Perm works, you have some works to finish yet. 55 | 56 | You must register current user loader before using `require_permission` 57 | and `require_group`. These two methods can not works well if they don't 58 | know whom to require permission or group:: 59 | 60 | @perm.current_user_loader 61 | def load_current_user(): 62 | if 'user_id' in session: 63 | return User.query.get(session['user_id']) 64 | 65 | If you have used `Flask-Login`, register can be like this:: 66 | 67 | from flask_login import current_user 68 | @perm.current_user_loader(lambda: current_user) 69 | 70 | You must register user loader, users loader, users count loader before 71 | using admin dashboard:: 72 | 73 | @perm.user_loader 74 | def load_user(user_id): 75 | return User.query.get(user_id) 76 | 77 | @perm.users_count_loader 78 | def load_users_count(): 79 | return User.query.all() 80 | 81 | @perm.users_loader 82 | def load_users(filter_by={}, sort_field='created_at', sort_dir='desc', offset=0, limit=20): 83 | sort = getattr(getattr(User, sort_field), sort_dir)() 84 | return User.query.filter_by(**filter_by).order_by(sort).offset(offset).limit(limit).all() 85 | 86 | 87 | Configurations 88 | ```````````````` 89 | 90 | * `PERM_ADMIN_URL`, default `/perm-admin`. This url_prefix determins which 91 | url your dashboard will be visited. 92 | 93 | Quick Start 94 | ----------- 95 | 96 | Require Permission 97 | `````````````````` 98 | 99 | code:: 100 | 101 | @perm.require_permission('post.publish') 102 | def publish_post(): 103 | Post.publish() 104 | 105 | code:: 106 | 107 | @perm.require_permission('post.publish', 'post.schedule') 108 | def schedule_post_publish(): 109 | Post.schedule_publish() 110 | 111 | In template, you can protect a block by writing code:: 112 | 113 | {% if require_permission('post.publish') %} 114 | 115 | Publish Post 116 | 117 | {% endif %} 118 | 119 | Require Group 120 | ````````````` 121 | 122 | code:: 123 | 124 | @perm.require_group('editor') 125 | def publish_post(): 126 | Post.publish() 127 | 128 | In template, you can protect a block by writing code:: 129 | 130 | {% if require_group('editor') %} 131 | 132 | Publish Post 133 | 134 | {% endif %} 135 | 136 | Low Level API 137 | ````````````` 138 | 139 | Validate user's permission:: 140 | 141 | perm.has_permission(user_id, 'post.publish') 142 | 143 | Validate user's membership:: 144 | 145 | perm.is_user_in_groups(user_id, 'editor') 146 | 147 | Script 148 | `````` 149 | 150 | Initialize Script Manager:: 151 | 152 | from flask.ext.manager import Manager 153 | 154 | manager = Manager(app) 155 | perm.register_commands(manager) 156 | 157 | Create super admin account:: 158 | 159 | $ python manage.py perm create_superadmin admin@example.org 160 | Please input password: 161 | Please input password again: 162 | Success! 163 | 164 | Reseting password shares same command with creating super admin. 165 | 166 | List all super admins:: 167 | 168 | $ python manage.py perm list_superadmin 169 | admin@example.org 170 | editor@example.org 171 | 172 | Delete a super admin account:: 173 | 174 | $ python example.py perm delete_superadmin admin@example.org 175 | Do you really want to delete this account? [y/n] [n]: y 176 | Success! 177 | 178 | Dashboard 179 | `````````` 180 | 181 | Before using dashboard, please create superadmin, which is described above. 182 | Visit http://SERVER_NAME:PORT/PERM_ADMIN_URL, login and manage permissions. 183 | There are several pages: 184 | 185 | * Dashboard Index 186 | * Users 187 | * User Groups 188 | * Permissions 189 | * User Permissions 190 | * User Group Permissions 191 | * User Group Members 192 | 193 | Other Library 194 | -------------- 195 | 196 | Django Auth Contrib 197 | ```````````````````` 198 | 199 | Flask-Perm is a subset of django.contrib.auth. 200 | Flask-Perm have no assumption of your user module, while django.contrib.auth 201 | have builtin user support. 202 | Flask-Perm is model-agnostic, while django.contrib.auth relate permission with 203 | a specific model. 204 | 205 | 206 | Flask-Principle 207 | ``````````````` 208 | 209 | The permission of Flask-Perm is similar to the ActionNeed of Flask-Principle; 210 | and the group of Flask-Perm is similar to the RoleNeed of Flask-Principle. 211 | All permisions and groups are created or deleted by superadmin's account; 212 | while Need in Flask-Principle is hard coded. 213 | 214 | Flask-Perm is not designed to system involved a large number of users; 215 | Flask-Principle may be a good choice in this case. 216 | 217 | Flask-Permissions 218 | `````````````````` 219 | 220 | The basic concept in Flask-Permissions is `role`, `ability`. 221 | In Flask-Permissions, user has several roles, and each role has several abibities. 222 | The use case of Flask-Perm is a little bit more complex than Flask-Permissions. 223 | In Flask-Perm, user gain permissions via both group and directly authorize permission. 224 | 225 | Notice 226 | ------ 227 | 228 | Is it safe to store superadmin's password? 229 | `````````````````````````````````````````` 230 | 231 | Superadmins have great power to control access permission. 232 | Their password has encrypted in Bcrypt algorithm, which is considered very hard to be 233 | cracked. 234 | Use HTTPS to protect your site. 235 | Don't use simple password for superadmin. 236 | Keep changing password for superadmin. 237 | 238 | How did Flask-Perm implement it? 239 | ```````````````````````````````` 240 | 241 | Flask-Perm manipulates data in several database tables with helps of Flask-SQLAlchemy. 242 | 243 | Thanks to Ng-Admin, an administration dashboard application built in Angular. 244 | Flask-Perm is allowed to supply RESTful APIs and has complete GUI to be used. 245 | 246 | API 247 | --- 248 | .. autoclass:: flask_perm.Perm 249 | :members: 250 | 251 | Indices and tables 252 | ================== 253 | 254 | * :ref:`genindex` 255 | * :ref:`modindex` 256 | * :ref:`search` 257 | 258 | -------------------------------------------------------------------------------- /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 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Perm.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Perm.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import namedtuple 3 | from flask import Flask, g, render_template, render_template_string, abort 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_perm import Perm 6 | from flask_script import Manager 7 | 8 | app = Flask(__name__) 9 | manager = Manager(app) 10 | db = SQLAlchemy() 11 | perm = Perm() 12 | app.config['DEBUG'] = True 13 | app.config['SECRET_KEY'] = 'secret key' 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_perm.db' 15 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 16 | app.config['PERM_ADMIN_ECHO'] = True 17 | 18 | db.app = app 19 | db.init_app(app) 20 | perm.app = app 21 | perm.init_app(app) 22 | perm.register_commands(manager) 23 | 24 | class User(namedtuple('User', 'id nickname')): 25 | pass 26 | 27 | @app.before_request 28 | def before_request(): 29 | g.user = User(**{'id': 1, 'nickname': 'user1'}) 30 | 31 | @perm.user_loader 32 | def load_user(user_id): 33 | return User(**{'id': user_id, 'nickname': 'user%d' % user_id}) 34 | 35 | @perm.users_loader 36 | def load_users(filter_by, sort_field, sort_dir, offset, limit): 37 | return [User(**{'id': id, 'nickname': 'user%d' % id}) for id in range(20)] 38 | 39 | @perm.current_user_loader 40 | def load_current_user(): 41 | return g.user 42 | 43 | @app.errorhandler(perm.Denied) 44 | def permission_denied(e): 45 | return 'FORBIDDEN', 403 46 | 47 | @app.route('/post/publish') 48 | @perm.require_permission('post.publish') 49 | def publish_post(): 50 | return 'Hey, you can publish post!' 51 | 52 | @app.route('/post/publish/template') 53 | def template_level_visible(): 54 | return render_template_string(""" 55 | {% if require_permission('post.publish') %} 56 | Hey, you can publish post! 57 | {% else %} 58 | No, you can't see this. 59 | {% endif %} 60 | """) 61 | 62 | if __name__ == '__main__': 63 | """ 64 | To create superadmin, run 65 | 66 | $ python example.py perm create_superadmin your_admin_account 67 | Please input password: 68 | Please input password again: 69 | Success! 70 | 71 | To run server, run 72 | 73 | $ python example.py runserver 74 | """ 75 | manager.run() 76 | -------------------------------------------------------------------------------- /flask_perm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .app import Perm 4 | from . import services 5 | 6 | __version__ = '0.2.8' 7 | -------------------------------------------------------------------------------- /flask_perm/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import Blueprint, render_template, current_app, url_for, redirect, request, flash 4 | 5 | bp = Blueprint('perm-admin', __name__, template_folder='templates', static_folder='static') 6 | 7 | @bp.route('/') 8 | def index(): 9 | if not current_app.extensions['perm'].has_perm_admin_logined(): 10 | return redirect(url_for('perm-admin.login')) 11 | 12 | render_data = { 13 | 'base_api_url': current_app.config.get('PERM_ADMIN_PREFIX') + '/api', 14 | 'base_web_url': current_app.config.get('PERM_ADMIN_PREFIX'), 15 | 'debug': current_app.config.get('DEBUG'), 16 | } 17 | 18 | return render_template('/perm-admin/index.html', **render_data) 19 | 20 | @bp.route('/login', methods=['GET', 'POST']) 21 | def login(): 22 | if request.method == 'POST': 23 | username = request.form['username'] 24 | password = request.form['password'] 25 | admin_id = current_app.extensions['perm'].get_perm_admin_id_by_auth(username, password) 26 | if admin_id: 27 | current_app.extensions['perm'].login_perm_admin(admin_id) 28 | return redirect(url_for('perm-admin.index')) 29 | else: 30 | flash(u'Invalid Password', 'error') 31 | return redirect(url_for('perm-admin.login')) 32 | return render_template('/perm-admin/login.html') 33 | 34 | @bp.route('/logout') 35 | def logout(): 36 | current_app.extensions['perm'].logout_perm_admin() 37 | return redirect(url_for('perm-admin.login')) 38 | -------------------------------------------------------------------------------- /flask_perm/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import json 5 | from flask import Blueprint, request, jsonify, current_app 6 | from sqlalchemy.exc import IntegrityError 7 | from .core import db 8 | from .services import ( 9 | UserGroupService, PermissionService, UserGroupMemberService, 10 | UserPermissionService, UserGroupPermissionService, 11 | VerificationService, 12 | ) 13 | 14 | bp = Blueprint('flask_perm_api', __name__) 15 | 16 | def ok(data=None, count=1): 17 | response = jsonify(code=0, message='success', data=data) 18 | if count: 19 | response.headers['X-Total-Count'] = count 20 | return response 21 | return response 22 | 23 | def bad_request(message='bad request', **data): 24 | return jsonify(code=1, message=message, data=data), 400 25 | 26 | def not_found(message='not found', **data): 27 | return jsonify(code=1, message=message, data=data), 404 28 | 29 | def forbidden(message='forbidden', **data): 30 | return jsonify(code=1, message=message, data=data), 403 31 | 32 | def check_auth(username, password): 33 | return current_app.config['PERM_ADMIN_USERNAME'] == username and \ 34 | current_app.config['PERM_ADMIN_PASSWORD'] == password 35 | 36 | def current_perm(): 37 | return current_app.extensions['perm'] 38 | 39 | def log_action(data, **kwargs): 40 | data = dict(data) 41 | data.update(kwargs) 42 | current_perm().log_admin_action(data) 43 | 44 | @bp.before_request 45 | def before_request(): 46 | if not current_perm().has_perm_admin_logined(): 47 | return forbidden() 48 | 49 | @bp.errorhandler(IntegrityError) 50 | def detect_integrity_error(e): 51 | return bad_request('conflict') 52 | 53 | @bp.route('/permissions', methods=['POST']) 54 | def add_permission(): 55 | data = request.get_json() 56 | if 'title' not in data: 57 | return bad_request('missing title field') 58 | if not data['title']: 59 | return bad_request('title is blank') 60 | title = data['title'] 61 | code = data.get('code') 62 | permission = PermissionService.create(title, code) 63 | permission = PermissionService.rest(permission) 64 | log_action(permission, action='add', model='permission') 65 | return ok(permission) 66 | 67 | def _get_filter_by(): 68 | filter_by = request.args.get('_filters') 69 | if filter_by: 70 | try: 71 | filter_by = json.loads(filter_by) 72 | except ValueError: 73 | pass 74 | return filter_by 75 | 76 | @bp.route('/permissions') 77 | def get_permissions(): 78 | offset = request.args.get('offset', type=int, default=0) 79 | limit = request.args.get('limit', type=int, default=20) 80 | sort_field = request.args.get('_sortField', 'created_at').lower() 81 | sort_dir = request.args.get('_sortDir', 'DESC').lower() 82 | filter_by = _get_filter_by() 83 | 84 | permissions = PermissionService.filter_permissions( 85 | filter_by, offset, limit, sort_field, sort_dir) 86 | count = PermissionService.count_filter_permission(filter_by, offset, limit) 87 | 88 | permissions = map(PermissionService.rest, permissions) 89 | return ok(permissions, count) 90 | 91 | @bp.route('/permissions/') 92 | def get_permission(permission_id): 93 | permission = PermissionService.get(permission_id) 94 | if not permission: 95 | return not_found() 96 | permission = PermissionService.rest(permission) 97 | return ok(permission) 98 | 99 | @bp.route('/permissions/', methods=['PUT']) 100 | def update_permission(permission_id): 101 | permission = PermissionService.get(permission_id) 102 | if not permission: 103 | return not_found() 104 | if request.get_json().get('title'): 105 | PermissionService.rename(permission_id, request.get_json().get('title')) 106 | if request.get_json().get('code'): 107 | PermissionService.set_code(permission_id, request.get_json().get('code')) 108 | permission = PermissionService.rest(PermissionService.get(permission_id)) 109 | log_action(permission, action='update', model='permission') 110 | return ok(permission) 111 | 112 | @bp.route('/permissions/', methods=['DELETE']) 113 | def delete_permission(permission_id): 114 | permission = PermissionService.get(permission_id) 115 | if not permission: 116 | return not_found() 117 | log_action(PermissionService.rest(permission), action='delete', model='permission') 118 | UserPermissionService.delete_by_permission(permission_id) 119 | UserGroupPermissionService.delete_by_permission(permission_id) 120 | PermissionService.delete(permission_id) 121 | return ok() 122 | 123 | @bp.route('/user_permissions') 124 | def get_user_permissions(): 125 | offset = request.args.get('offset', type=int, default=0) 126 | limit = request.args.get('limit', type=int, default=20) 127 | sort_field = request.args.get('_sortField', 'created_at').lower() 128 | sort_dir = request.args.get('_sortDir', 'DESC').lower() 129 | filter_by = _get_filter_by() 130 | 131 | user_permissions = UserPermissionService.filter_user_permissions( 132 | filter_by, offset, limit, sort_field, sort_dir) 133 | count = UserPermissionService.count_filter_user_permission(filter_by, offset, limit) 134 | 135 | user_permissions = map(UserPermissionService.rest, user_permissions) 136 | return ok(user_permissions, count) 137 | 138 | @bp.route('/user_permissions', methods=['POST']) 139 | def add_user_permission(): 140 | data = request.get_json() 141 | try: 142 | permission_id = data['permission_id'] 143 | user_id = data['user_id'] 144 | except KeyError: 145 | return bad_request() 146 | 147 | permission = PermissionService.get(permission_id) 148 | if not permission: 149 | return not_found() 150 | user_permission = UserPermissionService.create(user_id, permission_id) 151 | user_permission = UserPermissionService.rest(user_permission) 152 | log_action(user_permission, action='add', model='user_permission') 153 | return ok(user_permission) 154 | 155 | @bp.route('/user_permissions/', methods=['DELETE']) 156 | def revoke_user_permission(user_permission_id): 157 | user_permission = UserPermissionService.get(user_permission_id) 158 | if not user_permission: 159 | return not_found() 160 | log_action(UserPermissionService.rest(user_permission), action='delete', model='user_permission') 161 | UserPermissionService.delete(user_permission_id) 162 | return ok() 163 | 164 | @bp.route('/user_group_permissions') 165 | def get_user_group_permissions(): 166 | offset = request.args.get('offset', type=int, default=0) 167 | limit = request.args.get('limit', type=int, default=20) 168 | sort_field = request.args.get('_sortField', 'created_at').lower() 169 | sort_dir = request.args.get('_sortDir', 'DESC').lower() 170 | filter_by = _get_filter_by() 171 | 172 | user_group_permissions = UserGroupPermissionService.filter_user_group_permissions( 173 | filter_by, offset, limit, sort_field, sort_dir) 174 | count = UserGroupPermissionService.count_filter_user_group_permissions( 175 | filter_by, offset, limit) 176 | 177 | user_group_permissions = map(UserGroupPermissionService.rest, user_group_permissions) 178 | return ok(user_group_permissions, count) 179 | 180 | @bp.route('/user_group_permissions', methods=['POST']) 181 | def add_user_group_permission(): 182 | data = request.get_json() 183 | try: 184 | permission_id = data['permission_id'] 185 | user_group_id = data['user_group_id'] 186 | except KeyError: 187 | return bad_request() 188 | permission = PermissionService.get(permission_id) 189 | if not permission: 190 | return not_found() 191 | user_group_permission = UserGroupPermissionService.create(user_group_id, permission_id) 192 | user_group_permission = UserGroupPermissionService.rest(user_group_permission) 193 | log_action(user_group_permission, action='add', model='user_permission') 194 | return ok(user_group_permission) 195 | 196 | @bp.route('/user_group_permissions/', methods=['DELETE']) 197 | def revoke_user_group_permission(user_group_permission_id): 198 | user_group_permission = UserGroupPermissionService.get(user_group_permission_id) 199 | if not user_group_permission: 200 | return not_found() 201 | log_action(UserGroupPermissionService.rest(user_group_permission), 202 | action='delete', model='user_group_permission') 203 | UserGroupPermissionService.delete(user_group_permission_id) 204 | return ok() 205 | 206 | @bp.route('/user_groups', methods=['POST']) 207 | def add_user_group(): 208 | try: 209 | data = request.get_json() 210 | title = data['title'] 211 | code = data['code'] 212 | except KeyError: 213 | return bad_request() 214 | user_group = UserGroupService.create(title, code) 215 | user_group = UserGroupService.rest(user_group) 216 | log_action(user_group, action='add', model='user_group') 217 | return ok(user_group) 218 | 219 | @bp.route('/user_groups') 220 | def get_user_groups(): 221 | offset = request.args.get('offset', type=int, default=0) 222 | limit = request.args.get('limit', type=int, default=20) 223 | sort_field = request.args.get('_sortField', 'created_at').lower() 224 | sort_dir = request.args.get('_sortDir', 'DESC').lower() 225 | filter_by = _get_filter_by() 226 | 227 | user_groups = UserGroupService.filter_user_groups( 228 | filter_by, offset, limit, sort_field, sort_dir) 229 | count = UserGroupService.count_filter_user_group(filter_by, offset, limit) 230 | 231 | user_groups = map(UserGroupService.rest, user_groups) 232 | return ok(user_groups, count) 233 | 234 | @bp.route('/user_groups/') 235 | def get_user_group(user_group_id): 236 | user_group = UserGroupService.get(user_group_id) 237 | if not user_group: 238 | return not_found() 239 | user_group = UserGroupService.rest(user_group) 240 | return ok(user_group) 241 | 242 | @bp.route('/user_groups/', methods=['PUT']) 243 | def update_user_group(user_group_id): 244 | user_group = UserGroupService.get(user_group_id) 245 | if not user_group: 246 | return not_found() 247 | data = request.get_json() 248 | if 'title' in data and data['title']: 249 | UserGroupService.rename(user_group_id, data['title']) 250 | if 'code' in data and data['code']: 251 | UserGroupService.update_code(user_group_id, data['code']) 252 | user_group = UserGroupService.rest(UserGroupService.get(user_group_id)) 253 | log_action(user_group, action='update', model='user_group') 254 | return ok(user_group) 255 | 256 | @bp.route('/user_groups/', methods=['DELETE']) 257 | def delete_user_group(user_group_id): 258 | user_group = UserGroupService.get(user_group_id) 259 | if not user_group: 260 | return not_found() 261 | log_action(UserGroupService.rest(user_group), action='delete', model='user_group') 262 | UserGroupPermissionService.delete_by_user_group(user_group_id) 263 | UserGroupService.delete(user_group_id) 264 | return ok() 265 | 266 | @bp.route('/user_group_members') 267 | def get_user_group_members(): 268 | offset = request.args.get('offset', type=int, default=0) 269 | limit = request.args.get('limit', type=int, default=20) 270 | sort_field = request.args.get('_sortField', 'created_at').lower() 271 | sort_dir = request.args.get('_sortDir', 'DESC').lower() 272 | filter_by = _get_filter_by() 273 | 274 | members = UserGroupMemberService.filter_user_group_members( 275 | filter_by, offset, limit, sort_field, sort_dir) 276 | count = UserGroupMemberService.count_filter_user_group_members(filter_by, offset, limit) 277 | 278 | members = map(UserGroupMemberService.rest, members) 279 | return ok(members, count) 280 | 281 | @bp.route('/user_group_members', methods=['POST']) 282 | def add_user_group_member(): 283 | data = request.get_json() 284 | try: 285 | user_id = data['user_id'] 286 | user_group_id = data['user_group_id'] 287 | except KeyError: 288 | return bad_request() 289 | user_group = UserGroupService.get(user_group_id) 290 | if not user_group: 291 | return not_found() 292 | member = UserGroupMemberService.create(user_id, user_group_id) 293 | member = UserGroupMemberService.rest(member) 294 | log_action(member, action='add', model='user_group_member') 295 | return ok(member) 296 | 297 | @bp.route('/user_group_members/', methods=['DELETE']) 298 | def delete_user_from_user_group(user_group_member_id): 299 | user_group_member = UserGroupMemberService.get(user_group_member_id) 300 | if not user_group_member: 301 | return not_found() 302 | log_action(UserGroupMemberService.rest(user_group_member), action='delete', model='user_group_member') 303 | UserGroupMemberService.delete(user_group_member_id) 304 | return ok() 305 | 306 | def jsonify_user(user): 307 | return dict(id=user.id, nickname=user.nickname) 308 | 309 | @bp.route('/users') 310 | def get_users(): 311 | offset = request.args.get('offset', type=int, default=0) 312 | limit = request.args.get('limit', type=int, default=20) 313 | sort_field = request.args.get('_sortField', 'created_at').lower() 314 | sort_dir = request.args.get('_sortDir', 'DESC').lower() 315 | filter_by = _get_filter_by() 316 | users = current_perm().load_users(filter_by, sort_field, sort_dir, offset, limit) 317 | users = map(jsonify_user, users) 318 | return ok(users) 319 | 320 | @bp.route('/users/') 321 | def get_user(user_id): 322 | user = current_perm().load_user(user_id) 323 | if not user: 324 | return not_found() 325 | return ok(jsonify_user(user)) 326 | -------------------------------------------------------------------------------- /flask_perm/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | from functools import wraps 5 | from flask import session, request 6 | from sqlalchemy.exc import IntegrityError 7 | from werkzeug.security import generate_password_hash 8 | from werkzeug.security import check_password_hash 9 | from .core import db, bcrypt 10 | 11 | class Perm(object): 12 | 13 | class Denied(Exception): 14 | pass 15 | 16 | def __init__(self, app=None): 17 | self.app = app 18 | self.user_callback = None 19 | self.current_user_callback = None 20 | self.users_callback = None 21 | self.users_count_callback = None 22 | self._admin_logger = logging.getLogger('flask_perm.admin') 23 | self.registered_permissions = set() 24 | if app is not None: 25 | self.init_app(app) 26 | 27 | @property 28 | def admin_logger(self): 29 | return self._admin_logger 30 | 31 | @admin_logger.setter 32 | def admin_logger(self, logger): 33 | self._admin_logger = logger 34 | 35 | def init_app(self, app): 36 | """Initialize Perm object. 37 | """ 38 | if not hasattr(app, 'extensions'): 39 | app.extensions = {} 40 | app.extensions['perm'] = self 41 | 42 | db.app = app 43 | db.init_app(app) 44 | 45 | bcrypt.app = app 46 | bcrypt.init_app(app) 47 | 48 | app.config.setdefault('PERM_ADMIN_PREFIX', '/perm-admin') 49 | app.config.setdefault('PERM_ADMIN_ECHO', False) 50 | 51 | from . import models 52 | db.create_all() 53 | 54 | from .api import bp as api_bp 55 | app.register_blueprint(api_bp, url_prefix=app.config.get('PERM_ADMIN_PREFIX') + '/api') 56 | 57 | from .admin import bp as admin_bp 58 | app.register_blueprint(admin_bp, url_prefix=app.config.get('PERM_ADMIN_PREFIX')) 59 | 60 | self.register_context_processors(app) 61 | 62 | 63 | 64 | def log_admin_action(self, msg): 65 | '''Log msg to `flask.admin` logger.''' 66 | if self.app.config.get('PERM_ADMIN_ECHO'): 67 | self.admin_logger.info(msg) 68 | 69 | def require_perm_admin(self, f): 70 | '''A decorator that can protect function from unauthorized request. 71 | 72 | Used in perm admin dashboard.''' 73 | @wraps(f) 74 | def _(*args, **kwargs): 75 | if not session.get('perm_admin_id'): 76 | raise self.Denied 77 | return f(*args, **kwargs) 78 | return _ 79 | 80 | def create_super_admin(self, email, password): 81 | """Create superadmin / Reset password.""" 82 | from .services import SuperAdminService 83 | try: 84 | superadmin = SuperAdminService.create(email, password) 85 | except IntegrityError: 86 | superadmin = SuperAdminService.get_by_email(email) 87 | superadmin = SuperAdminService.reset_password(superadmin.id, password) 88 | return SuperAdminService.to_dict(superadmin) 89 | 90 | def login_perm_admin(self, super_admin_id): 91 | """Get authorization to access perm admin dashboard.""" 92 | session['perm_admin_id'] = super_admin_id 93 | 94 | def logout_perm_admin(self): 95 | """Revoke authorization from accessing perm admin dashboard.""" 96 | session.pop('perm_admin_id', None) 97 | 98 | def get_perm_admin_id_from_session(self): 99 | from .services import SuperAdminService 100 | admin_id = session.get('perm_admin_id') 101 | super_admin = admin_id and SuperAdminService.get(admin_id) 102 | return super_admin and super_admin.id 103 | 104 | def get_perm_admin_id_by_auth(self, email, password): 105 | from .services import SuperAdminService 106 | if SuperAdminService.verify_password(email, password): 107 | super_admin = SuperAdminService.get_by_email(email) 108 | return super_admin and super_admin.id 109 | 110 | def get_perm_admin_id(self): 111 | """Get super admin id. Both basic authorization and cookie are support.""" 112 | if request.authorization: 113 | auth = request.authorization 114 | return self.get_perm_admin_id_by_auth(auth.username, auth.password) 115 | return self.get_perm_admin_id_from_session() 116 | 117 | def has_perm_admin_logined(self): 118 | """""" 119 | return bool(self.get_perm_admin_id()) 120 | 121 | def user_loader(self, callback): 122 | """Define user loader. 123 | 124 | Required if you plan to use perm admin dashboard. 125 | The callback will be used to render user basic information in dashboard. 126 | 127 | Callback must take `user_id` integer parameter. 128 | """ 129 | self.user_callback = callback 130 | return callback 131 | 132 | def current_user_loader(self, callback): 133 | """Define current user loader. 134 | 135 | Required if you plan to use decorator to protect your function. 136 | The callback will be used in deciding whether current user has authority. 137 | 138 | Callback takes no parameters. 139 | """ 140 | self.current_user_callback = callback 141 | return callback 142 | 143 | def users_loader(self, callback): 144 | """Define users loader. 145 | 146 | Required if you plan to use perm admin dashboard. 147 | The callback will be used to render whole user list in dashboard. 148 | 149 | Callback must take 5 parameters: 150 | * filter_by={}, 151 | * sort_field='created_at', 152 | * sort_dir='desc', 153 | * offset=0, 154 | * limit=20 155 | """ 156 | self.users_callback = callback 157 | return callback 158 | 159 | def users_count_loader(self, callback): 160 | """Define users count loader. 161 | 162 | Required if you plan to use perm admin dashboard. 163 | The callback will be used in paginating user list. 164 | 165 | Callback takes no parameters.""" 166 | self.users_count_callback = callback 167 | return callback 168 | 169 | def load_user(self, user_id): 170 | if self.user_callback is None: 171 | raise NotImplementedError('You must register user_loader!') 172 | return self.user_callback(user_id) 173 | 174 | def load_current_user(self): 175 | if self.current_user_callback is None: 176 | raise NotImplementedError('You must register current_user_loader!') 177 | return self.current_user_callback() 178 | 179 | def load_users(self, filter_by={}, sort_field='created_at', sort_dir='desc', offset=0, limit=20): 180 | if self.users_callback is None: 181 | raise NotImplementedError('You must register users_loader!') 182 | return self.users_callback(**dict( 183 | filter_by=filter_by, 184 | sort_field=sort_field, 185 | sort_dir=sort_dir, 186 | offset=offset, 187 | limit=limit, 188 | )) 189 | 190 | def load_users_count(self): 191 | if self.users_count_callback is None: 192 | raise NotImplementedError('You must register users_count_loader') 193 | return self.users_count_callback() 194 | 195 | def has_permission(self, user_id, code): 196 | """Decide whether a user has a permission identified by `codes`. 197 | 198 | Code is defined in perm admin dashboard.""" 199 | 200 | from .services import VerificationService, PermissionService 201 | 202 | permission = PermissionService.get_by_code(code) 203 | 204 | if not permission: 205 | return False 206 | 207 | return VerificationService.has_permission(user_id, permission.id) 208 | 209 | def has_permissions(self, user_id, *codes): 210 | """Decide whether a user has permissions identified by `codes`. 211 | 212 | Codes are defined in perm admin dashboard.""" 213 | if not codes: 214 | return True 215 | 216 | if '*' in codes: 217 | return any( 218 | self.has_permission(user_id, code) 219 | for code in self.get_all_permission_codes() 220 | ) 221 | else: 222 | return any(self.has_permission(user_id, code) for code in codes) 223 | 224 | def get_user_permissions(self, user_id): 225 | """Define all permission codes that authorized to a user. 226 | 227 | Codes are defined in perm admin dashboard.""" 228 | from .services import VerificationService, PermissionService 229 | 230 | permission_ids = VerificationService.get_user_permissions(user_id) 231 | 232 | permissions = map(PermissionService.get, permission_ids) 233 | permissions = filter(None, permissions) 234 | permissions = map(PermissionService.rest, permissions) 235 | 236 | return permissions 237 | 238 | def get_all_permission_codes(self): 239 | """Get all permission codes. 240 | 241 | WARNING: this might have performance issue.""" 242 | 243 | from .services import PermissionService 244 | 245 | permissions = PermissionService.get_permissions() 246 | 247 | return [permission.code for permission in permissions] 248 | 249 | def is_user_in_groups(self, user_id, *groups): 250 | """Decide whether a user is in groups. 251 | 252 | Groups are defined in perm admin dashboard.""" 253 | from .services import UserGroupService, UserGroupMemberService 254 | 255 | if not groups: 256 | return False 257 | 258 | if '*' in groups: 259 | user_group_ids = UserGroupService.get_all_user_group_ids() 260 | else: 261 | user_groups = UserGroupService.get_user_groups_by_codes(groups) 262 | user_group_ids = [user_group.id for user_group in user_groups] 263 | 264 | if not user_group_ids: 265 | return False 266 | 267 | return UserGroupMemberService.is_user_in_groups(user_id, user_group_ids) 268 | 269 | def require_group(self, *groups): 270 | """A decorator that can decide whether current user is in listed groups. 271 | 272 | Groups are defined in perm admin dashboard.""" 273 | from .services import UserGroupService, UserGroupMemberService 274 | 275 | 276 | def deco(func): 277 | @wraps(func) 278 | def _(*args, **kwargs): 279 | 280 | current_user = self.load_current_user() 281 | 282 | if not current_user: 283 | raise self.Denied 284 | 285 | current_user_id = current_user.id 286 | is_allowed = self.is_user_in_groups(current_user_id, *groups) 287 | 288 | if is_allowed: 289 | return func(*args, **kwargs) 290 | else: 291 | raise self.Denied 292 | return _ 293 | return deco 294 | 295 | def require_group_in_template(self, *groups): 296 | """Require group in template""" 297 | from .services import UserGroupService, UserGroupMemberService 298 | 299 | current_user = self.load_current_user() 300 | 301 | if not current_user: 302 | return False 303 | 304 | current_user_id = current_user.id 305 | return self.is_user_in_groups(current_user_id, *groups) 306 | 307 | 308 | def require_permission(self, *codes): 309 | """A decorator that can decide whether current user has listed permission codes. 310 | 311 | Codes are defined in perm admin dashboard.""" 312 | 313 | for code in codes: 314 | self.registered_permissions.add(code) 315 | 316 | def deco(func): 317 | @wraps(func) 318 | def _(*args, **kwargs): 319 | current_user = self.load_current_user() 320 | 321 | if not current_user: 322 | raise self.Denied 323 | 324 | is_allowed = self.has_permissions(current_user.id, *codes) 325 | 326 | if is_allowed: 327 | return func(*args, **kwargs) 328 | else: 329 | raise self.Denied 330 | 331 | return _ 332 | return deco 333 | 334 | def require_permission_in_template(self, *codes): 335 | """Require permission in template.""" 336 | for code in codes: 337 | self.registered_permissions.add(code) 338 | 339 | current_user = self.load_current_user() 340 | 341 | if not current_user: 342 | return False 343 | 344 | return self.has_permissions(current_user.id, *codes) 345 | 346 | def default_context_processors(self): 347 | return { 348 | 'require_permission': self.require_permission_in_template, 349 | 'require_group': self.require_group_in_template, 350 | } 351 | 352 | def register_commands(self, flask_script_manager): 353 | """Register several convinient Flask-Script commands. 354 | 355 | WARNING: make sure you have installed Flask-Script. 356 | 357 | :param flask_script_manager: a flask.ext.script.Manager object. 358 | """ 359 | from .script import perm_manager 360 | flask_script_manager.add_command('perm', perm_manager) 361 | 362 | def register_context_processors(self, app): 363 | """Register default context processors to app. 364 | """ 365 | app.context_processor(self.default_context_processors) 366 | -------------------------------------------------------------------------------- /flask_perm/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_bcrypt import Bcrypt 5 | 6 | db = SQLAlchemy() 7 | bcrypt = Bcrypt() 8 | -------------------------------------------------------------------------------- /flask_perm/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | 5 | from .core import db 6 | 7 | class SuperAdmin(db.Model): 8 | 9 | __tablename__ = 'perm_super_admin' 10 | __table_args__ = ( 11 | db.UniqueConstraint('email', name='ux_super_admin_email'), 12 | ) 13 | 14 | id = db.Column(db.Integer, primary_key=True) 15 | email = db.Column(db.String(128), nullable=False) 16 | password = db.Column(db.String(128), nullable=False) 17 | created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) 18 | 19 | class Permission(db.Model): 20 | 21 | __tablename__ = 'perm_permission' 22 | __table_args__ = ( 23 | db.UniqueConstraint('code', name='ux_permission_code'), 24 | ) 25 | 26 | id = db.Column(db.Integer, primary_key=True) 27 | title = db.Column(db.String(64), default='', nullable=False) 28 | code = db.Column(db.String(64)) 29 | created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) 30 | 31 | def __str__(self): 32 | return ''.format(self) 33 | 34 | class UserGroup(db.Model): 35 | 36 | __tablename__ = 'perm_user_group' 37 | __table_args__ = ( 38 | db.UniqueConstraint('code', name='ux_user_group_code'), 39 | ) 40 | 41 | id = db.Column(db.Integer, primary_key=True) 42 | title = db.Column(db.String(64), default='', nullable=False) 43 | code = db.Column(db.String(64)) 44 | created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) 45 | 46 | def __str__(self): 47 | return ''.format(self) 48 | 49 | class UserGroupMember(db.Model): 50 | 51 | __tablename__ = 'perm_user_group_member' 52 | 53 | __table_args__ = ( 54 | db.UniqueConstraint('user_id', 'user_group_id', name='ux_user_in_user_group'), 55 | ) 56 | 57 | id = db.Column(db.Integer, primary_key=True) 58 | user_id = db.Column(db.Integer, nullable=False) 59 | user_group_id = db.Column(db.Integer, nullable=False) 60 | created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) 61 | 62 | def __str__(self): 63 | return ''.format(self) 64 | 65 | class UserPermission(db.Model): 66 | 67 | __tablename__ = 'perm_user_permission' 68 | __table_args__ = ( 69 | db.UniqueConstraint('user_id', 'permission_id', name='ux_user_permission'), 70 | ) 71 | 72 | id = db.Column(db.Integer, primary_key=True) 73 | user_id = db.Column(db.Integer, nullable=False) 74 | permission_id = db.Column(db.Integer, nullable=False) 75 | created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) 76 | 77 | def __str__(self): 78 | return ''.format(self) 79 | 80 | class UserGroupPermission(db.Model): 81 | 82 | __tablename__ = 'perm_user_group_permission' 83 | __table_args__ = ( 84 | db.UniqueConstraint('user_group_id', 'permission_id', name='ux_user_group_permission'), 85 | ) 86 | 87 | id = db.Column(db.Integer, primary_key=True) 88 | user_group_id = db.Column(db.Integer, nullable=False) 89 | permission_id = db.Column(db.Integer, nullable=False) 90 | created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) 91 | 92 | def __str__(self): 93 | return ''.format(self) 94 | -------------------------------------------------------------------------------- /flask_perm/script.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import current_app 4 | from flask.ext.script import Manager, prompt_pass, prompt_bool 5 | from flask_perm.services import SuperAdminService 6 | 7 | perm_manager = Manager(usage="Perform permission operations") 8 | 9 | 10 | @perm_manager.command 11 | @perm_manager.option('-e', '--email', help='Super admin email') 12 | def create_superadmin(email): 13 | password = prompt_pass('Please input password') 14 | confirm_password = prompt_pass('Please input password again') 15 | if password != confirm_password: 16 | print('Password mismatch! Please confirm that you type same passwords.') 17 | return 18 | current_app.extensions['perm'].create_super_admin(email, password) 19 | print('Success!') 20 | 21 | @perm_manager.command 22 | @perm_manager.option('-e', '--email', help='Super admin email') 23 | def delete_superadmin(email): 24 | super_admin = SuperAdminService.get_by_email(email) 25 | if not super_admin: 26 | print('Super admin not found!') 27 | return 28 | if prompt_bool('Do you really want to delete this account? [y/n]'): 29 | SuperAdminService.delete(super_admin.id) 30 | print('Success!') 31 | 32 | @perm_manager.command 33 | def list_superadmin(): 34 | superadmins = map(SuperAdminService.to_dict, SuperAdminService.list()) 35 | for superadmin in superadmins: 36 | print(superadmin['email']) 37 | -------------------------------------------------------------------------------- /flask_perm/services/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __all__ = [ 4 | 'UserGroupService', 'PermissionService', 'UserGroupMemberService', 5 | 'UserGroupPermissionService', 'VerificationService', 6 | 'SuperAdminService', 7 | ] 8 | 9 | from . import user_group as UserGroupService 10 | from . import permission as PermissionService 11 | from . import user_group_member as UserGroupMemberService 12 | from . import user_permission as UserPermissionService 13 | from . import user_group_permission as UserGroupPermissionService 14 | from . import verification as VerificationService 15 | from . import super_admin as SuperAdminService 16 | -------------------------------------------------------------------------------- /flask_perm/services/permission.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import func 3 | 4 | from ..core import db 5 | from ..models import Permission 6 | 7 | def create(title, code=None): 8 | permission = Permission( 9 | title=title, 10 | code=code 11 | ) 12 | db.session.add(permission) 13 | db.session.commit() 14 | return permission 15 | 16 | def delete(id): 17 | permission = Permission.query.get(id) 18 | if permission: 19 | db.session.delete(permission) 20 | db.session.commit() 21 | 22 | def rename(id, title): 23 | permission = Permission.query.get(id) 24 | permission.title = title 25 | db.session.add(permission) 26 | db.session.commit() 27 | return permission 28 | 29 | def rest(permission): 30 | return dict( 31 | id=permission.id, 32 | title=permission.title, 33 | code=permission.code 34 | ) 35 | 36 | def set_code(id, code): 37 | permission = Permission.query.get(id) 38 | permission.code = code 39 | db.session.add(permission) 40 | db.session.commit() 41 | return permission 42 | 43 | def get(id): 44 | return Permission.query.get(id) 45 | 46 | def get_permissions(): 47 | return Permission.query.all() 48 | 49 | def filter_permissions(filter_by, offset, limit, sort_field='created_at', sort_dir='desc'): 50 | query = Permission.query 51 | if filter_by: 52 | query = query.filter_by(**filter_by) 53 | field = getattr(Permission, sort_field) 54 | order_by = getattr(field, sort_dir.lower())() 55 | return query.order_by(order_by).offset(offset).limit(limit).all() 56 | 57 | def count_filter_permission(filter_by, offset, limit, sort_field='created_at', sort_dir='desc'): 58 | query = Permission.query 59 | if filter_by: 60 | query = query.filter_by(**filter_by) 61 | return query.value(func.count(Permission.id)) 62 | 63 | 64 | def get_permissions_by_ids(ids): 65 | return Permission.query.filter(Permission.id.in_(ids)).all() 66 | 67 | def get_by_code(code): 68 | return Permission.query.filter_by(code=code).first() 69 | -------------------------------------------------------------------------------- /flask_perm/services/super_admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..core import db, bcrypt 4 | from ..models import SuperAdmin 5 | 6 | def create(email, password): 7 | super_admin = SuperAdmin( 8 | email=email, 9 | password=bcrypt.generate_password_hash(password) 10 | ) 11 | db.session.add(super_admin) 12 | db.session.commit() 13 | return super_admin 14 | 15 | def to_dict(obj): 16 | return dict( 17 | id=obj.id, 18 | email=obj.email, 19 | ) 20 | 21 | def verify_password(email, password): 22 | super_admin = get_by_email(email) 23 | if not super_admin: 24 | return False 25 | if not bcrypt.check_password_hash(super_admin.password, password): 26 | return False 27 | return True 28 | 29 | def delete(id): 30 | super_admin = get(id) 31 | if super_admin: 32 | db.session.delete(super_admin) 33 | db.session.commit() 34 | 35 | def reset_password(id, password): 36 | super_admin = get(id) 37 | if super_admin: 38 | super_admin.password = bcrypt.generate_password_hash(password) 39 | db.session.add(super_admin) 40 | db.session.commit() 41 | return super_admin 42 | 43 | def get(id): 44 | return SuperAdmin.query.get(id) 45 | 46 | def get_by_email(email): 47 | return SuperAdmin.query.filter_by(email=email).first() 48 | 49 | def list(): 50 | return SuperAdmin.query.all() 51 | -------------------------------------------------------------------------------- /flask_perm/services/user_group.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import func 3 | 4 | from ..core import db 5 | from ..models import UserGroup 6 | 7 | def create(title, code=None): 8 | user_group = UserGroup( 9 | title=title, 10 | code=code, 11 | ) 12 | db.session.add(user_group) 13 | db.session.commit() 14 | return user_group 15 | 16 | def delete(id): 17 | user_group = UserGroup.query.get(id) 18 | if user_group: 19 | db.session.delete(user_group) 20 | db.session.commit() 21 | 22 | def rename(id, title): 23 | user_group = UserGroup.query.get(id) 24 | user_group.title = title 25 | db.session.add(user_group) 26 | db.session.commit() 27 | return user_group 28 | 29 | def update_code(id, code): 30 | user_group = UserGroup.query.get(id) 31 | user_group.code = code 32 | db.session.add(user_group) 33 | db.session.commit() 34 | return user_group 35 | 36 | def get_by_code(code): 37 | return UserGroup.query.filter_by(code=code).first() 38 | 39 | def get_user_groups_by_codes(codes): 40 | return UserGroup.query.filter(UserGroup.code.in_(codes)).all() 41 | 42 | def get_user_groups(): 43 | return UserGroup.query.all() 44 | 45 | def get_user_groups_by_ids(ids): 46 | return UserGroup.query.filter(UserGroup.id.in_(ids)).all() 47 | 48 | def rest(user_group): 49 | return dict( 50 | id=user_group.id, 51 | title=user_group.title, 52 | code=user_group.code, 53 | ) 54 | 55 | def get(id): 56 | return UserGroup.query.get(id) 57 | 58 | def filter_user_groups(filter_by, offset, limit, sort_field='created_at', sort_dir='desc'): 59 | query = UserGroup.query 60 | if filter_by: 61 | query = query.filter_by(**filter_by) 62 | field = getattr(UserGroup, sort_field) 63 | order_by = getattr(field, sort_dir.lower())() 64 | return query.order_by(order_by).offset(offset).limit(limit).all() 65 | 66 | def count_filter_user_group(filter_by, offset, limit): 67 | query = UserGroup.query 68 | if filter_by: 69 | query = query.filter_by(**filter_by) 70 | return query.value(func.count(UserGroup.id)) 71 | 72 | def get_all_user_group_ids(): 73 | return [group.id for group in UserGroup.query.all()] 74 | -------------------------------------------------------------------------------- /flask_perm/services/user_group_member.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import func 3 | from sqlalchemy.exc import IntegrityError 4 | from ..core import db 5 | from ..models import UserGroupMember 6 | 7 | def create(user_id, user_group_id): 8 | member = UserGroupMember( 9 | user_id=user_id, 10 | user_group_id=user_group_id, 11 | ) 12 | db.session.add(member) 13 | try: 14 | db.session.commit() 15 | except: 16 | db.session.rollback() 17 | member = UserGroupMember.query.filter_by( 18 | user_id=user_id, 19 | user_group_id=user_group_id, 20 | ).first() 21 | return member 22 | 23 | def get(id): 24 | return UserGroupMember.query.get(id) 25 | 26 | def delete(id): 27 | member = UserGroupMember.query.get(id) 28 | if member: 29 | db.session.delete(member) 30 | db.session.commit() 31 | 32 | def get_users_by_group(user_group_id): 33 | rows = UserGroupMember.query.filter_by( 34 | user_group_id=user_group_id 35 | ).with_entities( 36 | UserGroupMember.user_id 37 | ).all() 38 | return [row.user_id for row in rows] 39 | 40 | def get_user_groups_by_user(user_id): 41 | rows = UserGroupMember.query.filter_by( 42 | user_id=user_id 43 | ).with_entities( 44 | UserGroupMember.user_group_id 45 | ).all() 46 | return [row.user_group_id for row in rows] 47 | 48 | def is_user_in_groups(user_id, user_group_ids): 49 | return bool(UserGroupMember.query.filter( 50 | UserGroupMember.user_id == user_id, 51 | UserGroupMember.user_group_id.in_(user_group_ids) 52 | ).first()) 53 | 54 | def filter_user_group_members(filter_by, offset, limit, sort_field='created_at', sort_dir='desc'): 55 | query = UserGroupMember.query 56 | if filter_by: 57 | query = query.filter_by(**filter_by) 58 | field = getattr(UserGroupMember, sort_field) 59 | order_by = getattr(field, sort_dir.lower())() 60 | return query.order_by(order_by).offset(offset).limit(limit).all() 61 | 62 | def count_filter_user_group_members(filter_by, offset, limit): 63 | query = UserGroupMember.query 64 | if filter_by: 65 | query = query.filter_by(**filter_by) 66 | return query.value(func.count(UserGroupMember.id)) 67 | 68 | def rest(obj): 69 | return dict( 70 | id=obj.id, 71 | user_id=obj.user_id, 72 | user_group_id=obj.user_group_id, 73 | ) 74 | -------------------------------------------------------------------------------- /flask_perm/services/user_group_permission.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import func 3 | from sqlalchemy.exc import IntegrityError 4 | 5 | from ..core import db 6 | from ..models import UserGroupPermission 7 | 8 | def create(user_group_id, permission_id): 9 | user_group_permission = UserGroupPermission( 10 | user_group_id=user_group_id, 11 | permission_id=permission_id, 12 | ) 13 | db.session.add(user_group_permission) 14 | try: 15 | db.session.commit() 16 | except IntegrityError: 17 | db.session.rollback() 18 | user_group_permission = UserGroupPermission.query.filter_by( 19 | user_group_id=user_group_id, 20 | permission_id=permission_id, 21 | ).first() 22 | return user_group_permission 23 | 24 | def get(id): 25 | return UserGroupPermission.query.get(id) 26 | 27 | def delete(id): 28 | user_group_permission = UserGroupPermission.query.get(id) 29 | if user_group_permission: 30 | db.session.delete(user_group_permission) 31 | db.session.commit() 32 | 33 | def delete_by_permission(permission_id): 34 | user_group_permissions = UserGroupPermission.query.filter_by( 35 | permission_id=permission_id 36 | ).all() 37 | for user_group_permission in user_group_permissions: 38 | db.session.delete(user_group_permission) 39 | db.session.commit() 40 | 41 | def delete_by_user_group(user_group_id): 42 | user_group_permissions = UserGroupPermission.query.filter_by( 43 | user_group_id=user_group_id, 44 | ).all() 45 | for user_group_permission in user_group_permissions: 46 | db.session.delete(user_group_permission) 47 | db.session.commit() 48 | 49 | def get_user_groups_by_permission(permission_id): 50 | rows = UserGroupPermission.query.filter_by( 51 | permission_id=permission_id 52 | ).with_entities( 53 | UserGroupPermission.user_group_id 54 | ).all() 55 | return [row.user_group_id for row in rows] 56 | 57 | def get_permissions_by_user_group(user_group_id): 58 | rows = UserGroupPermission.query.filter_by( 59 | user_group_id=user_group_id 60 | ).with_entities( 61 | UserGroupPermission.permission_id 62 | ).all() 63 | return [row.permission_id for row in rows] 64 | 65 | def filter_user_group_permissions(filter_by, offset, limit, sort_field='created_at', sort_dir='desc'): 66 | query = UserGroupPermission.query 67 | if filter_by: 68 | query = query.filter_by(**filter_by) 69 | field = getattr(UserGroupPermission, sort_field) 70 | order_by = getattr(field, sort_dir.lower())() 71 | return query.order_by(order_by).offset(offset).limit(limit).all() 72 | 73 | def count_filter_user_group_permissions(filter_by, offset, limit): 74 | query = UserGroupPermission.query 75 | if filter_by: 76 | query = query.filter_by(**filter_by) 77 | return query.value(func.count(UserGroupPermission.id)) 78 | 79 | def rest(user_permission): 80 | return dict( 81 | id=user_permission.id, 82 | user_group_id=user_permission.user_group_id, 83 | permission_id=user_permission.permission_id, 84 | ) 85 | -------------------------------------------------------------------------------- /flask_perm/services/user_permission.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import func 3 | from sqlalchemy.exc import IntegrityError 4 | 5 | from ..core import db 6 | from ..models import UserPermission 7 | 8 | def create(user_id, permission_id): 9 | user_permission = UserPermission( 10 | user_id=user_id, 11 | permission_id=permission_id, 12 | ) 13 | db.session.add(user_permission) 14 | try: 15 | db.session.commit() 16 | except IntegrityError: 17 | db.session.rollback() 18 | user_permission = UserPermission.query.filter_by( 19 | user_id=user_id, 20 | permission_id=permission_id, 21 | ).first() 22 | return user_permission 23 | 24 | def delete(user_permission_id): 25 | user_permission = UserPermission.query.get(user_permission_id) 26 | if user_permission: 27 | db.session.delete(user_permission) 28 | db.session.commit() 29 | 30 | def get(id): 31 | return UserPermission.query.get(id) 32 | 33 | def delete_by_user(user_id): 34 | user_permissions = UserPermission.query.filter_by( 35 | user_id=user_id 36 | ).all() 37 | for user_permission in user_permissions: 38 | db.session.delete(user_permission) 39 | db.session.commit() 40 | 41 | def delete_by_permission(permission_id): 42 | user_permissions = UserPermission.query.filter_by( 43 | permission_id=permission_id, 44 | ).all() 45 | for user_permission in user_permissions: 46 | db.session.delete(user_permission) 47 | db.session.commit() 48 | 49 | def get_users_by_permission(permission_id): 50 | rows = UserPermission.query.filter_by( 51 | permission_id=permission_id 52 | ).with_entities( 53 | UserPermission.user_id 54 | ).all() 55 | return [row.user_id for row in rows] 56 | 57 | def get_permissions_by_user(user_id): 58 | rows = UserPermission.query.filter_by( 59 | user_id=user_id 60 | ).with_entities( 61 | UserPermission.permission_id 62 | ).all() 63 | return [row.permission_id for row in rows] 64 | 65 | def filter_user_permissions(filter_by, offset, limit, sort_field='created_at', sort_dir='desc'): 66 | query = UserPermission.query 67 | if filter_by: 68 | query = query.filter_by(**filter_by) 69 | field = getattr(UserPermission, sort_field) 70 | order_by = getattr(field, sort_dir.lower())() 71 | return query.order_by(order_by).offset(offset).limit(limit).all() 72 | 73 | def count_filter_user_permission(filter_by, offset, limit): 74 | query = UserPermission.query 75 | if filter_by: 76 | query = query.filter_by(**filter_by) 77 | return query.value(func.count(UserPermission.id)) 78 | 79 | def rest(user_permission): 80 | return dict( 81 | id=user_permission.id, 82 | user_id=user_permission.user_id, 83 | permission_id=user_permission.permission_id, 84 | ) 85 | -------------------------------------------------------------------------------- /flask_perm/services/verification.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..core import db 4 | from .user_permission import ( 5 | get_permissions_by_user, 6 | delete_by_user as delete_user_permissions_by_user, 7 | ) 8 | from .user_group_permission import get_permissions_by_user_group 9 | from .user_group_member import get_user_groups_by_user 10 | 11 | def get_user_group_permissions_by_user(user_id): 12 | user_permission_ids = set() 13 | user_group_ids = get_user_groups_by_user(user_id) 14 | for user_group_id in user_group_ids: 15 | user_group_permission_ids = set(get_permissions_by_user_group(user_group_id)) 16 | user_permission_ids.update(user_group_permission_ids) 17 | return user_permission_ids 18 | 19 | def get_user_permissions(user_id): 20 | user_permission_ids = set(get_permissions_by_user(user_id)) 21 | user_group_permission_ids = get_user_group_permissions_by_user(user_id) 22 | return user_permission_ids | user_group_permission_ids 23 | 24 | def has_permission(user_id, permission_id): 25 | return permission_id in get_user_permissions(user_id) 26 | -------------------------------------------------------------------------------- /flask_perm/static/admin.js: -------------------------------------------------------------------------------- 1 | // declare a new module called 'myApp', and make it require the `ng-admin` module as a dependency 2 | var PermAdmin = angular.module('PermAdmin', ['ng-admin']); 3 | // declare a function to run when the module bootstraps (during the 'config' phase) 4 | PermAdmin.config(['NgAdminConfigurationProvider', function (nga) { 5 | // create an admin application 6 | var applicationName = 'Permission Management Admininistration'; 7 | var admin = nga.application( 8 | applicationName, 9 | window.g.debug 10 | ).baseApiUrl(window.g.baseApiUrl); 11 | // more configuration here later 12 | var permission = nga.entity('permissions'); 13 | var all_permission = nga.entity('permissions?limit=1000'); 14 | var user = nga.entity('users').readOnly(); 15 | var userGroup = nga.entity('user_groups'); 16 | var userPermission = nga.entity('user_permissions'); 17 | var userGroupMember = nga.entity('user_group_members'); 18 | var userGroupPermission = nga.entity('user_group_permissions'); 19 | var fields = { 20 | title: nga.field('title').label('Title'), 21 | code: nga.field('code').label('Code'), 22 | nickname: nga.field('nickname').label('Nickname'), 23 | user: nga.field('user_id', 'reference') 24 | .targetEntity(user) 25 | .targetField(nga.field('nickname')) 26 | .label('User'), 27 | userGroup: nga.field('user_group_id', 'reference') 28 | .targetEntity(userGroup) 29 | .targetField(nga.field('title')) 30 | .label('User Group'), 31 | permission: nga.field('permission_id', 'reference') 32 | .targetEntity(permission) 33 | .targetField(nga.field('title')) 34 | .label('Permission'), 35 | all_permission: nga.field('permission_id', 'reference') 36 | .targetEntity(all_permission) 37 | .targetField(nga.field('title')) 38 | .label('Permission') 39 | }; 40 | 41 | admin.addEntity(permission); 42 | admin.addEntity(userGroup); 43 | admin.addEntity(user); 44 | admin.addEntity(userPermission); 45 | admin.addEntity(userGroupPermission); 46 | admin.addEntity(userGroupMember); 47 | 48 | permission.listView().fields([ 49 | fields.title.isDetailLink(true), 50 | fields.code, 51 | ]).filters([ 52 | fields.title, 53 | fields.code, 54 | ]); 55 | 56 | permission.creationView().fields([ 57 | fields.title.validation({ required: true}), 58 | fields.code.validation({ required: true}), 59 | ]); 60 | 61 | permission.editionView().fields(permission.creationView().fields()); 62 | 63 | userGroup.listView().fields([ 64 | fields.title.isDetailLink(true), 65 | fields.code, 66 | ]); 67 | 68 | userGroup.creationView().fields([ 69 | fields.title.validation({ required: true }), 70 | fields.code.validation({ required: true }), 71 | ]); 72 | 73 | userGroup.editionView().fields(userGroup.creationView().fields()); 74 | 75 | user.listView().fields([ 76 | fields.nickname, 77 | ]); 78 | 79 | userPermission.listView().fields([ 80 | fields.user, 81 | fields.permission, 82 | ]).filters([ 83 | fields.user, 84 | fields.permission, 85 | ]); 86 | 87 | userPermission.creationView().fields([ 88 | fields.user, 89 | fields.all_permission, 90 | ]); 91 | userPermission.showView().disable(); 92 | 93 | 94 | userGroupPermission.listView().fields([ 95 | fields.userGroup, 96 | fields.permission, 97 | ]).filters([ 98 | fields.userGroup, 99 | fields.permission, 100 | ]); 101 | 102 | userGroupPermission.creationView().fields([ 103 | fields.userGroup, 104 | fields.all_permission, 105 | ]); 106 | 107 | userGroupPermission.showView().disable(); 108 | 109 | userGroupMember.listView().fields([ 110 | fields.userGroup, 111 | fields.user, 112 | ]).filters([ 113 | fields.userGroup, 114 | fields.user, 115 | ]); 116 | 117 | userGroupMember.creationView().fields([ 118 | fields.userGroup, 119 | fields.user, 120 | ]); 121 | 122 | 123 | // ... 124 | // attach the admin application to the DOM and execute it 125 | var makeIcon = function(cls) { 126 | return '' 127 | }; 128 | 129 | var menuIcons = { 130 | user: makeIcon('glyphicon-user'), 131 | userGroup: makeIcon('glyphicon-user'), 132 | permission: makeIcon('glyphicon-minus-sign'), 133 | userPermission: makeIcon('glyphicon-pencil'), 134 | userGroupPermission: makeIcon('glyphicon-pencil'), 135 | userGroupMember: makeIcon('glyphicon-pencil'), 136 | }; 137 | admin.menu( 138 | nga.menu() 139 | .addChild(nga.menu(user).icon(menuIcons.user)) 140 | .addChild(nga.menu(userGroup).icon(menuIcons.userGroup)) 141 | .addChild(nga.menu(permission).icon(menuIcons.permission)) 142 | .addChild(nga.menu(userPermission).icon(menuIcons.userPermission)) 143 | .addChild(nga.menu(userGroupPermission).icon(menuIcons.userGroupPermission)) 144 | .addChild(nga.menu(userGroupMember).icon(menuIcons.userGroupMember)) 145 | ); 146 | 147 | var customHeaderTemplate = '' + 152 | ''; 157 | admin.header(customHeaderTemplate); 158 | 159 | nga.configure(admin); 160 | }]); 161 | 162 | PermAdmin.config(['RestangularProvider', function(RestangularProvider) { 163 | RestangularProvider.addFullRequestInterceptor( 164 | function(element, operation, what, url, headers, params) { 165 | String.prototype.endWith=function(endStr){ 166 | var d=this.length-endStr.length; 167 | return (d>=0&&this.lastIndexOf(endStr)==d) 168 | } 169 | if (operation === "getList") { 170 | if (params._page && !url.endWith("?limit=1000")) { 171 | params.offset = (params._page - 1) * params._perPage; 172 | params.limit = params._perPage; 173 | } 174 | } 175 | return {params: params}; 176 | } 177 | ); 178 | 179 | RestangularProvider.addResponseInterceptor( 180 | function(data, operation, what, url, response, deferred) { 181 | var extractedData; 182 | extractedData = data.data; 183 | return extractedData; 184 | } 185 | ); 186 | 187 | }]); 188 | -------------------------------------------------------------------------------- /flask_perm/templates/perm-admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flask-Perm Admin 6 | 7 | 8 | 9 |
10 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /flask_perm/templates/perm-admin/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login 7 | 8 | 9 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /migrate/init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `perm_permission` ( 2 | `id` int(11) NOT NULL AUTO_INCREMENT, 3 | `title` varchar(64) COLLATE utf8_unicode_ci NOT NULL, 4 | `code` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL, 5 | `created_at` datetime NOT NULL, 6 | PRIMARY KEY (`id`), 7 | UNIQUE KEY `ux_permission_code` (`code`) 8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 9 | CREATE TABLE `perm_super_admin` ( 10 | `id` int(11) NOT NULL AUTO_INCREMENT, 11 | `email` varchar(128) NOT NULL, 12 | `password` varchar(128) NOT NULL, 13 | `created_at` datetime NOT NULL, 14 | PRIMARY KEY (`id`), 15 | UNIQUE KEY `ux_super_admin_email` (`email`) 16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 17 | CREATE TABLE `perm_user_group` ( 18 | `id` int(11) NOT NULL AUTO_INCREMENT, 19 | `title` varchar(64) COLLATE utf8_unicode_ci NOT NULL, 20 | `code` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL, 21 | `created_at` datetime NOT NULL, 22 | PRIMARY KEY (`id`), 23 | UNIQUE KEY `ux_user_group_code` (`code`) 24 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 25 | CREATE TABLE `perm_user_group_member` ( 26 | `id` int(11) NOT NULL AUTO_INCREMENT, 27 | `user_id` int(11) NOT NULL, 28 | `user_group_id` int(11) NOT NULL, 29 | `created_at` datetime NOT NULL, 30 | PRIMARY KEY (`id`), 31 | UNIQUE KEY `ux_user_in_user_group` (`user_id`,`user_group_id`) 32 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 33 | CREATE TABLE `perm_user_group_permission` ( 34 | `id` int(11) NOT NULL AUTO_INCREMENT, 35 | `user_group_id` int(11) NOT NULL, 36 | `permission_id` int(11) NOT NULL, 37 | `created_at` datetime NOT NULL, 38 | PRIMARY KEY (`id`), 39 | UNIQUE KEY `ux_user_permission` (`user_group_id`,`permission_id`) 40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 41 | CREATE TABLE `perm_user_permission` ( 42 | `id` int(11) NOT NULL AUTO_INCREMENT, 43 | `user_id` int(11) NOT NULL, 44 | `permission_id` int(11) NOT NULL, 45 | `created_at` datetime NOT NULL, 46 | PRIMARY KEY (`id`), 47 | UNIQUE KEY `ux_user_permission` (`user_id`,`permission_id`) 48 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.7 2 | Babel==2.2.0 3 | bcrypt==2.0.0 4 | bumpversion==0.5.3 5 | cffi==1.5.2 6 | docutils==0.12 7 | Flask==1.0 8 | Flask-Bcrypt==0.7.1 9 | Flask-Login==0.3.2 10 | Flask-Script==2.0.3 11 | Flask-SQLAlchemy==2.1 12 | itsdangerous==0.24 13 | Jinja2==2.11.3 14 | MarkupSafe==0.23 15 | pkginfo==1.2.1 16 | py==1.10.0 17 | pycparser==2.14 18 | Pygments==2.7.4 19 | pytest==2.8.5 20 | pytz==2015.7 21 | requests==2.20.0 22 | requests-toolbelt==0.6.0 23 | six==1.10.0 24 | snowballstemmer==1.2.1 25 | Sphinx==1.3.5 26 | Sphinx-PyPI-upload==0.2.1 27 | sphinx-rtd-theme==0.1.9 28 | SQLAlchemy==1.0.10 29 | twine==1.6.5 30 | Werkzeug==0.15.3 31 | wheel==0.24.0 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.8 3 | commit = True 4 | tag = True 5 | 6 | [build_sphinx] 7 | source-dir = docs/ 8 | build-dir = docs/_build 9 | all_files = 1 10 | 11 | [upload_sphinx] 12 | upload-dir = docs/_build/html 13 | 14 | [metadata] 15 | description-file = README.md 16 | 17 | [bumpversion:file:setup.py] 18 | 19 | [bumpversion:file:flask_perm/__init__.py] 20 | 21 | [bumpversion:file:docs/conf.py] 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | A permission flask extension inspired by Django. 3 | """ 4 | 5 | from setuptools import setup 6 | 7 | 8 | setup( 9 | name='Flask-Perm', 10 | version='0.2.8', 11 | url='https://github.com/soasme/flask-perm', 12 | license='MIT', 13 | author='Ju Lin', 14 | author_email='soasme@gmail.com', 15 | description='Flask Permission Management Extension', 16 | long_description=__doc__, 17 | packages=['flask_perm'], 18 | zip_safe=False, 19 | include_package_data=True, 20 | platforms='any', 21 | install_requires=[ 22 | 'Flask', 23 | 'Flask-SQLAlchemy', 24 | 'Flask-Bcrypt', 25 | ], 26 | classifiers=[ 27 | 'Framework :: Flask', 28 | 'Natural Language :: English', 29 | 'Environment :: Web Environment', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 35 | 'Topic :: Software Development :: Libraries :: Python Modules' 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soasme/flask-perm/2167cece85c5e4c2a734eedf07c9a551497f859b/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import namedtuple 4 | 5 | from pytest import fixture 6 | from flask import Flask, g 7 | from flask_sqlalchemy import SQLAlchemy 8 | from flask_perm import Perm 9 | from flask_perm.core import db 10 | 11 | 12 | @fixture 13 | def app(request): 14 | flask_app = Flask(__name__) 15 | flask_app.config['TESTING'] = True 16 | flask_app.config['DEBUG'] = True 17 | flask_app.config['SERVER_NAME'] = 'localhost' 18 | ctx = flask_app.app_context() 19 | ctx.push() 20 | request.addfinalizer(ctx.pop) 21 | return flask_app 22 | 23 | class User(namedtuple('User', 'id nickname')): 24 | pass 25 | 26 | @fixture 27 | def perm(app, request): 28 | def user_loader(id): 29 | return User(id=id, nickname='User%d'%id) 30 | 31 | def current_user_loader(): 32 | return user_loader(1) 33 | 34 | def users_loader(**kwargs): 35 | return [user_loader(id) for id in range(20)] 36 | 37 | app.config['PERM_URL_PREFIX'] = '/perm' 38 | app.config['PERM_ADMIN_USERNAME'] = 'admin' 39 | app.config['PERM_ADMIN_PASSWORD'] = 'test' 40 | 41 | perm = Perm() 42 | perm.app = app 43 | perm.init_app(app) 44 | perm.user_loader(user_loader) 45 | perm.current_user_loader(current_user_loader) 46 | perm.users_loader(users_loader) 47 | 48 | db.create_all() 49 | 50 | return perm 51 | -------------------------------------------------------------------------------- /tests/setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soasme/flask-perm/2167cece85c5e4c2a734eedf07c9a551497f859b/tests/setup.cfg -------------------------------------------------------------------------------- /tests/test_blueprint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import base64 5 | from functools import partial 6 | from pytest import fixture 7 | from flask import url_for 8 | from flask_perm.services import SuperAdminService 9 | 10 | class Client(object): 11 | 12 | def __init__(self, app, current_user=None): 13 | self.app = app 14 | self.current_user = current_user 15 | 16 | @property 17 | def client(self): 18 | return self.app.test_client() 19 | 20 | def request(self, method, url, **kwargs): 21 | headers = kwargs.get('headers', {}) 22 | code = '%s:%s' % ( 23 | 'admin@example.org', 24 | 'test' 25 | ) 26 | headers['Authorization'] = 'Basic ' + base64.b64encode(code) 27 | kwargs['headers'] = headers 28 | return self.client.open(url, method=method, **kwargs) 29 | 30 | def __getattr__(self, method): 31 | return partial(self.request, method) 32 | 33 | @fixture 34 | def super_admin(app, perm): 35 | return SuperAdminService.create('admin@example.org', 'test') 36 | 37 | @fixture 38 | def client(app, perm, super_admin): 39 | return Client(app, super_admin) 40 | 41 | 42 | @fixture 43 | def permission(request, client): 44 | code = '%s.%s' % (request.module.__name__, request.function.__name__) 45 | resp = client.post( 46 | url_for('flask_perm_api.add_permission'), 47 | data=json.dumps(dict(title='Test Permission', code=code)), 48 | content_type='application/json', 49 | ) 50 | return json.loads(resp.data)['data'] 51 | 52 | @fixture 53 | def user_group(request, client): 54 | code = 'code.%s.%s' % (request.module.__name__, request.function.__name__) 55 | resp = client.post( 56 | url_for('flask_perm_api.add_user_group'), 57 | data=json.dumps(dict(title='Test UserGroup', code=code)), 58 | content_type='application/json', 59 | ) 60 | return json.loads(resp.data)['data'] 61 | 62 | def test_add_permission(permission): 63 | assert permission['id'] 64 | assert permission['title'] == 'Test Permission' 65 | assert permission['code'] == 'tests.test_blueprint.test_add_permission' 66 | 67 | def test_get_permissions(client, permission): 68 | resp = client.get(url_for('flask_perm_api.get_permissions')) 69 | assert resp.status_code == 200 70 | assert permission in json.loads(resp.data)['data'] 71 | 72 | def test_filter_permission_by_id0(client, permission): 73 | resp = client.get(url_for('flask_perm_api.get_permissions'), query_string={ 74 | '_filters': '{"id": 0}', 75 | }) 76 | assert resp.status_code == 200 77 | assert not json.loads(resp.data)['data'] 78 | 79 | def test_filter_permission_by_permission_id(client, permission): 80 | resp = client.get(url_for('flask_perm_api.get_permissions'), query_string={ 81 | '_filters': '{"id": %s}' % permission['id'], 82 | }) 83 | assert resp.status_code == 200 84 | assert permission in json.loads(resp.data)['data'] 85 | 86 | def test_get_permission(client, permission): 87 | resp = client.get( 88 | url_for( 89 | 'flask_perm_api.get_permission', 90 | permission_id=permission['id'] 91 | ) 92 | ) 93 | assert resp.status_code == 200 94 | assert permission == json.loads(resp.data)['data'] 95 | 96 | def test_update_permission(client, permission): 97 | resp = client.put( 98 | url_for( 99 | 'flask_perm_api.update_permission', 100 | permission_id=permission['id'], 101 | ), 102 | data=json.dumps(dict(title='Test Permission!', code='test_blueprint.test_update_permission!')), 103 | content_type='application/json', 104 | ) 105 | assert resp.status_code == 200 106 | data = json.loads(resp.data) 107 | assert data['data']['id'] 108 | assert data['data']['title'] == 'Test Permission!' 109 | assert data['data']['code'] == 'test_blueprint.test_update_permission!' 110 | 111 | 112 | 113 | def test_delete_permission(client, permission): 114 | resp = client.delete( 115 | url_for( 116 | 'flask_perm_api.delete_permission', 117 | permission_id=permission['id'] 118 | ) 119 | ) 120 | assert resp.status_code == 200 121 | resp = client.get( 122 | url_for( 123 | 'flask_perm_api.get_permission', 124 | permission_id=permission['id'] 125 | ), 126 | ) 127 | assert resp.status_code == 404 128 | 129 | def add_user_permission(client, user_id, permission_id): 130 | return client.post( 131 | url_for( 132 | 'flask_perm_api.add_user_permission', 133 | ), 134 | data=json.dumps(dict( 135 | user_id=user_id, 136 | permission_id=permission_id 137 | )), 138 | content_type='application/json', 139 | ) 140 | 141 | def add_user_group_member(client, user_id, user_group_id): 142 | return client.post( 143 | url_for( 144 | 'flask_perm_api.add_user_group_member', 145 | ), 146 | data=json.dumps(dict( 147 | user_id=user_id, 148 | user_group_id=user_group_id, 149 | )), 150 | content_type='application/json', 151 | ) 152 | 153 | def add_user_group_permission(client, user_group_id, permission_id): 154 | return client.post( 155 | url_for( 156 | 'flask_perm_api.add_user_group_permission', 157 | ), 158 | data=json.dumps(dict( 159 | user_group_id=user_group_id, 160 | permission_id=permission_id 161 | )), 162 | content_type='application/json', 163 | ) 164 | 165 | def test_add_user_permission(client, permission, perm): 166 | resp = add_user_permission(client, 1, permission['id']) 167 | assert resp.status_code == 200 168 | assert perm.has_permission(1, 'tests.test_blueprint.test_add_user_permission') 169 | 170 | def test_revoke_user_permission(client, perm, permission): 171 | resp = add_user_permission(client, 1, permission['id']) 172 | id = json.loads(resp.data)['data']['id'] 173 | resp = client.delete( 174 | url_for( 175 | 'flask_perm_api.revoke_user_permission', 176 | user_permission_id=id, 177 | ) 178 | ) 179 | assert resp.status_code == 200 180 | assert not perm.has_permission(1, 'tests.test_blueprint.test_revoke_user_permission') 181 | 182 | def test_add_user_group_permissions(client, permission, user_group): 183 | resp = add_user_group_permission(client, user_group['id'], permission['id']) 184 | assert resp.status_code == 200 185 | 186 | def test_revoke_user_group_permissions(client, permission, user_group): 187 | resp = add_user_group_permission(client, user_group['id'], permission['id']) 188 | id = json.loads(resp.data)['data']['id'] 189 | resp = client.delete( 190 | url_for( 191 | 'flask_perm_api.revoke_user_group_permission', 192 | user_group_permission_id=id, 193 | ) 194 | ) 195 | assert resp.status_code == 200 196 | 197 | def test_get_user_permissions_by_user_id(client, permission): 198 | resp = add_user_permission(client, 1, permission['id']) 199 | resp = client.get( 200 | url_for( 201 | 'flask_perm_api.get_user_permissions', 202 | ), 203 | query_string={'_filters': '{"user_id":1}'} 204 | ) 205 | assert resp.status_code ==200 206 | assert json.loads(resp.data)['data'] 207 | 208 | def test_get_user_permissions_by_permission_id(client, permission): 209 | resp = add_user_permission(client, 1, permission['id']) 210 | resp = client.get( 211 | url_for( 212 | 'flask_perm_api.get_user_permissions', 213 | ), 214 | query_string={'_filters': '{"permission_id":%s}' % permission['id']} 215 | ) 216 | assert resp.status_code ==200 217 | assert json.loads(resp.data)['data'] 218 | 219 | def test_get_user_group_permissions_by_user_id(client, permission): 220 | resp = add_user_group_permission(client, 1, permission['id']) 221 | resp = client.get( 222 | url_for( 223 | 'flask_perm_api.get_user_group_permissions', 224 | ), 225 | query_string={'_filters': '{"user_group_id":1}'} 226 | ) 227 | assert resp.status_code ==200 228 | assert json.loads(resp.data)['data'] 229 | 230 | def test_get_user_group_permissions_by_permission_id(client, permission): 231 | resp = add_user_group_permission(client, 1, permission['id']) 232 | resp = client.get( 233 | url_for( 234 | 'flask_perm_api.get_user_group_permissions', 235 | ), 236 | query_string={'_filters': '{"permission_id":%s}' % permission['id']} 237 | ) 238 | assert resp.status_code ==200 239 | assert json.loads(resp.data)['data'] 240 | 241 | def test_add_user_group(client, user_group): 242 | assert user_group['id'] 243 | 244 | def test_get_user_groups(client, user_group): 245 | resp = client.get( 246 | url_for( 247 | 'flask_perm_api.get_user_groups' 248 | ) 249 | ) 250 | assert resp.status_code == 200 251 | assert json.loads(resp.data)['data'] 252 | 253 | def test_update_user_group(client, user_group): 254 | resp = client.put( 255 | url_for( 256 | 'flask_perm_api.update_user_group', 257 | user_group_id=user_group['id'], 258 | ), 259 | data=json.dumps(dict(title='updated')), 260 | content_type='application/json' 261 | ) 262 | assert resp.status_code == 200 263 | assert json.loads(resp.data)['data']['title'] == 'updated' 264 | 265 | def test_delete_user_group(client, user_group): 266 | resp = client.delete( 267 | url_for( 268 | 'flask_perm_api.delete_user_group', 269 | user_group_id=user_group['id'], 270 | ), 271 | ) 272 | assert resp.status_code == 200 273 | 274 | def test_add_user_to_user_group(client, user_group): 275 | resp = add_user_group_member(client, 1, user_group['id']) 276 | assert resp.status_code == 200 277 | 278 | def test_delete_user_from_user_group(client, user_group): 279 | resp = add_user_group_member(client, 1, user_group['id']) 280 | id = json.loads(resp.data)['data']['id'] 281 | resp = client.delete( 282 | url_for( 283 | 'flask_perm_api.delete_user_from_user_group', 284 | user_group_member_id=id 285 | ) 286 | ) 287 | assert resp.status_code == 200 288 | 289 | def test_get_user_group_members(client, user_group): 290 | add_user_group_member(client, 1, user_group['id']) 291 | resp = client.get( 292 | url_for( 293 | 'flask_perm_api.get_user_group_members', 294 | ), 295 | query_string={ 296 | '_filters': '{"user_group_id":%s}' % user_group['id'], 297 | } 298 | 299 | ) 300 | assert resp.status_code == 200 301 | assert json.loads(resp.data)['data'] 302 | 303 | def test_get_users(client): 304 | resp = client.get(url_for('flask_perm_api.get_users')) 305 | assert resp.status_code == 200 306 | assert isinstance(json.loads(resp.data)['data'], list) 307 | 308 | def test_get_user(client): 309 | resp = client.get(url_for('flask_perm_api.get_user', user_id=1)) 310 | assert resp.status_code == 200 311 | assert json.loads(resp.data)['data']['id'] == 1 312 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | from flask import Flask 5 | from flask_sqlalchemy import SQLAlchemy 6 | from flask_perm.core import db 7 | -------------------------------------------------------------------------------- /tests/test_perm_app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import g 4 | from pytest import raises 5 | 6 | def test_get_require_permission_not_passed(perm): 7 | g.user = {'id': 1, 'is_allowed': False} 8 | f = lambda: True 9 | with raises(perm.Denied): 10 | perm.require_permission('code')(f)() 11 | 12 | def test_get_require_permission_passed(perm): 13 | from flask_perm.services import PermissionService, UserPermissionService 14 | permission = PermissionService.create( 15 | 'Test get_require_permission passed', 16 | 'test.get_require_permission.passed' 17 | ) 18 | UserPermissionService.create(user_id=1, permission_id=permission.id) 19 | 20 | g.user = {'id': 1, 'is_allowed': False} 21 | assert perm.require_permission('test.get_require_permission.passed')(lambda: True)() 22 | assert perm.require_permission_in_template('test.get_require_permission.passed') 23 | 24 | def test_get_permissions(perm): 25 | from flask_perm.services import PermissionService, UserPermissionService 26 | permission = PermissionService.create( 27 | 'Test get_permissions passed', 28 | 'test_perm_app.test_get_permissions' 29 | ) 30 | UserPermissionService.create(user_id=1, permission_id=permission.id) 31 | 32 | assert 'test_perm_app.test_get_permissions' in map( 33 | lambda p: p['code'], perm.get_user_permissions(1)) 34 | assert perm.has_permission(1, 'test_perm_app.test_get_permissions') 35 | 36 | def test_require_group_passed(perm): 37 | from flask_perm.services import UserGroupService, UserGroupMemberService 38 | user_group = UserGroupService.create( 39 | 'Test require_group passed', 40 | 'test.require_group_passed', 41 | ) 42 | member = UserGroupMemberService.create(user_id=1, user_group_id=user_group.id) 43 | assert perm.require_group('test.require_group_passed')(lambda: True)() 44 | assert perm.require_group_in_template('test.require_group_passed') 45 | assert perm.require_group('*')(lambda: True)() 46 | 47 | def test_require_group_failed(perm): 48 | from flask_perm.services import UserGroupService, UserGroupMemberService 49 | user_group = UserGroupService.create( 50 | 'Test require_group failed', 51 | 'test.require_group_failed', 52 | ) 53 | member = UserGroupMemberService.create(user_id=2, user_group_id=user_group.id) 54 | with raises(perm.Denied): 55 | assert perm.require_group('test.require_group_passed')(lambda: True)() 56 | assert not perm.require_group_in_template('test.require_group_passed') 57 | with raises(perm.Denied): 58 | assert perm.require_group('*')(lambda: True)() 59 | --------------------------------------------------------------------------------