├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .releaserc.json ├── .travis.yml ├── LICENSE ├── README.md ├── adapter.go ├── adapter_test.go ├── examples ├── object_conditions_model.conf ├── object_conditions_policy.csv ├── rbac_model.conf ├── rbac_policy.csv ├── rbac_with_domains_model.conf └── rbac_with_domains_policy.csv ├── go.mod └── go.sum /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | mysql: 16 | image: mysql 17 | env: 18 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 19 | MYSQL_DATABASE: casbin 20 | ports: 21 | - 3306:3306 22 | postgres: 23 | image: postgres 24 | env: 25 | POSTGRES_PASSWORD: postgres 26 | options: >- 27 | --health-cmd pg_isready 28 | --health-interval 10s 29 | --health-timeout 5s 30 | --health-retries 5 31 | ports: 32 | - 5432:5432 33 | sqlserver: 34 | image: mcr.microsoft.com/mssql/server:2022-latest 35 | env: 36 | ACCEPT_EULA: Y 37 | MSSQL_SA_PASSWORD: SqlServer123 38 | ports: 39 | - 1433:1433 40 | 41 | steps: 42 | - name: Set up Go 43 | uses: actions/setup-go@v4 44 | with: 45 | go-version: '1.20' 46 | 47 | - uses: actions/checkout@v2 48 | - name: Run Unit tests 49 | run: go test -v -coverprofile=./profile.cov ./... 50 | 51 | #- uses: actions/checkout@v2 52 | #- uses: shogo82148/actions-goveralls@v1 53 | # with: 54 | # path-to-profile: ./profile.cov 55 | 56 | semantic-release: 57 | needs: [test] 58 | runs-on: ubuntu-latest 59 | steps: 60 | 61 | - uses: actions/checkout@v2 62 | 63 | - name: Run semantic-release 64 | if: github.repository == 'casbin/gorm-adapter' && github.event_name == 'push' 65 | run: | 66 | npm install --save-dev semantic-release@17.2.4 67 | npx semantic-release 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | .idea/ 17 | *.iml 18 | 19 | casbin.db -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true, 3 | "branches": [ 4 | "+([0-9])?(.{+([0-9]),x}).x", 5 | "master", 6 | { 7 | "name": "beta", 8 | "prerelease": true 9 | } 10 | ], 11 | "plugins": [ 12 | "@semantic-release/commit-analyzer", 13 | "@semantic-release/release-notes-generator", 14 | "@semantic-release/github" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - tip 7 | 8 | before_install: 9 | - go get github.com/mattn/goveralls 10 | 11 | script: 12 | - $HOME/gopath/bin/goveralls -service=travis-ci 13 | 14 | services: 15 | - mysql 16 | - postgresql 17 | - sqlserver 18 | -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gorm Adapter 2 | ==== 3 | 4 | > In v3.0.3, method `NewAdapterByDB` creates table named `casbin_rules`, 5 | > we fix it to `casbin_rule` after that. 6 | > If you used v3.0.3 and less, and you want to update it, 7 | > you might need to *migrate* data manually. 8 | > Find out more at: https://github.com/casbin/gorm-adapter/issues/78 9 | 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/casbin/gorm-adapter)](https://goreportcard.com/report/github.com/casbin/gorm-adapter) 11 | [![Go](https://github.com/casbin/gorm-adapter/actions/workflows/ci.yml/badge.svg)](https://github.com/casbin/gorm-adapter/actions/workflows/ci.yml) 12 | [![Coverage Status](https://coveralls.io/repos/github/casbin/gorm-adapter/badge.svg?branch=master)](https://coveralls.io/github/casbin/gorm-adapter?branch=master) 13 | [![Godoc](https://godoc.org/github.com/casbin/gorm-adapter?status.svg)](https://godoc.org/github.com/casbin/gorm-adapter) 14 | [![Release](https://img.shields.io/github/release/casbin/gorm-adapter.svg)](https://github.com/casbin/gorm-adapter/releases/latest) 15 | [![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN) 16 | [![Sourcegraph](https://sourcegraph.com/github.com/casbin/gorm-adapter/-/badge.svg)](https://sourcegraph.com/github.com/casbin/gorm-adapter?badge) 17 | 18 | Gorm Adapter is the [Gorm](https://gorm.io/gorm) adapter for [Casbin](https://github.com/casbin/casbin). With this library, Casbin can load policy from Gorm supported database or save policy to it. 19 | 20 | Based on [Officially Supported Databases](https://v1.gorm.io/docs/connecting_to_the_database.html#Supported-Databases), The current supported databases are: 21 | 22 | - MySQL 23 | - PostgreSQL 24 | - SQL Server 25 | - Sqlite3 26 | > gorm-adapter use ``github.com/glebarez/sqlite`` instead of gorm official sqlite driver ``gorm.io/driver/sqlite`` because the latter needs ``cgo`` support. But there is almost no difference between the two driver. If there is a difference in use, please submit an issue. 27 | 28 | - other 3rd-party supported DBs in Gorm website or other places. 29 | 30 | ## Installation 31 | 32 | go get github.com/casbin/gorm-adapter/v3 33 | 34 | ## Simple Example 35 | 36 | ```go 37 | package main 38 | 39 | import ( 40 | "github.com/casbin/casbin/v2" 41 | gormadapter "github.com/casbin/gorm-adapter/v3" 42 | _ "github.com/go-sql-driver/mysql" 43 | ) 44 | 45 | func main() { 46 | // Initialize a Gorm adapter and use it in a Casbin enforcer: 47 | // The adapter will use the MySQL database named "casbin". 48 | // If it doesn't exist, the adapter will create it automatically. 49 | // You can also use an already existing gorm instance with gormadapter.NewAdapterByDB(gormInstance) 50 | a, _ := gormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/") // Your driver and data source. 51 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 52 | 53 | // Or you can use an existing DB "abc" like this: 54 | // The adapter will use the table named "casbin_rule". 55 | // If it doesn't exist, the adapter will create it automatically. 56 | // a := gormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/abc", true) 57 | 58 | // Load the policy from DB. 59 | e.LoadPolicy() 60 | 61 | // Check the permission. 62 | e.Enforce("alice", "data1", "read") 63 | 64 | // Modify the policy. 65 | // e.AddPolicy(...) 66 | // e.RemovePolicy(...) 67 | 68 | // Save the policy back to DB. 69 | e.SavePolicy() 70 | } 71 | ``` 72 | ## Turn off AutoMigrate 73 | New an adapter will use ``AutoMigrate`` by default for create table, if you want to turn it off, please use API ``TurnOffAutoMigrate(db *gorm.DB) *gorm.DB``. See example: 74 | ```go 75 | db, err := gorm.Open(mysql.Open("root:@tcp(127.0.0.1:3306)/casbin"), &gorm.Config{}) 76 | TurnOffAutoMigrate(db) 77 | // a,_ := NewAdapterByDB(...) 78 | // a,_ := NewAdapterByDBUseTableName(...) 79 | a,_ := NewAdapterByDBWithCustomTable(...) 80 | ``` 81 | Find out more details at [gorm-adapter#162](https://github.com/casbin/gorm-adapter/issues/162) 82 | ## Customize table columns example 83 | You can change the gorm struct tags, but the table structure must stay the same. 84 | ```go 85 | package main 86 | 87 | import ( 88 | "github.com/casbin/casbin/v2" 89 | gormadapter "github.com/casbin/gorm-adapter/v3" 90 | "gorm.io/gorm" 91 | ) 92 | 93 | func main() { 94 | // Increase the column size to 512. 95 | type CasbinRule struct { 96 | ID uint `gorm:"primaryKey;autoIncrement"` 97 | Ptype string `gorm:"size:512;uniqueIndex:unique_index"` 98 | V0 string `gorm:"size:512;uniqueIndex:unique_index"` 99 | V1 string `gorm:"size:512;uniqueIndex:unique_index"` 100 | V2 string `gorm:"size:512;uniqueIndex:unique_index"` 101 | V3 string `gorm:"size:512;uniqueIndex:unique_index"` 102 | V4 string `gorm:"size:512;uniqueIndex:unique_index"` 103 | V5 string `gorm:"size:512;uniqueIndex:unique_index"` 104 | } 105 | 106 | db, _ := gorm.Open(...) 107 | 108 | // Initialize a Gorm adapter and use it in a Casbin enforcer: 109 | // The adapter will use an existing gorm.DB instnace. 110 | a, _ := gormadapter.NewAdapterByDBWithCustomTable(db, &CasbinRule{}) 111 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 112 | 113 | // Load the policy from DB. 114 | e.LoadPolicy() 115 | 116 | // Check the permission. 117 | e.Enforce("alice", "data1", "read") 118 | 119 | // Modify the policy. 120 | // e.AddPolicy(...) 121 | // e.RemovePolicy(...) 122 | 123 | // Save the policy back to DB. 124 | e.SavePolicy() 125 | } 126 | ``` 127 | ## Transaction 128 | You can modify policies within a transaction.See example: 129 | ```go 130 | package main 131 | 132 | func main() { 133 | a, err := NewAdapterByDB(db) 134 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 135 | err = e.GetAdapter().(*Adapter).Transaction(e, func(e casbin.IEnforcer) error { 136 | _, err := e.AddPolicy("jack", "data1", "write") 137 | if err != nil { 138 | return err 139 | } 140 | _, err = e.AddPolicy("jack", "data2", "write") 141 | if err != nil { 142 | return err 143 | } 144 | return nil 145 | }) 146 | if err != nil { 147 | // handle if transaction failed 148 | return 149 | } 150 | } 151 | ``` 152 | ## ConditionsToGormQuery 153 | 154 | `ConditionsToGormQuery()` is a function that converts multiple query conditions into a GORM query statement 155 | You can use the `GetAllowedObjectConditions()` API of Casbin to get conditions, 156 | and choose the way of combining conditions through `combineType`. 157 | 158 | `ConditionsToGormQuery()` allows Casbin to be combined with SQL, and you can use it to implement many functions. 159 | ### Example: GetAllowedRecordsForUser 160 | * model example: [object_conditions_model.conf](examples/object_conditions_model.conf) 161 | * policy example: [object_conditions_policy.csv](examples/object_conditions_policy.csv) 162 | 163 | DataBase example: 164 | 165 | |id|title|author|publisher|publish_data|price|category_id| 166 | |--|--|--|--|--|--|--| 167 | |1|book1|author1|publisher1|2023-04-09 16:23:42|10|1| 168 | |2|book2|author1|publisher1|2023-04-09 16:23:44|20|2| 169 | |3|book3|author2|publisher1|2023-04-09 16:23:44|30|1| 170 | |4|book4|author2|publisher2|2023-04-09 16:23:45|10|3| 171 | |5|book5|author3|publisher2|2023-04-09 16:23:45|50|1| 172 | |6|book6|author3|publisher2|2023-04-09 16:23:46|60|2| 173 | 174 | 175 | ```go 176 | type Book struct { 177 | ID int 178 | Title string 179 | Author string 180 | Publisher string 181 | PublishDate time.Time 182 | Price float64 183 | CategoryID int 184 | } 185 | 186 | func TestGetAllowedRecordsForUser(t *testing.T) { 187 | e, _ := casbin.NewEnforcer("examples/object_conditions_model.conf", "examples/object_conditions_policy.csv") 188 | 189 | conditions, err := e.GetAllowedObjectConditions("alice", "read", "r.obj.") 190 | if err != nil { 191 | panic(err) 192 | } 193 | fmt.Println(conditions) 194 | 195 | dsn := "root:root@tcp(127.0.0.1:3307)/test?charset=utf8mb4&parseTime=True&loc=Local" 196 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 197 | if err != nil { 198 | panic(err) 199 | } 200 | 201 | fmt.Println("CombineTypeOr") 202 | rows, err := ConditionsToGormQuery(db, conditions, CombineTypeOr).Model(&Book{}).Rows() 203 | defer rows.Close() 204 | var b Book 205 | for rows.Next() { 206 | err := db.ScanRows(rows, &b) 207 | if err != nil { 208 | panic(err) 209 | } 210 | log.Println(b) 211 | } 212 | 213 | fmt.Println("CombineTypeAnd") 214 | rows, err = ConditionsToGormQuery(db, conditions, CombineTypeAnd).Model(&Book{}).Rows() 215 | defer rows.Close() 216 | for rows.Next() { 217 | err := db.ScanRows(rows, &b) 218 | if err != nil { 219 | panic(err) 220 | } 221 | log.Println(b) 222 | } 223 | } 224 | ``` 225 | 226 | ## Context Adapter 227 | 228 | `gormadapter` supports adapter with context, the following is a timeout control implemented using context 229 | 230 | ```go 231 | a, _ := gormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/") // Your driver and data source. 232 | // Limited time 300s 233 | ctx, cancel := context.WithTimeout(context.Background(), 300*time.Microsecond) 234 | defer cancel() 235 | err := a.AddPolicyCtx(ctx, "p", "p", []string{"alice", "data1", "read"}) 236 | if err != nil { 237 | panic(err) 238 | } 239 | ``` 240 | 241 | ## Getting Help 242 | 243 | - [Casbin](https://github.com/casbin/casbin) 244 | 245 | ## License 246 | 247 | This project is under Apache 2.0 License. See the [LICENSE](LICENSE) file for the full license text. 248 | -------------------------------------------------------------------------------- /adapter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The casbin Authors. All Rights Reserved. 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 gormadapter 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "errors" 21 | "fmt" 22 | "runtime" 23 | "strings" 24 | "sync" 25 | 26 | "github.com/casbin/casbin/v2" 27 | "github.com/casbin/casbin/v2/model" 28 | "github.com/casbin/casbin/v2/persist" 29 | "github.com/glebarez/sqlite" 30 | "gorm.io/driver/mysql" 31 | "gorm.io/driver/postgres" 32 | "gorm.io/driver/sqlserver" 33 | "gorm.io/gorm" 34 | "gorm.io/gorm/clause" 35 | "gorm.io/gorm/logger" 36 | "gorm.io/plugin/dbresolver" 37 | ) 38 | 39 | const ( 40 | defaultDatabaseName = "casbin" 41 | defaultTableName = "casbin_rule" 42 | ) 43 | 44 | const disableMigrateKey = "disableMigrateKey" 45 | const customTableKey = "customTableKey" 46 | 47 | type CasbinRule struct { 48 | ID uint `gorm:"primaryKey;autoIncrement"` 49 | Ptype string `gorm:"size:100"` 50 | V0 string `gorm:"size:100"` 51 | V1 string `gorm:"size:100"` 52 | V2 string `gorm:"size:100"` 53 | V3 string `gorm:"size:100"` 54 | V4 string `gorm:"size:100"` 55 | V5 string `gorm:"size:100"` 56 | } 57 | 58 | func (CasbinRule) TableName() string { 59 | return "casbin_rule" 60 | } 61 | 62 | type Filter struct { 63 | Ptype []string 64 | V0 []string 65 | V1 []string 66 | V2 []string 67 | V3 []string 68 | V4 []string 69 | V5 []string 70 | } 71 | 72 | type BatchFilter struct { 73 | filters []Filter 74 | } 75 | 76 | // Adapter represents the Gorm adapter for policy storage. 77 | type Adapter struct { 78 | driverName string 79 | dataSourceName string 80 | databaseName string 81 | tablePrefix string 82 | tableName string 83 | dbSpecified bool 84 | db *gorm.DB 85 | isFiltered bool 86 | transactionMu *sync.Mutex 87 | muInitialize sync.Once 88 | } 89 | 90 | // finalizer is the destructor for Adapter. 91 | func finalizer(a *Adapter) { 92 | sqlDB, err := a.db.DB() 93 | if err != nil { 94 | panic(err) 95 | } 96 | err = sqlDB.Close() 97 | if err != nil { 98 | panic(err) 99 | } 100 | } 101 | 102 | // Select conn according to table name(use map store name-index) 103 | type specificPolicy int 104 | 105 | func (p *specificPolicy) Resolve(connPools []gorm.ConnPool) gorm.ConnPool { 106 | return connPools[*p] 107 | } 108 | 109 | type DbPool struct { 110 | dbMap map[string]specificPolicy 111 | policy *specificPolicy 112 | source *gorm.DB 113 | } 114 | 115 | func (dbPool *DbPool) switchDb(dbName string) *gorm.DB { 116 | *dbPool.policy = dbPool.dbMap[dbName] 117 | return dbPool.source.Clauses(dbresolver.Write) 118 | } 119 | 120 | // NewAdapter is the constructor for Adapter. 121 | // Params : databaseName,tableName,dbSpecified 122 | // 123 | // databaseName,{tableName/dbSpecified} 124 | // {database/dbSpecified} 125 | // 126 | // databaseName and tableName are user defined. 127 | // Their default value are "casbin" and "casbin_rule" 128 | // 129 | // dbSpecified is an optional bool parameter. The default value is false. 130 | // It's up to whether you have specified an existing DB in dataSourceName. 131 | // If dbSpecified == true, you need to make sure the DB in dataSourceName exists. 132 | // If dbSpecified == false, the adapter will automatically create a DB named databaseName. 133 | func NewAdapter(driverName string, dataSourceName string, params ...interface{}) (*Adapter, error) { 134 | a := &Adapter{} 135 | a.driverName = driverName 136 | a.dataSourceName = dataSourceName 137 | 138 | a.tableName = defaultTableName 139 | a.databaseName = defaultDatabaseName 140 | a.dbSpecified = false 141 | a.transactionMu = &sync.Mutex{} 142 | 143 | if len(params) == 1 { 144 | switch p1 := params[0].(type) { 145 | case bool: 146 | a.dbSpecified = p1 147 | case string: 148 | a.databaseName = p1 149 | default: 150 | return nil, errors.New("wrong format") 151 | } 152 | } else if len(params) == 2 { 153 | switch p2 := params[1].(type) { 154 | case bool: 155 | a.dbSpecified = p2 156 | p1, ok := params[0].(string) 157 | if !ok { 158 | return nil, errors.New("wrong format") 159 | } 160 | a.databaseName = p1 161 | case string: 162 | p1, ok := params[0].(string) 163 | if !ok { 164 | return nil, errors.New("wrong format") 165 | } 166 | a.databaseName = p1 167 | a.tableName = p2 168 | default: 169 | return nil, errors.New("wrong format") 170 | } 171 | } else if len(params) == 3 { 172 | if p3, ok := params[2].(bool); ok { 173 | a.dbSpecified = p3 174 | a.databaseName = params[0].(string) 175 | a.tableName = params[1].(string) 176 | } else { 177 | return nil, errors.New("wrong format") 178 | } 179 | } else if len(params) != 0 { 180 | return nil, errors.New("too many parameters") 181 | } 182 | 183 | // Open the DB, create it if not existed. 184 | err := a.Open() 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | // Call the destructor when the object is released. 190 | runtime.SetFinalizer(a, finalizer) 191 | 192 | return a, nil 193 | } 194 | 195 | // NewAdapterByDBUseTableName creates gorm-adapter by an existing Gorm instance and the specified table prefix and table name 196 | // Example: gormadapter.NewAdapterByDBUseTableName(&db, "cms", "casbin") Automatically generate table name like this "cms_casbin" 197 | func NewAdapterByDBUseTableName(db *gorm.DB, prefix string, tableName string) (*Adapter, error) { 198 | if len(tableName) == 0 { 199 | tableName = defaultTableName 200 | } 201 | 202 | a := &Adapter{ 203 | tablePrefix: prefix, 204 | tableName: tableName, 205 | transactionMu: &sync.Mutex{}, 206 | } 207 | 208 | a.db = db.Scopes(a.casbinRuleTable()).Session(&gorm.Session{Context: db.Statement.Context}) 209 | 210 | err := a.createTable() 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | return a, nil 216 | } 217 | 218 | // InitDbResolver multiple databases support 219 | // Example usage: 220 | // dbPool,err := InitDbResolver([]gorm.Dialector{mysql.Open(dsn),mysql.Open(dsn2)},[]string{"casbin1","casbin2"}) 221 | // a := initAdapterWithGormInstanceByMulDb(t,dbPool,"casbin1","","casbin_rule1") 222 | // a = initAdapterWithGormInstanceByMulDb(t,dbPool,"casbin2","","casbin_rule2")/* 223 | func InitDbResolver(dbArr []gorm.Dialector, dbNames []string) (DbPool, error) { 224 | if len(dbArr) == 0 { 225 | panic("dbArr len is 0") 226 | } 227 | source, e := gorm.Open(dbArr[0]) 228 | if e != nil { 229 | panic(e.Error()) 230 | } 231 | var p specificPolicy 232 | p = 0 233 | err := source.Use(dbresolver.Register(dbresolver.Config{Policy: &p, Sources: dbArr})) 234 | dbMap := make(map[string]specificPolicy) 235 | for i := 0; i < len(dbNames); i++ { 236 | dbMap[dbNames[i]] = specificPolicy(i) 237 | } 238 | return DbPool{dbMap: dbMap, policy: &p, source: source}, err 239 | } 240 | 241 | func NewAdapterByMulDb(dbPool DbPool, dbName string, prefix string, tableName string) (*Adapter, error) { 242 | //change DB 243 | db := dbPool.switchDb(dbName) 244 | 245 | return NewAdapterByDBUseTableName(db, prefix, tableName) 246 | } 247 | 248 | // NewFilteredAdapter is the constructor for FilteredAdapter. 249 | // Casbin will not automatically call LoadPolicy() for a filtered adapter. 250 | func NewFilteredAdapter(driverName string, dataSourceName string, params ...interface{}) (*Adapter, error) { 251 | adapter, err := NewAdapter(driverName, dataSourceName, params...) 252 | if err != nil { 253 | return nil, err 254 | } 255 | adapter.isFiltered = true 256 | return adapter, err 257 | } 258 | 259 | // NewFilteredAdapterByDB is the constructor for FilteredAdapter. 260 | // Casbin will not automatically call LoadPolicy() for a filtered adapter. 261 | func NewFilteredAdapterByDB(db *gorm.DB, prefix string, tableName string) (*Adapter, error) { 262 | adapter := &Adapter{ 263 | tablePrefix: prefix, 264 | tableName: tableName, 265 | isFiltered: true, 266 | transactionMu: &sync.Mutex{}, 267 | } 268 | adapter.db = db.Scopes(adapter.casbinRuleTable()).Session(&gorm.Session{Context: db.Statement.Context}) 269 | 270 | return adapter, nil 271 | } 272 | 273 | // NewAdapterByDB creates gorm-adapter by an existing Gorm instance 274 | func NewAdapterByDB(db *gorm.DB) (*Adapter, error) { 275 | return NewAdapterByDBUseTableName(db, "", defaultTableName) 276 | } 277 | 278 | func TurnOffAutoMigrate(db *gorm.DB) { 279 | ctx := db.Statement.Context 280 | if ctx == nil { 281 | ctx = context.Background() 282 | } 283 | 284 | ctx = context.WithValue(ctx, disableMigrateKey, false) 285 | 286 | *db = *db.WithContext(ctx) 287 | } 288 | 289 | func NewAdapterByDBWithCustomTable(db *gorm.DB, t interface{}, tableName ...string) (*Adapter, error) { 290 | ctx := db.Statement.Context 291 | if ctx == nil { 292 | ctx = context.Background() 293 | } 294 | 295 | ctx = context.WithValue(ctx, customTableKey, t) 296 | 297 | curTableName := defaultTableName 298 | if len(tableName) > 0 { 299 | curTableName = tableName[0] 300 | } 301 | 302 | return NewAdapterByDBUseTableName(db.WithContext(ctx), "", curTableName) 303 | } 304 | 305 | func openDBConnection(driverName, dataSourceName string) (*gorm.DB, error) { 306 | var err error 307 | var db *gorm.DB 308 | if driverName == "postgres" { 309 | db, err = gorm.Open(postgres.Open(dataSourceName), &gorm.Config{}) 310 | } else if driverName == "mysql" { 311 | db, err = gorm.Open(mysql.Open(dataSourceName), &gorm.Config{}) 312 | } else if driverName == "sqlserver" { 313 | db, err = gorm.Open(sqlserver.Open(dataSourceName), &gorm.Config{}) 314 | } else if driverName == "sqlite3" { 315 | db, err = gorm.Open(sqlite.Open(dataSourceName), &gorm.Config{}) 316 | } else { 317 | return nil, errors.New("Database dialect '" + driverName + "' is not supported. Supported databases are postgres, mysql, sqlserver and sqlite3") 318 | } 319 | if err != nil { 320 | return nil, err 321 | } 322 | return db, err 323 | } 324 | 325 | func (a *Adapter) createDatabase() error { 326 | var err error 327 | db, err := openDBConnection(a.driverName, a.dataSourceName) 328 | if err != nil { 329 | return err 330 | } 331 | if a.driverName == "postgres" { 332 | if err = db.Exec("CREATE DATABASE " + a.databaseName).Error; err != nil { 333 | // 42P04 is duplicate_database 334 | if strings.Contains(fmt.Sprintf("%s", err), "42P04") { 335 | return nil 336 | } 337 | } 338 | } else if a.driverName != "sqlite3" && a.driverName != "sqlserver" { 339 | err = db.Exec("CREATE DATABASE IF NOT EXISTS " + a.databaseName).Error 340 | } 341 | if err != nil { 342 | return err 343 | } 344 | return nil 345 | } 346 | 347 | func (a *Adapter) Open() error { 348 | var err error 349 | var db *gorm.DB 350 | 351 | if a.dbSpecified { 352 | db, err = openDBConnection(a.driverName, a.dataSourceName) 353 | if err != nil { 354 | return err 355 | } 356 | } else { 357 | if err = a.createDatabase(); err != nil { 358 | return err 359 | } 360 | if a.driverName == "postgres" { 361 | db, err = openDBConnection(a.driverName, a.dataSourceName+" dbname="+a.databaseName) 362 | } else if a.driverName == "sqlite3" { 363 | db, err = openDBConnection(a.driverName, a.dataSourceName) 364 | } else if a.driverName == "sqlserver" { 365 | db, err = openDBConnection(a.driverName, a.dataSourceName+"?database="+a.databaseName) 366 | } else { 367 | db, err = openDBConnection(a.driverName, a.dataSourceName+a.databaseName) 368 | } 369 | if err != nil { 370 | return err 371 | } 372 | } 373 | 374 | a.db = db.Scopes(a.casbinRuleTable()).Session(&gorm.Session{}) 375 | return a.createTable() 376 | } 377 | 378 | // AddLogger adds logger to db 379 | func (a *Adapter) AddLogger(l logger.Interface) { 380 | a.db = a.db.Session(&gorm.Session{Logger: l, Context: a.db.Statement.Context}) 381 | } 382 | 383 | func (a *Adapter) Close() error { 384 | finalizer(a) 385 | return nil 386 | } 387 | 388 | // getTableInstance return the dynamic table name 389 | func (a *Adapter) getTableInstance() *CasbinRule { 390 | return &CasbinRule{} 391 | } 392 | 393 | func (a *Adapter) getFullTableName() string { 394 | if a.tablePrefix != "" { 395 | if strings.HasSuffix(a.tablePrefix, "_") { 396 | return a.tablePrefix + a.tableName 397 | } 398 | return a.tablePrefix + "_" + a.tableName 399 | } 400 | return a.tableName 401 | } 402 | 403 | func (a *Adapter) casbinRuleTable() func(db *gorm.DB) *gorm.DB { 404 | return func(db *gorm.DB) *gorm.DB { 405 | tableName := a.getFullTableName() 406 | return db.Table(tableName) 407 | } 408 | } 409 | 410 | func (a *Adapter) createTable() error { 411 | disableMigrate := a.db.Statement.Context.Value(disableMigrateKey) 412 | if disableMigrate != nil { 413 | return nil 414 | } 415 | 416 | t := a.db.Statement.Context.Value(customTableKey) 417 | 418 | if t != nil { 419 | return a.db.AutoMigrate(t) 420 | } 421 | 422 | t = a.getTableInstance() 423 | if err := a.db.AutoMigrate(t); err != nil { 424 | return err 425 | } 426 | 427 | tableName := a.getFullTableName() 428 | index := strings.ReplaceAll("idx_"+tableName, ".", "_") 429 | hasIndex := a.db.Migrator().HasIndex(t, index) 430 | if !hasIndex { 431 | if err := a.db.Exec(fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s (ptype,v0,v1,v2,v3,v4,v5)", index, tableName)).Error; err != nil { 432 | return err 433 | } 434 | } 435 | return nil 436 | } 437 | 438 | func (a *Adapter) dropTable() error { 439 | t := a.db.Statement.Context.Value(customTableKey) 440 | if t == nil { 441 | return a.db.Migrator().DropTable(a.getTableInstance()) 442 | } 443 | 444 | return a.db.Migrator().DropTable(t) 445 | } 446 | 447 | func (a *Adapter) truncateTable() error { 448 | var sql string 449 | switch a.db.Config.Name() { 450 | case sqlite.DriverName: 451 | sql = fmt.Sprintf("delete from %s", a.getFullTableName()) 452 | case "sqlite3": 453 | sql = fmt.Sprintf("delete from %s", a.getFullTableName()) 454 | case "postgres": 455 | sql = fmt.Sprintf("truncate table %s RESTART IDENTITY", a.getFullTableName()) 456 | case "sqlserver": 457 | sql = fmt.Sprintf("truncate table %s", a.getFullTableName()) 458 | case "mysql": 459 | sql = fmt.Sprintf("truncate table %s", a.getFullTableName()) 460 | default: 461 | sql = fmt.Sprintf("truncate table %s", a.getFullTableName()) 462 | } 463 | return a.db.Exec(sql).Error 464 | } 465 | 466 | func loadPolicyLine(line CasbinRule, model model.Model) error { 467 | var p = []string{line.Ptype, 468 | line.V0, line.V1, line.V2, 469 | line.V3, line.V4, line.V5} 470 | 471 | index := len(p) - 1 472 | for p[index] == "" { 473 | index-- 474 | } 475 | index += 1 476 | p = p[:index] 477 | err := persist.LoadPolicyArray(p, model) 478 | if err != nil { 479 | return err 480 | } 481 | return nil 482 | } 483 | 484 | // LoadPolicy loads policy from database. 485 | func (a *Adapter) LoadPolicy(model model.Model) error { 486 | return a.LoadPolicyCtx(context.Background(), model) 487 | } 488 | 489 | // LoadPolicyCtx loads policy from database. 490 | func (a *Adapter) LoadPolicyCtx(ctx context.Context, model model.Model) error { 491 | var lines []CasbinRule 492 | if err := a.db.WithContext(ctx).Order("ID").Find(&lines).Error; err != nil { 493 | return err 494 | } 495 | err := a.Preview(&lines, model) 496 | if err != nil { 497 | return err 498 | } 499 | for _, line := range lines { 500 | err := loadPolicyLine(line, model) 501 | if err != nil { 502 | return err 503 | } 504 | } 505 | 506 | return nil 507 | } 508 | 509 | // LoadFilteredPolicy loads only policy rules that match the filter. 510 | func (a *Adapter) LoadFilteredPolicy(model model.Model, filter interface{}) error { 511 | var lines []CasbinRule 512 | 513 | batchFilter := BatchFilter{ 514 | filters: []Filter{}, 515 | } 516 | switch filterValue := filter.(type) { 517 | case Filter: 518 | batchFilter.filters = []Filter{filterValue} 519 | case *Filter: 520 | batchFilter.filters = []Filter{*filterValue} 521 | case []Filter: 522 | batchFilter.filters = filterValue 523 | case BatchFilter: 524 | batchFilter = filterValue 525 | case *BatchFilter: 526 | batchFilter = *filterValue 527 | default: 528 | return errors.New("unsupported filter type") 529 | } 530 | 531 | for _, f := range batchFilter.filters { 532 | if err := a.db.Scopes(a.filterQuery(a.db, f)).Order("ID").Find(&lines).Error; err != nil { 533 | return err 534 | } 535 | 536 | for _, line := range lines { 537 | err := loadPolicyLine(line, model) 538 | if err != nil { 539 | return err 540 | } 541 | } 542 | } 543 | a.isFiltered = true 544 | 545 | return nil 546 | } 547 | 548 | // IsFiltered returns true if the loaded policy has been filtered. 549 | func (a *Adapter) IsFiltered() bool { 550 | return a.isFiltered 551 | } 552 | 553 | // filterQuery builds the gorm query to match the rule filter to use within a scope. 554 | func (a *Adapter) filterQuery(db *gorm.DB, filter Filter) func(db *gorm.DB) *gorm.DB { 555 | return func(db *gorm.DB) *gorm.DB { 556 | if len(filter.Ptype) > 0 { 557 | db = db.Where("ptype in (?)", filter.Ptype) 558 | } 559 | if len(filter.V0) > 0 { 560 | db = db.Where("v0 in (?)", filter.V0) 561 | } 562 | if len(filter.V1) > 0 { 563 | db = db.Where("v1 in (?)", filter.V1) 564 | } 565 | if len(filter.V2) > 0 { 566 | db = db.Where("v2 in (?)", filter.V2) 567 | } 568 | if len(filter.V3) > 0 { 569 | db = db.Where("v3 in (?)", filter.V3) 570 | } 571 | if len(filter.V4) > 0 { 572 | db = db.Where("v4 in (?)", filter.V4) 573 | } 574 | if len(filter.V5) > 0 { 575 | db = db.Where("v5 in (?)", filter.V5) 576 | } 577 | return db 578 | } 579 | } 580 | 581 | func (a *Adapter) savePolicyLine(ptype string, rule []string) CasbinRule { 582 | line := a.getTableInstance() 583 | 584 | line.Ptype = ptype 585 | if len(rule) > 0 { 586 | line.V0 = rule[0] 587 | } 588 | if len(rule) > 1 { 589 | line.V1 = rule[1] 590 | } 591 | if len(rule) > 2 { 592 | line.V2 = rule[2] 593 | } 594 | if len(rule) > 3 { 595 | line.V3 = rule[3] 596 | } 597 | if len(rule) > 4 { 598 | line.V4 = rule[4] 599 | } 600 | if len(rule) > 5 { 601 | line.V5 = rule[5] 602 | } 603 | 604 | return *line 605 | } 606 | 607 | // SavePolicy saves policy to database. 608 | func (a *Adapter) SavePolicy(model model.Model) error { 609 | return a.SavePolicyCtx(context.Background(), model) 610 | } 611 | 612 | // SavePolicyCtx saves policy to database. 613 | func (a *Adapter) SavePolicyCtx(ctx context.Context, model model.Model) error { 614 | var err error 615 | tx := a.db.WithContext(ctx).Clauses(dbresolver.Write).Begin() 616 | 617 | err = a.truncateTable() 618 | 619 | if err != nil { 620 | tx.Rollback() 621 | return err 622 | } 623 | 624 | var lines []CasbinRule 625 | flushEvery := 1000 626 | for ptype, ast := range model["p"] { 627 | for _, rule := range ast.Policy { 628 | lines = append(lines, a.savePolicyLine(ptype, rule)) 629 | if len(lines) > flushEvery { 630 | if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&lines).Error; err != nil { 631 | tx.Rollback() 632 | return err 633 | } 634 | lines = nil 635 | } 636 | } 637 | } 638 | 639 | for ptype, ast := range model["g"] { 640 | for _, rule := range ast.Policy { 641 | lines = append(lines, a.savePolicyLine(ptype, rule)) 642 | if len(lines) > flushEvery { 643 | if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&lines).Error; err != nil { 644 | tx.Rollback() 645 | return err 646 | } 647 | lines = nil 648 | } 649 | } 650 | } 651 | if len(lines) > 0 { 652 | if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&lines).Error; err != nil { 653 | tx.Rollback() 654 | return err 655 | } 656 | } 657 | 658 | err = tx.Commit().Error 659 | return err 660 | } 661 | 662 | // AddPolicy adds a policy rule to the storage. 663 | func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error { 664 | return a.AddPolicyCtx(context.Background(), sec, ptype, rule) 665 | } 666 | 667 | // AddPolicyCtx adds a policy rule to the storage. 668 | func (a *Adapter) AddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error { 669 | line := a.savePolicyLine(ptype, rule) 670 | err := a.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&line).Error 671 | return err 672 | } 673 | 674 | // RemovePolicy removes a policy rule from the storage. 675 | func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error { 676 | return a.RemovePolicyCtx(context.Background(), sec, ptype, rule) 677 | } 678 | 679 | // RemovePolicyCtx removes a policy rule from the storage. 680 | func (a *Adapter) RemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error { 681 | line := a.savePolicyLine(ptype, rule) 682 | err := a.rawDelete(ctx, a.db, line) //can't use db.Delete as we're not using primary key https://gorm.io/docs/update.html 683 | return err 684 | } 685 | 686 | // AddPolicies adds multiple policy rules to the storage. 687 | func (a *Adapter) AddPolicies(sec string, ptype string, rules [][]string) error { 688 | var lines []CasbinRule 689 | for _, rule := range rules { 690 | line := a.savePolicyLine(ptype, rule) 691 | lines = append(lines, line) 692 | } 693 | return a.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&lines).Error 694 | } 695 | 696 | // Transaction perform a set of operations within a transaction 697 | func (a *Adapter) Transaction(e casbin.IEnforcer, fc func(casbin.IEnforcer) error, opts ...*sql.TxOptions) error { 698 | // ensure the transactionMu is initialized 699 | if a.transactionMu == nil { 700 | a.muInitialize.Do(func() { 701 | if a.transactionMu == nil { 702 | a.transactionMu = &sync.Mutex{} 703 | } 704 | }) 705 | } 706 | // lock the transactionMu to ensure the transaction is thread-safe 707 | a.transactionMu.Lock() 708 | defer a.transactionMu.Unlock() 709 | var err error 710 | // reload policy from database to sync with the transaction 711 | defer func() { 712 | e.SetAdapter(a.Copy()) 713 | err = e.LoadPolicy() 714 | if err != nil { 715 | panic(err) 716 | } 717 | }() 718 | copyDB := *a.db 719 | tx := copyDB.Begin(opts...) 720 | b := a.Copy() 721 | // copy enforcer to set the new adapter with transaction tx 722 | copyEnforcer := e 723 | copyEnforcer.SetAdapter(b) 724 | err = fc(copyEnforcer) 725 | if err != nil { 726 | tx.Rollback() 727 | return err 728 | } 729 | err = tx.Commit().Error 730 | return err 731 | } 732 | 733 | // RemovePolicies removes multiple policy rules from the storage. 734 | func (a *Adapter) RemovePolicies(sec string, ptype string, rules [][]string) error { 735 | return a.RemovePoliciesCtx(context.Background(), sec, ptype, rules) 736 | } 737 | 738 | // RemovePoliciesCtx removes multiple policy rules from the storage. 739 | func (a *Adapter) RemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) error { 740 | return a.db.Transaction(func(tx *gorm.DB) error { 741 | for _, rule := range rules { 742 | line := a.savePolicyLine(ptype, rule) 743 | if err := a.rawDelete(ctx, tx, line); err != nil { //can't use db.Delete as we're not using primary key https://gorm.io/docs/update.html 744 | } 745 | } 746 | return nil 747 | }) 748 | } 749 | 750 | // RemoveFilteredPolicy removes policy rules that match the filter from the storage. 751 | func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { 752 | return a.RemoveFilteredPolicyCtx(context.Background(), sec, ptype, fieldIndex, fieldValues...) 753 | } 754 | 755 | // RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage. 756 | func (a *Adapter) RemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) error { 757 | line := a.getTableInstance() 758 | 759 | line.Ptype = ptype 760 | 761 | if fieldIndex == -1 { 762 | return a.rawDelete(ctx, a.db, *line) 763 | } 764 | 765 | err := checkQueryField(fieldValues) 766 | if err != nil { 767 | return err 768 | } 769 | 770 | if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) { 771 | line.V0 = fieldValues[0-fieldIndex] 772 | } 773 | if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) { 774 | line.V1 = fieldValues[1-fieldIndex] 775 | } 776 | if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) { 777 | line.V2 = fieldValues[2-fieldIndex] 778 | } 779 | if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) { 780 | line.V3 = fieldValues[3-fieldIndex] 781 | } 782 | if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) { 783 | line.V4 = fieldValues[4-fieldIndex] 784 | } 785 | if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) { 786 | line.V5 = fieldValues[5-fieldIndex] 787 | } 788 | err = a.rawDelete(ctx, a.db, *line) 789 | return err 790 | } 791 | 792 | // checkQueryfield make sure the fields won't all be empty (string --> "") 793 | func checkQueryField(fieldValues []string) error { 794 | for _, fieldValue := range fieldValues { 795 | if fieldValue != "" { 796 | return nil 797 | } 798 | } 799 | return errors.New("the query field cannot all be empty string (\"\"), please check") 800 | } 801 | 802 | func (a *Adapter) rawDelete(ctx context.Context, db *gorm.DB, line CasbinRule) error { 803 | queryArgs := []interface{}{line.Ptype} 804 | 805 | queryStr := "ptype = ?" 806 | if line.V0 != "" { 807 | queryStr += " and v0 = ?" 808 | queryArgs = append(queryArgs, line.V0) 809 | } 810 | if line.V1 != "" { 811 | queryStr += " and v1 = ?" 812 | queryArgs = append(queryArgs, line.V1) 813 | } 814 | if line.V2 != "" { 815 | queryStr += " and v2 = ?" 816 | queryArgs = append(queryArgs, line.V2) 817 | } 818 | if line.V3 != "" { 819 | queryStr += " and v3 = ?" 820 | queryArgs = append(queryArgs, line.V3) 821 | } 822 | if line.V4 != "" { 823 | queryStr += " and v4 = ?" 824 | queryArgs = append(queryArgs, line.V4) 825 | } 826 | if line.V5 != "" { 827 | queryStr += " and v5 = ?" 828 | queryArgs = append(queryArgs, line.V5) 829 | } 830 | args := append([]interface{}{queryStr}, queryArgs...) 831 | err := db.WithContext(ctx).Delete(a.getTableInstance(), args...).Error 832 | return err 833 | } 834 | 835 | func appendWhere(line CasbinRule) (string, []interface{}) { 836 | queryArgs := []interface{}{line.Ptype} 837 | 838 | queryStr := "ptype = ?" 839 | if line.V0 != "" { 840 | queryStr += " and v0 = ?" 841 | queryArgs = append(queryArgs, line.V0) 842 | } 843 | if line.V1 != "" { 844 | queryStr += " and v1 = ?" 845 | queryArgs = append(queryArgs, line.V1) 846 | } 847 | if line.V2 != "" { 848 | queryStr += " and v2 = ?" 849 | queryArgs = append(queryArgs, line.V2) 850 | } 851 | if line.V3 != "" { 852 | queryStr += " and v3 = ?" 853 | queryArgs = append(queryArgs, line.V3) 854 | } 855 | if line.V4 != "" { 856 | queryStr += " and v4 = ?" 857 | queryArgs = append(queryArgs, line.V4) 858 | } 859 | if line.V5 != "" { 860 | queryStr += " and v5 = ?" 861 | queryArgs = append(queryArgs, line.V5) 862 | } 863 | return queryStr, queryArgs 864 | } 865 | 866 | // UpdatePolicy updates a new policy rule to DB. 867 | func (a *Adapter) UpdatePolicy(sec string, ptype string, oldRule, newPolicy []string) error { 868 | oldLine := a.savePolicyLine(ptype, oldRule) 869 | newLine := a.savePolicyLine(ptype, newPolicy) 870 | return a.db.Model(&oldLine).Where(&oldLine).Updates(newLine).Error 871 | } 872 | 873 | func (a *Adapter) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error { 874 | oldPolicies := make([]CasbinRule, 0, len(oldRules)) 875 | newPolicies := make([]CasbinRule, 0, len(oldRules)) 876 | for _, oldRule := range oldRules { 877 | oldPolicies = append(oldPolicies, a.savePolicyLine(ptype, oldRule)) 878 | } 879 | for _, newRule := range newRules { 880 | newPolicies = append(newPolicies, a.savePolicyLine(ptype, newRule)) 881 | } 882 | tx := a.db.Begin() 883 | for i := range oldPolicies { 884 | if err := tx.Model(&oldPolicies[i]).Where(&oldPolicies[i]).Updates(newPolicies[i]).Error; err != nil { 885 | tx.Rollback() 886 | return err 887 | } 888 | } 889 | return tx.Commit().Error 890 | } 891 | 892 | func (a *Adapter) UpdateFilteredPolicies(sec string, ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { 893 | // UpdateFilteredPolicies deletes old rules and adds new rules. 894 | line := a.getTableInstance() 895 | 896 | line.Ptype = ptype 897 | if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) { 898 | line.V0 = fieldValues[0-fieldIndex] 899 | } 900 | if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) { 901 | line.V1 = fieldValues[1-fieldIndex] 902 | } 903 | if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) { 904 | line.V2 = fieldValues[2-fieldIndex] 905 | } 906 | if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) { 907 | line.V3 = fieldValues[3-fieldIndex] 908 | } 909 | if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) { 910 | line.V4 = fieldValues[4-fieldIndex] 911 | } 912 | if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) { 913 | line.V5 = fieldValues[5-fieldIndex] 914 | } 915 | 916 | newP := make([]CasbinRule, 0, len(newPolicies)) 917 | oldP := make([]CasbinRule, 0) 918 | for _, newRule := range newPolicies { 919 | newP = append(newP, a.savePolicyLine(ptype, newRule)) 920 | } 921 | 922 | tx := a.db.Begin() 923 | str, args := line.queryString() 924 | if err := tx.Where(str, args...).Find(&oldP).Error; err != nil { 925 | tx.Rollback() 926 | return nil, err 927 | } 928 | if err := tx.Where(str, args...).Delete([]CasbinRule{}).Error; err != nil { 929 | tx.Rollback() 930 | return nil, err 931 | } 932 | for i := range newP { 933 | if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&newP[i]).Error; err != nil { 934 | tx.Rollback() 935 | return nil, err 936 | } 937 | } 938 | 939 | // return deleted rulues 940 | oldPolicies := make([][]string, 0) 941 | for _, v := range oldP { 942 | oldPolicy := v.toStringPolicy() 943 | oldPolicies = append(oldPolicies, oldPolicy) 944 | } 945 | return oldPolicies, tx.Commit().Error 946 | } 947 | 948 | func (a *Adapter) Copy() *Adapter { 949 | oriAdapter := a.db 950 | return &Adapter{ 951 | db: oriAdapter, 952 | transactionMu: a.transactionMu, 953 | driverName: a.driverName, 954 | dataSourceName: a.dataSourceName, 955 | databaseName: a.databaseName, 956 | tablePrefix: a.tablePrefix, 957 | tableName: a.tableName, 958 | dbSpecified: a.dbSpecified, 959 | isFiltered: a.isFiltered, 960 | } 961 | } 962 | 963 | // Preview Pre-checking to avoid causing partial load success and partial failure deep 964 | func (a *Adapter) Preview(rules *[]CasbinRule, model model.Model) error { 965 | j := 0 966 | for i, rule := range *rules { 967 | r := []string{rule.Ptype, 968 | rule.V0, rule.V1, rule.V2, 969 | rule.V3, rule.V4, rule.V5} 970 | index := len(r) - 1 971 | for r[index] == "" { 972 | index-- 973 | } 974 | index += 1 975 | p := r[:index] 976 | key := p[0] 977 | sec := key[:1] 978 | ok, err := model.HasPolicyEx(sec, key, p[1:]) 979 | if err != nil { 980 | return err 981 | } 982 | if ok { 983 | (*rules)[j], (*rules)[i] = rule, (*rules)[j] 984 | j++ 985 | } 986 | } 987 | (*rules) = (*rules)[j:] 988 | return nil 989 | } 990 | 991 | func (a *Adapter) GetDb() *gorm.DB { 992 | return a.db 993 | } 994 | 995 | func (c *CasbinRule) queryString() (interface{}, []interface{}) { 996 | queryArgs := []interface{}{c.Ptype} 997 | 998 | queryStr := "ptype = ?" 999 | if c.V0 != "" { 1000 | queryStr += " and v0 = ?" 1001 | queryArgs = append(queryArgs, c.V0) 1002 | } 1003 | if c.V1 != "" { 1004 | queryStr += " and v1 = ?" 1005 | queryArgs = append(queryArgs, c.V1) 1006 | } 1007 | if c.V2 != "" { 1008 | queryStr += " and v2 = ?" 1009 | queryArgs = append(queryArgs, c.V2) 1010 | } 1011 | if c.V3 != "" { 1012 | queryStr += " and v3 = ?" 1013 | queryArgs = append(queryArgs, c.V3) 1014 | } 1015 | if c.V4 != "" { 1016 | queryStr += " and v4 = ?" 1017 | queryArgs = append(queryArgs, c.V4) 1018 | } 1019 | if c.V5 != "" { 1020 | queryStr += " and v5 = ?" 1021 | queryArgs = append(queryArgs, c.V5) 1022 | } 1023 | 1024 | return queryStr, queryArgs 1025 | } 1026 | 1027 | func (c *CasbinRule) toStringPolicy() []string { 1028 | policy := make([]string, 0) 1029 | if c.Ptype != "" { 1030 | policy = append(policy, c.Ptype) 1031 | } 1032 | if c.V0 != "" { 1033 | policy = append(policy, c.V0) 1034 | } 1035 | if c.V1 != "" { 1036 | policy = append(policy, c.V1) 1037 | } 1038 | if c.V2 != "" { 1039 | policy = append(policy, c.V2) 1040 | } 1041 | if c.V3 != "" { 1042 | policy = append(policy, c.V3) 1043 | } 1044 | if c.V4 != "" { 1045 | policy = append(policy, c.V4) 1046 | } 1047 | if c.V5 != "" { 1048 | policy = append(policy, c.V5) 1049 | } 1050 | return policy 1051 | } 1052 | 1053 | // CombineType represents different types of condition combining strategies 1054 | type CombineType uint32 1055 | 1056 | const ( 1057 | CombineTypeOr CombineType = iota // Combine conditions with OR operator 1058 | CombineTypeAnd // Combine conditions with AND operator 1059 | ) 1060 | 1061 | // ConditionsToGormQuery is a function that converts multiple query conditions into a GORM query statement 1062 | // You can use the GetAllowedObjectConditions() API of Casbin to get conditions, 1063 | // and choose the way of combining conditions through combineType. 1064 | func ConditionsToGormQuery(db *gorm.DB, conditions []string, combineType CombineType) *gorm.DB { 1065 | queryDB := db 1066 | for _, cond := range conditions { 1067 | switch combineType { 1068 | case CombineTypeOr: 1069 | queryDB = queryDB.Or(cond) 1070 | case CombineTypeAnd: 1071 | queryDB = queryDB.Where(cond) 1072 | } 1073 | } 1074 | return queryDB 1075 | } 1076 | -------------------------------------------------------------------------------- /adapter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The casbin Authors. All Rights Reserved. 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 gormadapter 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "os" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/casbin/casbin/v2" 25 | "github.com/casbin/casbin/v2/util" 26 | "github.com/glebarez/sqlite" 27 | _ "github.com/go-sql-driver/mysql" 28 | _ "github.com/lib/pq" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/stretchr/testify/require" 31 | "golang.org/x/sync/errgroup" 32 | "gorm.io/driver/mysql" 33 | "gorm.io/driver/postgres" 34 | "gorm.io/driver/sqlserver" 35 | "gorm.io/gorm" 36 | "gorm.io/gorm/logger" 37 | ) 38 | 39 | func testGetPolicy(t *testing.T, e *casbin.Enforcer, res [][]string) { 40 | myRes, err := e.GetPolicy() 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | log.Print("Policy: ", myRes) 46 | 47 | if !util.Array2DEquals(res, myRes) { 48 | t.Error("Policy: ", myRes, ", supposed to be ", res) 49 | } 50 | } 51 | 52 | func testGetPolicyWithoutOrder(t *testing.T, e *casbin.Enforcer, res [][]string) { 53 | myRes, err := e.GetPolicy() 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | log.Print("Policy: ", myRes) 59 | 60 | if !arrayEqualsWithoutOrder(myRes, res) { 61 | t.Error("Policy: ", myRes, ", supposed to be ", res) 62 | } 63 | } 64 | 65 | func arrayEqualsWithoutOrder(a [][]string, b [][]string) bool { 66 | if len(a) != len(b) { 67 | return false 68 | } 69 | 70 | mapA := make(map[int]string) 71 | mapB := make(map[int]string) 72 | order := make(map[int]struct{}) 73 | l := len(a) 74 | 75 | for i := 0; i < l; i++ { 76 | mapA[i] = util.ArrayToString(a[i]) 77 | mapB[i] = util.ArrayToString(b[i]) 78 | } 79 | 80 | for i := 0; i < l; i++ { 81 | for j := 0; j < l; j++ { 82 | if _, ok := order[j]; ok { 83 | if j == l-1 { 84 | return false 85 | } else { 86 | continue 87 | } 88 | } 89 | if mapA[i] == mapB[j] { 90 | order[j] = struct{}{} 91 | break 92 | } else if j == l-1 { 93 | return false 94 | } 95 | } 96 | } 97 | return true 98 | } 99 | 100 | func initPolicy(t *testing.T, a *Adapter) { 101 | // Because the DB is empty at first, 102 | // so we need to load the policy from the file adapter (.CSV) first. 103 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | // This is a trick to save the current policy to the DB. 109 | // We can't call e.SavePolicy() because the adapter in the enforcer is still the file adapter. 110 | // The current policy means the policy in the Casbin enforcer (aka in memory). 111 | err = a.SavePolicy(e.GetModel()) 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | // Clear the current policy. 117 | e.ClearPolicy() 118 | testGetPolicy(t, e, [][]string{}) 119 | 120 | // Load the policy from DB. 121 | err = a.LoadPolicy(e.GetModel()) 122 | if err != nil { 123 | panic(err) 124 | } 125 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 126 | } 127 | 128 | func testSaveLoad(t *testing.T, a *Adapter) { 129 | // Initialize some policy in DB. 130 | initPolicy(t, a) 131 | // Note: you don't need to look at the above code 132 | // if you already have a working DB with policy inside. 133 | 134 | // Now the DB has policy, so we can provide a normal use case. 135 | // Create an adapter and an enforcer. 136 | // NewEnforcer() will load the policy automatically. 137 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 138 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 139 | } 140 | 141 | func initAdapter(t *testing.T, driverName string, dataSourceName string, params ...interface{}) *Adapter { 142 | // Create an adapter 143 | a, err := NewAdapter(driverName, dataSourceName, params...) 144 | if err != nil { 145 | panic(err) 146 | } 147 | 148 | // Initialize some policy in DB. 149 | initPolicy(t, a) 150 | // Now the DB has policy, so we can provide a normal use case. 151 | // Note: you don't need to look at the above code 152 | // if you already have a working DB with policy inside. 153 | 154 | return a 155 | } 156 | 157 | func initAdapterWithGormInstance(t *testing.T, db *gorm.DB) *Adapter { 158 | // Create an adapter 159 | a, _ := NewAdapterByDB(db) 160 | // Initialize some policy in DB. 161 | initPolicy(t, a) 162 | // Now the DB has policy, so we can provide a normal use case. 163 | // Note: you don't need to look at the above code 164 | // if you already have a working DB with policy inside. 165 | 166 | return a 167 | } 168 | 169 | func initAdapterWithGormInstanceAndCustomTable(t *testing.T, db *gorm.DB) *Adapter { 170 | type TestCasbinRule struct { 171 | ID uint `gorm:"primaryKey;autoIncrement"` 172 | Ptype string `gorm:"size:128;uniqueIndex:unique_index"` 173 | V0 string `gorm:"size:128;uniqueIndex:unique_index"` 174 | V1 string `gorm:"size:128;uniqueIndex:unique_index"` 175 | V2 string `gorm:"size:128;uniqueIndex:unique_index"` 176 | V3 string `gorm:"size:128;uniqueIndex:unique_index"` 177 | V4 string `gorm:"size:128;uniqueIndex:unique_index"` 178 | V5 string `gorm:"size:128;uniqueIndex:unique_index"` 179 | } 180 | 181 | // Create an adapter 182 | a, _ := NewAdapterByDBWithCustomTable(db, &TestCasbinRule{}, "test_casbin_rule") 183 | // Initialize some policy in DB. 184 | initPolicy(t, a) 185 | // Now the DB has policy, so we can provide a normal use case. 186 | // Note: you don't need to look at the above code 187 | // if you already have a working DB with policy inside. 188 | 189 | return a 190 | } 191 | 192 | func initAdapterWithGormInstanceByName(t *testing.T, db *gorm.DB, name string) *Adapter { 193 | //Create an Adapter 194 | a, _ := NewAdapterByDBUseTableName(db, "", name) 195 | // Initialize some policy in DB. 196 | initPolicy(t, a) 197 | // Now the DB has policy, so we can provide a normal use case. 198 | // Note: you don't need to look at the above code 199 | // if you already have a working DB with policy inside. 200 | 201 | return a 202 | } 203 | 204 | func initAdapterWithoutAutoMigrate(t *testing.T, db *gorm.DB) *Adapter { 205 | var err error 206 | var customTableName = "without_auto_migrate_custom_table" 207 | hasTable := db.Migrator().HasTable(customTableName) 208 | if hasTable { 209 | err = db.Migrator().DropTable(customTableName) 210 | if err != nil { 211 | panic(err) 212 | } 213 | } 214 | 215 | TurnOffAutoMigrate(db) 216 | 217 | type CustomCasbinRule struct { 218 | ID uint `gorm:"primaryKey;autoIncrement"` 219 | Ptype string `gorm:"size:50"` 220 | V0 string `gorm:"size:50"` 221 | V1 string `gorm:"size:50"` 222 | V2 string `gorm:"size:50"` 223 | V3 string `gorm:"size:50"` 224 | V4 string `gorm:"size:50"` 225 | V5 string `gorm:"size:50"` 226 | V6 string `gorm:"size:50"` 227 | V7 string `gorm:"size:50"` 228 | } 229 | a, err := NewAdapterByDBWithCustomTable(db, &CustomCasbinRule{}, customTableName) 230 | 231 | hasTable = a.db.Migrator().HasTable(a.getFullTableName()) 232 | if hasTable { 233 | t.Fatal("AutoMigration has been disabled but tables are still created in NewAdapterWithoutAutoMigrate method") 234 | } 235 | err = a.db.Migrator().CreateTable(&CustomCasbinRule{}) 236 | if err != nil { 237 | panic(err) 238 | } 239 | initPolicy(t, a) 240 | return a 241 | } 242 | 243 | func initAdapterWithGormInstanceByMulDb(t *testing.T, dbPool DbPool, dbName string, prefix string, tableName string) *Adapter { 244 | //Create an Adapter 245 | a, _ := NewAdapterByMulDb(dbPool, dbName, prefix, tableName) 246 | // Initialize some policy in DB. 247 | initPolicy(t, a) 248 | // Now the DB has policy, so we can provide a normal use case. 249 | // Note: you don't need to look at the above code 250 | // if you already have a working DB with policy inside. 251 | 252 | return a 253 | } 254 | 255 | func initAdapterWithGormInstanceByPrefixAndName(t *testing.T, db *gorm.DB, prefix, name string) *Adapter { 256 | //Create an Adapter 257 | a, _ := NewAdapterByDBUseTableName(db, prefix, name) 258 | // Initialize some policy in DB. 259 | initPolicy(t, a) 260 | // Now the DB has policy, so we can provide a normal use case. 261 | // Note: you don't need to look at the above code 262 | // if you already have a working DB with policy inside. 263 | 264 | return a 265 | } 266 | 267 | func TestNilField(t *testing.T) { 268 | a, err := NewAdapter("sqlite3", "test.db") 269 | assert.Nil(t, err) 270 | defer os.Remove("test.db") 271 | 272 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 273 | assert.Nil(t, err) 274 | e.EnableAutoSave(false) 275 | 276 | ok, err := e.AddPolicy("", "data1", "write") 277 | assert.Nil(t, err) 278 | e.SavePolicy() 279 | assert.Nil(t, e.LoadPolicy()) 280 | 281 | ok, err = e.Enforce("", "data1", "write") 282 | assert.Nil(t, err) 283 | assert.Equal(t, ok, true) 284 | } 285 | 286 | func testAutoSave(t *testing.T, a *Adapter) { 287 | 288 | // NewEnforcer() will load the policy automatically. 289 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 290 | // AutoSave is enabled by default. 291 | // Now we disable it. 292 | e.EnableAutoSave(false) 293 | 294 | // Because AutoSave is disabled, the policy change only affects the policy in Casbin enforcer, 295 | // it doesn't affect the policy in the storage. 296 | e.AddPolicy("alice", "data1", "write") 297 | // Reload the policy from the storage to see the effect. 298 | e.LoadPolicy() 299 | // This is still the original policy. 300 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 301 | 302 | // Now we enable the AutoSave. 303 | e.EnableAutoSave(true) 304 | 305 | // Because AutoSave is enabled, the policy change not only affects the policy in Casbin enforcer, 306 | // but also affects the policy in the storage. 307 | e.AddPolicy("alice", "data1", "write") 308 | // Reload the policy from the storage to see the effect. 309 | e.LoadPolicy() 310 | // The policy has a new rule: {"alice", "data1", "write"}. 311 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data1", "write"}}) 312 | 313 | // Remove the added rule. 314 | e.RemovePolicy("alice", "data1", "write") 315 | e.LoadPolicy() 316 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 317 | 318 | // Remove "data2_admin" related policy rules via a filter. 319 | // Two rules: {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"} are deleted. 320 | e.RemoveFilteredPolicy(0, "data2_admin") 321 | e.LoadPolicy() 322 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}}) 323 | } 324 | 325 | func testFilteredPolicy(t *testing.T, a *Adapter) { 326 | // NewEnforcer() without an adapter will not auto load the policy 327 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf") 328 | // Now set the adapter 329 | e.SetAdapter(a) 330 | 331 | // Load only alice's policies 332 | assert.Nil(t, e.LoadFilteredPolicy(Filter{V0: []string{"alice"}})) 333 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}}) 334 | 335 | // Load only bob's policies 336 | assert.Nil(t, e.LoadFilteredPolicy(Filter{V0: []string{"bob"}})) 337 | testGetPolicy(t, e, [][]string{{"bob", "data2", "write"}}) 338 | 339 | // Load policies for data2_admin 340 | assert.Nil(t, e.LoadFilteredPolicy(Filter{V0: []string{"data2_admin"}})) 341 | testGetPolicy(t, e, [][]string{{"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 342 | 343 | // Load policies for alice and bob 344 | assert.Nil(t, e.LoadFilteredPolicy(Filter{V0: []string{"alice", "bob"}})) 345 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}}) 346 | 347 | assert.Nil(t, e.LoadFilteredPolicy(BatchFilter{ 348 | filters: []Filter{ 349 | {V0: []string{"alice"}}, 350 | {V1: []string{"data2"}}, 351 | }, 352 | })) 353 | testGetPolicy(t, e, [][]string{ 354 | {"alice", "data1", "read"}, 355 | {"bob", "data2", "write"}, 356 | {"data2_admin", "data2", "read"}, 357 | {"data2_admin", "data2", "write"}, 358 | }) 359 | } 360 | 361 | func testUpdatePolicy(t *testing.T, a *Adapter) { 362 | // NewEnforcer() will load the policy automatically. 363 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 364 | 365 | e.EnableAutoSave(true) 366 | e.UpdatePolicy([]string{"alice", "data1", "read"}, []string{"alice", "data1", "write"}) 367 | e.LoadPolicy() 368 | testGetPolicy(t, e, [][]string{{"alice", "data1", "write"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 369 | } 370 | 371 | func testUpdatePolicies(t *testing.T, a *Adapter) { 372 | // NewEnforcer() will load the policy automatically. 373 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 374 | 375 | e.EnableAutoSave(true) 376 | e.UpdatePolicies([][]string{{"alice", "data1", "write"}, {"bob", "data2", "write"}}, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "read"}}) 377 | e.LoadPolicy() 378 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "read"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 379 | } 380 | 381 | func testUpdateFilteredPolicies(t *testing.T, a *Adapter) { 382 | // NewEnforcer() will load the policy automatically. 383 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 384 | 385 | e.EnableAutoSave(true) 386 | e.UpdateFilteredPolicies([][]string{{"alice", "data1", "write"}}, 0, "alice", "data1", "read") 387 | e.UpdateFilteredPolicies([][]string{{"bob", "data2", "read"}}, 0, "bob", "data2", "write") 388 | e.LoadPolicy() 389 | testGetPolicyWithoutOrder(t, e, [][]string{{"alice", "data1", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"bob", "data2", "read"}}) 390 | } 391 | 392 | func TestAdapterWithCustomTable(t *testing.T) { 393 | db, err := gorm.Open(postgres.Open("user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable"), &gorm.Config{}) 394 | if err != nil { 395 | panic(err) 396 | } 397 | 398 | if err = db.Exec("CREATE DATABASE casbin_custom_table").Error; err != nil { 399 | // 42P04 is duplicate_database 400 | if !strings.Contains(fmt.Sprintf("%s", err), "42P04") { 401 | panic(err) 402 | } 403 | } 404 | 405 | db, err = gorm.Open(postgres.Open("user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable dbname=casbin_custom_table"), &gorm.Config{}) 406 | if err != nil { 407 | panic(err) 408 | } 409 | 410 | a := initAdapterWithGormInstanceAndCustomTable(t, db) 411 | testAutoSave(t, a) 412 | testSaveLoad(t, a) 413 | 414 | a = initAdapterWithGormInstanceAndCustomTable(t, db) 415 | testFilteredPolicy(t, a) 416 | } 417 | 418 | func TestAdapterWithoutAutoMigrate(t *testing.T) { 419 | db, err := gorm.Open(mysql.Open("root:@tcp(127.0.0.1:3306)/casbin"), &gorm.Config{}) 420 | if err != nil { 421 | panic(err) 422 | } 423 | 424 | a := initAdapterWithoutAutoMigrate(t, db) 425 | testAutoSave(t, a) 426 | testSaveLoad(t, a) 427 | 428 | a = initAdapterWithoutAutoMigrate(t, db) 429 | testFilteredPolicy(t, a) 430 | 431 | db, err = gorm.Open(postgres.Open("user=postgres password=postgres host=localhost port=5432 sslmode=disable TimeZone=Asia/Shanghai"), &gorm.Config{}) 432 | if err != nil { 433 | panic(err) 434 | } 435 | 436 | if err = db.Exec("CREATE DATABASE casbin_custom_table").Error; err != nil { 437 | // 42P04 is duplicate_database 438 | if !strings.Contains(fmt.Sprintf("%s", err), "42P04") { 439 | panic(err) 440 | } 441 | } 442 | 443 | db, err = gorm.Open(postgres.Open("user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable dbname=casbin_custom_table"), &gorm.Config{}) 444 | if err != nil { 445 | panic(err) 446 | } 447 | 448 | a = initAdapterWithoutAutoMigrate(t, db) 449 | testAutoSave(t, a) 450 | testSaveLoad(t, a) 451 | 452 | a = initAdapterWithoutAutoMigrate(t, db) 453 | testFilteredPolicy(t, a) 454 | 455 | db, err = gorm.Open(sqlite.Open("casbin.db"), &gorm.Config{}) 456 | if err != nil { 457 | panic(err) 458 | } 459 | 460 | a = initAdapterWithoutAutoMigrate(t, db) 461 | testAutoSave(t, a) 462 | testSaveLoad(t, a) 463 | 464 | a = initAdapterWithoutAutoMigrate(t, db) 465 | testFilteredPolicy(t, a) 466 | 467 | db, err = gorm.Open(sqlserver.Open("sqlserver://sa:SqlServer123@localhost:1433?database=master"), &gorm.Config{}) 468 | if err != nil { 469 | panic(err) 470 | } 471 | 472 | a = initAdapterWithoutAutoMigrate(t, db) 473 | testAutoSave(t, a) 474 | testSaveLoad(t, a) 475 | 476 | a = initAdapterWithoutAutoMigrate(t, db) 477 | testFilteredPolicy(t, a) 478 | } 479 | 480 | func TestAdapterWithMulDb(t *testing.T) { 481 | //create new database 482 | NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/", "casbin") 483 | NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/", "casbin2") 484 | 485 | testBasicFeatures(t) 486 | testIndependenceBetweenMulDb(t) 487 | } 488 | 489 | func testIndependenceBetweenMulDb(t *testing.T) { 490 | dsn := "root:@tcp(127.0.0.1:3306)/casbin" 491 | dsn2 := "root:@tcp(127.0.0.1:3306)/casbin2" 492 | 493 | dbPool, err := InitDbResolver([]gorm.Dialector{mysql.Open(dsn), mysql.Open(dsn2)}, []string{"casbin", "casbin2"}) 494 | 495 | if err != nil { 496 | panic(err) 497 | } 498 | 499 | //test independence between multi adapter 500 | a1 := initAdapterWithGormInstanceByMulDb(t, dbPool, "casbin", "", "casbin_rule") 501 | a1.AddPolicy("p", "p", []string{"alice", "book", "read"}) 502 | a2 := initAdapterWithGormInstanceByMulDb(t, dbPool, "casbin2", "", "casbin_rule2") 503 | e, _ := casbin.NewEnforcer("./examples/rbac_model.conf", a2) 504 | res, err := e.Enforce("alice", "book", "read") 505 | if err != nil || res { 506 | t.Error("switch DB fail because data don't change") 507 | } 508 | } 509 | 510 | func testBasicFeatures(t *testing.T) { 511 | dsn := "root:@tcp(127.0.0.1:3306)/casbin" 512 | dsn2 := "root:@tcp(127.0.0.1:3306)/casbin2" 513 | 514 | dbPool, err := InitDbResolver([]gorm.Dialector{mysql.Open(dsn), mysql.Open(dsn2)}, []string{"casbin", "casbin2"}) 515 | 516 | if err != nil { 517 | panic(err) 518 | } 519 | //test basic features 520 | a := initAdapterWithGormInstanceByMulDb(t, dbPool, "casbin", "", "casbin_rule") 521 | testAutoSave(t, a) 522 | testSaveLoad(t, a) 523 | a = initAdapterWithGormInstanceByMulDb(t, dbPool, "casbin", "", "casbin_rule") 524 | testFilteredPolicy(t, a) 525 | 526 | a = initAdapterWithGormInstanceByMulDb(t, dbPool, "casbin2", "", "casbin_rule2") 527 | testAutoSave(t, a) 528 | testSaveLoad(t, a) 529 | a = initAdapterWithGormInstanceByMulDb(t, dbPool, "casbin2", "", "casbin_rule2") 530 | testFilteredPolicy(t, a) 531 | } 532 | 533 | func TestAdapters(t *testing.T) { 534 | a := initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/", "casbin", "casbin_rule") 535 | testAutoSave(t, a) 536 | testSaveLoad(t, a) 537 | 538 | a = initAdapter(t, "postgres", "user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable") 539 | testAutoSave(t, a) 540 | testSaveLoad(t, a) 541 | 542 | a = initAdapter(t, "sqlite3", "casbin.db") 543 | testAutoSave(t, a) 544 | testSaveLoad(t, a) 545 | 546 | a = initAdapter(t, "sqlserver", "sqlserver://sa:SqlServer123@localhost:1433", "master", "casbin_rule") 547 | testAutoSave(t, a) 548 | testSaveLoad(t, a) 549 | 550 | db, err := gorm.Open(mysql.Open("root:@tcp(127.0.0.1:3306)/casbin"), &gorm.Config{}) 551 | if err != nil { 552 | panic(err) 553 | } 554 | a = initAdapterWithGormInstance(t, db) 555 | testAutoSave(t, a) 556 | testSaveLoad(t, a) 557 | 558 | a = initAdapterWithGormInstance(t, db) 559 | testFilteredPolicy(t, a) 560 | 561 | db, err = gorm.Open(postgres.Open("user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable dbname=casbin"), &gorm.Config{}) 562 | if err != nil { 563 | panic(err) 564 | } 565 | a = initAdapterWithGormInstance(t, db) 566 | testAutoSave(t, a) 567 | testSaveLoad(t, a) 568 | 569 | a = initAdapterWithGormInstance(t, db) 570 | testFilteredPolicy(t, a) 571 | 572 | db, err = gorm.Open(sqlite.Open("casbin.db"), &gorm.Config{}) 573 | if err != nil { 574 | panic(err) 575 | } 576 | a = initAdapterWithGormInstance(t, db) 577 | testAutoSave(t, a) 578 | testSaveLoad(t, a) 579 | 580 | a = initAdapterWithGormInstance(t, db) 581 | testFilteredPolicy(t, a) 582 | 583 | db, err = gorm.Open(sqlserver.Open("sqlserver://sa:SqlServer123@localhost:1433?database=master"), &gorm.Config{}) 584 | if err != nil { 585 | panic(err) 586 | } 587 | a = initAdapterWithGormInstance(t, db) 588 | testAutoSave(t, a) 589 | testSaveLoad(t, a) 590 | 591 | a = initAdapterWithGormInstance(t, db) 592 | testFilteredPolicy(t, a) 593 | 594 | db, err = gorm.Open(mysql.Open("root:@tcp(127.0.0.1:3306)/casbin"), &gorm.Config{}) 595 | if err != nil { 596 | panic(err) 597 | } 598 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 599 | testAutoSave(t, a) 600 | testSaveLoad(t, a) 601 | 602 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 603 | testFilteredPolicy(t, a) 604 | 605 | db, err = gorm.Open(postgres.Open("user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable dbname=casbin"), &gorm.Config{}) 606 | if err != nil { 607 | panic(err) 608 | } 609 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 610 | testAutoSave(t, a) 611 | testSaveLoad(t, a) 612 | 613 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 614 | testFilteredPolicy(t, a) 615 | 616 | a = initAdapterWithGormInstanceByPrefixAndName(t, db, "casbin", "first") 617 | testAutoSave(t, a) 618 | testSaveLoad(t, a) 619 | 620 | a = initAdapterWithGormInstanceByPrefixAndName(t, db, "casbin", "second") 621 | testFilteredPolicy(t, a) 622 | 623 | db, err = gorm.Open(sqlite.Open("casbin.db"), &gorm.Config{}) 624 | if err != nil { 625 | panic(err) 626 | } 627 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 628 | testAutoSave(t, a) 629 | testSaveLoad(t, a) 630 | 631 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 632 | testFilteredPolicy(t, a) 633 | 634 | db, err = gorm.Open(sqlserver.Open("sqlserver://sa:SqlServer123@localhost:1433?database=master"), &gorm.Config{}) 635 | if err != nil { 636 | panic(err) 637 | } 638 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 639 | testAutoSave(t, a) 640 | testSaveLoad(t, a) 641 | 642 | a = initAdapterWithGormInstanceByName(t, db, "casbin_rule") 643 | testFilteredPolicy(t, a) 644 | 645 | a = initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/", "casbin", "casbin_rule") 646 | testUpdatePolicy(t, a) 647 | testUpdatePolicies(t, a) 648 | testUpdateFilteredPolicies(t, a) 649 | 650 | a = initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/", "casbin", "casbin_rule") 651 | a.AddLogger(logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{})) 652 | testUpdatePolicy(t, a) 653 | testUpdatePolicies(t, a) 654 | testUpdateFilteredPolicies(t, a) 655 | 656 | a = initAdapter(t, "postgres", "user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable") 657 | testUpdatePolicy(t, a) 658 | testUpdatePolicies(t, a) 659 | testUpdateFilteredPolicies(t, a) 660 | 661 | a = initAdapter(t, "postgres", "user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable") 662 | a.AddLogger(logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{})) 663 | testUpdatePolicy(t, a) 664 | testUpdatePolicies(t, a) 665 | testUpdateFilteredPolicies(t, a) 666 | 667 | a = initAdapter(t, "sqlite3", "casbin.db") 668 | testUpdatePolicy(t, a) 669 | testUpdatePolicies(t, a) 670 | 671 | a = initAdapter(t, "sqlserver", "sqlserver://sa:SqlServer123@localhost:1433", "master", "casbin_rule") 672 | testUpdatePolicy(t, a) 673 | testUpdatePolicies(t, a) 674 | testUpdateFilteredPolicies(t, a) 675 | 676 | a = initAdapter(t, "sqlserver", "sqlserver://sa:SqlServer123@localhost:1433", "master", "casbin_rule") 677 | a.AddLogger(logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{})) 678 | testUpdatePolicy(t, a) 679 | testUpdatePolicies(t, a) 680 | testUpdateFilteredPolicies(t, a) 681 | } 682 | 683 | func TestAddPolicies(t *testing.T) { 684 | a := initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/", "casbin", "casbin_rule") 685 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 686 | e.AddPolicies([][]string{{"jack", "data1", "read"}, {"jack2", "data1", "read"}}) 687 | e.LoadPolicy() 688 | 689 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"jack", "data1", "read"}, {"jack2", "data1", "read"}}) 690 | } 691 | 692 | func TestAddPolicy(t *testing.T) { 693 | tests := []struct { 694 | driverName string 695 | dataSourceName string 696 | params []any 697 | }{ 698 | {"mysql", "root:@tcp(127.0.0.1:3306)/", []any{"casbin", "casbin_rule"}}, 699 | {"postgres", "user=postgres password=postgres host=127.0.0.1 port=5432 sslmode=disable", nil}, 700 | // {"sqlserver", "sqlserver://sa:SqlServer123@localhost:1433", []any{"master", "casbin_rule"}}, 701 | } 702 | 703 | for _, test := range tests { 704 | test := test 705 | t.Run(test.driverName, func(t *testing.T) { 706 | t.Parallel() 707 | a := initAdapter(t, test.driverName, test.dataSourceName, test.params...) 708 | e1, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 709 | e2, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 710 | 711 | policy := []string{"alice", "data1", "TestAddPolicy"} 712 | 713 | ok, err := e1.AddPolicy(policy) 714 | if err != nil { 715 | t.Errorf("e1.AddPolicy() got err %v", err) 716 | } 717 | if !ok { 718 | t.Errorf("e1.AddPolicy() got false, want true") 719 | } 720 | 721 | ok, err = e2.AddPolicy(policy) 722 | if err != nil { 723 | t.Errorf("e2.AddPolicy() got err %v", err) 724 | } 725 | if !ok { 726 | t.Errorf("e2.AddPolicy() got false, want true") 727 | } 728 | }) 729 | } 730 | } 731 | 732 | func TestTransaction(t *testing.T) { 733 | a := initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/", "casbin", "casbin_rule") 734 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 735 | err := e.GetAdapter().(*Adapter).Transaction(e, func(e casbin.IEnforcer) error { 736 | _, err := e.AddPolicy("jack", "data1", "write") 737 | if err != nil { 738 | return err 739 | } 740 | _, err = e.AddPolicy("jack", "data2", "write") 741 | //err = errors.New("some error") 742 | if err != nil { 743 | return err 744 | } 745 | return nil 746 | }) 747 | if err != nil { 748 | return 749 | } 750 | } 751 | 752 | func TestTransactionRace(t *testing.T) { 753 | a := initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/", "casbin", "casbin_rule") 754 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 755 | 756 | concurrency := 100 757 | 758 | var g errgroup.Group 759 | for i := 0; i < concurrency; i++ { 760 | i := i 761 | g.Go(func() error { 762 | return e.GetAdapter().(*Adapter).Transaction(e, func(e casbin.IEnforcer) error { 763 | _, err := e.AddPolicy("jack", fmt.Sprintf("data%d", i), "write") 764 | if err != nil { 765 | return err 766 | } 767 | return nil 768 | }) 769 | }) 770 | } 771 | require.NoError(t, g.Wait()) 772 | 773 | for i := 0; i < concurrency; i++ { 774 | hasPolicy, err := e.HasPolicy("jack", fmt.Sprintf("data%d", i), "write") 775 | if err != nil { 776 | panic(err) 777 | } 778 | 779 | require.True(t, hasPolicy) 780 | } 781 | } 782 | 783 | func TestTransactionWithSavePolicy(t *testing.T) { 784 | a := initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/", "casbin", "casbin_rule") 785 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 786 | defer func() { 787 | e.ClearPolicy() 788 | err := e.SavePolicy() 789 | if err != nil { 790 | t.Fatalf("save policy err %v", err) 791 | } 792 | }() 793 | err := e.GetAdapter().(*Adapter).Transaction(e, func(e casbin.IEnforcer) error { 794 | _, err := e.AddPolicy("jack", "data1", "write") 795 | if err != nil { 796 | return err 797 | } 798 | _, err = e.AddPolicy("jack", "data2", "write") 799 | if err != nil { 800 | return err 801 | } 802 | err = e.SavePolicy() 803 | if err != nil { 804 | return err 805 | } 806 | return nil 807 | }) 808 | if err != nil { 809 | return 810 | } 811 | } 812 | -------------------------------------------------------------------------------- /examples/object_conditions_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, sub_rule, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && eval(p.sub_rule) && r.act == p.act -------------------------------------------------------------------------------- /examples/object_conditions_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, r.obj.price < 25, read 2 | p, admin, r.obj.category_id = 2, read 3 | p, bob, r.obj.author = bob, write 4 | 5 | g, alice, admin -------------------------------------------------------------------------------- /examples/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /examples/rbac_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, data1, read 2 | p, bob, data2, write 3 | p, data2_admin, data2, read 4 | p, data2_admin, data2, write 5 | g, alice, data2_admin -------------------------------------------------------------------------------- /examples/rbac_with_domains_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, dom, obj, act 3 | 4 | [policy_definition] 5 | p = sub, dom, obj, act 6 | 7 | [role_definition] 8 | g = _, _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /examples/rbac_with_domains_policy.csv: -------------------------------------------------------------------------------- 1 | p, admin, domain1, data1, read 2 | p, admin, domain1, data1, write 3 | p, admin, domain2, data2, read 4 | p, admin, domain2, data2, write 5 | g, alice, admin, domain1 6 | g, bob, admin, domain2 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/casbin/gorm-adapter/v3 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/casbin/casbin/v2 v2.100.0 7 | github.com/glebarez/sqlite v1.7.0 8 | github.com/go-sql-driver/mysql v1.7.0 9 | github.com/lib/pq v1.10.2 10 | github.com/stretchr/testify v1.8.4 11 | golang.org/x/sync v0.1.0 12 | gorm.io/driver/mysql v1.5.7 13 | gorm.io/driver/postgres v1.5.9 14 | gorm.io/driver/sqlserver v1.5.3 15 | gorm.io/gorm v1.25.12 16 | gorm.io/plugin/dbresolver v1.5.3 17 | ) 18 | 19 | require ( 20 | github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect 21 | github.com/casbin/govaluate v1.2.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/dustin/go-humanize v1.0.1 // indirect 24 | github.com/glebarez/go-sqlite v1.20.3 // indirect 25 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 26 | github.com/golang-sql/sqlexp v0.1.0 // indirect 27 | github.com/google/uuid v1.3.0 // indirect 28 | github.com/jackc/pgpassfile v1.0.0 // indirect 29 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 30 | github.com/jackc/pgx/v5 v5.5.5 // indirect 31 | github.com/jackc/puddle/v2 v2.2.1 // indirect 32 | github.com/jinzhu/inflection v1.0.0 // indirect 33 | github.com/jinzhu/now v1.1.5 // indirect 34 | github.com/kr/text v0.1.0 // indirect 35 | github.com/mattn/go-isatty v0.0.17 // indirect 36 | github.com/microsoft/go-mssqldb v1.6.0 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect 39 | github.com/rogpeppe/go-internal v1.12.0 // indirect 40 | golang.org/x/crypto v0.17.0 // indirect 41 | golang.org/x/sys v0.15.0 // indirect 42 | golang.org/x/text v0.14.0 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | modernc.org/libc v1.22.2 // indirect 45 | modernc.org/mathutil v1.5.0 // indirect 46 | modernc.org/memory v1.5.0 // indirect 47 | modernc.org/sqlite v1.20.3 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= 2 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= 3 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= 4 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= 5 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= 6 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= 7 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= 8 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= 9 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= 10 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= 11 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= 12 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= 13 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= 14 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4= 15 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= 16 | github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= 17 | github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os= 18 | github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 19 | github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= 20 | github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 21 | github.com/casbin/casbin/v2 v2.100.0 h1:aeugSNjjHfCrgA22nHkVvw2xsscboHv5r0a13ljQKGQ= 22 | github.com/casbin/casbin/v2 v2.100.0/go.mod h1:LO7YPez4dX3LgoTCqSQAleQDo0S0BeZBDxYnPUl95Ng= 23 | github.com/casbin/govaluate v1.2.0 h1:wXCXFmqyY+1RwiKfYo3jMKyrtZmOL3kHwaqDyCPOYak= 24 | github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= 29 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 30 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 31 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 32 | github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= 33 | github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= 34 | github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI= 35 | github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk= 36 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 37 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 38 | github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 39 | github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 40 | github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= 41 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 42 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 43 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 44 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 45 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 46 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 47 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 48 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 49 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 50 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 52 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 53 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 54 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 55 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 56 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 57 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 58 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 59 | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 60 | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 61 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 62 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 63 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 64 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 65 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 66 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 67 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 68 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 69 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 70 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 71 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 72 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 73 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 74 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 75 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 76 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 77 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 78 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 79 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 80 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 81 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 82 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 83 | github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc= 84 | github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= 85 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 86 | github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 87 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= 88 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 89 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 90 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 91 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 92 | github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M= 93 | github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 94 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 95 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 96 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 98 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 101 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 102 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 103 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 104 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 105 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 106 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 107 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 108 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 109 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 110 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 111 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 112 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 113 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 114 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 115 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 116 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 117 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 118 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 119 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 120 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 121 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 122 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 123 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 124 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 125 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 126 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 127 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 128 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 129 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 130 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 131 | golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= 132 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 133 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 136 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 151 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 152 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 153 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 154 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 155 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 156 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 157 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 158 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 159 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 160 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 161 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 162 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 163 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 164 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 165 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 166 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 167 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 168 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 169 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 170 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 171 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 172 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 173 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 174 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 175 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 178 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 179 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 180 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 181 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 182 | gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= 183 | gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 184 | gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= 185 | gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= 186 | gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= 187 | gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= 188 | gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 189 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 190 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 191 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 192 | gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= 193 | gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= 194 | modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= 195 | modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= 196 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 197 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 198 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 199 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 200 | modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs= 201 | modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= 202 | --------------------------------------------------------------------------------