├── .circleci └── config.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── README-chinese.md └── screenshot │ ├── grbac.png │ └── wallstreetcn.png ├── go.mod ├── go.sum ├── grbac.go ├── grbac_test.go ├── pkg ├── loader │ ├── advanced_rule.go │ ├── json.go │ ├── rule.go │ └── yaml.go ├── meta │ ├── meta.go │ ├── permission.go │ ├── permission_state.go │ ├── permission_state_test.go │ ├── permission_test.go │ ├── query.go │ ├── query_test.go │ ├── resource.go │ ├── resource_test.go │ ├── rule.go │ └── rule_test.go ├── path │ ├── doublestar │ │ ├── doublestar.go │ │ └── doublestar_test.go │ ├── path.go │ └── path_test.go ├── tree │ ├── node.go │ ├── tree.go │ └── tree_test.go └── util │ ├── util.go │ └── util_test.go └── type.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version 9 | - image: circleci/golang:1.12 10 | 11 | # Specify service dependencies here if necessary 12 | # CircleCI maintains a library of pre-built images 13 | # documented at https://circleci.com/docs/2.0/circleci-images/ 14 | # - image: circleci/postgres:9.4 15 | 16 | #### TEMPLATE_NOTE: go expects specific checkout path representing url 17 | #### expecting it in the form of 18 | #### /go/src/github.com/circleci/go-tool 19 | #### /go/src/bitbucket.org/circleci/go-tool 20 | working_directory: /go/src/github.com/storyicon/grbac 21 | steps: 22 | - checkout 23 | 24 | # specify any bash command here prefixed with `run: ` 25 | - run: go get -v -t -d ./... 26 | - run: go test -v ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.12.x 4 | before_install: 5 | - go get github.com/mattn/goveralls 6 | script: 7 | - go test -v ./... 8 | after_success: 9 | - bash <(curl -s https://codecov.io/bash) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GRBAC [![CircleCI](https://circleci.com/gh/storyicon/grbac/tree/master.svg?style=svg)](https://circleci.com/gh/storyicon/grbac/tree/master) [![Go Report Card](https://goreportcard.com/badge/github.com/storyicon/grbac)](https://goreportcard.com/report/github.com/storyicon/grbac) [![Build Status](https://travis-ci.org/storyicon/grbac.svg?branch=master)](https://travis-ci.org/storyicon/grbac) [![GoDoc](https://godoc.org/github.com/storyicon/grbac?status.svg)](https://godoc.org/github.com/storyicon/grbac) [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/storyicon/Lobby) 2 | 3 | ![grbac](https://raw.githubusercontent.com/storyicon/grbac/master/docs/screenshot/grbac.png) 4 | 5 | [中文文档](https://github.com/storyicon/grbac/blob/master/docs/README-chinese.md) 6 | 7 | Grbac is a fast, elegant and concise [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) framework. It supports [enhanced wildcards](#4-enhanced-wildcards) and matches HTTP requests using [Radix](https://en.wikipedia.org/wiki/Radix) trees. Even more amazing is that you can easily use it in any existing database and data structure. 8 | 9 | What grbac does is ensure that the specified resource can only be accessed by the specified role. Please note that grbac is not responsible for the storage of rule configurations and "what roles the current request initiator has". It means you should configure the rule information first and provide the roles that the initiator of each request has. 10 | 11 | grbac treats the combination of `Host`, `Path`, and `Method` as a `Resource`, and binds the `Resource` to a set of role rules (called `Permission`). Only users who meet these rules can access the corresponding `Resource`. 12 | 13 | The component that reads the rule information is called `Loader`. grbac presets some loaders, you can also customize a loader by implementing `func()(grbac.Rules, error)` and load it via `grbac.WithLoader`. 14 | 15 | - [1. Most Common Use Case](#1-most-common-use-case) 16 | - [2. Concept](#2-concept) 17 | - [2.1. Rule](#21-rule) 18 | - [2.2. Resource](#22-resource) 19 | - [2.3. Permission](#23-permission) 20 | - [2.4. Loader](#24-loader) 21 | - [3. Other Examples](#3-other-examples) 22 | - [3.1. gin && grbac.WithJSON](#31-gin--grbacwithjson) 23 | - [3.2. echo && grbac.WithYaml](#32-echo--grbacwithyaml) 24 | - [3.3. iris && grbac.WithRules](#33-iris--grbacwithrules) 25 | - [3.4. ace && grbac.WithAdvancedRules](#34-ace--grbacwithadvancedrules) 26 | - [3.5. gin && grbac.WithLoader](#35-gin--grbacwithloader) 27 | - [4. Enhanced wildcards](#4-enhanced-wildcards) 28 | - [5. BenchMark](#5-benchmark) 29 | - [6. Production](#6-production) 30 | 31 | ## 1. Most Common Use Case 32 | 33 | Below is the most common use case, which uses `gin` and wraps `grbac` as a middleware. With this example, you can easily know how to use `grbac` in other http frameworks(like `echo`, `iris`, `ace`, etc): 34 | 35 | ```go 36 | package main 37 | 38 | import ( 39 | "github.com/gin-gonic/gin" 40 | "github.com/storyicon/grbac" 41 | "net/http" 42 | "time" 43 | ) 44 | 45 | func LoadAuthorizationRules() (rules grbac.Rules, err error) { 46 | // Implement your logic here 47 | // ... 48 | // You can load authorization rules from database or file 49 | // But you need to return your authentication rules in the form of grbac.Rules 50 | // tips: You can also bind this function to a golang struct 51 | return 52 | } 53 | 54 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 55 | // Implement your logic here 56 | // ... 57 | // This logic maybe take a token from the headers and 58 | // query the user's corresponding roles from the database based on the token. 59 | return roles, err 60 | } 61 | 62 | func Authorization() gin.HandlerFunc { 63 | // Here, we use a custom Loader function via "grbac.WithLoader" 64 | // and specify that this function should be called every minute to update the authentication rules. 65 | // Grbac also offers some ready-made Loaders: 66 | // grbac.WithYAML 67 | // grbac.WithRules 68 | // grbac.WithJSON 69 | // ... 70 | rbac, err := grbac.New(grbac.WithLoader(LoadAuthorizationRules, time.Minute)) 71 | if err != nil { 72 | panic(err) 73 | } 74 | return func(c *gin.Context) { 75 | roles, err := QueryRolesByHeaders(c.Request.Header) 76 | if err != nil { 77 | c.AbortWithError(http.StatusInternalServerError, err) 78 | return 79 | } 80 | state, _ := rbac.IsRequestGranted(c.Request, roles) 81 | if !state.IsGranted() { 82 | c.AbortWithStatus(http.StatusUnauthorized) 83 | return 84 | } 85 | } 86 | } 87 | 88 | func main(){ 89 | c := gin.New() 90 | c.Use(Authorization()) 91 | 92 | // Bind your API here 93 | // ... 94 | 95 | c.Run(":8080") 96 | } 97 | ``` 98 | ## 2. Concept 99 | 100 | Here are some concepts about `grbac`. It's very simple, you may only need three minutes to understand. 101 | 102 | ### 2.1. Rule 103 | 104 | ```go 105 | // Rule is used to define the relationship between "resource" and "permission" 106 | type Rule struct { 107 | // The ID controls the priority of the rule. 108 | // The higher the ID means the higher the priority of the rule. 109 | // When a request is matched to more than one rule, 110 | // then authentication will only use the permission configuration for the rule with the highest ID value. 111 | // If there are multiple rules that are the largest ID, then one of them will be used randomly. 112 | ID int `json:"id"` 113 | *Resource 114 | *Permission 115 | } 116 | ``` 117 | 118 | As you can see, the `Rule` consists of three parts: `ID`, `Resource`, and `Permission`. 119 | The `ID` determines the priority of the Rule. 120 | When a request meets multiple rules at the same time (such as in a wildcard), 121 | `grbac` will select the one with the highest ID, then authenticate with its Permission definition. 122 | If multiple rules of the same ID are matched at the same time, grbac will randomly select one from them. 123 | 124 | Here is a very simple example: 125 | 126 | ```yaml 127 | #Rule 128 | - id: 0 129 | # Resource 130 | host: "*" 131 | path: "**" 132 | method: "*" 133 | # Permission 134 | authorized_roles: 135 | - "*" 136 | forbidden_roles: [] 137 | allow_anyone: false 138 | 139 | #Rule 140 | - id: 1 141 | # Resource 142 | host: domain.com 143 | path: "/article" 144 | method: "{DELETE,POST,PUT}" 145 | # Permission 146 | authorized_roles: 147 | - editor 148 | forbidden_roles: [] 149 | allow_anyone: false 150 | ``` 151 | 152 | In this configuration file written in yaml format, the rule with `ID=0` states that all resources can be accessed by anyone with any role. 153 | But the rule with `ID=1` states that only the `editor` can operate on the article. 154 | Then, except that the operation of the article can only be accessed by the `editor`, all other resources can be accessed by anyone with any role. 155 | 156 | ### 2.2. Resource 157 | 158 | ```go 159 | // Resource defines resources 160 | type Resource struct { 161 | // Host defines the host of the resource, allowing wildcards to be used. 162 | Host string `json:"host"` 163 | // Path defines the path of the resource, allowing wildcards to be used. 164 | Path string `json:"path"` 165 | // Method defines the method of the resource, allowing wildcards to be used. 166 | Method string `json:"method"` 167 | } 168 | ``` 169 | 170 | Resource is used to describe which resources a rule applies to. 171 | When `IsRequestGranted(c.Request, roles)` is executed, grbac first matches the current `Request` with the `Resources` in all `Rule`s. 172 | 173 | Each field of Resource supports [enhanced wildcards](#4-enhanced-wildcards) 174 | 175 | ### 2.3. Permission 176 | 177 | ```go 178 | // Permission is used to define permission control information 179 | type Permission struct { 180 | // AuthorizedRoles defines roles that allow access to specified resource 181 | // Accepted type: non-empty string, * 182 | // *: means any role, but visitors should have at least one role, 183 | // non-empty string: specified role 184 | AuthorizedRoles []string `json:"authorized_roles"` 185 | // ForbiddenRoles defines roles that not allow access to specified resource 186 | // ForbiddenRoles has a higher priority than AuthorizedRoles 187 | // Accepted type: non-empty string, * 188 | // *: means any role, but visitors should have at least one role, 189 | // non-empty string: specified role 190 | // 191 | ForbiddenRoles []string `json:"forbidden_roles"` 192 | // AllowAnyone has a higher priority than ForbiddenRoles/AuthorizedRoles 193 | // If set to true, anyone will be able to pass authentication. 194 | // Note that this will include people without any role. 195 | AllowAnyone bool `json:"allow_anyone"` 196 | } 197 | ``` 198 | 199 | `Permission` is used to define the authorization rules of the `Resource` to which it is bound. 200 | That's understandable. When the roles of the requester meets the definition of `Permission`, he will be allowed access, otherwise he will be denied access. 201 | 202 | For faster speeds, fields in `Permission` do not support `enhanced wildcards`. 203 | Only `*` is allowed in `AuthorizedRoles` and `ForbiddenRoles` to indicate `all`. 204 | 205 | ### 2.4. Loader 206 | 207 | Loader is used to load authorization rules. grbac presets some loaders, you can also customize a loader by implementing `func()(grbac.Rules, error)` and load it via `grbac.WithLoader`. 208 | 209 | | method | description | 210 | | --- | --- | 211 | | WithJSON(path, interval) | periodically load rules configuration from `json` file | 212 | | WithYaml(path, interval) | periodically load rules configuration from `yaml` file | 213 | | WithRules(Rules) | load rules configuration from `grbac.Rules` | 214 | | WithAdvancedRules(loader.AdvancedRules) | load advanced rules from `loader.AdvancedRules`| 215 | | WithLoader(loader func()(Rules, error), interval) | periodically load rules with custom functions | 216 | 217 | `interval` defines the reload period of the authentication rule. 218 | When `interval < 0`, `grbac` will abandon periodically loading the configuration file; 219 | When `interval∈[0,1s)`, `grbac` will automatically set the `interval` to `5s`; 220 | 221 | ## 3. Other Examples 222 | 223 | Here are some simple examples to make it easier to understand how `grbac` works. 224 | Although `grbac` works well in most http frameworks, I am sorry that I only use gin now, so if there are some flaws in the example below, please let me know. 225 | ### 3.1. gin && grbac.WithJSON 226 | 227 | If you want to write the configuration file in a `JSON` file, you can load it via `grbac.WithJSON(file, interval)`, `file` is your json file path, and grbac will reload the file every `interval`. 228 | 229 | ```json 230 | [ 231 | { 232 | "id": 0, 233 | "host": "*", 234 | "path": "**", 235 | "method": "*", 236 | "authorized_roles": [ 237 | "*" 238 | ], 239 | "forbidden_roles": [ 240 | "black_user" 241 | ], 242 | "allow_anyone": false 243 | }, 244 | { 245 | "id":1, 246 | "host": "domain.com", 247 | "path": "/article", 248 | "method": "{DELETE,POST,PUT}", 249 | "authorized_roles": ["editor"], 250 | "forbidden_roles": [], 251 | "allow_anyone": false 252 | } 253 | ] 254 | ``` 255 | The above is an example of authentication rule in `JSON` format. It's structure is based on [grbac.Rules](#21-rule). 256 | 257 | ```go 258 | 259 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 260 | // Implement your logic here 261 | // ... 262 | // This logic maybe take a token from the headers and 263 | // query the user's corresponding roles from the database based on the token. 264 | return roles, err 265 | } 266 | 267 | func Authentication() gin.HandlerFunc { 268 | rbac, err := grbac.New(grbac.WithJSON("config.json", time.Minute * 10)) 269 | if err != nil { 270 | panic(err) 271 | } 272 | return func(c *gin.Context) { 273 | roles, err := QueryRolesByHeaders(c.Request.Header) 274 | if err != nil { 275 | c.AbortWithError(http.StatusInternalServerError, err) 276 | return 277 | } 278 | 279 | state, err := rbac.IsRequestGranted(c.Request, roles) 280 | if err != nil { 281 | c.AbortWithStatus(http.StatusInternalServerError) 282 | return 283 | } 284 | 285 | if !state.IsGranted() { 286 | c.AbortWithStatus(http.StatusUnauthorized) 287 | return 288 | } 289 | } 290 | } 291 | 292 | func main(){ 293 | c := gin.New() 294 | c.Use(Authentication()) 295 | 296 | // Bind your API here 297 | // ... 298 | 299 | c.Run(":8080") 300 | } 301 | 302 | ``` 303 | 304 | ### 3.2. echo && grbac.WithYaml 305 | 306 | If you want to write the configuration file in a `YAML` file, you can load it via `grbac.WithYAML(file, interval)`, `file` is your yaml file path, and grbac will reload the file every `interval`. 307 | 308 | ```yaml 309 | #Rule 310 | - id: 0 311 | # Resource 312 | host: "*" 313 | path: "**" 314 | method: "*" 315 | # Permission 316 | authorized_roles: 317 | - "*" 318 | forbidden_roles: [] 319 | allow_anyone: false 320 | 321 | #Rule 322 | - id: 1 323 | # Resource 324 | host: domain.com 325 | path: "/article" 326 | method: "{DELETE,POST,PUT}" 327 | # Permission 328 | authorized_roles: 329 | - editor 330 | forbidden_roles: [] 331 | allow_anyone: false 332 | ``` 333 | 334 | The above is an example of authentication rule in `YAML` format. It's structure is based on [grbac.Rules](#21-rule). 335 | 336 | ```go 337 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 338 | // Implement your logic here 339 | // ... 340 | // This logic maybe take a token from the headers and 341 | // query the user's corresponding roles from the database based on the token. 342 | return roles, err 343 | } 344 | 345 | func Authentication() echo.MiddlewareFunc { 346 | rbac, err := grbac.New(grbac.WithYAML("config.yaml", time.Minute * 10)) 347 | if err != nil { 348 | panic(err) 349 | } 350 | return func(echo.HandlerFunc) echo.HandlerFunc { 351 | return func(c echo.Context) error { 352 | roles, err := QueryRolesByHeaders(c.Request().Header) 353 | if err != nil { 354 | c.NoContent(http.StatusInternalServerError) 355 | return nil 356 | } 357 | state, err := rbac.IsRequestGranted(c.Request(), roles) 358 | if err != nil { 359 | c.NoContent(http.StatusInternalServerError) 360 | return nil 361 | } 362 | if state.IsGranted() { 363 | return nil 364 | } 365 | c.NoContent(http.StatusUnauthorized) 366 | return nil 367 | } 368 | } 369 | } 370 | 371 | func main(){ 372 | c := echo.New() 373 | c.Use(Authentication()) 374 | 375 | // Implement your logic here 376 | // ... 377 | } 378 | ``` 379 | 380 | ### 3.3. iris && grbac.WithRules 381 | 382 | If you want to write the authentication rules directly in the code, `grbac.WithRules(rules)` provides this way, you can use it like this: 383 | 384 | ```go 385 | 386 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 387 | // Implement your logic here 388 | // ... 389 | // This logic maybe take a token from the headers and 390 | // query the user's corresponding roles from the database based on the token. 391 | return roles, err 392 | } 393 | 394 | func Authentication() iris.Handler { 395 | var rules = grbac.Rules{ 396 | { 397 | ID: 0, 398 | Resource: &grbac.Resource{ 399 | Host: "*", 400 | Path: "**", 401 | Method: "*", 402 | }, 403 | Permission: &grbac.Permission{ 404 | AuthorizedRoles: []string{"*"}, 405 | ForbiddenRoles: []string{"black_user"}, 406 | AllowAnyone: false, 407 | }, 408 | }, 409 | { 410 | ID: 1, 411 | Resource: &grbac.Resource{ 412 | Host: "domain.com", 413 | Path: "/article", 414 | Method: "{DELETE,POST,PUT}", 415 | }, 416 | Permission: &grbac.Permission{ 417 | AuthorizedRoles: []string{"editor"}, 418 | ForbiddenRoles: []string{}, 419 | AllowAnyone: false, 420 | }, 421 | }, 422 | } 423 | rbac, err := grbac.New(grbac.WithRules(rules)) 424 | if err != nil { 425 | panic(err) 426 | } 427 | return func(c context.Context) { 428 | roles, err := QueryRolesByHeaders(c.Request().Header) 429 | if err != nil { 430 | c.StatusCode(http.StatusInternalServerError) 431 | c.StopExecution() 432 | return 433 | } 434 | state, err := rbac.IsRequestGranted(c.Request(), roles) 435 | if err != nil { 436 | c.StatusCode(http.StatusInternalServerError) 437 | c.StopExecution() 438 | return 439 | } 440 | if !state.IsGranted() { 441 | c.StatusCode(http.StatusUnauthorized) 442 | c.StopExecution() 443 | return 444 | } 445 | } 446 | } 447 | 448 | func main(){ 449 | c := iris.New() 450 | c.Use(Authentication()) 451 | 452 | // Implement your logic here 453 | // ... 454 | } 455 | ``` 456 | 457 | ### 3.4. ace && grbac.WithAdvancedRules 458 | 459 | If you want to write the authentication rules directly in the code, `grbac.WithAdvancedRules(rules)` provides this way, you can use it like this: 460 | 461 | ```go 462 | 463 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 464 | // Implement your logic here 465 | // ... 466 | // This logic maybe take a token from the headers and 467 | // query the user's corresponding roles from the database based on the token. 468 | return roles, err 469 | } 470 | 471 | func Authentication() ace.HandlerFunc { 472 | var advancedRules = loader.AdvancedRules{ 473 | { 474 | Host: []string{"*"}, 475 | Path: []string{"**"}, 476 | Method: []string{"*"}, 477 | Permission: &grbac.Permission{ 478 | AuthorizedRoles: []string{}, 479 | ForbiddenRoles: []string{"black_user"}, 480 | AllowAnyone: false, 481 | }, 482 | }, 483 | { 484 | Host: []string{"domain.com"}, 485 | Path: []string{"/article"}, 486 | Method: []string{"PUT","DELETE","POST"}, 487 | Permission: &grbac.Permission{ 488 | AuthorizedRoles: []string{"editor"}, 489 | ForbiddenRoles: []string{}, 490 | AllowAnyone: false, 491 | }, 492 | }, 493 | } 494 | auth, err := grbac.New(grbac.WithAdvancedRules(advancedRules)) 495 | if err != nil { 496 | panic(err) 497 | } 498 | return func(c *ace.C) { 499 | roles, err := QueryRolesByHeaders(c.Request.Header) 500 | if err != nil { 501 | c.AbortWithStatus(http.StatusInternalServerError) 502 | return 503 | } 504 | state, err := auth.IsRequestGranted(c.Request, roles) 505 | if err != nil { 506 | c.AbortWithStatus(http.StatusInternalServerError) 507 | return 508 | } 509 | if !state.IsGranted() { 510 | c.AbortWithStatus(http.StatusUnauthorized) 511 | return 512 | } 513 | } 514 | } 515 | func main(){ 516 | c := ace.New() 517 | c.Use(Authentication()) 518 | 519 | // Implement your logic here 520 | // ... 521 | } 522 | 523 | ``` 524 | 525 | `loader.AdvancedRules` attempts to provide a simpler way to define authentication rules than `grbac.Rules`. 526 | 527 | 528 | ### 3.5. gin && grbac.WithLoader 529 | 530 | ```go 531 | 532 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 533 | // Implement your logic here 534 | // ... 535 | // This logic maybe take a token from the headers and 536 | // query the user's corresponding roles from the database based on the token. 537 | return roles, err 538 | } 539 | 540 | type MySQLLoader struct { 541 | session *gorm.DB 542 | } 543 | 544 | func NewMySQLLoader(dsn string) (*MySQLLoader, error) { 545 | loader := &MySQLLoader{} 546 | db, err := gorm.Open("mysql", dsn) 547 | if err != nil { 548 | return nil, err 549 | } 550 | loader.session = db 551 | return loader, nil 552 | } 553 | 554 | func (loader *MySQLLoader) LoadRules() (rules grbac.Rules, err error) { 555 | // Implement your logic here 556 | // ... 557 | // Extract data from the database, assemble it into grbac.Rules and return 558 | return 559 | } 560 | 561 | func Authentication() gin.HandlerFunc { 562 | loader, err := NewMySQLLoader("user:password@/dbname?charset=utf8&parseTime=True&loc=Local") 563 | if err != nil { 564 | panic(err) 565 | } 566 | rbac, err := grbac.New(grbac.WithLoader(loader.LoadRules, time.Second * 5)) 567 | if err != nil { 568 | panic(err) 569 | } 570 | return func(c *gin.Context) { 571 | roles, err := QueryRolesByHeaders(c.Request.Header) 572 | if err != nil { 573 | c.AbortWithStatus(http.StatusInternalServerError) 574 | return 575 | } 576 | 577 | state, err := rbac.IsRequestGranted(c.Request, roles) 578 | if err != nil { 579 | c.AbortWithStatus(http.StatusInternalServerError) 580 | return 581 | } 582 | if !state.IsGranted() { 583 | c.AbortWithStatus(http.StatusUnauthorized) 584 | return 585 | } 586 | } 587 | } 588 | 589 | func main(){ 590 | c := gin.New() 591 | c.Use(Authorization()) 592 | 593 | // Bind your API here 594 | // ... 595 | 596 | c.Run(":8080") 597 | } 598 | ``` 599 | 600 | ## 4. Enhanced wildcards 601 | 602 | `Wildcard` supported syntax: 603 | ```text 604 | pattern: 605 | { term } 606 | term: 607 | '*' matches any sequence of non-path-separators 608 | '**' matches any sequence of characters, including 609 | path separators. 610 | '?' matches any single non-path-separator character 611 | '[' [ '^' ] { character-range } ']' 612 | character class (must be non-empty) 613 | '{' { term } [ ',' { term } ... ] '}' 614 | c matches character c (c != '*', '?', '\\', '[') 615 | '\\' c matches character c 616 | 617 | character-range: 618 | c matches character c (c != '\\', '-', ']') 619 | '\\' c matches character c 620 | lo '-' hi matches character c for lo <= c <= hi 621 | ``` 622 | 623 | ## 5. BenchMark 624 | 625 | ```go 626 | ➜ gos test -bench=. 627 | goos: linux 628 | goarch: amd64 629 | pkg: github.com/storyicon/grbac/pkg/tree 630 | BenchmarkTree_Query 2000 541397 ns/op 631 | BenchmarkTree_Foreach_Query 2000 1360719 ns/op 632 | PASS 633 | ok github.com/storyicon/grbac/pkg/tree 13.182s 634 | ``` 635 | The test case contains 1000 random rules, and the `BenchmarkTree_Query` and `BenchmarkTree_Foreach_Query` functions test four requests separately, after calculation: 636 | 637 | ``` 638 | 541397/(4*1e9)=0.0001s 639 | ``` 640 | 641 | When there are 1000 rules, the average verification time per request is `0.0001s`. 642 | 643 | ## 6. Production 644 | 645 | `grbac` has been used in the `production` environment by the following companies: 646 | 647 | ![wallstreetcn](https://raw.githubusercontent.com/storyicon/grbac/master/docs/screenshot/wallstreetcn.png) -------------------------------------------------------------------------------- /docs/README-chinese.md: -------------------------------------------------------------------------------- 1 | # GRBAC [![CircleCI](https://circleci.com/gh/storyicon/grbac/tree/master.svg?style=svg)](https://circleci.com/gh/storyicon/grbac/tree/master) [![Go Report Card](https://goreportcard.com/badge/github.com/storyicon/grbac)](https://goreportcard.com/report/github.com/storyicon/grbac) [![Build Status](https://travis-ci.org/storyicon/grbac.svg?branch=master)](https://travis-ci.org/storyicon/grbac) [![GoDoc](https://godoc.org/github.com/storyicon/grbac?status.svg)](https://godoc.org/github.com/storyicon/grbac) [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/storyicon/Lobby) 2 | 3 | ![grbac](https://raw.githubusercontent.com/storyicon/grbac/master/docs/screenshot/grbac.png) 4 | 5 | Grbac是一个快速,优雅和简洁的[RBAC](https://en.wikipedia.org/wiki/Role-based_access_control)框架。它支持[增强的通配符](#4-enhanced-wildcards)并使用[Radix](https://en.wikipedia.org/wiki/Radix)树匹配HTTP请求。令人惊奇的是,您可以在任何现有的数据库和数据结构中轻松使用它。 6 | 7 | grbac的作用是确保指定的资源只能由指定的角色访问。请注意,grbac不负责存储鉴权规则和分辨“当前请求发起者具有哪些角色”,更不负责角色的创建、分配等。这意味着您应该首先配置规则信息,并提供每个请求的发起者具有的角色。 8 | 9 | grbac将`Host`、`Path`和`Method`的组合视为`Resource`,并将`Resource`绑定到一组角色规则(称为`Permission`)。只有符合这些规则的用户才能访问相应的`Resource`。 10 | 11 | 读取鉴权规则的组件称为`Loader`。grbac预置了一些`Loader`,你也可以通过实现`func()(grbac.Rules,error)`来根据你的设计来自定义`Loader`,并通过`grbac.WithLoader`加载它。 12 | 13 | - [1. 最常见的用例](#1-最常见的用例) 14 | - [2. 概念](#2-概念) 15 | - [2.1. Rule](#21-rule) 16 | - [2.2. Resource](#22-resource) 17 | - [2.3. Permission](#23-permission) 18 | - [2.4. Loader](#24-loader) 19 | - [3. 其他例子](#3-其他例子) 20 | - [3.1. gin && grbac.WithJSON](#31-gin--grbacwithjson) 21 | - [3.2. echo && grbac.WithYaml](#32-echo--grbacwithyaml) 22 | - [3.3. iris && grbac.WithRules](#33-iris--grbacwithrules) 23 | - [3.4. ace && grbac.WithAdvancedRules](#34-ace--grbacwithadvancedrules) 24 | - [3.5. gin && grbac.WithLoader](#35-gin--grbacwithloader) 25 | - [4. 增强的通配符](#4-增强的通配符) 26 | - [5. 运行效率](#5-运行效率) 27 | - [6. 生产环境](#6-生产环境) 28 | 29 | ## 1. 最常见的用例 30 | 31 | 下面是最常见的用例,它使用`gin`,并将`grbac`包装成了一个中间件。通过这个例子,你可以很容易地知道如何在其他http框架中使用`grbac`(比如`echo`,`iris`,`ace`等): 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "github.com/gin-gonic/gin" 38 | "github.com/storyicon/grbac" 39 | "net/http" 40 | "time" 41 | ) 42 | 43 | func LoadAuthorizationRules() (rules grbac.Rules, err error) { 44 | // 在这里实现你的逻辑 45 | // ... 46 | // 你可以从数据库或文件加载授权规则 47 | // 但是你需要以 grbac.Rules 的格式返回你的身份验证规则 48 | // 提示:你还可以将此函数绑定到golang结构体 49 | return 50 | } 51 | 52 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 53 | // 在这里实现你的逻辑 54 | // ... 55 | // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。 56 | return roles, err 57 | } 58 | 59 | func Authorization() gin.HandlerFunc { 60 | // 在这里,我们通过“grbac.WithLoader”接口使用自定义Loader功能 61 | // 并指定应每分钟调用一次LoadAuthorizationRules函数以获取最新的身份验证规则。 62 | // Grbac还提供一些现成的Loader: 63 | // grbac.WithYAML 64 | // grbac.WithRules 65 | // grbac.WithJSON 66 | // ... 67 | rbac, err := grbac.New(grbac.WithLoader(LoadAuthorizationRules, time.Minute)) 68 | if err != nil { 69 | panic(err) 70 | } 71 | return func(c *gin.Context) { 72 | roles, err := QueryRolesByHeaders(c.Request.Header) 73 | if err != nil { 74 | c.AbortWithError(http.StatusInternalServerError, err) 75 | return 76 | } 77 | state, _ := rbac.IsRequestGranted(c.Request, roles) 78 | if !state.IsGranted() { 79 | c.AbortWithStatus(http.StatusUnauthorized) 80 | return 81 | } 82 | } 83 | } 84 | 85 | func main(){ 86 | c := gin.New() 87 | c.Use(Authorization()) 88 | 89 | // 在这里通过c.Get、c.Post等函数绑定你的API 90 | // ... 91 | 92 | c.Run(":8080") 93 | } 94 | ``` 95 | ## 2. 概念 96 | 97 | 这里有一些关于`grbac`的概念。这很简单,你可能只需要三分钟就能理解。 98 | 99 | ### 2.1. Rule 100 | 101 | ```go 102 | // Rule即规则,用于定义Resource和Permission之间的关系 103 | type Rule struct { 104 | // ID决定了Rule的优先级。 105 | // ID值越大意味着Rule的优先级越高。 106 | // 当请求被多个规则同时匹配时,grbac将仅使用具有最高ID值的规则。 107 | // 如果有多个规则同时具有最大的ID,则将随机使用其中一个规则。 108 | ID int `json:"id"` 109 | *Resource 110 | *Permission 111 | } 112 | ``` 113 | 114 | 如你所见,`Rule`由三部分组成:`ID`,`Resource`和`Permission`。 115 | “ID”确定规则的优先级。 116 | 当请求同时满足多个规则时(例如在通配符中), 117 | `grbac`将选择具有最高ID的那个,然后使用其权限定义进行身份验证。 118 | 如果有多个规则同时具有最大的ID,则将随机使用其中一个规则(所以请避免这种情况)。 119 | 120 | 下面有一个非常简单的例子: 121 | 122 | ```yaml 123 | #Rule 124 | - id: 0 125 | # Resource 126 | host: "*" 127 | path: "**" 128 | method: "*" 129 | # Permission 130 | authorized_roles: 131 | - "*" 132 | forbidden_roles: [] 133 | allow_anyone: false 134 | 135 | #Rule 136 | - id: 1 137 | # Resource 138 | host: domain.com 139 | path: "/article" 140 | method: "{DELETE,POST,PUT}" 141 | # Permission 142 | authorized_roles: 143 | - editor 144 | forbidden_roles: [] 145 | allow_anyone: false 146 | ``` 147 | 148 | 在以yaml格式编写的此配置文件中,ID=0 的规则表明任何具有任何角色的人都可以访问所有资源。 149 | 但是ID=1的规则表明只有`editor`可以对文章进行增删改操作。 150 | 这样,除了文章的操作只能由`editor`访问之外,任何具有任何角色的人都可以访问所有其他资源。 151 | 152 | ### 2.2. Resource 153 | 154 | ```go 155 | type Resource struct { 156 | // Host 定义资源的Host,允许使用增强的通配符。 157 | Host string `json:"host"` 158 | // Path 定义资源的Path,允许使用增强的通配符。 159 | Path string `json:"path"` 160 | // Method 定义资源的Method,允许使用增强的通配符。 161 | Method string `json:"method"` 162 | } 163 | ``` 164 | 165 | Resource用于描述Rule适用的资源。 166 | 当执行`IsRequestGranted(c.Request,roles)`时,grbac首先将当前的`Request`与所有`Rule`中的`Resources`匹配。 167 | 168 | Resource的每个字段都支持[增强的通配符](#4-enhanced-wildcards) 169 | 170 | ### 2.3. Permission 171 | 172 | ```go 173 | // Permission用于定义权限控制信息 174 | type Permission struct { 175 | // AuthorizedRoles定义允许访问资源的角色 176 | // 支持的类型: 非空字符串,* 177 | // *: 意味着任何角色,但访问者应该至少有一个角色, 178 | // 非空字符串:指定的角色 179 | AuthorizedRoles []string `json:"authorized_roles"` 180 | // ForbiddenRoles 定义不允许访问指定资源的角色 181 | // ForbiddenRoles 优先级高于AuthorizedRoles 182 | // 支持的类型:非空字符串,* 183 | // *: 意味着任何角色,但访问者应该至少有一个角色, 184 | // 非空字符串:指定的角色 185 | // 186 | ForbiddenRoles []string `json:"forbidden_roles"` 187 | // AllowAnyone的优先级高于 ForbiddenRoles、AuthorizedRoles 188 |     // 如果设置为true,任何人都可以通过验证。 189 |     // 请注意,这将包括“没有角色的人”。 190 | AllowAnyone bool `json:"allow_anyone"` 191 | } 192 | ``` 193 | 194 | “Permission”用于定义绑定到的“Resource”的授权规则。 195 | 这是易于理解的,当请求者的角色符合“Permission”的定义时,他将被允许访问Resource,否则他将被拒绝访问。 196 | 197 | 为了加快验证的速度,`Permission`中的字段不支持“增强的通配符”。 198 | 在`AuthorizedRoles`和`ForbiddenRoles`中只允许`*`表示所有。 199 | 200 | ### 2.4. Loader 201 | 202 | Loader用于加载Rule。 grbac预置了一些加载器,你也可以通过实现`func()(grbac.Rules, error)` 来自定义加载器并通过 `grbac.WithLoader` 加载它。 203 | 204 | | method | description | 205 | | --- | --- | 206 | | WithJSON(path, interval) | 定期从`json`文件加载规则配置 | 207 | | WithYaml(path, interval) | 定期从`yaml`文件加载规则配置 | 208 | | WithRules(Rules) | 从`grbac.Rules`加载规则配置 | 209 | | WithAdvancedRules(loader.AdvancedRules) | 以一种更紧凑的方式定义Rule,并使用`loader.AdvancedRules`加载 | 210 | | WithLoader(loader func()(Rules, error), interval) | 使用自定义函数定期加载规则 | 211 | 212 | `interval`定义了Rules的重载周期。 213 | 当`interval <0`时,`grbac`会放弃周期加载Rules配置; 214 | 当`interval∈[0,1s)`时,`grbac`会自动将`interval`设置为`5s`; 215 | 216 | ## 3. 其他例子 217 | 218 | 这里有一些简单的例子,可以让你更容易理解`grbac`的工作原理。 219 | 虽然`grbac`在大多数http框架中运行良好,但很抱歉我现在只使用gin,所以如果下面的例子中有一些缺陷,请告诉我。 220 | 221 | ### 3.1. gin && grbac.WithJSON 222 | 223 | 如果你想在`JSON`文件中编写配置文件,你可以通过`grbac.WithJSON(filepath,interval)`加载它,`filepath`是你的json文件路径,并且grbac将每隔interval重新加载一次文件。 。 224 | 225 | ```json 226 | [ 227 | { 228 | "id": 0, 229 | "host": "*", 230 | "path": "**", 231 | "method": "*", 232 | "authorized_roles": [ 233 | "*" 234 | ], 235 | "forbidden_roles": [ 236 | "black_user" 237 | ], 238 | "allow_anyone": false 239 | }, 240 | { 241 | "id":1, 242 | "host": "domain.com", 243 | "path": "/article", 244 | "method": "{DELETE,POST,PUT}", 245 | "authorized_roles": ["editor"], 246 | "forbidden_roles": [], 247 | "allow_anyone": false 248 | } 249 | ] 250 | ``` 251 | 252 | 以上是“JSON”格式的身份验证规则示例。它的结构基于[grbac.Rules](#21-rule)。 253 | 254 | ```go 255 | 256 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 257 | // 在这里实现你的逻辑 258 | // ... 259 | // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。 260 | return roles, err 261 | } 262 | 263 | func Authentication() gin.HandlerFunc { 264 | rbac, err := grbac.New(grbac.WithJSON("config.json", time.Minute * 10)) 265 | if err != nil { 266 | panic(err) 267 | } 268 | return func(c *gin.Context) { 269 | roles, err := QueryRolesByHeaders(c.Request.Header) 270 | if err != nil { 271 | c.AbortWithError(http.StatusInternalServerError, err) 272 | return 273 | } 274 | 275 | state, err := rbac.IsRequestGranted(c.Request, roles) 276 | if err != nil { 277 | c.AbortWithStatus(http.StatusInternalServerError) 278 | return 279 | } 280 | 281 | if !state.IsGranted() { 282 | c.AbortWithStatus(http.StatusUnauthorized) 283 | return 284 | } 285 | } 286 | } 287 | 288 | func main(){ 289 | c := gin.New() 290 | c.Use(Authentication()) 291 | 292 | // 在这里通过c.Get、c.Post等函数绑定你的API 293 | // ... 294 | 295 | c.Run(":8080") 296 | } 297 | 298 | ``` 299 | 300 | ### 3.2. echo && grbac.WithYaml 301 | 302 | 如果你想在`YAML`文件中编写配置文件,你可以通过`grbac.WithYAML(file,interval)`加载它,`file`是你的yaml文件路径,并且grbac将每隔一个interval重新加载一次文件。 303 | 304 | ```yaml 305 | #Rule 306 | - id: 0 307 | # Resource 308 | host: "*" 309 | path: "**" 310 | method: "*" 311 | # Permission 312 | authorized_roles: 313 | - "*" 314 | forbidden_roles: [] 315 | allow_anyone: false 316 | 317 | #Rule 318 | - id: 1 319 | # Resource 320 | host: domain.com 321 | path: "/article" 322 | method: "{DELETE,POST,PUT}" 323 | # Permission 324 | authorized_roles: 325 | - editor 326 | forbidden_roles: [] 327 | allow_anyone: false 328 | ``` 329 | 330 | 以上是“YAML”格式的认证规则的示例。它的结构基于[grbac.Rules](#21-rule)。 331 | 332 | ```go 333 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 334 | // 在这里实现你的逻辑 335 | // ... 336 | // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。 337 | return roles, err 338 | } 339 | 340 | func Authentication() echo.MiddlewareFunc { 341 | rbac, err := grbac.New(grbac.WithYAML("config.yaml", time.Minute * 10)) 342 | if err != nil { 343 | panic(err) 344 | } 345 | return func(echo.HandlerFunc) echo.HandlerFunc { 346 | return func(c echo.Context) error { 347 | roles, err := QueryRolesByHeaders(c.Request().Header) 348 | if err != nil { 349 | c.NoContent(http.StatusInternalServerError) 350 | return nil 351 | } 352 | state, err := rbac.IsRequestGranted(c.Request(), roles) 353 | if err != nil { 354 | c.NoContent(http.StatusInternalServerError) 355 | return nil 356 | } 357 | if state.IsGranted() { 358 | return nil 359 | } 360 | c.NoContent(http.StatusUnauthorized) 361 | return nil 362 | } 363 | } 364 | } 365 | 366 | func main(){ 367 | c := echo.New() 368 | c.Use(Authentication()) 369 | 370 | // 在这里通过c.Get、c.Post等函数绑定你的API 371 | // ... 372 | 373 | } 374 | ``` 375 | 376 | ### 3.3. iris && grbac.WithRules 377 | 378 | 如果你想直接在代码中编写认证规则,`grbac.WithRules(rules)`提供了这种方式,你可以像这样使用它: 379 | ```go 380 | 381 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 382 | // 在这里实现你的逻辑 383 | // ... 384 | // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。 385 | return roles, err 386 | } 387 | 388 | func Authentication() iris.Handler { 389 | var rules = grbac.Rules{ 390 | { 391 | ID: 0, 392 | Resource: &grbac.Resource{ 393 | Host: "*", 394 | Path: "**", 395 | Method: "*", 396 | }, 397 | Permission: &grbac.Permission{ 398 | AuthorizedRoles: []string{"*"}, 399 | ForbiddenRoles: []string{"black_user"}, 400 | AllowAnyone: false, 401 | }, 402 | }, 403 | { 404 | ID: 1, 405 | Resource: &grbac.Resource{ 406 | Host: "domain.com", 407 | Path: "/article", 408 | Method: "{DELETE,POST,PUT}", 409 | }, 410 | Permission: &grbac.Permission{ 411 | AuthorizedRoles: []string{"editor"}, 412 | ForbiddenRoles: []string{}, 413 | AllowAnyone: false, 414 | }, 415 | }, 416 | } 417 | rbac, err := grbac.New(grbac.WithRules(rules)) 418 | if err != nil { 419 | panic(err) 420 | } 421 | return func(c context.Context) { 422 | roles, err := QueryRolesByHeaders(c.Request().Header) 423 | if err != nil { 424 | c.StatusCode(http.StatusInternalServerError) 425 | c.StopExecution() 426 | return 427 | } 428 | state, err := rbac.IsRequestGranted(c.Request(), roles) 429 | if err != nil { 430 | c.StatusCode(http.StatusInternalServerError) 431 | c.StopExecution() 432 | return 433 | } 434 | if !state.IsGranted() { 435 | c.StatusCode(http.StatusUnauthorized) 436 | c.StopExecution() 437 | return 438 | } 439 | } 440 | } 441 | 442 | func main(){ 443 | c := iris.New() 444 | c.Use(Authentication()) 445 | 446 | // 在这里通过c.Get、c.Post等函数绑定你的API 447 | // ... 448 | 449 | } 450 | ``` 451 | 452 | ### 3.4. ace && grbac.WithAdvancedRules 453 | 454 | 如果你想直接在代码中编写认证规则,`grbac.WithAdvancedRules(rules)`提供了这种方式,你可以像这样使用它: 455 | 456 | ```go 457 | 458 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 459 | // 在这里实现你的逻辑 460 | // ... 461 | // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。 462 | return roles, err 463 | } 464 | 465 | func Authentication() ace.HandlerFunc { 466 | var advancedRules = loader.AdvancedRules{ 467 | { 468 | Host: []string{"*"}, 469 | Path: []string{"**"}, 470 | Method: []string{"*"}, 471 | Permission: &grbac.Permission{ 472 | AuthorizedRoles: []string{}, 473 | ForbiddenRoles: []string{"black_user"}, 474 | AllowAnyone: false, 475 | }, 476 | }, 477 | { 478 | Host: []string{"domain.com"}, 479 | Path: []string{"/article"}, 480 | Method: []string{"PUT","DELETE","POST"}, 481 | Permission: &grbac.Permission{ 482 | AuthorizedRoles: []string{"editor"}, 483 | ForbiddenRoles: []string{}, 484 | AllowAnyone: false, 485 | }, 486 | }, 487 | } 488 | auth, err := grbac.New(grbac.WithAdvancedRules(advancedRules)) 489 | if err != nil { 490 | panic(err) 491 | } 492 | return func(c *ace.C) { 493 | roles, err := QueryRolesByHeaders(c.Request.Header) 494 | if err != nil { 495 | c.AbortWithStatus(http.StatusInternalServerError) 496 | return 497 | } 498 | state, err := auth.IsRequestGranted(c.Request, roles) 499 | if err != nil { 500 | c.AbortWithStatus(http.StatusInternalServerError) 501 | return 502 | } 503 | if !state.IsGranted() { 504 | c.AbortWithStatus(http.StatusUnauthorized) 505 | return 506 | } 507 | } 508 | } 509 | 510 | func main(){ 511 | c := ace.New() 512 | c.Use(Authentication()) 513 | 514 | // 在这里通过c.Get、c.Post等函数绑定你的API 515 | // ... 516 | 517 | } 518 | 519 | ``` 520 | 521 | `loader.AdvancedRules`试图提供一种比`grbac.Rules`更紧凑的定义鉴权规则的方法。 522 | 523 | ### 3.5. gin && grbac.WithLoader 524 | 525 | ```go 526 | 527 | func QueryRolesByHeaders(header http.Header) (roles []string,err error){ 528 | // 在这里实现你的逻辑 529 | // ... 530 | // 这个逻辑可能是从请求的Headers中获取token,并且根据token从数据库中查询用户的相应角色。 531 | return roles, err 532 | } 533 | 534 | type MySQLLoader struct { 535 | session *gorm.DB 536 | } 537 | 538 | func NewMySQLLoader(dsn string) (*MySQLLoader, error) { 539 | loader := &MySQLLoader{} 540 | db, err := gorm.Open("mysql", dsn) 541 | if err != nil { 542 | return nil, err 543 | } 544 | loader.session = db 545 | return loader, nil 546 | } 547 | 548 | func (loader *MySQLLoader) LoadRules() (rules grbac.Rules, err error) { 549 | // 在这里实现你的逻辑 550 | // ... 551 | // 你可以从数据库或文件加载授权规则 552 | // 但是你需要以 grbac.Rules 的格式返回你的身份验证规则 553 | // 提示:你还可以将此函数绑定到golang结构体 554 | return 555 | } 556 | 557 | func Authentication() gin.HandlerFunc { 558 | loader, err := NewMySQLLoader("user:password@/dbname?charset=utf8&parseTime=True&loc=Local") 559 | if err != nil { 560 | panic(err) 561 | } 562 | rbac, err := grbac.New(grbac.WithLoader(loader.LoadRules, time.Second * 5)) 563 | if err != nil { 564 | panic(err) 565 | } 566 | return func(c *gin.Context) { 567 | roles, err := QueryRolesByHeaders(c.Request.Header) 568 | if err != nil { 569 | c.AbortWithStatus(http.StatusInternalServerError) 570 | return 571 | } 572 | 573 | state, err := rbac.IsRequestGranted(c.Request, roles) 574 | if err != nil { 575 | c.AbortWithStatus(http.StatusInternalServerError) 576 | return 577 | } 578 | if !state.IsGranted() { 579 | c.AbortWithStatus(http.StatusUnauthorized) 580 | return 581 | } 582 | } 583 | } 584 | 585 | func main(){ 586 | c := gin.New() 587 | c.Use(Authorization()) 588 | 589 | // 在这里通过c.Get、c.Post等函数绑定你的API 590 | // ... 591 | 592 | c.Run(":8080") 593 | } 594 | ``` 595 | 596 | ## 4. 增强的通配符 597 | 598 | `Wildcard`支持的语法: 599 | ```text 600 | pattern: 601 | { term } 602 | term: 603 | '*' 匹配任何非路径分隔符的字符串 604 | '**' 匹配任何字符串,包括路径分隔符. 605 | '?' 匹配任何单个非路径分隔符 606 | '[' [ '^' ] { character-range } ']' 607 | character class (must be non-empty) 608 | '{' { term } [ ',' { term } ... ] '}' 609 | c 匹配字符 c (c != '*', '?', '\\', '[') 610 | '\\' c 匹配字符 c 611 | 612 | character-range: 613 | c 匹配字符 c (c != '\\', '-', ']') 614 | '\\' c 匹配字符 c 615 | lo '-' hi 匹配字符 c for lo <= c <= hi 616 | ``` 617 | 618 | ## 5. 运行效率 619 | 620 | ```go 621 | ➜ gos test -bench=. 622 | goos: linux 623 | goarch: amd64 624 | pkg: github.com/storyicon/grbac/pkg/tree 625 | BenchmarkTree_Query 2000 541397 ns/op 626 | BenchmarkTree_Foreach_Query 2000 1360719 ns/op 627 | PASS 628 | ok github.com/storyicon/grbac/pkg/tree 13.182s 629 | ``` 630 | 631 | 测试用例包含1000个随机规则,“BenchmarkTree_Query”和“BenchmarkTree_Foreach_Query”函数分别测试四个请求: 632 | 633 | ``` 634 | 541397/(4*1e9)=0.0001s 635 | ``` 636 | 637 | 当有1000条规则时,每个请求的平均验证时间为“0.0001s”,这很快(大多数时间在通配符的匹配上)。 638 | 639 | ## 6. 生产环境 640 | 641 | `grbac` 已经被以下企业用于生产环境: 642 | 643 | ![wallstreetcn](https://raw.githubusercontent.com/storyicon/grbac/master/docs/screenshot/wallstreetcn.png) -------------------------------------------------------------------------------- /docs/screenshot/grbac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storyicon/grbac/a0461737df7eba2b06fd4a6a6a15159c407ba0c3/docs/screenshot/grbac.png -------------------------------------------------------------------------------- /docs/screenshot/wallstreetcn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storyicon/grbac/a0461737df7eba2b06fd4a6a6a15159c407ba0c3/docs/screenshot/wallstreetcn.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/storyicon/grbac 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/bxcodec/faker/v3 v3.1.0 7 | github.com/hashicorp/go-immutable-radix v1.1.0 8 | github.com/hashicorp/go-multierror v1.0.0 9 | github.com/json-iterator/go v1.1.6 10 | github.com/kr/pretty v0.1.0 // indirect 11 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 12 | github.com/modern-go/reflect2 v1.0.1 // indirect 13 | github.com/robfig/cron v1.1.0 14 | github.com/sirupsen/logrus v1.4.2 15 | github.com/stretchr/testify v1.3.0 16 | golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed // indirect 17 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 18 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bxcodec/faker/v3 v3.1.0 h1:VCCPusvvk1My6RjWFnqVbh6EdHDqjWmrHJCHduUksV0= 2 | github.com/bxcodec/faker/v3 v3.1.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 8 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 9 | github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= 10 | github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 11 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 12 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 13 | github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= 14 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 15 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 16 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 17 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 18 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 19 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 20 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 21 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 22 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 23 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 24 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 25 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 29 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/robfig/cron v1.1.0 h1:jk4/Hud3TTdcrJgUOBgsqrZBarcxl6ADIjSC2iniwLY= 33 | github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 34 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 35 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 39 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 40 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 41 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed h1:uPxWBzB3+mlnjy9W58qY1j/cjyFjutgw/Vhan2zLy/A= 43 | golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 46 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA= 48 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | -------------------------------------------------------------------------------- /grbac.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package grbac 16 | 17 | import ( 18 | "errors" 19 | "net/http" 20 | "sync" 21 | "time" 22 | 23 | "github.com/sirupsen/logrus" 24 | "github.com/storyicon/grbac/pkg/loader" 25 | "github.com/storyicon/grbac/pkg/meta" 26 | "github.com/storyicon/grbac/pkg/tree" 27 | ) 28 | 29 | // defines a set of errors 30 | var ( 31 | ErrInvalidRequest = errors.New("invalid request") 32 | ErrUndefinedLoader = errors.New("loader undefined") 33 | ) 34 | 35 | // Controller defines the structure of the controller 36 | type Controller struct { 37 | cron *time.Ticker 38 | loader func() (Rules, error) 39 | loadInterval time.Duration 40 | 41 | rules Rules 42 | rulesLock sync.RWMutex 43 | 44 | tree *tree.Tree 45 | treeLock sync.RWMutex 46 | 47 | logger *logrus.Logger 48 | } 49 | 50 | // ControllerOption provides an interface for user to define controller. 51 | type ControllerOption func(*Controller) error 52 | 53 | // WithJSON is used to load configuration via json file 54 | func WithJSON(name string, loadInterval time.Duration) ControllerOption { 55 | return func(c *Controller) error { 56 | fd, err := loader.NewJSONLoader(name) 57 | if err != nil { 58 | return err 59 | } 60 | c.loader = fd.Load 61 | c.loadInterval = loadInterval 62 | return nil 63 | } 64 | } 65 | 66 | // WithYAML is used to load configuration via yaml file 67 | func WithYAML(name string, loadInterval time.Duration) ControllerOption { 68 | return func(c *Controller) error { 69 | fd, err := loader.NewYAMLLoader(name) 70 | if err != nil { 71 | return err 72 | } 73 | c.loader = fd.Load 74 | c.loadInterval = loadInterval 75 | return nil 76 | } 77 | } 78 | 79 | // WithAdvancedRules provides a more concise way to define rules 80 | func WithAdvancedRules(rules loader.AdvancedRules) ControllerOption { 81 | return func(c *Controller) error { 82 | fd, err := loader.NewAdvancedRulesLoader(rules) 83 | if err != nil { 84 | return nil 85 | } 86 | c.loader = fd.Load 87 | c.loadInterval = -1 88 | return nil 89 | } 90 | } 91 | 92 | // WithRules is used to load config via user defined rules 93 | func WithRules(rules Rules) ControllerOption { 94 | return func(c *Controller) error { 95 | fd, err := loader.NewRulesLoader(rules) 96 | if err != nil { 97 | return nil 98 | } 99 | c.loader = fd.Load 100 | c.loadInterval = -1 101 | return nil 102 | } 103 | } 104 | 105 | // WithLoader provides a custom Loader entry that you can use to load arbitrary storage. 106 | func WithLoader(loader func() (Rules, error), loadInterval time.Duration) ControllerOption { 107 | return func(c *Controller) error { 108 | if loader == nil { 109 | return ErrUndefinedLoader 110 | } 111 | c.loader = loader 112 | c.loadInterval = loadInterval 113 | return nil 114 | } 115 | } 116 | 117 | // New is used to initialize an RBAC instance 118 | func New(loaderOptions ControllerOption, options ...ControllerOption) (*Controller, error) { 119 | c := &Controller{ 120 | logger: logrus.New(), 121 | } 122 | 123 | opts := append([]ControllerOption{loaderOptions}, options...) 124 | for _, opt := range opts { 125 | err := opt(c) 126 | if err != nil { 127 | return nil, err 128 | } 129 | } 130 | 131 | if c.loader == nil { 132 | return nil, ErrUndefinedLoader 133 | } 134 | 135 | err := c.reload() 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | go c.runCronTab() 141 | 142 | return c, nil 143 | } 144 | 145 | // SetLogger is used to modify the default logger 146 | func (c *Controller) SetLogger(logger *logrus.Logger) { 147 | if logger != nil { 148 | c.logger = logger 149 | } 150 | } 151 | 152 | func (c *Controller) reload() error { 153 | if c.loader == nil { 154 | return ErrUndefinedLoader 155 | } 156 | 157 | rules, err := c.loader() 158 | if err != nil { 159 | return err 160 | } 161 | 162 | err = rules.IsValid() 163 | if err != nil { 164 | return err 165 | } 166 | 167 | c.rulesLock.Lock() 168 | c.rules = rules 169 | c.rulesLock.Unlock() 170 | 171 | err = c.buildTree() 172 | if err != nil { 173 | return err 174 | } 175 | 176 | return nil 177 | } 178 | 179 | func (c *Controller) buildTree() error { 180 | t := tree.NewTree() 181 | c.rulesLock.RLock() 182 | defer c.rulesLock.RUnlock() 183 | for _, rule := range c.rules { 184 | t.Insert(rule.GetArguments(), rule) 185 | } 186 | c.treeLock.Lock() 187 | c.tree = t 188 | c.treeLock.Unlock() 189 | return nil 190 | } 191 | 192 | func (c *Controller) runCronTab() { 193 | if c.loadInterval < time.Second && c.loadInterval >= 0 { 194 | c.loadInterval = 5 * time.Second 195 | } 196 | if c.loadInterval < 0 { 197 | c.logger.Warning("grbac abandoned the periodic loader because loadInterval is less than 0") 198 | return 199 | } 200 | 201 | ticker := time.NewTicker(c.loadInterval) 202 | c.cron = ticker 203 | for { 204 | select { 205 | case <-ticker.C: 206 | c.logger.Debugln("grbac loader is scheduled") 207 | err := c.reload() 208 | if err != nil { 209 | c.logger.Errorln("error occurred while loading the configuration in grbac: ", err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func getQueryByRequest(r *http.Request) *Query { 216 | if r.URL == nil { 217 | return nil 218 | } 219 | return &Query{ 220 | Path: r.URL.Path, 221 | Host: r.Host, 222 | Method: r.Method, 223 | } 224 | } 225 | 226 | func (c *Controller) find(query *Query) (Rules, error) { 227 | c.treeLock.RLock() 228 | defer c.treeLock.RUnlock() 229 | records, err := c.tree.Query(query.GetArguments()) 230 | if err != nil { 231 | return nil, err 232 | } 233 | var perms Rules 234 | for _, record := range records { 235 | perm, ok := record.(*Rule) 236 | if !ok { 237 | continue 238 | } 239 | perms = append(perms, perm) 240 | } 241 | return perms, nil 242 | } 243 | 244 | // IsRequestGranted is used to verify whether a request has permission. 245 | // * The parameter roles is the role of the current user. 246 | func (c *Controller) IsRequestGranted(r *http.Request, roles []string) (PermissionState, error) { 247 | query := getQueryByRequest(r) 248 | if query == nil { 249 | return meta.PermissionUnknown, ErrInvalidRequest 250 | } 251 | return c.IsQueryGranted(query, roles) 252 | } 253 | 254 | // IsQueryGranted allows query permissions with the given Query parameter 255 | // * The parameter roles is the role of the current user. 256 | func (c *Controller) IsQueryGranted(q *Query, roles []string) (PermissionState, error) { 257 | rules, err := c.find(q) 258 | if err != nil { 259 | return meta.PermissionUnknown, err 260 | } 261 | return rules.IsRolesGranted(roles) 262 | } 263 | -------------------------------------------------------------------------------- /grbac_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package grbac 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/storyicon/grbac/pkg/meta" 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | type Result struct { 25 | State PermissionState 26 | Error error 27 | } 28 | 29 | func NewQuery(c *Controller, host, path, method string, roles []string) *Result { 30 | state, err := c.IsQueryGranted(&meta.Query{ 31 | Host: host, 32 | Path: path, 33 | Method: method, 34 | }, roles) 35 | return &Result{ 36 | State: state, 37 | Error: err, 38 | } 39 | } 40 | 41 | func TestNew(t *testing.T) { 42 | var rules Rules 43 | rules = append(rules, 44 | &Rule{Resource: &Resource{Host: `*`, Path: `**`, Method: `*`}, Permission: &Permission{AuthorizedRoles: []string{"*"}, ForbiddenRoles: []string{"black_user"}, AllowAnyone: false}}, 45 | &Rule{Resource: &Resource{Host: `domain.com`, Path: `**`, Method: `*`}, Permission: &Permission{AuthorizedRoles: []string{}, ForbiddenRoles: []string{}, AllowAnyone: true}}, 46 | &Rule{Resource: &Resource{Host: `dashboard-{prod,sit}.domain.com`, Path: `/config`, Method: `*`}, Permission: &Permission{AuthorizedRoles: []string{"sre"}, ForbiddenRoles: []string{}, AllowAnyone: false}}, 47 | &Rule{Resource: &Resource{Host: `pprof.domain.com`, Path: `/**`, Method: `*`}, Permission: &Permission{AuthorizedRoles: []string{"engineer", "sre"}, ForbiddenRoles: []string{}, AllowAnyone: false}}, 48 | &Rule{Resource: &Resource{Host: `pprof.domain.com`, Path: `/virtual/*`, Method: `*`}, Permission: &Permission{AuthorizedRoles: []string{}, ForbiddenRoles: []string{}, AllowAnyone: true}}, 49 | &Rule{Resource: &Resource{Host: `domain.com`, Path: `/api/**`, Method: `POST`}, Permission: &Permission{AuthorizedRoles: []string{"editor", "engineer"}, ForbiddenRoles: []string{}, AllowAnyone: false}}, 50 | &Rule{Resource: &Resource{Host: `domain.com`, Path: `/api/**`, Method: `DELETE`}, Permission: &Permission{AuthorizedRoles: []string{"super_editor"}, ForbiddenRoles: []string{}, AllowAnyone: false}}, 51 | &Rule{Resource: &Resource{Host: `x-domain.com`, Path: `/articles`, Method: `{DELETE,POST,PUT}`}, Permission: &Permission{AuthorizedRoles: []string{"editor"}, ForbiddenRoles: []string{}, AllowAnyone: false}}, 52 | &Rule{Resource: &Resource{Host: `x-domain.com`, Path: `/articles`, Method: `{GET}`}, Permission: &Permission{AuthorizedRoles: []string{}, ForbiddenRoles: []string{}, AllowAnyone: true}}, 53 | ) 54 | 55 | c, err := New(WithRules(rules)) 56 | assert.Equal(t, nil, err) 57 | 58 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "domain.com", "/index.html", "GET", []string{})) 59 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "dashboard-prod.domain.com", "/index.html", "GET", []string{"visitor"})) 60 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "dashboard-prod.domain.com", "/index.html", "GET", []string{"black_user"})) 61 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "dashboard-prod.domain.com", "/index.html", "GET", []string{})) 62 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "dashboard-sit.domain.com", "/index.html", "GET", []string{"visitor"})) 63 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "pprof.domain.com", "/index.html", "GET", []string{"visitor"})) 64 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "pprof.domain.com", "/index.html", "GET", []string{"engineer"})) 65 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "pprof.domain.com", "/virtual/f91fj2f1rj043rj9043e21esfhasdh09a", "GET", []string{})) 66 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "pprof.domain.com", "/config/get", "GET", []string{"sre"})) 67 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "pprof.domain.com", "/config/get", "GET", []string{"anyone"})) 68 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "domain.com", "/api/get", "POST", []string{"anyone"})) 69 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "domain.com", "/api/get", "POST", []string{})) 70 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "domain.com", "/api/get", "POST", []string{"editor"})) 71 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "domain.com", "/api/get", "POST", []string{"engineer"})) 72 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "domain.com", "/api/get", "DELETE", []string{"anyone"})) 73 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "domain.com", "/api/get", "DELETE", []string{"super_editor"})) 74 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "DELETE", []string{"editor"})) 75 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "POST", []string{"editor"})) 76 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "PUT", []string{"editor"})) 77 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "GET", []string{"editor"})) 78 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "DELETE", []string{"visitor"})) 79 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "POST", []string{"visitor"})) 80 | assert.Equal(t, &Result{State: meta.PermissionUngranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "PUT", []string{"visitor"})) 81 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "GET", []string{"visitor"})) 82 | assert.Equal(t, &Result{State: meta.PermissionGranted, Error: nil}, NewQuery(c, "x-domain.com", "/articles", "GET", []string{})) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/loader/advanced_rule.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package loader 16 | 17 | import ( 18 | "github.com/storyicon/grbac/pkg/meta" 19 | ) 20 | 21 | // AdvancedRule allows you to write RBAC rules in a more concise way 22 | type AdvancedRule struct { 23 | Host []string `json:"host"` 24 | Path []string `json:"path"` 25 | Method []string `json:"method"` 26 | 27 | *meta.Permission 28 | } 29 | 30 | // AdvancedRules is the list of AdvancedRules 31 | type AdvancedRules []*AdvancedRule 32 | 33 | // GetRules is used to convert AdvancedRules to meta.Rules 34 | func (adv AdvancedRules) GetRules() meta.Rules { 35 | var rules meta.Rules 36 | for _, item := range adv { 37 | for _, host := range item.Host { 38 | for _, path := range item.Path { 39 | for _, method := range item.Method { 40 | rules = append(rules, &meta.Rule{ 41 | Resource: &meta.Resource{ 42 | Host: host, 43 | Path: path, 44 | Method: method, 45 | }, 46 | Permission: item.Permission, 47 | }) 48 | } 49 | } 50 | } 51 | } 52 | return rules 53 | } 54 | 55 | // AdvancedRulesLoader implements the Loader interface 56 | // it is used to load configuration from advanced data. 57 | type AdvancedRulesLoader struct { 58 | rules AdvancedRules 59 | } 60 | 61 | // NewAdvancedRulesLoader is used to initialize a AdvancedRulesLoader 62 | func NewAdvancedRulesLoader(rules AdvancedRules) (*AdvancedRulesLoader, error) { 63 | return &AdvancedRulesLoader{ 64 | rules: rules, 65 | }, nil 66 | } 67 | 68 | // Load is used to return a list of rules 69 | func (loader *AdvancedRulesLoader) Load() (meta.Rules, error) { 70 | return loader.rules.GetRules(), nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/loader/json.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package loader 16 | 17 | import ( 18 | "io/ioutil" 19 | 20 | jsoniter "github.com/json-iterator/go" 21 | "github.com/storyicon/grbac/pkg/meta" 22 | ) 23 | 24 | // JSONLoader implements the Loader interface 25 | // it is used to load configuration from a local json file. 26 | type JSONLoader struct { 27 | path string 28 | } 29 | 30 | // NewJSONLoader is used to initialize a JSONLoader 31 | func NewJSONLoader(file string) (*JSONLoader, error) { 32 | loader := &JSONLoader{ 33 | path: file, 34 | } 35 | _, err := loader.Load() 36 | if err != nil { 37 | return nil, err 38 | } 39 | return loader, nil 40 | } 41 | 42 | // Load is used to return a list of rules 43 | func (loader *JSONLoader) Load() (meta.Rules, error) { 44 | bytes, err := ioutil.ReadFile(loader.path) 45 | if err != nil { 46 | return nil, err 47 | } 48 | rules := meta.Rules{} 49 | err = jsoniter.Unmarshal(bytes, &rules) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return rules, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/loader/rule.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package loader 16 | 17 | import ( 18 | "github.com/storyicon/grbac/pkg/meta" 19 | ) 20 | 21 | // RulesLoader implements the Loader interface 22 | // it is used to load configuration from given rules. 23 | type RulesLoader struct { 24 | rules meta.Rules 25 | } 26 | 27 | // NewRulesLoader is used to initialize a RulesLoader 28 | func NewRulesLoader(rules meta.Rules) (*RulesLoader, error) { 29 | return &RulesLoader{ 30 | rules: rules, 31 | }, nil 32 | } 33 | 34 | // Load is used to return a list of rules 35 | func (loader *RulesLoader) Load() (meta.Rules, error) { 36 | return loader.rules, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/loader/yaml.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package loader 16 | 17 | import ( 18 | "io/ioutil" 19 | 20 | "github.com/storyicon/grbac/pkg/meta" 21 | "gopkg.in/yaml.v3" 22 | ) 23 | 24 | // YAMLLoader implements the Loader interface 25 | // it is used to load configuration from a local yaml file. 26 | type YAMLLoader struct { 27 | path string 28 | } 29 | 30 | // NewYAMLLoader is used to initialize a YAMLLoader 31 | func NewYAMLLoader(file string) (*YAMLLoader, error) { 32 | loader := &YAMLLoader{ 33 | path: file, 34 | } 35 | _, err := loader.Load() 36 | if err != nil { 37 | return nil, err 38 | } 39 | return loader, nil 40 | } 41 | 42 | // Load is used to return a list of rules 43 | func (loader *YAMLLoader) Load() (meta.Rules, error) { 44 | bytes, err := ioutil.ReadFile(loader.path) 45 | if err != nil { 46 | return nil, err 47 | } 48 | rules := meta.Rules{} 49 | err = yaml.Unmarshal(bytes, &rules) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return rules, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/meta/meta.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import ( 18 | "errors" 19 | ) 20 | 21 | // define a set of errors 22 | var ( 23 | ErrFieldIncomplete = errors.New("incomplete fields") 24 | ErrEmptyStructure = errors.New("empty structure") 25 | ) 26 | -------------------------------------------------------------------------------- /pkg/meta/permission.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import ( 18 | "github.com/hashicorp/go-multierror" 19 | ) 20 | 21 | // Permissions is the set of Permission 22 | type Permissions []*Permission 23 | 24 | // Permission is used to define permission control information 25 | type Permission struct { 26 | // AuthorizedRoles defines roles that allow access to specified resource 27 | // Accepted type: non-empty string, * 28 | // *: means any role, but visitors should have at least one role, 29 | // non-empty string: specified role 30 | AuthorizedRoles []string `json:"authorized_roles" yaml:"authorized_roles"` 31 | // ForbiddenRoles defines roles that not allow access to specified resource 32 | // ForbiddenRoles has a higher priority than AuthorizedRoles 33 | // Accepted type: non-empty string, * 34 | // *: means any role, but visitors should have at least one role, 35 | // non-empty string: specified role 36 | // 37 | ForbiddenRoles []string `json:"forbidden_roles" yaml:"forbidden_roles"` 38 | // AllowAnyone has a higher priority than ForbiddenRoles/AuthorizedRoles 39 | // If set to true, anyone will be able to pass authentication. 40 | // Note that this will include people without any role. 41 | AllowAnyone bool `json:"allow_anyone" yaml:"allow_anyone"` 42 | } 43 | 44 | // IsValid is used to test the validity of the Rule 45 | func (p *Permission) IsValid() error { 46 | if p.AllowAnyone == false && len(p.AuthorizedRoles) == 0 && len(p.ForbiddenRoles) == 0 { 47 | return multierror.Prefix(ErrEmptyStructure, "permission: ") 48 | } 49 | return nil 50 | } 51 | 52 | // IsGranted is used to determine whether the given role can pass the authentication of *Permission. 53 | func (p *Permission) IsGranted(roles []string) (PermissionState, error) { 54 | if p.AllowAnyone { 55 | return PermissionGranted, nil 56 | } 57 | 58 | if len(roles) == 0 { 59 | return PermissionUngranted, nil 60 | } 61 | 62 | for _, role := range roles { 63 | for _, forbidden := range p.ForbiddenRoles { 64 | if forbidden == "*" || (role == forbidden) { 65 | return PermissionUngranted, nil 66 | } 67 | } 68 | for _, authorized := range p.AuthorizedRoles { 69 | if authorized == "*" || (role == authorized) { 70 | return PermissionGranted, nil 71 | } 72 | } 73 | } 74 | return PermissionUngranted, nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/meta/permission_state.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | // PermissionState identifies the status of the permission 18 | type PermissionState uint8 19 | 20 | const ( 21 | // PermissionUnknown is an initial state, usually specified when an error occurs 22 | PermissionUnknown PermissionState = iota 23 | // PermissionGranted means permission is granted 24 | PermissionGranted 25 | // PermissionUngranted means permission is ungranted 26 | PermissionUngranted 27 | // PermissionNeglected means could not find the matching rule in the list of rules 28 | PermissionNeglected 29 | ) 30 | 31 | // IsLooselyGranted is used to determine whether a request is authorized in a non-strict sense 32 | // It returns true when state equals PermissionGranted or PermissionNeglected 33 | // * This means if you forget to configure some addresses, they may be accessed by anyone. 34 | func (state PermissionState) IsLooselyGranted() bool { 35 | return (state == PermissionGranted) || (state == PermissionNeglected) 36 | } 37 | 38 | // IsNeglected is used to determine if the current state is equal to PermissionNeglected 39 | // PermissionNeglected means could not find the matching rule in the list of rules 40 | func (state PermissionState) IsNeglected() bool { 41 | return state == PermissionNeglected 42 | } 43 | 44 | // IsGranted is used to determine whether the current request is granted in a strict sense. 45 | // Note that it only returns true when state equals PermissionGranted 46 | // Because we recommend that you configure permissions for all possible requests to prevent forgetting to configure some addresses 47 | // * If you want it to return true when PermissionNeglected as well, you should use IsLooselyGranted 48 | func (state PermissionState) IsGranted() bool { 49 | return state == PermissionGranted 50 | } 51 | 52 | func (state PermissionState) String() string { 53 | switch state { 54 | case PermissionGranted: 55 | return "Permission Granted" 56 | case PermissionUngranted: 57 | return "Permission Ungranted" 58 | case PermissionNeglected: 59 | return "Permission Neglected" 60 | default: 61 | return "Permission Unknown" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/meta/permission_state_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import "testing" 18 | 19 | func TestPermissionState_IsLooselyGranted(t *testing.T) { 20 | tests := []struct { 21 | name string 22 | state PermissionState 23 | want bool 24 | }{ 25 | { 26 | name: "test0", 27 | state: PermissionNeglected, 28 | want: true, 29 | }, 30 | } 31 | for _, tt := range tests { 32 | if got := tt.state.IsLooselyGranted(); got != tt.want { 33 | t.Errorf("%q. PermissionState.IsLooselyGranted() = %v, want %v", tt.name, got, tt.want) 34 | } 35 | } 36 | } 37 | 38 | func TestPermissionState_IsNeglected(t *testing.T) { 39 | tests := []struct { 40 | name string 41 | state PermissionState 42 | want bool 43 | }{ 44 | { 45 | name: "test0", 46 | state: PermissionUngranted, 47 | want: false, 48 | }, 49 | { 50 | name: "test1", 51 | state: PermissionNeglected, 52 | want: true, 53 | }, 54 | } 55 | for _, tt := range tests { 56 | if got := tt.state.IsNeglected(); got != tt.want { 57 | t.Errorf("%q. PermissionState.IsNeglected() = %v, want %v", tt.name, got, tt.want) 58 | } 59 | } 60 | } 61 | 62 | func TestPermissionState_IsGranted(t *testing.T) { 63 | tests := []struct { 64 | name string 65 | state PermissionState 66 | want bool 67 | }{ 68 | { 69 | name: "test0", 70 | state: PermissionUngranted, 71 | want: false, 72 | }, 73 | { 74 | name: "test1", 75 | state: PermissionUnknown, 76 | want: false, 77 | }, 78 | { 79 | name: "test2", 80 | state: PermissionNeglected, 81 | want: false, 82 | }, 83 | } 84 | for _, tt := range tests { 85 | if got := tt.state.IsGranted(); got != tt.want { 86 | t.Errorf("%q. PermissionState.IsGranted() = %v, want %v", tt.name, got, tt.want) 87 | } 88 | } 89 | } 90 | 91 | func TestPermissionState_String(t *testing.T) { 92 | tests := []struct { 93 | name string 94 | state PermissionState 95 | want string 96 | }{ 97 | { 98 | name: "test0", 99 | state: PermissionNeglected, 100 | want: "Permission Neglected", 101 | }, 102 | } 103 | for _, tt := range tests { 104 | if got := tt.state.String(); got != tt.want { 105 | t.Errorf("%q. PermissionState.String() = %v, want %v", tt.name, got, tt.want) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/meta/permission_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | ) 21 | 22 | func TestPermission_IsValid(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | p *Permission 26 | wantErr bool 27 | }{ 28 | { 29 | name: "test0", 30 | p: &Permission{ 31 | AuthorizedRoles: []string{}, 32 | ForbiddenRoles: []string{}, 33 | AllowAnyone: false, 34 | }, 35 | wantErr: true, 36 | }, 37 | { 38 | name: "test1", 39 | p: &Permission{ 40 | AuthorizedRoles: []string{}, 41 | ForbiddenRoles: []string{}, 42 | AllowAnyone: true, 43 | }, 44 | wantErr: false, 45 | }, 46 | } 47 | for _, tt := range tests { 48 | if err := tt.p.IsValid(); (err != nil) != tt.wantErr { 49 | t.Errorf("%q. Permission.IsValid() error = %v, wantErr %v", tt.name, err, tt.wantErr) 50 | } 51 | } 52 | } 53 | 54 | func TestPermission_IsGranted(t *testing.T) { 55 | type args struct { 56 | roles []string 57 | } 58 | tests := []struct { 59 | name string 60 | p *Permission 61 | args args 62 | want PermissionState 63 | wantErr bool 64 | }{ 65 | { 66 | name: "test0", 67 | p: &Permission{ 68 | AllowAnyone: true, 69 | }, 70 | args: args{}, 71 | want: PermissionGranted, 72 | wantErr: false, 73 | }, 74 | { 75 | name: "test1", 76 | p: &Permission{ 77 | AuthorizedRoles: []string{"editor"}, 78 | AllowAnyone: false, 79 | }, 80 | args: args{roles: []string{"editor"}}, 81 | want: PermissionGranted, 82 | wantErr: false, 83 | }, 84 | } 85 | for _, tt := range tests { 86 | got, err := tt.p.IsGranted(tt.args.roles) 87 | if (err != nil) != tt.wantErr { 88 | t.Errorf("%q. Permission.IsGranted() error = %v, wantErr %v", tt.name, err, tt.wantErr) 89 | continue 90 | } 91 | if !reflect.DeepEqual(got, tt.want) { 92 | t.Errorf("%q. Permission.IsGranted() = %v, want %v", tt.name, got, tt.want) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/meta/query.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | // Query defines the data structure of the query parameters 18 | type Query Resource 19 | 20 | // GetArguments is used to convert the current argument to a string slice 21 | func (query *Query) GetArguments() []string { 22 | return []string{ 23 | query.Host, 24 | query.Path, 25 | query.Method, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/meta/query_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | ) 21 | 22 | func TestQuery_GetArguments(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | query *Query 26 | want []string 27 | }{ 28 | { 29 | name: "test0", 30 | query: &Query{ 31 | Host: "host", 32 | Path: "path", 33 | Method: "method", 34 | }, 35 | want: []string{"host", "path", "method"}, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | if got := tt.query.GetArguments(); !reflect.DeepEqual(got, tt.want) { 40 | t.Errorf("%q. Query.GetArguments() = %v, want %v", tt.name, got, tt.want) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/meta/resource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import ( 18 | "github.com/storyicon/grbac/pkg/path" 19 | ) 20 | 21 | // Resource defines resources 22 | type Resource struct { 23 | // Host defines the host of the resource, allowing wildcards to be used. 24 | Host string `json:"host" yaml:"host"` 25 | // Path defines the path of the resource, allowing wildcards to be used. 26 | Path string `json:"path" yaml:"path"` 27 | // Method defines the method of the resource, allowing wildcards to be used. 28 | Method string `json:"method" yaml:"method"` 29 | } 30 | 31 | // Match is used to calculate whether the query matches the resource 32 | func (r *Resource) Match(query *Query) (bool, error) { 33 | args := query.GetArguments() 34 | for i, res := range r.GetArguments() { 35 | matched, err := path.Match(res, args[i]) 36 | if err != nil { 37 | return false, err 38 | } 39 | if !matched { 40 | return false, nil 41 | } 42 | } 43 | return true, nil 44 | } 45 | 46 | // GetArguments is used to convert the current argument to a string slice 47 | func (r *Resource) GetArguments() []string { 48 | return []string{ 49 | r.Host, 50 | r.Path, 51 | r.Method, 52 | } 53 | } 54 | 55 | // IsValid is used to test the validity of the Rule 56 | func (r *Resource) IsValid() error { 57 | if r.Host == "" || r.Method == "" || r.Path == "" { 58 | return ErrFieldIncomplete 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/meta/resource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | ) 21 | 22 | func TestResource_Match(t *testing.T) { 23 | type fields struct { 24 | Host string 25 | Path string 26 | Method string 27 | } 28 | type args struct { 29 | query *Query 30 | } 31 | tests := []struct { 32 | name string 33 | fields fields 34 | args args 35 | want bool 36 | wantErr bool 37 | }{ 38 | { 39 | name: "test0", 40 | fields: fields{ 41 | Host: "*", 42 | Path: "*", 43 | Method: "*", 44 | }, 45 | args: args{ 46 | query: &Query{ 47 | Host: "host", 48 | Path: "path", 49 | Method: "method", 50 | }, 51 | }, 52 | want: true, 53 | wantErr: false, 54 | }, 55 | { 56 | name: "test1", 57 | fields: fields{ 58 | Host: "host", 59 | Path: "path", 60 | Method: "method", 61 | }, 62 | args: args{ 63 | query: &Query{ 64 | Host: "host", 65 | Path: "path", 66 | Method: "method", 67 | }, 68 | }, 69 | want: true, 70 | wantErr: false, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | r := &Resource{ 75 | Host: tt.fields.Host, 76 | Path: tt.fields.Path, 77 | Method: tt.fields.Method, 78 | } 79 | got, err := r.Match(tt.args.query) 80 | if (err != nil) != tt.wantErr { 81 | t.Errorf("%q. Resource.Match() error = %v, wantErr %v", tt.name, err, tt.wantErr) 82 | continue 83 | } 84 | if got != tt.want { 85 | t.Errorf("%q. Resource.Match() = %v, want %v", tt.name, got, tt.want) 86 | } 87 | } 88 | } 89 | 90 | func TestResource_GetArguments(t *testing.T) { 91 | type fields struct { 92 | Host string 93 | Path string 94 | Method string 95 | } 96 | tests := []struct { 97 | name string 98 | fields fields 99 | want []string 100 | }{ 101 | { 102 | name: "test0", 103 | fields: fields{ 104 | Host: "host", 105 | Path: "path", 106 | Method: "method", 107 | }, 108 | want: []string{"host", "path", "method"}, 109 | }, 110 | { 111 | name: "test1", 112 | fields: fields{ 113 | Host: "", 114 | Path: "", 115 | Method: "", 116 | }, 117 | want: []string{"", "", ""}, 118 | }, 119 | } 120 | for _, tt := range tests { 121 | r := &Resource{ 122 | Host: tt.fields.Host, 123 | Path: tt.fields.Path, 124 | Method: tt.fields.Method, 125 | } 126 | if got := r.GetArguments(); !reflect.DeepEqual(got, tt.want) { 127 | t.Errorf("%q. Resource.GetArguments() = %v, want %v", tt.name, got, tt.want) 128 | } 129 | } 130 | } 131 | 132 | func TestResource_IsValid(t *testing.T) { 133 | type fields struct { 134 | Host string 135 | Path string 136 | Method string 137 | } 138 | tests := []struct { 139 | name string 140 | fields fields 141 | wantErr bool 142 | }{ 143 | { 144 | name: "test0", 145 | fields: fields{}, 146 | wantErr: true, 147 | }, 148 | { 149 | name: "test1", 150 | fields: fields{ 151 | Host: "host", 152 | Path: "path", 153 | Method: "method", 154 | }, 155 | wantErr: false, 156 | }, 157 | { 158 | name: "test2", 159 | fields: fields{ 160 | Host: "host", 161 | Path: "", 162 | Method: "method", 163 | }, 164 | wantErr: true, 165 | }, 166 | } 167 | for _, tt := range tests { 168 | r := &Resource{ 169 | Host: tt.fields.Host, 170 | Path: tt.fields.Path, 171 | Method: tt.fields.Method, 172 | } 173 | if err := r.IsValid(); (err != nil) != tt.wantErr { 174 | t.Errorf("%q. Resource.IsValid() error = %v, wantErr %v", tt.name, err, tt.wantErr) 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /pkg/meta/rule.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import ( 18 | "github.com/hashicorp/go-multierror" 19 | jsoniter "github.com/json-iterator/go" 20 | ) 21 | 22 | // Rules is the list of Rule 23 | type Rules []*Rule 24 | 25 | // Rule is used to define the relationship between "resource" and "permission" 26 | type Rule struct { 27 | // The ID controls the priority of the rule. 28 | // The higher the ID means the higher the priority of the rule. 29 | // When a request is matched to more than one rule, 30 | // then authentication will only use the permission configuration for the rule with the highest ID value. 31 | // If there are multiple rules that are the largest ID, then one of them will be used randomly. 32 | ID int `json:"id" yaml:"id"` 33 | *Resource `yaml:",inline"` 34 | *Permission `yaml:",inline"` 35 | } 36 | 37 | // IsValid is used to test the validity of the Rule 38 | func (rule *Rule) IsValid() error { 39 | if rule.Resource == nil || rule.Permission == nil { 40 | return ErrEmptyStructure 41 | } 42 | err := rule.Resource.IsValid() 43 | if err != nil { 44 | return err 45 | } 46 | return rule.Permission.IsValid() 47 | } 48 | 49 | // IsValid is used to test the validity of the Rule 50 | func (rules Rules) IsValid() error { 51 | var errs error 52 | for _, rule := range rules { 53 | err := rule.IsValid() 54 | if err != nil { 55 | errs = multierror.Append(errs, err) 56 | } 57 | } 58 | if errs != nil { 59 | return errs 60 | } 61 | return nil 62 | } 63 | 64 | // IsRolesGranted is used to determine whether the current role is admitted by the current rule. 65 | func (rules Rules) IsRolesGranted(roles []string) (PermissionState, error) { 66 | if len(rules) == 0 { 67 | return PermissionNeglected, nil 68 | } 69 | tail := rules[0] 70 | for i := 0; i < len(rules); i++ { 71 | if tail.ID <= rules[i].ID { 72 | tail = rules[i] 73 | } 74 | } 75 | return tail.IsGranted(roles) 76 | } 77 | 78 | func (rules Rules) String() string { 79 | s, _ := jsoniter.MarshalToString(rules) 80 | return s 81 | } 82 | -------------------------------------------------------------------------------- /pkg/meta/rule_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package meta 16 | 17 | import "testing" 18 | 19 | func TestRule_IsValid(t *testing.T) { 20 | type fields struct { 21 | ID int 22 | Resource *Resource 23 | Permission *Permission 24 | } 25 | tests := []struct { 26 | name string 27 | fields fields 28 | wantErr bool 29 | }{ 30 | { 31 | name: "test0", 32 | fields: fields{ 33 | ID: 0, 34 | Resource: nil, 35 | Permission: nil, 36 | }, 37 | wantErr: true, 38 | }, 39 | { 40 | name: "test1", 41 | fields: fields{ 42 | ID: 0, 43 | Resource: nil, 44 | Permission: &Permission{}, 45 | }, 46 | wantErr: true, 47 | }, 48 | { 49 | name: "test2", 50 | fields: fields{ 51 | ID: 0, 52 | Resource: &Resource{}, 53 | Permission: nil, 54 | }, 55 | wantErr: true, 56 | }, 57 | { 58 | name: "test3", 59 | fields: fields{ 60 | ID: 0, 61 | Resource: &Resource{}, 62 | Permission: &Permission{}, 63 | }, 64 | wantErr: true, 65 | }, 66 | } 67 | for _, tt := range tests { 68 | rule := &Rule{ 69 | ID: tt.fields.ID, 70 | Resource: tt.fields.Resource, 71 | Permission: tt.fields.Permission, 72 | } 73 | if err := rule.IsValid(); (err != nil) != tt.wantErr { 74 | t.Errorf("%q. Rule.IsValid() error = %v, wantErr %v", tt.name, err, tt.wantErr) 75 | } 76 | } 77 | } 78 | 79 | func TestRules_IsValid(t *testing.T) { 80 | tests := []struct { 81 | name string 82 | rules Rules 83 | wantErr bool 84 | }{ 85 | { 86 | name: "test0", 87 | rules: Rules{}, 88 | wantErr: false, 89 | }, 90 | { 91 | name: "test1", 92 | rules: Rules{ 93 | {}, 94 | }, 95 | wantErr: true, 96 | }, 97 | } 98 | for _, tt := range tests { 99 | if err := tt.rules.IsValid(); (err != nil) != tt.wantErr { 100 | t.Errorf("%q. Rules.IsValid() error = %v, wantErr %v", tt.name, err, tt.wantErr) 101 | } 102 | } 103 | } 104 | 105 | func TestRules_IsRolesGranted(t *testing.T) { 106 | type args struct { 107 | roles []string 108 | } 109 | tests := []struct { 110 | name string 111 | rules Rules 112 | args args 113 | want PermissionState 114 | wantErr bool 115 | }{ 116 | { 117 | name: "test0", 118 | rules: Rules{}, 119 | args: args{}, 120 | want: PermissionNeglected, 121 | wantErr: false, 122 | }, 123 | { 124 | name: "test1", 125 | rules: Rules{ 126 | { 127 | Permission: &Permission{ 128 | AuthorizedRoles: []string{"visitor"}, 129 | }, 130 | Resource: &Resource{Host: "test"}, 131 | }, 132 | }, 133 | args: args{ 134 | roles: []string{"editor"}, 135 | }, 136 | want: PermissionUngranted, 137 | wantErr: false, 138 | }, 139 | { 140 | name: "test1", 141 | rules: Rules{ 142 | { 143 | Permission: &Permission{ 144 | AuthorizedRoles: []string{"visitor"}, 145 | }, 146 | Resource: &Resource{Host: "test"}, 147 | }, 148 | }, 149 | args: args{ 150 | roles: []string{"editor", "visitor"}, 151 | }, 152 | want: PermissionGranted, 153 | wantErr: false, 154 | }, 155 | { 156 | name: "test2", 157 | rules: Rules{ 158 | { 159 | Permission: &Permission{ 160 | AuthorizedRoles: []string{"*"}, 161 | }, 162 | Resource: &Resource{Host: "test"}, 163 | }, 164 | }, 165 | args: args{ 166 | roles: []string{"editor", "visitor"}, 167 | }, 168 | want: PermissionGranted, 169 | wantErr: false, 170 | }, 171 | { 172 | name: "test3", 173 | rules: Rules{ 174 | { 175 | Permission: &Permission{ 176 | AuthorizedRoles: []string{"*"}, 177 | }, 178 | Resource: &Resource{Host: "test"}, 179 | }, 180 | }, 181 | args: args{ 182 | roles: []string{}, 183 | }, 184 | want: PermissionUngranted, 185 | wantErr: false, 186 | }, 187 | } 188 | for _, tt := range tests { 189 | got, err := tt.rules.IsRolesGranted(tt.args.roles) 190 | if (err != nil) != tt.wantErr { 191 | t.Errorf("%q. Rules.IsRolesGranted() error = %v, wantErr %v", tt.name, err, tt.wantErr) 192 | continue 193 | } 194 | if got != tt.want { 195 | t.Errorf("%q. Rules.IsRolesGranted() = %v, want %v", tt.name, got, tt.want) 196 | } 197 | } 198 | } 199 | 200 | func TestRules_String(t *testing.T) { 201 | tests := []struct { 202 | name string 203 | rules Rules 204 | want string 205 | }{ 206 | { 207 | name: "test0", 208 | rules: Rules{}, 209 | want: "[]", 210 | }, 211 | } 212 | for _, tt := range tests { 213 | if got := tt.rules.String(); got != tt.want { 214 | t.Errorf("%q. Rules.String() = %v, want %v", tt.name, got, tt.want) 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /pkg/path/doublestar/doublestar.go: -------------------------------------------------------------------------------- 1 | // Source Code: https://github.com/bmatcuk/doublestar 2 | // Copyright: bmatuck 3 | // Author Github: https://github.com/bmatcuk 4 | 5 | package doublestar 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | "unicode/utf8" 14 | ) 15 | 16 | // ErrBadPattern indicates a pattern was malformed. 17 | var ErrBadPattern = path.ErrBadPattern 18 | 19 | // Split a path on the given separator, respecting escaping. 20 | func splitPathOnSeparator(path string, separator rune) (ret []string) { 21 | idx := 0 22 | if separator == '\\' { 23 | // if the separator is '\\', then we can just split... 24 | ret = strings.Split(path, string(separator)) 25 | idx = len(ret) 26 | } else { 27 | // otherwise, we need to be careful of situations where the separator was escaped 28 | cnt := strings.Count(path, string(separator)) 29 | if cnt == 0 { 30 | return []string{path} 31 | } 32 | 33 | ret = make([]string, cnt+1) 34 | pathlen := len(path) 35 | separatorLen := utf8.RuneLen(separator) 36 | emptyEnd := false 37 | for start := 0; start < pathlen; { 38 | end := indexRuneWithEscaping(path[start:], separator) 39 | if end == -1 { 40 | emptyEnd = false 41 | end = pathlen 42 | } else { 43 | emptyEnd = true 44 | end += start 45 | } 46 | ret[idx] = path[start:end] 47 | start = end + separatorLen 48 | idx++ 49 | } 50 | 51 | // If the last rune is a path separator, we need to append an empty string to 52 | // represent the last, empty path component. By default, the strings from 53 | // make([]string, ...) will be empty, so we just need to icrement the count 54 | if emptyEnd { 55 | idx++ 56 | } 57 | } 58 | 59 | return ret[:idx] 60 | } 61 | 62 | // Find the first index of a rune in a string, 63 | // ignoring any times the rune is escaped using "\". 64 | func indexRuneWithEscaping(s string, r rune) int { 65 | end := strings.IndexRune(s, r) 66 | if end == -1 { 67 | return -1 68 | } 69 | if end > 0 && s[end-1] == '\\' { 70 | start := end + utf8.RuneLen(r) 71 | end = indexRuneWithEscaping(s[start:], r) 72 | if end != -1 { 73 | end += start 74 | } 75 | } 76 | return end 77 | } 78 | 79 | // Match returns true if name matches the shell file name pattern. 80 | // The pattern syntax is: 81 | // 82 | // pattern: 83 | // { term } 84 | // term: 85 | // '*' matches any sequence of non-path-separators 86 | // '**' matches any sequence of characters, including 87 | // path separators. 88 | // '?' matches any single non-path-separator character 89 | // '[' [ '^' ] { character-range } ']' 90 | // character class (must be non-empty) 91 | // '{' { term } [ ',' { term } ... ] '}' 92 | // c matches character c (c != '*', '?', '\\', '[') 93 | // '\\' c matches character c 94 | // 95 | // character-range: 96 | // c matches character c (c != '\\', '-', ']') 97 | // '\\' c matches character c 98 | // lo '-' hi matches character c for lo <= c <= hi 99 | // 100 | // Match requires pattern to match all of name, not just a substring. 101 | // The path-separator defaults to the '/' character. The only possible 102 | // returned error is ErrBadPattern, when pattern is malformed. 103 | // 104 | // Note: this is meant as a drop-in replacement for path.Match() which 105 | // always uses '/' as the path separator. If you want to support systems 106 | // which use a different path separator (such as Windows), what you want 107 | // is the PathMatch() function below. 108 | // 109 | func Match(pattern, name string) (bool, error) { 110 | return matchWithSeparator(pattern, name, '/') 111 | } 112 | 113 | // PathMatch is like Match except that it uses your system's path separator. 114 | // For most systems, this will be '/'. However, for Windows, it would be '\\'. 115 | // Note that for systems where the path separator is '\\', escaping is 116 | // disabled. 117 | // 118 | // Note: this is meant as a drop-in replacement for filepath.Match(). 119 | // 120 | func PathMatch(pattern, name string) (bool, error) { 121 | return matchWithSeparator(pattern, name, os.PathSeparator) 122 | } 123 | 124 | // Match returns true if name matches the shell file name pattern. 125 | // The pattern syntax is: 126 | // 127 | // pattern: 128 | // { term } 129 | // term: 130 | // '*' matches any sequence of non-path-separators 131 | // '**' matches any sequence of characters, including 132 | // path separators. 133 | // '?' matches any single non-path-separator character 134 | // '[' [ '^' ] { character-range } ']' 135 | // character class (must be non-empty) 136 | // '{' { term } [ ',' { term } ... ] '}' 137 | // c matches character c (c != '*', '?', '\\', '[') 138 | // '\\' c matches character c 139 | // 140 | // character-range: 141 | // c matches character c (c != '\\', '-', ']') 142 | // '\\' c matches character c, unless separator is '\\' 143 | // lo '-' hi matches character c for lo <= c <= hi 144 | // 145 | // Match requires pattern to match all of name, not just a substring. 146 | // The only possible returned error is ErrBadPattern, when pattern 147 | // is malformed. 148 | // 149 | func matchWithSeparator(pattern, name string, separator rune) (bool, error) { 150 | patternComponents := splitPathOnSeparator(pattern, separator) 151 | nameComponents := splitPathOnSeparator(name, separator) 152 | return doMatching(patternComponents, nameComponents) 153 | } 154 | 155 | func doMatching(patternComponents, nameComponents []string) (matched bool, err error) { 156 | // check for some base-cases 157 | patternLen, nameLen := len(patternComponents), len(nameComponents) 158 | if patternLen == 0 && nameLen == 0 { 159 | return true, nil 160 | } 161 | if patternLen == 0 || nameLen == 0 { 162 | return false, nil 163 | } 164 | 165 | patIdx, nameIdx := 0, 0 166 | for patIdx < patternLen && nameIdx < nameLen { 167 | if patternComponents[patIdx] == "**" { 168 | // if our last pattern component is a doublestar, we're done - 169 | // doublestar will match any remaining name components, if any. 170 | if patIdx++; patIdx >= patternLen { 171 | return true, nil 172 | } 173 | 174 | // otherwise, try matching remaining components 175 | for ; nameIdx < nameLen; nameIdx++ { 176 | if m, _ := doMatching(patternComponents[patIdx:], nameComponents[nameIdx:]); m { 177 | return true, nil 178 | } 179 | } 180 | return false, nil 181 | } 182 | 183 | // try matching components 184 | matched, err = matchComponent(patternComponents[patIdx], nameComponents[nameIdx]) 185 | if !matched || err != nil { 186 | return 187 | } 188 | 189 | patIdx++ 190 | nameIdx++ 191 | } 192 | return patIdx >= patternLen && nameIdx >= nameLen, nil 193 | } 194 | 195 | // Glob returns the names of all files matching pattern or nil 196 | // if there is no matching file. The syntax of pattern is the same 197 | // as in Match. The pattern may describe hierarchical names such as 198 | // /usr/*/bin/ed (assuming the Separator is '/'). 199 | // 200 | // Glob ignores file system errors such as I/O errors reading directories. 201 | // The only possible returned error is ErrBadPattern, when pattern 202 | // is malformed. 203 | // 204 | // Your system path separator is automatically used. This means on 205 | // systems where the separator is '\\' (Windows), escaping will be 206 | // disabled. 207 | // 208 | // Note: this is meant as a drop-in replacement for filepath.Glob(). 209 | // 210 | func Glob(pattern string) (matches []string, err error) { 211 | patternComponents := splitPathOnSeparator(filepath.ToSlash(pattern), '/') 212 | if len(patternComponents) == 0 { 213 | return nil, nil 214 | } 215 | 216 | // On Windows systems, this will return the drive name ('C:'), on others, 217 | // it will return an empty string. 218 | volumeName := filepath.VolumeName(pattern) 219 | 220 | // If the first pattern component is equal to the volume name, then the 221 | // pattern is an absolute path. 222 | if patternComponents[0] == volumeName { 223 | return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), patternComponents[1:], matches) 224 | } 225 | 226 | // otherwise, it's a relative pattern 227 | return doGlob(".", patternComponents, matches) 228 | } 229 | 230 | // Perform a glob 231 | func doGlob(basedir string, components, matches []string) (m []string, e error) { 232 | m = matches 233 | e = nil 234 | 235 | // figure out how many components we don't need to glob because they're 236 | // just names without patterns - we'll use os.Lstat below to check if that 237 | // path actually exists 238 | patLen := len(components) 239 | patIdx := 0 240 | for ; patIdx < patLen; patIdx++ { 241 | if strings.IndexAny(components[patIdx], "*?[{\\") >= 0 { 242 | break 243 | } 244 | } 245 | if patIdx > 0 { 246 | basedir = filepath.Join(basedir, filepath.Join(components[0:patIdx]...)) 247 | } 248 | 249 | // Lstat will return an error if the file/directory doesn't exist 250 | fi, err := os.Lstat(basedir) 251 | if err != nil { 252 | return 253 | } 254 | 255 | // if there are no more components, we've found a match 256 | if patIdx >= patLen { 257 | m = append(m, basedir) 258 | return 259 | } 260 | 261 | // otherwise, we need to check each item in the directory... 262 | // first, if basedir is a symlink, follow it... 263 | if (fi.Mode() & os.ModeSymlink) != 0 { 264 | fi, err = os.Stat(basedir) 265 | if err != nil { 266 | return 267 | } 268 | } 269 | 270 | // confirm it's a directory... 271 | if !fi.IsDir() { 272 | return 273 | } 274 | 275 | // read directory 276 | dir, err := os.Open(basedir) 277 | if err != nil { 278 | return 279 | } 280 | defer dir.Close() 281 | 282 | files, _ := dir.Readdir(-1) 283 | lastComponent := (patIdx + 1) >= patLen 284 | if components[patIdx] == "**" { 285 | // if the current component is a doublestar, we'll try depth-first 286 | for _, file := range files { 287 | // if symlink, we may want to follow 288 | if (file.Mode() & os.ModeSymlink) != 0 { 289 | file, err = os.Stat(filepath.Join(basedir, file.Name())) 290 | if err != nil { 291 | continue 292 | } 293 | } 294 | 295 | if file.IsDir() { 296 | // recurse into directories 297 | if lastComponent { 298 | m = append(m, filepath.Join(basedir, file.Name())) 299 | } 300 | m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx:], m) 301 | } else if lastComponent { 302 | // if the pattern's last component is a doublestar, we match filenames, too 303 | m = append(m, filepath.Join(basedir, file.Name())) 304 | } 305 | } 306 | if lastComponent { 307 | return // we're done 308 | } 309 | patIdx++ 310 | lastComponent = (patIdx + 1) >= patLen 311 | } 312 | 313 | // check items in current directory and recurse 314 | var match bool 315 | for _, file := range files { 316 | match, e = matchComponent(components[patIdx], file.Name()) 317 | if e != nil { 318 | return 319 | } 320 | if match { 321 | if lastComponent { 322 | m = append(m, filepath.Join(basedir, file.Name())) 323 | } else { 324 | m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx+1:], m) 325 | } 326 | } 327 | } 328 | return 329 | } 330 | 331 | // Attempt to match a single pattern component with a path component 332 | func matchComponent(pattern, name string) (bool, error) { 333 | // check some base cases 334 | patternLen, nameLen := len(pattern), len(name) 335 | if patternLen == 0 && nameLen == 0 { 336 | return true, nil 337 | } 338 | if patternLen == 0 { 339 | return false, nil 340 | } 341 | if nameLen == 0 && pattern != "*" { 342 | return false, nil 343 | } 344 | 345 | // check for matches one rune at a time 346 | patIdx, nameIdx := 0, 0 347 | for patIdx < patternLen && nameIdx < nameLen { 348 | patRune, patAdj := utf8.DecodeRuneInString(pattern[patIdx:]) 349 | nameRune, nameAdj := utf8.DecodeRuneInString(name[nameIdx:]) 350 | if patRune == '\\' { 351 | // handle escaped runes 352 | patIdx += patAdj 353 | patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]) 354 | if patRune == utf8.RuneError { 355 | return false, ErrBadPattern 356 | } else if patRune == nameRune { 357 | patIdx += patAdj 358 | nameIdx += nameAdj 359 | } else { 360 | return false, nil 361 | } 362 | } else if patRune == '*' { 363 | // handle stars 364 | if patIdx += patAdj; patIdx >= patternLen { 365 | // a star at the end of a pattern will always 366 | // match the rest of the path 367 | return true, nil 368 | } 369 | 370 | // check if we can make any matches 371 | for ; nameIdx < nameLen; nameIdx += nameAdj { 372 | if m, _ := matchComponent(pattern[patIdx:], name[nameIdx:]); m { 373 | return true, nil 374 | } 375 | } 376 | return false, nil 377 | } else if patRune == '[' { 378 | // handle character sets 379 | patIdx += patAdj 380 | endClass := indexRuneWithEscaping(pattern[patIdx:], ']') 381 | if endClass == -1 { 382 | return false, ErrBadPattern 383 | } 384 | endClass += patIdx 385 | classRunes := []rune(pattern[patIdx:endClass]) 386 | classRunesLen := len(classRunes) 387 | if classRunesLen > 0 { 388 | classIdx := 0 389 | matchClass := false 390 | if classRunes[0] == '^' { 391 | classIdx++ 392 | } 393 | for classIdx < classRunesLen { 394 | low := classRunes[classIdx] 395 | if low == '-' { 396 | return false, ErrBadPattern 397 | } 398 | classIdx++ 399 | if low == '\\' { 400 | if classIdx < classRunesLen { 401 | low = classRunes[classIdx] 402 | classIdx++ 403 | } else { 404 | return false, ErrBadPattern 405 | } 406 | } 407 | high := low 408 | if classIdx < classRunesLen && classRunes[classIdx] == '-' { 409 | // we have a range of runes 410 | if classIdx++; classIdx >= classRunesLen { 411 | return false, ErrBadPattern 412 | } 413 | high = classRunes[classIdx] 414 | if high == '-' { 415 | return false, ErrBadPattern 416 | } 417 | classIdx++ 418 | if high == '\\' { 419 | if classIdx < classRunesLen { 420 | high = classRunes[classIdx] 421 | classIdx++ 422 | } else { 423 | return false, ErrBadPattern 424 | } 425 | } 426 | } 427 | if low <= nameRune && nameRune <= high { 428 | matchClass = true 429 | } 430 | } 431 | if matchClass == (classRunes[0] == '^') { 432 | return false, nil 433 | } 434 | } else { 435 | return false, ErrBadPattern 436 | } 437 | patIdx = endClass + 1 438 | nameIdx += nameAdj 439 | } else if patRune == '{' { 440 | // handle alternatives such as {alt1,alt2,...} 441 | patIdx += patAdj 442 | endOptions := indexRuneWithEscaping(pattern[patIdx:], '}') 443 | if endOptions == -1 { 444 | return false, ErrBadPattern 445 | } 446 | endOptions += patIdx 447 | options := splitPathOnSeparator(pattern[patIdx:endOptions], ',') 448 | patIdx = endOptions + 1 449 | for _, o := range options { 450 | m, e := matchComponent(o+pattern[patIdx:], name[nameIdx:]) 451 | if e != nil { 452 | return false, e 453 | } 454 | if m { 455 | return true, nil 456 | } 457 | } 458 | return false, nil 459 | } else if patRune == '?' || patRune == nameRune { 460 | // handle single-rune wildcard 461 | patIdx += patAdj 462 | nameIdx += nameAdj 463 | } else { 464 | return false, nil 465 | } 466 | } 467 | if patIdx >= patternLen && nameIdx >= nameLen { 468 | return true, nil 469 | } 470 | if nameIdx >= nameLen && pattern[patIdx:] == "*" || pattern[patIdx:] == "**" { 471 | return true, nil 472 | } 473 | return false, nil 474 | } 475 | -------------------------------------------------------------------------------- /pkg/path/doublestar/doublestar_test.go: -------------------------------------------------------------------------------- 1 | // This file is mostly copied from Go's path/match_test.go 2 | 3 | package doublestar 4 | 5 | import ( 6 | "path" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type MatchTest struct { 14 | pattern, testPath []string // a pattern and path to test the pattern on 15 | shouldMatch bool // true if the pattern should match the path 16 | expectedErr error // an expected error 17 | testOnDisk bool // true: test pattern against files in "test" directory 18 | } 19 | 20 | // Tests which contain escapes and symlinks will not work on Windows 21 | var onWindows = runtime.GOOS == "windows" 22 | 23 | var matchTests = []MatchTest{ 24 | {[]string{"*"}, []string{""}, true, nil, false}, 25 | {[]string{"*"}, []string{"/"}, false, nil, false}, 26 | {[]string{"/*"}, []string{"/"}, true, nil, false}, 27 | {[]string{"/*"}, []string{"/debug/"}, false, nil, false}, 28 | {[]string{"abc"}, []string{"abc"}, true, nil, true}, 29 | {[]string{"*"}, []string{"abc"}, true, nil, true}, 30 | {[]string{"*c"}, []string{"abc"}, true, nil, true}, 31 | {[]string{"a*"}, []string{"a"}, true, nil, true}, 32 | {[]string{"a*"}, []string{"abc"}, true, nil, true}, 33 | {[]string{"a*"}, []string{"ab", "c"}, false, nil, true}, 34 | {[]string{"a*", "b"}, []string{"abc", "b"}, true, nil, true}, 35 | {[]string{"a*", "b"}, []string{"a", "c", "b"}, false, nil, true}, 36 | {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxe", "f"}, true, nil, true}, 37 | {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxexxx", "f"}, true, nil, true}, 38 | {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxe", "xxx", "f"}, false, nil, true}, 39 | {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxexxx", "fff"}, false, nil, true}, 40 | {[]string{"a*b?c*x"}, []string{"abxbbxdbxebxczzx"}, true, nil, true}, 41 | {[]string{"a*b?c*x"}, []string{"abxbbxdbxebxczzy"}, false, nil, true}, 42 | {[]string{"ab[c]"}, []string{"abc"}, true, nil, true}, 43 | {[]string{"ab[b-d]"}, []string{"abc"}, true, nil, true}, 44 | {[]string{"ab[e-g]"}, []string{"abc"}, false, nil, true}, 45 | {[]string{"ab[^c]"}, []string{"abc"}, false, nil, true}, 46 | {[]string{"ab[^b-d]"}, []string{"abc"}, false, nil, true}, 47 | {[]string{"ab[^e-g]"}, []string{"abc"}, true, nil, true}, 48 | {[]string{"a\\*b"}, []string{"ab"}, false, nil, true}, 49 | {[]string{"a?b"}, []string{"a☺b"}, true, nil, true}, 50 | {[]string{"a[^a]b"}, []string{"a☺b"}, true, nil, true}, 51 | {[]string{"a???b"}, []string{"a☺b"}, false, nil, true}, 52 | {[]string{"a[^a][^a][^a]b"}, []string{"a☺b"}, false, nil, true}, 53 | {[]string{"[a-ζ]*"}, []string{"α"}, true, nil, true}, 54 | {[]string{"*[a-ζ]"}, []string{"A"}, false, nil, true}, 55 | {[]string{"a?b"}, []string{"a", "b"}, false, nil, true}, 56 | {[]string{"a*b"}, []string{"a", "b"}, false, nil, true}, 57 | {[]string{"[\\]a]"}, []string{"]"}, true, nil, !onWindows}, 58 | {[]string{"[\\-]"}, []string{"-"}, true, nil, !onWindows}, 59 | {[]string{"[x\\-]"}, []string{"x"}, true, nil, !onWindows}, 60 | {[]string{"[x\\-]"}, []string{"-"}, true, nil, !onWindows}, 61 | {[]string{"[x\\-]"}, []string{"z"}, false, nil, !onWindows}, 62 | {[]string{"[\\-x]"}, []string{"x"}, true, nil, !onWindows}, 63 | {[]string{"[\\-x]"}, []string{"-"}, true, nil, !onWindows}, 64 | {[]string{"[\\-x]"}, []string{"a"}, false, nil, !onWindows}, 65 | {[]string{"[]a]"}, []string{"]"}, false, ErrBadPattern, true}, 66 | {[]string{"[-]"}, []string{"-"}, false, ErrBadPattern, true}, 67 | {[]string{"[x-]"}, []string{"x"}, false, ErrBadPattern, true}, 68 | {[]string{"[x-]"}, []string{"-"}, false, ErrBadPattern, true}, 69 | {[]string{"[x-]"}, []string{"z"}, false, ErrBadPattern, true}, 70 | {[]string{"[-x]"}, []string{"x"}, false, ErrBadPattern, true}, 71 | {[]string{"[-x]"}, []string{"-"}, false, ErrBadPattern, true}, 72 | {[]string{"[-x]"}, []string{"a"}, false, ErrBadPattern, true}, 73 | {[]string{"\\"}, []string{"a"}, false, ErrBadPattern, !onWindows}, 74 | {[]string{"[a-b-c]"}, []string{"a"}, false, ErrBadPattern, true}, 75 | {[]string{"["}, []string{"a"}, false, ErrBadPattern, true}, 76 | {[]string{"[^"}, []string{"a"}, false, ErrBadPattern, true}, 77 | {[]string{"[^bc"}, []string{"a"}, false, ErrBadPattern, true}, 78 | {[]string{"a["}, []string{"a"}, false, nil, false}, 79 | {[]string{"a["}, []string{"ab"}, false, ErrBadPattern, true}, 80 | {[]string{"*x"}, []string{"xxx"}, true, nil, true}, 81 | {[]string{"[abc]"}, []string{"b"}, true, nil, true}, 82 | {[]string{"a", "**"}, []string{"a"}, false, nil, true}, 83 | {[]string{"a", "**"}, []string{"a", "b"}, true, nil, true}, 84 | {[]string{"a", "**"}, []string{"a", "b", "c"}, true, nil, true}, 85 | {[]string{"**", "c"}, []string{"c"}, true, nil, true}, 86 | {[]string{"**", "c"}, []string{"b", "c"}, true, nil, true}, 87 | {[]string{"**", "c"}, []string{"a", "b", "c"}, true, nil, true}, 88 | {[]string{"**", "c"}, []string{"a", "b"}, false, nil, true}, 89 | {[]string{"**", "c"}, []string{"abcd"}, false, nil, true}, 90 | {[]string{"**", "c"}, []string{"a", "abc"}, false, nil, true}, 91 | {[]string{"a", "**", "b"}, []string{"a", "b"}, true, nil, true}, 92 | {[]string{"a", "**", "c"}, []string{"a", "b", "c"}, true, nil, true}, 93 | {[]string{"a", "**", "d"}, []string{"a", "b", "c", "d"}, true, nil, true}, 94 | {[]string{"a", "\\**"}, []string{"a", "b", "c"}, false, nil, !onWindows}, 95 | {[]string{"a", "", "b", "c"}, []string{"a", "b", "c"}, true, nil, true}, 96 | {[]string{"a", "b", "c"}, []string{"a", "b", "", "c"}, true, nil, true}, 97 | {[]string{"ab{c,d}"}, []string{"abc"}, true, nil, true}, 98 | {[]string{"ab{c,d,*}"}, []string{"abcde"}, true, nil, true}, 99 | {[]string{"ab{c,d}["}, []string{"abcd"}, false, ErrBadPattern, true}, 100 | {[]string{"abc", "**"}, []string{"abc", "b"}, true, nil, true}, 101 | {[]string{"**", "abc"}, []string{"abc"}, true, nil, true}, 102 | {[]string{"abc**"}, []string{"abc", "b"}, false, nil, true}, 103 | {[]string{"broken-symlink"}, []string{"broken-symlink"}, true, nil, !onWindows}, 104 | {[]string{"working-symlink", "c", "*"}, []string{"working-symlink", "c", "d"}, true, nil, !onWindows}, 105 | {[]string{"working-sym*", "*"}, []string{"working-symlink", "c"}, true, nil, !onWindows}, 106 | {[]string{"b", "**", "f"}, []string{"b", "symlink-dir", "f"}, true, nil, !onWindows}, 107 | } 108 | 109 | func TestMatch(t *testing.T) { 110 | for idx, tt := range matchTests { 111 | // Since Match() always uses "/" as the separator, we 112 | // don't need to worry about the tt.testOnDisk flag 113 | testMatchWith(t, idx, tt) 114 | } 115 | } 116 | 117 | func testMatchWith(t *testing.T, idx int, tt MatchTest) { 118 | defer func() { 119 | if r := recover(); r != nil { 120 | t.Errorf("#%v. Match(%#q, %#q) panicked: %#v", idx, tt.pattern, tt.testPath, r) 121 | } 122 | }() 123 | 124 | // Match() always uses "/" as the separator 125 | pattern := tt.pattern[0] 126 | testPath := tt.testPath[0] 127 | if len(tt.pattern) > 1 { 128 | pattern = path.Join(tt.pattern...) 129 | } 130 | if len(tt.testPath) > 1 { 131 | testPath = path.Join(tt.testPath...) 132 | } 133 | 134 | ok, err := Match(pattern, testPath) 135 | if ok != tt.shouldMatch || err != tt.expectedErr { 136 | t.Errorf("#%v. Match(%#q, %#q) = %v, %v want %v, %v", idx, pattern, testPath, ok, err, tt.shouldMatch, tt.expectedErr) 137 | } 138 | 139 | if isStandardPattern(pattern) { 140 | stdOk, stdErr := path.Match(pattern, testPath) 141 | if ok != stdOk || !compareErrors(err, stdErr) { 142 | t.Errorf("#%v. Match(%#q, %#q) != path.Match(...). Got %v, %v want %v, %v", idx, pattern, testPath, ok, err, stdOk, stdErr) 143 | } 144 | } 145 | } 146 | 147 | func TestPathMatch(t *testing.T) { 148 | for idx, tt := range matchTests { 149 | // Even though we aren't actually matching paths on disk, we are using 150 | // PathMatch() which will use the system's separator. As a result, any 151 | // patterns that might cause problems on-disk need to also be avoided 152 | // here in this test. 153 | if tt.testOnDisk { 154 | testPathMatchWith(t, idx, tt) 155 | } 156 | } 157 | } 158 | 159 | func testPathMatchWith(t *testing.T, idx int, tt MatchTest) { 160 | defer func() { 161 | if r := recover(); r != nil { 162 | t.Errorf("#%v. Match(%#q, %#q) panicked: %#v", idx, tt.pattern, tt.testPath, r) 163 | } 164 | }() 165 | 166 | pattern := filepath.Join(tt.pattern...) 167 | testPath := filepath.Join(tt.testPath...) 168 | ok, err := PathMatch(pattern, testPath) 169 | if ok != tt.shouldMatch || err != tt.expectedErr { 170 | t.Errorf("#%v. Match(%#q, %#q) = %v, %v want %v, %v", idx, pattern, testPath, ok, err, tt.shouldMatch, tt.expectedErr) 171 | } 172 | 173 | if isStandardPattern(pattern) { 174 | stdOk, stdErr := filepath.Match(pattern, testPath) 175 | if ok != stdOk || !compareErrors(err, stdErr) { 176 | t.Errorf("#%v. PathMatch(%#q, %#q) != filepath.Match(...). Got %v, %v want %v, %v", idx, pattern, testPath, ok, err, stdOk, stdErr) 177 | } 178 | } 179 | } 180 | 181 | func isStandardPattern(pattern string) bool { 182 | return !strings.Contains(pattern, "**") && indexRuneWithEscaping(pattern, '{') == -1 183 | } 184 | 185 | func compareErrors(a, b error) bool { 186 | if a == nil { 187 | return b == nil 188 | } 189 | return b != nil 190 | } 191 | 192 | func inSlice(s string, a []string) bool { 193 | for _, i := range a { 194 | if i == s { 195 | return true 196 | } 197 | } 198 | return false 199 | } 200 | 201 | func compareSlices(a, b []string) bool { 202 | if len(a) != len(b) { 203 | return false 204 | } 205 | 206 | diff := make(map[string]int, len(a)) 207 | 208 | for _, x := range a { 209 | diff[x]++ 210 | } 211 | 212 | for _, y := range b { 213 | if _, ok := diff[y]; !ok { 214 | return false 215 | } 216 | 217 | diff[y]-- 218 | if diff[y] == 0 { 219 | delete(diff, y) 220 | } 221 | } 222 | 223 | return len(diff) == 0 224 | } 225 | -------------------------------------------------------------------------------- /pkg/path/path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package path 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/storyicon/grbac/pkg/path/doublestar" 21 | ) 22 | 23 | // HasWildcardPrefix is​used to determine whether an expression is a wildcard at the beginning 24 | func HasWildcardPrefix(pattern string) bool { 25 | if len(pattern) == 0 { 26 | return false 27 | } 28 | switch pattern[0] { 29 | case '?', '*', '[', '{': 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | // TrimWildcard is used to intercept the pattern before the first wildcard 36 | func TrimWildcard(pattern string) (trimmed string, hasWildcard bool) { 37 | var chars []byte 38 | Pattern: 39 | for i := 0; i < len(pattern); i++ { 40 | switch pattern[i] { 41 | case '\\': 42 | if i == len(pattern)-1 { 43 | break Pattern 44 | } 45 | i++ 46 | case '?', '*', '[', '{': 47 | hasWildcard = true 48 | break Pattern 49 | } 50 | chars = append(chars, pattern[i]) 51 | } 52 | return string(chars), hasWildcard 53 | } 54 | 55 | // Match returns true if name matches the shell file name pattern. 56 | // The pattern syntax is: 57 | // 58 | // pattern: 59 | // { term } 60 | // term: 61 | // '*' matches any sequence of non-path-separators 62 | // '**' matches any sequence of characters, including 63 | // path separators. 64 | // '?' matches any single non-path-separator character 65 | // '[' [ '^' ] { character-range } ']' 66 | // character class (must be non-empty) 67 | // '{' { term } [ ',' { term } ... ] '}' 68 | // c matches character c (c != '*', '?', '\\', '[') 69 | // '\\' c matches character c 70 | // 71 | // character-range: 72 | // c matches character c (c != '\\', '-', ']') 73 | // '\\' c matches character c 74 | // lo '-' hi matches character c for lo <= c <= hi 75 | // 76 | // Match requires pattern to match all of name, not just a substring. 77 | // The path-separator defaults to the '/' character. The only possible 78 | // returned error is ErrBadPattern, when pattern is malformed. 79 | // 80 | // Note: this is meant as a drop-in replacement for path.Match() which 81 | // always uses '/' as the path separator. If you want to support systems 82 | // which use a different path separator (such as Windows), what you want 83 | // is the PathMatch() function below. 84 | // 85 | func Match(pattern string, s string) (bool, error) { 86 | switch pattern { 87 | case "**": 88 | return true, nil 89 | case "*": 90 | if strings.Contains(s, "/") { 91 | return false, nil 92 | } 93 | return true, nil 94 | } 95 | return doublestar.Match(pattern, s) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/path/path_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 https://github.com/bmatcuk 2 | // 3 | // This software is released under the MIT License. 4 | // https://opensource.org/licenses/MIT 5 | 6 | package path 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestHasWildcardPrefix(t *testing.T) { 16 | type args struct { 17 | pattern string 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want bool 23 | }{ 24 | { 25 | name: "test0", 26 | args: args{ 27 | pattern: "*", 28 | }, 29 | want: true, 30 | }, 31 | { 32 | name: "test1", 33 | args: args{ 34 | pattern: "jack*", 35 | }, 36 | want: false, 37 | }, 38 | { 39 | name: "test2", 40 | args: args{ 41 | pattern: `\*tom`, 42 | }, 43 | want: false, 44 | }, 45 | { 46 | name: "test3", 47 | args: args{ 48 | pattern: "/test", 49 | }, 50 | want: false, 51 | }, 52 | { 53 | name: "test4", 54 | args: args{ 55 | pattern: "[t]est", 56 | }, 57 | want: true, 58 | }, 59 | { 60 | name: "test5", 61 | args: args{ 62 | pattern: "{t,j}est", 63 | }, 64 | want: true, 65 | }, 66 | } 67 | for _, tt := range tests { 68 | if got := HasWildcardPrefix(tt.args.pattern); got != tt.want { 69 | t.Errorf("%q. HasWildcardPrefix() = %v, want %v", tt.name, got, tt.want) 70 | } 71 | } 72 | } 73 | 74 | func TestTrimWildcard(t *testing.T) { 75 | type args struct { 76 | pattern string 77 | } 78 | tests := []struct { 79 | name string 80 | args args 81 | wantTrimmed string 82 | wantHasWildcard bool 83 | }{ 84 | { 85 | name: "test0", 86 | args: args{ 87 | pattern: "*test", 88 | }, 89 | wantTrimmed: "", 90 | wantHasWildcard: true, 91 | }, 92 | { 93 | name: "test1", 94 | args: args{ 95 | pattern: "test*", 96 | }, 97 | wantTrimmed: "test", 98 | wantHasWildcard: true, 99 | }, 100 | { 101 | name: "test2", 102 | args: args{ 103 | pattern: "te*st", 104 | }, 105 | wantTrimmed: "te", 106 | wantHasWildcard: true, 107 | }, 108 | { 109 | name: "test3", 110 | args: args{ 111 | pattern: "test", 112 | }, 113 | wantTrimmed: "test", 114 | wantHasWildcard: false, 115 | }, 116 | { 117 | name: "test4", 118 | args: args{ 119 | pattern: `test\[]`, 120 | }, 121 | wantTrimmed: `test[]`, 122 | wantHasWildcard: false, 123 | }, 124 | } 125 | for _, tt := range tests { 126 | gotTrimmed, gotHasWildcard := TrimWildcard(tt.args.pattern) 127 | if gotTrimmed != tt.wantTrimmed { 128 | t.Errorf("%q. TrimWildcard() gotTrimmed = %v, want %v", tt.name, gotTrimmed, tt.wantTrimmed) 129 | } 130 | if gotHasWildcard != tt.wantHasWildcard { 131 | t.Errorf("%q. TrimWildcard() gotHasWildcard = %v, want %v", tt.name, gotHasWildcard, tt.wantHasWildcard) 132 | } 133 | } 134 | } 135 | 136 | func TestMatch(t *testing.T) { 137 | type Result struct { 138 | Matched bool 139 | Err bool 140 | } 141 | var TestMatchEqual = func(wanted Result, pattern, s string) { 142 | matched, err := Match(pattern, s) 143 | result := Result{matched, err != nil} 144 | assert.Equal(t, wanted, result, fmt.Sprintf("[Match(..) != Wanted] Match(..):%+v, Wanted:%+v, pattern: %s, s: %s ", result, wanted, pattern, s)) 145 | } 146 | TestMatchEqual(Result{true, false}, `*`, ``) 147 | TestMatchEqual(Result{false, false}, `*`, `/`) // Wrong 148 | TestMatchEqual(Result{false, false}, `/*`, `//`) // Wrong 149 | TestMatchEqual(Result{true, false}, `*/`, `debug/`) 150 | TestMatchEqual(Result{true, false}, `/*`, `/debug`) 151 | TestMatchEqual(Result{false, false}, `/*`, `/debug/`) 152 | TestMatchEqual(Result{false, false}, `/*`, `/debug/`) // Wrong 153 | TestMatchEqual(Result{false, false}, `/*`, `/debug/pprof`) 154 | TestMatchEqual(Result{true, false}, `/*/`, `/debug/`) 155 | TestMatchEqual(Result{true, false}, `/*/*`, `/debug/pprof`) 156 | TestMatchEqual(Result{true, false}, `debug/*/`, `debug/test/`) 157 | TestMatchEqual(Result{true, false}, `aa/*`, `aa/`) // Wrong 158 | TestMatchEqual(Result{true, false}, `**`, ``) 159 | TestMatchEqual(Result{true, false}, `/**`, `/debug`) 160 | TestMatchEqual(Result{true, false}, `/**`, `/debug/pprof/profile`) 161 | TestMatchEqual(Result{true, false}, `/**`, `/debug/pprof/profile/`) 162 | TestMatchEqual(Result{true, false}, `/in[d]ex`, `/index`) 163 | TestMatchEqual(Result{false, false}, `/in[d]ex`, `/inex`) 164 | TestMatchEqual(Result{true, false}, `/in\[d\]ex`, `/in[d]ex`) 165 | TestMatchEqual(Result{false, false}, `/**/profile`, `/debug/pprof/profile/`) // Wrong 166 | TestMatchEqual(Result{true, false}, `/**/profile`, `/debug/pprof/profile`) 167 | TestMatchEqual(Result{true, false}, `/*/*/profile`, `/debug/pprof/profile`) 168 | TestMatchEqual(Result{true, false}, `/**/*`, `/debug/pprof/profile`) 169 | TestMatchEqual(Result{true, false}, `/**/pprof/*`, `/debug/pprof/profile`) 170 | TestMatchEqual(Result{true, false}, `/**/pprof/*/`, `/debug/pprof/profile/`) 171 | TestMatchEqual(Result{true, false}, `/*/[pz]rofile/`, `/debug/profile/`) 172 | TestMatchEqual(Result{true, false}, `/{debug,test}/profile`, `/debug/profile`) 173 | TestMatchEqual(Result{false, false}, `/{debug,test}/profile`, `/debug/profile/`) 174 | TestMatchEqual(Result{true, false}, `\**`, `*GET`) 175 | TestMatchEqual(Result{true, false}, `\\[0-9]`, `\8`) 176 | TestMatchEqual(Result{true, false}, `\\\[0-9]`, `\[0-9]`) 177 | TestMatchEqual(Result{true, false}, `\\A`, `\A`) 178 | TestMatchEqual(Result{true, false}, `\A`, `A`) 179 | TestMatchEqual(Result{false, false}, `[^visitor]*`, `va`) 180 | TestMatchEqual(Result{true, false}, `dashboard*.xxxx.com`, `dashboard.xxxx.com`) 181 | TestMatchEqual(Result{true, false}, `dashboard{-sit,-prod}.xxxx.com`, `dashboard-sit.xxxx.com`) 182 | TestMatchEqual(Result{false, false}, `dashboard{-sit,-prod}.xxxx.com`, `dashboard-si.xxxx.com`) 183 | TestMatchEqual(Result{false, true}, `/{config/*,instance}`, `/config/delete`) 184 | TestMatchEqual(Result{true, false}, `**`, `/config`) 185 | } 186 | -------------------------------------------------------------------------------- /pkg/tree/node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tree 16 | 17 | import ( 18 | iradix "github.com/hashicorp/go-immutable-radix" 19 | "github.com/storyicon/grbac/pkg/path" 20 | ) 21 | 22 | // Node defines the wildcard node 23 | type Node struct { 24 | key string 25 | indexKey []byte 26 | isWildcardKey bool 27 | 28 | data Data 29 | 30 | tree *iradix.Tree 31 | catchAll []*Node 32 | } 33 | 34 | // Data is the data type of the data node 35 | type Data = interface{} 36 | 37 | // NewNode is used to create a new node 38 | func NewNode(key string, data Data) *Node { 39 | trimmed, isWildcardKey := path.TrimWildcard(key) 40 | return &Node{ 41 | key: key, 42 | indexKey: []byte(trimmed), 43 | isWildcardKey: isWildcardKey, 44 | data: data, 45 | tree: iradix.New(), 46 | catchAll: []*Node{}, 47 | } 48 | } 49 | 50 | // Match is used to determine whether the current node's key matches the given key. 51 | func (node *Node) match(key string) (bool, error) { 52 | if node.isWildcardKey { 53 | return path.Match(node.key, key) 54 | } 55 | return node.key == key, nil 56 | } 57 | 58 | // Find is used to find child nodes by a specified key 59 | func (node *Node) Find(key string) ([]*Node, []Data, error) { 60 | 61 | nodes := node.catchAll 62 | node.tree.Root().WalkPath([]byte(key), func(k []byte, v interface{}) bool { 63 | children, ok := v.([]*Node) 64 | if ok { 65 | nodes = append(nodes, children...) 66 | return false 67 | } 68 | return true 69 | }) 70 | 71 | var tmp []*Node 72 | var data []Data 73 | for _, node := range nodes { 74 | matched, err := node.match(key) 75 | if err != nil { 76 | return nil, nil, err 77 | } 78 | if matched { 79 | if node.data != nil { 80 | data = append(data, node.data) 81 | } 82 | tmp = append(tmp, node) 83 | } 84 | } 85 | 86 | return tmp, data, nil 87 | } 88 | 89 | // Insert used to insert a node into the child node of the current node 90 | func (node *Node) Insert(child *Node) { 91 | if path.HasWildcardPrefix(child.key) { 92 | node.catchAll = append(node.catchAll, child) 93 | } else { 94 | nodeData, exists := node.tree.Get(child.indexKey) 95 | nodes := []*Node{child} 96 | if exists { 97 | children, _ := nodeData.([]*Node) 98 | nodes = append(children, child) 99 | } 100 | node.tree, _, _ = node.tree.Insert(child.indexKey, nodes) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/tree/tree.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tree 16 | 17 | // Tree is a Radix search tree that supports wildcards 18 | type Tree struct { 19 | root *Node 20 | } 21 | 22 | // NewTree is used to initialize a wildcard tree 23 | func NewTree() *Tree { 24 | root := NewNode("ROOT", nil) 25 | return &Tree{ 26 | root: root, 27 | } 28 | } 29 | 30 | // Query is used to query the current tree by args 31 | func (tree *Tree) Query(args []string) ([]Data, error) { 32 | var data []Data 33 | 34 | parents := []*Node{tree.root} 35 | for i, arg := range args { 36 | eof := i == len(args)-1 37 | 38 | var nodes []*Node 39 | for _, parent := range parents { 40 | children, childData, err := parent.Find(arg) 41 | if err != nil { 42 | return nil, err 43 | } 44 | nodes = append(nodes, children...) 45 | if eof { 46 | data = append(data, childData...) 47 | } 48 | } 49 | parents = nodes 50 | } 51 | return data, nil 52 | } 53 | 54 | // Insert is used to insert a node into the current tree 55 | func (tree *Tree) Insert(args []string, data Data) { 56 | parent := tree.root 57 | 58 | var nodeData Data 59 | for i, arg := range args { 60 | eof := i == len(args)-1 61 | if eof { 62 | nodeData = data 63 | } 64 | child := NewNode(arg, nodeData) 65 | parent.Insert(child) 66 | parent = child 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pkg/tree/tree_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tree 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | faker "github.com/bxcodec/faker/v3" 22 | "github.com/storyicon/grbac/pkg/path" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | type RecordCase struct { 27 | args []string 28 | data interface{} 29 | } 30 | 31 | type QueryCase struct { 32 | args []string 33 | data interface{} 34 | } 35 | 36 | var ( 37 | TestTree *Tree 38 | TestQueryCase []QueryCase 39 | 40 | BenchTree *Tree 41 | BenchForeachRecords []RecordCase 42 | BenchQueryCase []QueryCase 43 | ) 44 | 45 | func init() { 46 | defaultRecordCase := []RecordCase{ 47 | {[]string{"*", "**", "*"}, "global category"}, 48 | {[]string{"api-{prod,sit}.domain.com", "/article", "*"}, "article global category"}, 49 | {[]string{"api-{prod,sit}.domain.com", "/article", "GET"}, "article get category"}, 50 | {[]string{"api-{prod,sit}.domain.com", "/article", "POST"}, "article post category"}, 51 | {[]string{"api-{prod,sit}.domain.com", "/article", "DELETE"}, "article delete category"}, 52 | {[]string{"api-{prod,sit}.domain.com", "/login", "*"}, "login category"}, 53 | {[]string{"api-{prod,sit}.domain.com", "/notice", "*"}, "notice category"}, 54 | {[]string{"api-{prod,sit}.domain.com", "/query/*", "GET"}, "query category"}, 55 | {[]string{"domain.com", "/login", "*"}, "login category"}, 56 | } 57 | defaultQueryCase := []QueryCase{ 58 | {[]string{"api-prod.domain.com", "/article", "GET"}, []interface{}{"global category", "article global category", "article get category"}}, 59 | {[]string{"api-sit.domain.com", "/article", "DELETE"}, []interface{}{"global category", "article global category", "article delete category"}}, 60 | {[]string{"api.domain.com", "/article", "POST"}, []interface{}{"global category"}}, 61 | {[]string{"api-prod.domain.com", "/query/keywords", "GET"}, []interface{}{"global category", "query category"}}, 62 | } 63 | 64 | TestTree = NewTree() 65 | TestQueryCase = defaultQueryCase 66 | for _, testCase := range defaultRecordCase { 67 | TestTree.Insert(testCase.args, testCase.data) 68 | } 69 | 70 | BenchForeachRecords = defaultRecordCase 71 | for i := 0; i < 1000; i++ { 72 | BenchForeachRecords = append(BenchForeachRecords, RecordCase{ 73 | args: []string{ 74 | "api-{prod,sit}.domain.com", 75 | "/" + faker.FirstName(), 76 | "*", 77 | }, 78 | }, RecordCase{ 79 | args: []string{ 80 | faker.FirstName() + ".domain.com", 81 | "/" + faker.FirstName(), 82 | "GET", 83 | }, 84 | }, RecordCase{ 85 | args: []string{ 86 | faker.FirstName(), 87 | fmt.Sprintf("%s/%s/%s/", faker.FirstName(), faker.FirstName(), faker.FirstName()), 88 | "GET", 89 | }, 90 | }) 91 | } 92 | 93 | BenchTree = NewTree() 94 | BenchQueryCase = defaultQueryCase 95 | for _, benchCase := range BenchForeachRecords { 96 | BenchTree.Insert(benchCase.args, benchCase.data) 97 | } 98 | } 99 | 100 | func TestTree_Query(t *testing.T) { 101 | tree := NewTree() 102 | conditions := []string{"layer1", "layer2", "layer3"} 103 | tree.Insert(conditions, "data1") 104 | tree.Insert(conditions, "data2") 105 | tree.Insert(conditions, "data3") 106 | tree.Insert(conditions, "data4") 107 | tree.Insert(conditions, "data5") 108 | data, err := tree.Query(conditions) 109 | assert.Equal(t, nil, err) 110 | assert.Equal(t, []interface{}{"data1", "data2", "data3", "data4", "data5"}, data) 111 | } 112 | 113 | func BenchmarkTree_Query(b *testing.B) { 114 | b.RunParallel(func(pb *testing.PB) { 115 | for pb.Next() { 116 | for _, testCase := range BenchQueryCase { 117 | BenchTree.Query(testCase.args) 118 | } 119 | } 120 | }) 121 | } 122 | 123 | func BenchmarkTree_Foreach_Query(b *testing.B) { 124 | b.RunParallel(func(pb *testing.PB) { 125 | for pb.Next() { 126 | for _, treeCase := range BenchForeachRecords { 127 | for _, queryCase := range BenchQueryCase { 128 | for i, arg := range queryCase.args { 129 | matched, _ := path.Match(treeCase.args[i], arg) 130 | if !matched { 131 | break 132 | } 133 | } 134 | } 135 | } 136 | } 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | // Contains is used to determine if arr contains s 18 | func Contains(arr []string, s string) bool { 19 | for _, a := range arr { 20 | if a == s { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 storyicon@foxmail.com 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import "testing" 18 | 19 | func TestContains(t *testing.T) { 20 | type args struct { 21 | arr []string 22 | s string 23 | } 24 | tests := []struct { 25 | name string 26 | args args 27 | want bool 28 | }{ 29 | { 30 | name: "test0", 31 | args: args{ 32 | arr: []string{"a", "b"}, 33 | s: "b", 34 | }, 35 | want: true, 36 | }, 37 | { 38 | name: "test1", 39 | args: args{ 40 | arr: []string{"a"}, 41 | s: "b", 42 | }, 43 | want: false, 44 | }, 45 | } 46 | for _, tt := range tests { 47 | if got := Contains(tt.args.arr, tt.args.s); got != tt.want { 48 | t.Errorf("%q. Contains() = %v, want %v", tt.name, got, tt.want) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 storyicon@foxmail.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grbac 18 | 19 | import ( 20 | "github.com/storyicon/grbac/pkg/meta" 21 | ) 22 | 23 | // Resource defines resources 24 | type Resource = meta.Resource 25 | 26 | // PermissionState identifies the status of the permission 27 | type PermissionState = meta.PermissionState 28 | 29 | // Permissions is the set of Permission 30 | type Permissions = meta.Permissions 31 | 32 | // Permission is used to define permission control information 33 | type Permission = meta.Permission 34 | 35 | // Rules is the list of Rule 36 | type Rules = meta.Rules 37 | 38 | // Rule is used to define the relationship between "resource" and "permission" 39 | type Rule = meta.Rule 40 | 41 | // Query defines the data structure of the query parameters 42 | type Query = meta.Query 43 | --------------------------------------------------------------------------------