├── examples ├── rbac_policy.csv ├── rbac_model.conf └── rbac_tenant_service.conf ├── .travis.yml ├── .releaserc.json ├── .gitignore ├── go.mod ├── .github └── workflows │ └── ci.yml ├── README.md ├── go.sum ├── LICENSE ├── adapter.go └── adapter_test.go /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 -------------------------------------------------------------------------------- /.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 | - mongodb -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | .vscode/ 18 | vendor/ 19 | *.iml -------------------------------------------------------------------------------- /examples/rbac_tenant_service.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = tenant, sub, obj, act, service 3 | 4 | [policy_definition] 5 | p =tenant, sub, obj, act, service, eft 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = priority(p.eft) || deny 12 | 13 | [matchers] 14 | m = r.tenant == p.tenant && g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") && (r.service == p.service || p.service == "*") 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/casbin/mongodb-adapter/v4 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/casbin/casbin/v2 v2.71.1 7 | go.mongodb.org/mongo-driver/v2 v2.2.1 8 | ) 9 | 10 | require ( 11 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect 12 | github.com/golang/snappy v1.0.0 // indirect 13 | github.com/klauspost/compress v1.16.7 // indirect 14 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 15 | github.com/xdg-go/scram v1.1.2 // indirect 16 | github.com/xdg-go/stringprep v1.0.4 // indirect 17 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 18 | golang.org/x/crypto v0.33.0 // indirect 19 | golang.org/x/sync v0.11.0 // indirect 20 | golang.org/x/text v0.22.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push,pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | runs-on: ubuntu-latest 9 | 10 | services: 11 | mongodb: 12 | image: mongo 13 | ports: 14 | - 27017:27017 15 | 16 | steps: 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: '1.20' 21 | 22 | - name: Start MongoDB 23 | uses: supercharge/mongodb-github-action@1.7.0 24 | with: 25 | mongodb-version: 4.4 26 | mongodb-replica-set: test-rs 27 | mongodb-port: 42069 28 | 29 | - uses: actions/checkout@v2 30 | - name: Run Unit tests 31 | run: go test -v -coverprofile=./profile.cov ./... 32 | 33 | - uses: shogo82148/actions-goveralls@v1 34 | with: 35 | path-to-profile: ./profile.cov 36 | 37 | semantic-release: 38 | needs: [test] 39 | runs-on: ubuntu-latest 40 | steps: 41 | 42 | - uses: actions/checkout@v2 43 | 44 | - name: Run semantic-release 45 | if: github.repository == 'casbin/mongodb-adapter' && github.event_name == 'push' 46 | run: | 47 | npm install --save-dev semantic-release@17.2.4 48 | npx semantic-release 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MongoDB Adapter [![CI](https://github.com/casbin/mongodb-adapter/actions/workflows/ci.yml/badge.svg)](https://github.com/casbin/mongodb-adapter/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/casbin/mongodb-adapter/badge.svg?branch=master)](https://coveralls.io/github/casbin/mongodb-adapter?branch=master) [![Godoc](https://godoc.org/github.com/casbin/mongodb-adapter?status.svg)](https://godoc.org/github.com/casbin/mongodb-adapter) 2 | ==== 3 | 4 | MongoDB Adapter is the [Mongo DB](https://www.mongodb.com) adapter for [Casbin](https://github.com/casbin/casbin). With this library, Casbin can load policy from MongoDB or save policy to it. 5 | 6 | ## Installation 7 | 8 | go get -u github.com/casbin/mongodb-adapter/v4 9 | 10 | ## Simple Example 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "github.com/casbin/casbin/v2" 17 | "github.com/casbin/mongodb-adapter/v4" 18 | ) 19 | 20 | func main() { 21 | // Initialize a MongoDB adapter and use it in a Casbin enforcer: 22 | // The adapter will use the database named "casbin". 23 | // If it doesn't exist, the adapter will create it automatically. 24 | a,err := mongodbadapter.NewAdapter("127.0.0.1:27017") // Your MongoDB URL. 25 | if err != nil { 26 | panic(err) 27 | } 28 | // Or you can use an existing DB "abc" like this: 29 | // The adapter will use the table named "casbin_rule". 30 | // If it doesn't exist, the adapter will create it automatically. 31 | // a := mongodbadapter.NewAdapter("127.0.0.1:27017/abc") 32 | 33 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | // Load the policy from DB. 39 | e.LoadPolicy() 40 | 41 | // Check the permission. 42 | e.Enforce("alice", "data1", "read") 43 | 44 | // Modify the policy. 45 | // e.AddPolicy(...) 46 | // e.RemovePolicy(...) 47 | 48 | // Save the policy back to DB. 49 | e.SavePolicy() 50 | } 51 | ``` 52 | ## Advanced Example 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "github.com/casbin/casbin/v2" 59 | "github.com/casbin/mongodb-adapter/v4" 60 | mongooptions "go.mongodb.org/mongo-driver/mongo/options" 61 | ) 62 | 63 | func main() { 64 | // Initialize a MongoDB adapter with NewAdapterWithClientOption: 65 | // The adapter will use custom mongo client options. 66 | // custom database name. 67 | // default collection name 'casbin_rule'. 68 | mongoClientOption := mongooptions.Client().ApplyURI("mongodb://127.0.0.1:27017") 69 | databaseName := "casbin" 70 | a,err := mongodbadapter.NewAdapterWithClientOption(mongoClientOption, databaseName) 71 | // Or you can use NewAdapterWithCollectionName for custom collection name. 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | // Load the policy from DB. 82 | e.LoadPolicy() 83 | 84 | // Check the permission. 85 | e.Enforce("alice", "data1", "read") 86 | 87 | // Modify the policy. 88 | // e.AddPolicy(...) 89 | // e.RemovePolicy(...) 90 | 91 | // Save the policy back to DB. 92 | e.SavePolicy() 93 | } 94 | ``` 95 | 96 | ## Filtered Policies 97 | 98 | ```go 99 | import "github.com/globalsign/mgo/bson" 100 | 101 | // This adapter also implements the FilteredAdapter interface. This allows for 102 | // efficent, scalable enforcement of very large policies: 103 | filter := &bson.M{"v0": "alice"} 104 | e.LoadFilteredPolicy(filter) 105 | 106 | // The loaded policy is now a subset of the policy in storage, containing only 107 | // the policy lines that match the provided filter. This filter should be a 108 | // valid MongoDB selector using BSON. A filtered policy cannot be saved. 109 | ``` 110 | 111 | ## Getting Help 112 | 113 | - [Casbin](https://github.com/casbin/casbin) 114 | 115 | ## License 116 | 117 | This project is under Apache 2.0 License. See the [LICENSE](LICENSE) file for the full license text. 118 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= 2 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 3 | github.com/casbin/casbin/v2 v2.71.1 h1:LRHyqM0S1LzM/K59PmfUIN0ZJfLgcOjL4OhOQI/FNXU= 4 | github.com/casbin/casbin/v2 v2.71.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 7 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 8 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 9 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 12 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 13 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 14 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 15 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 16 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 17 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 18 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 19 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 20 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 21 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 22 | go.mongodb.org/mongo-driver/v2 v2.2.1 h1:w5xra3yyu/sGrziMzK1D0cRRaH/b7lWCSsoN6+WV6AM= 23 | go.mongodb.org/mongo-driver/v2 v2.2.1/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps= 24 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 25 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 26 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 27 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 28 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 29 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 30 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 31 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 32 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 33 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 34 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 36 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 37 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 38 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 43 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 46 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 47 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 48 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 49 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 50 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 51 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 52 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 53 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 54 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /adapter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 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 mongodbadapter 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "log" 22 | "runtime" 23 | "strings" 24 | "time" 25 | 26 | "github.com/casbin/casbin/v2/model" 27 | "github.com/casbin/casbin/v2/persist" 28 | "go.mongodb.org/mongo-driver/v2/bson" 29 | "go.mongodb.org/mongo-driver/v2/mongo" 30 | "go.mongodb.org/mongo-driver/v2/mongo/options" 31 | "go.mongodb.org/mongo-driver/v2/x/mongo/driver/connstring" 32 | ) 33 | 34 | const defaultTimeout time.Duration = 30 * time.Second 35 | const defaultDatabaseName string = "casbin" 36 | const defaultCollectionName string = "casbin_rule" 37 | 38 | // CasbinRule represents a rule in Casbin. 39 | type CasbinRule struct { 40 | PType string 41 | V0 string 42 | V1 string 43 | V2 string 44 | V3 string 45 | V4 string 46 | V5 string 47 | } 48 | 49 | // adapter represents the MongoDB adapter for policy storage. 50 | type adapter struct { 51 | client *mongo.Client 52 | collection *mongo.Collection 53 | timeout time.Duration 54 | filtered bool 55 | } 56 | 57 | // finalizer is the destructor for adapter. 58 | func finalizer(a *adapter) { 59 | a.close() 60 | } 61 | 62 | // NewAdapter is the constructor for Adapter. If database name is not provided 63 | // in the Mongo URL, 'casbin' will be used as database name. 64 | // 'casbin_rule' will be used as a collection name. 65 | func NewAdapter(url string, timeout ...interface{}) (persist.BatchAdapter, error) { 66 | if !strings.HasPrefix(url, "mongodb+srv://") && !strings.HasPrefix(url, "mongodb://") { 67 | url = fmt.Sprint("mongodb://" + url) 68 | } 69 | 70 | // Parse and validate url before apply it. 71 | connString, err := connstring.ParseAndValidate(url) 72 | 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | clientOption := options.Client().ApplyURI(url) 78 | 79 | var databaseName string 80 | // Get database name from connString. 81 | if connString.Database != "" { 82 | databaseName = connString.Database 83 | } else { 84 | databaseName = defaultDatabaseName 85 | } 86 | 87 | return baseNewAdapter(clientOption, databaseName, defaultCollectionName, timeout...) 88 | } 89 | 90 | // NewAdapterWithClientOption is an alternative constructor for Adapter 91 | // that does the same as NewAdapter, but uses mongo.ClientOption instead of a Mongo URL + a databaseName option 92 | func NewAdapterWithClientOption(clientOption *options.ClientOptions, databaseName string, timeout ...interface{}) (persist.BatchAdapter, error) { 93 | return baseNewAdapter(clientOption, databaseName, defaultCollectionName, timeout...) 94 | } 95 | 96 | // NewAdapterWithCollectionName is an alternative constructor for Adapter 97 | // that does the same as NewAdapterWithClientOption, but with an extra collectionName option 98 | func NewAdapterWithCollectionName(clientOption *options.ClientOptions, databaseName string, collectionName string, timeout ...interface{}) (persist.BatchAdapter, error) { 99 | return baseNewAdapter(clientOption, databaseName, collectionName, timeout...) 100 | } 101 | 102 | // baseNewAdapter is a base constructor for Adapter 103 | func baseNewAdapter(clientOption *options.ClientOptions, databaseName string, collectionName string, timeout ...interface{}) (persist.BatchAdapter, error) { 104 | a := &adapter{} 105 | a.filtered = false 106 | 107 | if len(timeout) == 1 { 108 | a.timeout = timeout[0].(time.Duration) 109 | } else if len(timeout) > 1 { 110 | return nil, errors.New("too many arguments") 111 | } else { 112 | a.timeout = defaultTimeout 113 | } 114 | 115 | // Open the DB, create it if not existed. 116 | err := a.open(clientOption, databaseName, collectionName) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | // Call the destructor when the object is released. 122 | runtime.SetFinalizer(a, finalizer) 123 | 124 | return a, nil 125 | } 126 | 127 | // NewFilteredAdapter is the constructor for FilteredAdapter. 128 | // Casbin will not automatically call LoadPolicy() for a filtered adapter. 129 | func NewFilteredAdapter(url string) (persist.FilteredAdapter, error) { 130 | a, err := NewAdapter(url) 131 | if err != nil { 132 | return nil, err 133 | } 134 | a.(*adapter).filtered = true 135 | 136 | return a.(*adapter), nil 137 | } 138 | 139 | type AdapterConfig struct { 140 | DatabaseName string 141 | CollectionName string 142 | Timeout time.Duration 143 | IsFiltered bool 144 | } 145 | 146 | func NewAdapterByDB(client *mongo.Client, config *AdapterConfig) (persist.BatchAdapter, error) { 147 | if config == nil { 148 | config = &AdapterConfig{} 149 | } 150 | if config.CollectionName == "" { 151 | config.CollectionName = defaultCollectionName 152 | } 153 | if config.DatabaseName == "" { 154 | config.DatabaseName = defaultDatabaseName 155 | } 156 | if config.Timeout == 0 { 157 | config.Timeout = defaultTimeout 158 | } 159 | 160 | collection := client.Database(config.DatabaseName).Collection(config.CollectionName) 161 | 162 | a := &adapter{ 163 | client: client, 164 | collection: collection, 165 | timeout: config.Timeout, 166 | filtered: config.IsFiltered, 167 | } 168 | 169 | if err := a.prepareIndexes(); err != nil { 170 | return nil, err 171 | } 172 | 173 | // Call the destructor when the object is released. 174 | runtime.SetFinalizer(a, finalizer) 175 | 176 | return a, nil 177 | } 178 | 179 | func (a *adapter) open(clientOption *options.ClientOptions, databaseName string, collectionName string) error { 180 | client, err := mongo.Connect(clientOption) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | db := client.Database(databaseName) 186 | collection := db.Collection(collectionName) 187 | 188 | a.client = client 189 | a.collection = collection 190 | 191 | if err = a.prepareIndexes(); err != nil { 192 | return err 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func (a *adapter) prepareIndexes() error { 199 | indexes := []string{"ptype", "v0", "v1", "v2", "v3", "v4", "v5"} 200 | keysDoc := bson.D{} 201 | 202 | for _, k := range indexes { 203 | keyDoc := bson.E{} 204 | keyDoc.Key = k 205 | keyDoc.Value = 1 206 | keysDoc = append(keysDoc, keyDoc) 207 | } 208 | 209 | if _, err := a.collection.Indexes().CreateOne( 210 | context.Background(), 211 | mongo.IndexModel{ 212 | Keys: keysDoc, 213 | Options: options.Index().SetUnique(true), 214 | }, 215 | ); err != nil { 216 | return err 217 | } 218 | 219 | return nil 220 | } 221 | 222 | func (a *adapter) close() { 223 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 224 | defer cancel() 225 | 226 | _ = a.client.Disconnect(ctx) 227 | } 228 | 229 | func (a *adapter) dropTable() error { 230 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 231 | defer cancel() 232 | 233 | _, err := a.collection.DeleteMany(ctx, bson.D{{}}) 234 | if err != nil { 235 | return err 236 | } 237 | return nil 238 | } 239 | 240 | func loadPolicyLine(line CasbinRule, model model.Model) error { 241 | var p = []string{line.PType, 242 | line.V0, line.V1, line.V2, line.V3, line.V4, line.V5} 243 | 244 | index := len(p) - 1 245 | for p[index] == "" { 246 | index-- 247 | } 248 | index += 1 249 | p = p[:index] 250 | err := persist.LoadPolicyArray(p, model) 251 | if err != nil { 252 | return err 253 | } 254 | return nil 255 | } 256 | 257 | // LoadPolicy loads policy from database. 258 | func (a *adapter) LoadPolicy(model model.Model) error { 259 | return a.LoadFilteredPolicy(model, nil) 260 | } 261 | 262 | // LoadFilteredPolicy loads matching policy lines from database. If not nil, 263 | // the filter must be a valid MongoDB selector. 264 | func (a *adapter) LoadFilteredPolicy(model model.Model, filter interface{}) error { 265 | if filter == nil { 266 | a.filtered = false 267 | filter = bson.D{{}} 268 | } else { 269 | a.filtered = true 270 | } 271 | 272 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 273 | defer cancel() 274 | 275 | cursor, err := a.collection.Find(ctx, filter) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | for cursor.Next(ctx) { 281 | line := CasbinRule{} 282 | err := cursor.Decode(&line) 283 | if err != nil { 284 | return err 285 | } 286 | err = loadPolicyLine(line, model) 287 | if err != nil { 288 | return err 289 | } 290 | } 291 | 292 | return cursor.Close(ctx) 293 | } 294 | 295 | // IsFiltered returns true if the loaded policy has been filtered. 296 | func (a *adapter) IsFiltered() bool { 297 | return a.filtered 298 | } 299 | 300 | func savePolicyLine(ptype string, rule []string) CasbinRule { 301 | line := CasbinRule{ 302 | PType: ptype, 303 | } 304 | 305 | if len(rule) > 0 { 306 | line.V0 = rule[0] 307 | } 308 | if len(rule) > 1 { 309 | line.V1 = rule[1] 310 | } 311 | if len(rule) > 2 { 312 | line.V2 = rule[2] 313 | } 314 | if len(rule) > 3 { 315 | line.V3 = rule[3] 316 | } 317 | if len(rule) > 4 { 318 | line.V4 = rule[4] 319 | } 320 | if len(rule) > 5 { 321 | line.V5 = rule[5] 322 | } 323 | 324 | return line 325 | } 326 | 327 | // SavePolicy saves policy to database. 328 | func (a *adapter) SavePolicy(model model.Model) error { 329 | if a.filtered { 330 | return errors.New("cannot save a filtered policy") 331 | } 332 | if err := a.dropTable(); err != nil { 333 | return err 334 | } 335 | 336 | var lines []interface{} 337 | 338 | for ptype, ast := range model["p"] { 339 | for _, rule := range ast.Policy { 340 | line := savePolicyLine(ptype, rule) 341 | lines = append(lines, &line) 342 | } 343 | } 344 | 345 | for ptype, ast := range model["g"] { 346 | for _, rule := range ast.Policy { 347 | line := savePolicyLine(ptype, rule) 348 | lines = append(lines, &line) 349 | } 350 | } 351 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 352 | defer cancel() 353 | 354 | if _, err := a.collection.InsertMany(ctx, lines); err != nil { 355 | return err 356 | } 357 | 358 | return nil 359 | } 360 | 361 | // AddPolicy adds a policy rule to the storage. 362 | func (a *adapter) AddPolicy(sec string, ptype string, rule []string) error { 363 | line := savePolicyLine(ptype, rule) 364 | 365 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 366 | defer cancel() 367 | 368 | if _, err := a.collection.InsertOne(ctx, line); err != nil { 369 | return err 370 | } 371 | 372 | return nil 373 | } 374 | 375 | // AddPolicies adds policy rules to the storage. 376 | func (a *adapter) AddPolicies(sec string, ptype string, rules [][]string) error { 377 | var lines []interface{} 378 | for _, rule := range rules { 379 | line := savePolicyLine(ptype, rule) 380 | lines = append(lines, line) 381 | } 382 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 383 | defer cancel() 384 | if _, err := a.collection.InsertMany(ctx, lines); err != nil { 385 | return err 386 | } 387 | return nil 388 | } 389 | 390 | // RemovePolicies removes policy rules from the storage. 391 | func (a *adapter) RemovePolicies(sec string, ptype string, rules [][]string) error { 392 | var lines []CasbinRule 393 | for _, rule := range rules { 394 | line := savePolicyLine(ptype, rule) 395 | lines = append(lines, line) 396 | } 397 | 398 | for _, line := range lines { 399 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 400 | defer cancel() 401 | if _, err := a.collection.DeleteOne(ctx, line); err != nil { 402 | return err 403 | } 404 | } 405 | 406 | return nil 407 | } 408 | 409 | // RemovePolicy removes a policy rule from the storage. 410 | func (a *adapter) RemovePolicy(sec string, ptype string, rule []string) error { 411 | line := savePolicyLine(ptype, rule) 412 | 413 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 414 | defer cancel() 415 | 416 | if _, err := a.collection.DeleteOne(ctx, line); err != nil { 417 | return err 418 | } 419 | 420 | return nil 421 | } 422 | 423 | // RemoveFilteredPolicy removes policy rules that match the filter from the storage. 424 | func (a *adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { 425 | selector := make(map[string]interface{}) 426 | selector["ptype"] = ptype 427 | 428 | if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) { 429 | if fieldValues[0-fieldIndex] != "" { 430 | selector["v0"] = fieldValues[0-fieldIndex] 431 | } 432 | } 433 | if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) { 434 | if fieldValues[1-fieldIndex] != "" { 435 | selector["v1"] = fieldValues[1-fieldIndex] 436 | } 437 | } 438 | if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) { 439 | if fieldValues[2-fieldIndex] != "" { 440 | selector["v2"] = fieldValues[2-fieldIndex] 441 | } 442 | } 443 | if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) { 444 | if fieldValues[3-fieldIndex] != "" { 445 | selector["v3"] = fieldValues[3-fieldIndex] 446 | } 447 | } 448 | if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) { 449 | if fieldValues[4-fieldIndex] != "" { 450 | selector["v4"] = fieldValues[4-fieldIndex] 451 | } 452 | } 453 | if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) { 454 | if fieldValues[5-fieldIndex] != "" { 455 | selector["v5"] = fieldValues[5-fieldIndex] 456 | } 457 | } 458 | 459 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 460 | defer cancel() 461 | 462 | if _, err := a.collection.DeleteMany(ctx, selector); err != nil { 463 | return err 464 | } 465 | 466 | return nil 467 | } 468 | 469 | // UpdatePolicy updates a policy rule from storage. 470 | // This is part of the Auto-Save feature. 471 | func (a *adapter) UpdatePolicy(sec string, ptype string, oldRule, newPolicy []string) error { 472 | oldLine := savePolicyLine(ptype, oldRule) 473 | newLine := savePolicyLine(ptype, newPolicy) 474 | 475 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 476 | defer cancel() 477 | // Updating all the documents equals to replacing 478 | _, err := a.collection.ReplaceOne(ctx, oldLine, newLine) 479 | return err 480 | } 481 | 482 | // UpdatePolicies updates some policy rules to storage, like db, redis. 483 | func (a *adapter) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error { 484 | oldLines := make([]CasbinRule, 0, len(oldRules)) 485 | newLines := make([]CasbinRule, 0, len(oldRules)) 486 | for _, oldRule := range oldRules { 487 | oldLines = append(oldLines, savePolicyLine(ptype, oldRule)) 488 | } 489 | for _, newRule := range newRules { 490 | newLines = append(newLines, savePolicyLine(ptype, newRule)) 491 | } 492 | 493 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 494 | defer cancel() 495 | for i := range oldRules { 496 | _, err := a.collection.ReplaceOne(ctx, oldLines[i], newLines[i]) 497 | if err != nil { 498 | return err 499 | } 500 | } 501 | return nil 502 | } 503 | 504 | // UpdateFilteredPolicies deletes old rules and adds new rules. 505 | func (a *adapter) UpdateFilteredPolicies(sec string, ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { 506 | selector := make(map[string]interface{}) 507 | selector["ptype"] = ptype 508 | 509 | if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) { 510 | if fieldValues[0-fieldIndex] != "" { 511 | selector["v0"] = fieldValues[0-fieldIndex] 512 | } 513 | } 514 | if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) { 515 | if fieldValues[1-fieldIndex] != "" { 516 | selector["v1"] = fieldValues[1-fieldIndex] 517 | } 518 | } 519 | if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) { 520 | if fieldValues[2-fieldIndex] != "" { 521 | selector["v2"] = fieldValues[2-fieldIndex] 522 | } 523 | } 524 | if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) { 525 | if fieldValues[3-fieldIndex] != "" { 526 | selector["v3"] = fieldValues[3-fieldIndex] 527 | } 528 | } 529 | if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) { 530 | if fieldValues[4-fieldIndex] != "" { 531 | selector["v4"] = fieldValues[4-fieldIndex] 532 | } 533 | } 534 | if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) { 535 | if fieldValues[5-fieldIndex] != "" { 536 | selector["v5"] = fieldValues[5-fieldIndex] 537 | } 538 | } 539 | 540 | oldLines := make([]CasbinRule, 0) 541 | newLines := make([]CasbinRule, 0, len(newPolicies)) 542 | for _, newPolicy := range newPolicies { 543 | newLines = append(newLines, savePolicyLine(ptype, newPolicy)) 544 | } 545 | 546 | oldPolicies, err := a.updateFilteredPoliciesTxn(oldLines, newLines, selector) 547 | if err == nil { 548 | return oldPolicies, err 549 | } 550 | // (IllegalOperation) Transaction numbers are only allowed on a replica set member or mongos 551 | if mongoErr, ok := err.(mongo.CommandError); !ok || mongoErr.Code != 20 { 552 | return nil, err 553 | } 554 | 555 | log.Println("[WARNING]: As your mongodb server doesn't allow a replica set, transaction operation is not supported. So Casbin Adapter will run non-transactional updating!") 556 | return a.updateFilteredPolicies(oldLines, newLines, selector) 557 | } 558 | 559 | func (a *adapter) updateFilteredPoliciesTxn(oldLines, newLines []CasbinRule, selector map[string]interface{}) ([][]string, error) { 560 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 561 | defer cancel() 562 | 563 | session, err := a.client.StartSession() 564 | if err != nil { 565 | return nil, err 566 | } 567 | defer session.EndSession(context.TODO()) 568 | 569 | _, err = session.WithTransaction(ctx, func(sessionCtx context.Context) (interface{}, error) { 570 | // Load old policies 571 | cursor, err := a.collection.Find(ctx, selector) 572 | if err != nil { 573 | _ = session.AbortTransaction(context.Background()) 574 | return nil, err 575 | } 576 | for cursor.Next(ctx) { 577 | line := CasbinRule{} 578 | err := cursor.Decode(&line) 579 | if err != nil { 580 | _ = session.AbortTransaction(context.Background()) 581 | return nil, err 582 | } 583 | oldLines = append(oldLines, line) 584 | } 585 | if err = cursor.Close(ctx); err != nil { 586 | _ = session.AbortTransaction(context.Background()) 587 | return nil, err 588 | } 589 | 590 | // Delete all old policies 591 | if _, err := a.collection.DeleteMany(sessionCtx, selector); err != nil { 592 | _ = session.AbortTransaction(context.Background()) 593 | return nil, err 594 | } 595 | // Insert new policies 596 | for _, newLine := range newLines { 597 | if _, err := a.collection.InsertOne(sessionCtx, &newLine); err != nil { 598 | _ = session.AbortTransaction(context.Background()) 599 | return nil, err 600 | } 601 | } 602 | return nil, nil 603 | }) 604 | if err != nil { 605 | return nil, err 606 | } 607 | 608 | // return deleted rulues 609 | oldPolicies := make([][]string, 0) 610 | for _, v := range oldLines { 611 | oldPolicy := v.toStringPolicy() 612 | oldPolicies = append(oldPolicies, oldPolicy) 613 | } 614 | return oldPolicies, nil 615 | } 616 | 617 | func (a *adapter) updateFilteredPolicies(oldLines, newLines []CasbinRule, selector map[string]interface{}) ([][]string, error) { 618 | ctx, cancel := context.WithTimeout(context.TODO(), a.timeout) 619 | defer cancel() 620 | 621 | // Load old policies 622 | cursor, err := a.collection.Find(ctx, selector) 623 | if err != nil { 624 | return nil, err 625 | } 626 | for cursor.Next(ctx) { 627 | line := CasbinRule{} 628 | err := cursor.Decode(&line) 629 | if err != nil { 630 | return nil, err 631 | } 632 | oldLines = append(oldLines, line) 633 | } 634 | if err = cursor.Close(ctx); err != nil { 635 | return nil, err 636 | } 637 | 638 | // Delete all old policies 639 | if _, err := a.collection.DeleteMany(ctx, selector); err != nil { 640 | return nil, err 641 | } 642 | // Insert new policies 643 | for _, newLine := range newLines { 644 | if _, err := a.collection.InsertOne(ctx, &newLine); err != nil { 645 | return nil, err 646 | } 647 | } 648 | 649 | // return deleted rulues 650 | oldPolicies := make([][]string, 0) 651 | for _, v := range oldLines { 652 | oldPolicy := v.toStringPolicy() 653 | oldPolicies = append(oldPolicies, oldPolicy) 654 | } 655 | return oldPolicies, nil 656 | } 657 | 658 | func (c *CasbinRule) toStringPolicy() []string { 659 | policy := make([]string, 0) 660 | if c.PType != "" { 661 | policy = append(policy, c.PType) 662 | } 663 | if c.V0 != "" { 664 | policy = append(policy, c.V0) 665 | } 666 | if c.V1 != "" { 667 | policy = append(policy, c.V1) 668 | } 669 | if c.V2 != "" { 670 | policy = append(policy, c.V2) 671 | } 672 | if c.V3 != "" { 673 | policy = append(policy, c.V3) 674 | } 675 | if c.V4 != "" { 676 | policy = append(policy, c.V4) 677 | } 678 | if c.V5 != "" { 679 | policy = append(policy, c.V5) 680 | } 681 | return policy 682 | } 683 | -------------------------------------------------------------------------------- /adapter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 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 mongodbadapter 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "strings" 22 | "testing" 23 | 24 | "go.mongodb.org/mongo-driver/v2/mongo" 25 | 26 | "github.com/casbin/casbin/v2" 27 | "github.com/casbin/casbin/v2/util" 28 | "go.mongodb.org/mongo-driver/v2/bson" 29 | mongooptions "go.mongodb.org/mongo-driver/v2/mongo/options" 30 | ) 31 | 32 | var testDbURL = os.Getenv("TEST_MONGODB_URL") 33 | var testReplicaSetURL = os.Getenv("TEST_REPLICA_SET_URL") 34 | 35 | func getDbURL() string { 36 | if testDbURL == "" { 37 | testDbURL = "127.0.0.1:27017" 38 | } 39 | return testDbURL 40 | } 41 | 42 | func getReplicaSetURL() string { 43 | if testReplicaSetURL == "" { 44 | testReplicaSetURL = "127.0.0.1:42069" 45 | } 46 | return testReplicaSetURL 47 | } 48 | 49 | func testGetPolicy(t *testing.T, e *casbin.Enforcer, res [][]string) { 50 | t.Helper() 51 | myRes := e.GetPolicy() 52 | t.Log("Policy: ", myRes) 53 | 54 | if !util.Array2DEquals(res, myRes) { 55 | t.Error("Policy: ", myRes, ", supposed to be ", res) 56 | } 57 | } 58 | 59 | func testGetPolicyWithoutOrder(t *testing.T, e *casbin.Enforcer, res [][]string) { 60 | myRes := e.GetPolicy() 61 | 62 | if !arrayEqualsWithoutOrder(myRes, res) { 63 | t.Error("Policy: ", myRes, ", supposed to be ", res) 64 | } 65 | } 66 | 67 | func arrayEqualsWithoutOrder(a [][]string, b [][]string) bool { 68 | if len(a) != len(b) { 69 | return false 70 | } 71 | 72 | mapA := make(map[int]string) 73 | mapB := make(map[int]string) 74 | order := make(map[int]struct{}) 75 | l := len(a) 76 | 77 | for i := 0; i < l; i++ { 78 | mapA[i] = util.ArrayToString(a[i]) 79 | mapB[i] = util.ArrayToString(b[i]) 80 | } 81 | 82 | for i := 0; i < l; i++ { 83 | for j := 0; j < l; j++ { 84 | if _, ok := order[j]; ok { 85 | if j == l-1 { 86 | return false 87 | } else { 88 | continue 89 | } 90 | } 91 | if mapA[i] == mapB[j] { 92 | order[j] = struct{}{} 93 | break 94 | } else if j == l-1 { 95 | return false 96 | } 97 | } 98 | } 99 | return true 100 | } 101 | 102 | func initPolicy(t *testing.T, dbURL string) { 103 | // Because the DB is empty at first, 104 | // so we need to load the policy from the file adapter (.CSV) first. 105 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | a, err := NewAdapter(dbURL) 111 | if err != nil { 112 | panic(err) 113 | } 114 | // This is a trick to save the current policy to the DB. 115 | // We can't call e.SavePolicy() because the adapter in the enforcer is still the file adapter. 116 | // The current policy means the policy in the Casbin enforcer (aka in memory). 117 | err = a.SavePolicy(e.GetModel()) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | // Clear the current policy. 123 | e.ClearPolicy() 124 | testGetPolicy(t, e, [][]string{}) 125 | 126 | // Load the policy from DB. 127 | err = a.LoadPolicy(e.GetModel()) 128 | if err != nil { 129 | panic(err) 130 | } 131 | testGetPolicy(t, e, [][]string{ 132 | {"alice", "data1", "read"}, 133 | {"bob", "data2", "write"}, 134 | {"data2_admin", "data2", "read"}, 135 | {"data2_admin", "data2", "write"}, 136 | }, 137 | ) 138 | } 139 | 140 | func TestAdapter(t *testing.T) { 141 | initPolicy(t, getDbURL()) 142 | 143 | // Note: you don't need to look at the above code 144 | // if you already have a working DB with policy inside. 145 | 146 | // Now the DB has policy, so we can provide a normal use case. 147 | // Create an adapter and an enforcer. 148 | // NewEnforcer() will load the policy automatically. 149 | a, err := NewAdapter(getDbURL()) 150 | if err != nil { 151 | panic(err) 152 | } 153 | 154 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 155 | if err != nil { 156 | panic(err) 157 | } 158 | testGetPolicy(t, e, [][]string{ 159 | {"alice", "data1", "read"}, 160 | {"bob", "data2", "write"}, 161 | {"data2_admin", "data2", "read"}, 162 | {"data2_admin", "data2", "write"}, 163 | }, 164 | ) 165 | // AutoSave is enabled by default. 166 | // Now we disable it. 167 | e.EnableAutoSave(false) 168 | // Because AutoSave is disabled, the policy change only affects the policy in Casbin enforcer, 169 | // it doesn't affect the policy in the storage. 170 | e.AddPolicy("alice", "data1", "write") 171 | // Reload the policy from the storage to see the effect. 172 | if err := e.LoadPolicy(); err != nil { 173 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 174 | } 175 | // This is still the original policy. 176 | testGetPolicy(t, e, [][]string{ 177 | {"alice", "data1", "read"}, 178 | {"bob", "data2", "write"}, 179 | {"data2_admin", "data2", "read"}, 180 | {"data2_admin", "data2", "write"}, 181 | }, 182 | ) 183 | 184 | // Now we enable the AutoSave. 185 | e.EnableAutoSave(true) 186 | 187 | // Because AutoSave is enabled, the policy change not only affects the policy in Casbin enforcer, 188 | // but also affects the policy in the storage. 189 | e.AddPolicy("alice", "data1", "write") 190 | // Reload the policy from the storage to see the effect. 191 | if err := e.LoadPolicy(); err != nil { 192 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 193 | } 194 | // The policy has a new rule: {"alice", "data1", "write"}. 195 | testGetPolicy(t, e, [][]string{ 196 | {"alice", "data1", "read"}, 197 | {"bob", "data2", "write"}, 198 | {"data2_admin", "data2", "read"}, 199 | {"data2_admin", "data2", "write"}, 200 | {"alice", "data1", "write"}, 201 | }, 202 | ) 203 | 204 | // Remove the added rule. 205 | e.RemovePolicy("alice", "data1", "write") 206 | if err := a.RemovePolicy("p", "p", []string{"alice", "data1", "write"}); err != nil { 207 | t.Errorf("Expected RemovePolicy() to be successful; got %v", err) 208 | } 209 | if err := e.LoadPolicy(); err != nil { 210 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 211 | } 212 | testGetPolicy(t, e, [][]string{ 213 | {"alice", "data1", "read"}, 214 | {"bob", "data2", "write"}, 215 | {"data2_admin", "data2", "read"}, 216 | {"data2_admin", "data2", "write"}, 217 | }, 218 | ) 219 | 220 | // Remove "data2_admin" related policy rules via a filter. 221 | // Two rules: {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"} are deleted. 222 | e.RemoveFilteredPolicy(0, "data2_admin") 223 | if err := e.LoadPolicy(); err != nil { 224 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 225 | } 226 | testGetPolicy(t, e, [][]string{ 227 | {"alice", "data1", "read"}, 228 | {"bob", "data2", "write"}, 229 | }, 230 | ) 231 | 232 | e.RemoveFilteredPolicy(1, "data1") 233 | if err := e.LoadPolicy(); err != nil { 234 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 235 | } 236 | testGetPolicy(t, e, [][]string{{"bob", "data2", "write"}}) 237 | 238 | e.RemoveFilteredPolicy(2, "write") 239 | if err := e.LoadPolicy(); err != nil { 240 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 241 | } 242 | testGetPolicy(t, e, [][]string{}) 243 | } 244 | 245 | func TestAddPolicies(t *testing.T) { 246 | initPolicy(t, getDbURL()) 247 | 248 | a, err := NewAdapter(getDbURL()) 249 | if err != nil { 250 | panic(err) 251 | } 252 | 253 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 254 | if err != nil { 255 | panic(err) 256 | } 257 | 258 | testGetPolicy(t, e, [][]string{ 259 | {"alice", "data1", "read"}, 260 | {"bob", "data2", "write"}, 261 | {"data2_admin", "data2", "read"}, 262 | {"data2_admin", "data2", "write"}, 263 | }, 264 | ) 265 | a.AddPolicies("p", "p", [][]string{ 266 | {"bob", "data2", "read"}, 267 | {"alice", "data2", "write"}, 268 | {"alice", "data2", "read"}, 269 | {"bob", "data1", "write"}, 270 | {"bob", "data1", "read"}, 271 | }, 272 | ) 273 | 274 | if err := e.LoadPolicy(); err != nil { 275 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 276 | } 277 | 278 | testGetPolicy(t, e, [][]string{ 279 | {"alice", "data1", "read"}, 280 | {"bob", "data2", "write"}, 281 | {"data2_admin", "data2", "read"}, 282 | {"data2_admin", "data2", "write"}, 283 | {"bob", "data2", "read"}, 284 | {"alice", "data2", "write"}, 285 | {"alice", "data2", "read"}, 286 | {"bob", "data1", "write"}, 287 | {"bob", "data1", "read"}, 288 | }, 289 | ) 290 | 291 | // Remove the added rule. 292 | if err := a.RemovePolicies("p", "p", [][]string{ 293 | {"bob", "data2", "read"}, 294 | {"alice", "data2", "write"}, 295 | {"alice", "data2", "read"}, 296 | {"bob", "data1", "write"}, 297 | {"bob", "data1", "read"}, 298 | }); err != nil { 299 | t.Errorf("Expected RemovePolicies() to be successful; got %v", err) 300 | } 301 | if err := e.LoadPolicy(); err != nil { 302 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 303 | } 304 | testGetPolicy(t, e, [][]string{ 305 | {"alice", "data1", "read"}, 306 | {"bob", "data2", "write"}, 307 | {"data2_admin", "data2", "read"}, 308 | {"data2_admin", "data2", "write"}, 309 | }, 310 | ) 311 | } 312 | 313 | func TestDeleteFilteredAdapter(t *testing.T) { 314 | a, err := NewFilteredAdapter(getDbURL() + "/casbin_test_new") 315 | if err != nil { 316 | panic(err) 317 | } 318 | 319 | e, err := casbin.NewEnforcer("examples/rbac_tenant_service.conf", a) 320 | if err != nil { 321 | panic(err) 322 | } 323 | 324 | e.AddPolicy("domain1", "alice", "data3", "read", "accept", "service1") 325 | e.AddPolicy("domain1", "alice", "data3", "write", "accept", "service2") 326 | 327 | // Reload the policy from the storage to see the effect. 328 | if err := e.LoadPolicy(); err != nil { 329 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 330 | } 331 | // The policy has a new rule: {"alice", "data1", "write"}. 332 | testGetPolicy(t, e, [][]string{ 333 | {"domain1", "alice", "data3", "read", "accept", "service1"}, 334 | {"domain1", "alice", "data3", "write", "accept", "service2"}, 335 | }, 336 | ) 337 | // test RemoveFiltered Policy with "" fileds 338 | e.RemoveFilteredPolicy(0, "domain1", "", "", "read") 339 | if err := e.LoadPolicy(); err != nil { 340 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 341 | } 342 | testGetPolicy(t, e, [][]string{ 343 | {"domain1", "alice", "data3", "write", "accept", "service2"}, 344 | }, 345 | ) 346 | 347 | e.RemoveFilteredPolicy(0, "domain1", "", "", "", "", "service2") 348 | if err := e.LoadPolicy(); err != nil { 349 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 350 | } 351 | testGetPolicy(t, e, [][]string{}) 352 | } 353 | 354 | func TestFilteredAdapter(t *testing.T) { 355 | // Now the DB has policy, so we can provide a normal use case. 356 | // Create an adapter and an enforcer. 357 | // NewEnforcer() will load the policy automatically. 358 | a, err := NewFilteredAdapter(getDbURL()) 359 | if err != nil { 360 | panic(err) 361 | } 362 | 363 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 364 | if err != nil { 365 | panic(err) 366 | } 367 | 368 | // Load filtered policies from the database. 369 | e.AddPolicy("alice", "data1", "write") 370 | e.AddPolicy("bob", "data2", "write") 371 | // Reload the filtered policy from the storage. 372 | filter := &bson.M{"v0": "bob"} 373 | if err := e.LoadFilteredPolicy(filter); err != nil { 374 | t.Errorf("Expected LoadFilteredPolicy() to be successful; got %v", err) 375 | } 376 | // Only bob's policy should have been loaded 377 | testGetPolicy(t, e, [][]string{{"bob", "data2", "write"}}) 378 | 379 | // Verify that alice's policy remains intact in the database. 380 | filter = &bson.M{"v0": "alice"} 381 | if err := e.LoadFilteredPolicy(filter); err != nil { 382 | t.Errorf("Expected LoadFilteredPolicy() to be successful; got %v", err) 383 | } 384 | // Only alice's policy should have been loaded, 385 | testGetPolicy(t, e, [][]string{ 386 | {"alice", "data1", "read"}, 387 | {"alice", "data1", "write"}, 388 | }, 389 | ) 390 | 391 | // Test safe handling of SavePolicy when using filtered policies. 392 | if err := e.SavePolicy(); err == nil { 393 | t.Errorf("Expected SavePolicy() to fail for a filtered policy") 394 | } 395 | if err := e.LoadPolicy(); err != nil { 396 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 397 | } 398 | if err := e.SavePolicy(); err != nil { 399 | t.Errorf("Expected SavePolicy() to be successful; got %v", err) 400 | } 401 | 402 | e.RemoveFilteredPolicy(2, "write") 403 | if err := e.LoadPolicy(); err != nil { 404 | t.Errorf("Expected LoadPolicy() to be successful; got %v", err) 405 | } 406 | testGetPolicy(t, e, [][]string{ 407 | {"alice", "data1", "read"}, 408 | {"data2_admin", "data2", "read"}, 409 | }, 410 | ) 411 | } 412 | 413 | func TestNewAdapterWithInvalidURL(t *testing.T) { 414 | defer func() { 415 | if r := recover(); r == nil { 416 | t.Error("Expected recovery from panic") 417 | } 418 | }() 419 | 420 | _, err := NewAdapter("localhost:40001?foo=1&bar=2") 421 | if err != nil { 422 | panic(err) 423 | } 424 | } 425 | 426 | func TestNewAdapterWithUnknownURL(t *testing.T) { 427 | defer func() { 428 | if r := recover(); r == nil { 429 | t.Error("Expected recovery from panic") 430 | } 431 | }() 432 | 433 | _, err := NewAdapter("fakeserver:27017") 434 | if err != nil { 435 | panic(err) 436 | } 437 | } 438 | 439 | func TestNewAdapterWithDatabase(t *testing.T) { 440 | _, err := NewAdapter(fmt.Sprint(getDbURL() + "/abc")) 441 | if err != nil { 442 | panic(err) 443 | } 444 | } 445 | 446 | func TestNewAdapterWithClientOption(t *testing.T) { 447 | uri := getDbURL() 448 | if !strings.HasPrefix(uri, "mongodb+srv://") && !strings.HasPrefix(uri, "mongodb://") { 449 | uri = fmt.Sprint("mongodb://" + uri) 450 | } 451 | mongoClientOption := mongooptions.Client().ApplyURI(uri) 452 | databaseName := "casbin_custom" 453 | _, err := NewAdapterWithClientOption(mongoClientOption, databaseName) 454 | if err != nil { 455 | panic(err) 456 | } 457 | } 458 | 459 | func TestNewAdapterWithCollectionName(t *testing.T) { 460 | uri := getDbURL() 461 | if !strings.HasPrefix(uri, "mongodb+srv://") && !strings.HasPrefix(uri, "mongodb://") { 462 | uri = fmt.Sprint("mongodb://" + uri) 463 | } 464 | mongoClientOption := mongooptions.Client().ApplyURI(uri) 465 | databaseName := "casbin_custom" 466 | collectionName := "casbin_rule_custom" 467 | _, err := NewAdapterWithCollectionName(mongoClientOption, databaseName, collectionName) 468 | if err != nil { 469 | panic(err) 470 | } 471 | } 472 | 473 | func TestNewAdapterByDB(t *testing.T) { 474 | uri := getDbURL() 475 | if !strings.HasPrefix(uri, "mongodb+srv://") && !strings.HasPrefix(uri, "mongodb://") { 476 | uri = fmt.Sprint("mongodb://" + uri) 477 | } 478 | mongoClientOption := mongooptions.Client().ApplyURI(uri) 479 | client, err := mongo.Connect(mongoClientOption) 480 | if err != nil { 481 | panic(err) 482 | } 483 | 484 | config := AdapterConfig{ 485 | DatabaseName: "casbin_custom", 486 | CollectionName: "casbin_rule_custom", 487 | } 488 | _, err = NewAdapterByDB(client, &config) 489 | if err != nil { 490 | panic(err) 491 | } 492 | } 493 | 494 | func TestUpdatePolicy(t *testing.T) { 495 | initPolicy(t, getDbURL()) 496 | 497 | a, err := NewAdapter(getDbURL()) 498 | if err != nil { 499 | panic(err) 500 | } 501 | 502 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 503 | if err != nil { 504 | panic(err) 505 | } 506 | 507 | testGetPolicy(t, e, [][]string{ 508 | {"alice", "data1", "read"}, 509 | {"bob", "data2", "write"}, 510 | {"data2_admin", "data2", "read"}, 511 | {"data2_admin", "data2", "write"}, 512 | }, 513 | ) 514 | testUpdatePolicy(t, a.(*adapter)) 515 | testUpdatePolicies(t, a.(*adapter)) 516 | } 517 | 518 | func testUpdatePolicy(t *testing.T, a *adapter) { 519 | // NewEnforcer() will load the policy automatically. 520 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 521 | 522 | e.EnableAutoSave(true) 523 | e.UpdatePolicy([]string{"alice", "data1", "read"}, []string{"alice", "data1", "write"}) 524 | e.LoadPolicy() 525 | testGetPolicy(t, e, [][]string{{"alice", "data1", "write"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 526 | } 527 | 528 | func testUpdatePolicies(t *testing.T, a *adapter) { 529 | // NewEnforcer() will load the policy automatically. 530 | e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a) 531 | 532 | e.EnableAutoSave(true) 533 | e.UpdatePolicies([][]string{{"alice", "data1", "write"}, {"bob", "data2", "write"}}, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "read"}}) 534 | e.LoadPolicy() 535 | testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "read"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 536 | } 537 | 538 | func initUpdateFilteredPolicies(sec string, ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) ([]CasbinRule, []CasbinRule, map[string]interface{}) { 539 | selector := make(map[string]interface{}) 540 | selector["ptype"] = ptype 541 | 542 | if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) { 543 | if fieldValues[0-fieldIndex] != "" { 544 | selector["v0"] = fieldValues[0-fieldIndex] 545 | } 546 | } 547 | if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) { 548 | if fieldValues[1-fieldIndex] != "" { 549 | selector["v1"] = fieldValues[1-fieldIndex] 550 | } 551 | } 552 | if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) { 553 | if fieldValues[2-fieldIndex] != "" { 554 | selector["v2"] = fieldValues[2-fieldIndex] 555 | } 556 | } 557 | if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) { 558 | if fieldValues[3-fieldIndex] != "" { 559 | selector["v3"] = fieldValues[3-fieldIndex] 560 | } 561 | } 562 | if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) { 563 | if fieldValues[4-fieldIndex] != "" { 564 | selector["v4"] = fieldValues[4-fieldIndex] 565 | } 566 | } 567 | if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) { 568 | if fieldValues[5-fieldIndex] != "" { 569 | selector["v5"] = fieldValues[5-fieldIndex] 570 | } 571 | } 572 | 573 | oldLines := make([]CasbinRule, 0) 574 | newLines := make([]CasbinRule, 0, len(newPolicies)) 575 | for _, newPolicy := range newPolicies { 576 | newLines = append(newLines, savePolicyLine(ptype, newPolicy)) 577 | } 578 | return oldLines, newLines, selector 579 | } 580 | 581 | func TestUpdateFilteredPolicies(t *testing.T) { 582 | initPolicy(t, getDbURL()) 583 | 584 | a, err := NewAdapter(getDbURL()) 585 | if err != nil { 586 | panic(err) 587 | } 588 | 589 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 590 | if err != nil { 591 | panic(err) 592 | } 593 | 594 | testGetPolicy(t, e, [][]string{ 595 | {"alice", "data1", "read"}, 596 | {"bob", "data2", "write"}, 597 | {"data2_admin", "data2", "read"}, 598 | {"data2_admin", "data2", "write"}, 599 | }, 600 | ) 601 | 602 | initUpdateFilteredPolicies("p", "p", [][]string{{"alice", "data1", "write"}}, 0, "alice", "data1", "read") 603 | 604 | e.EnableAutoSave(true) 605 | e.UpdateFilteredPolicies([][]string{{"alice", "data1", "write"}}, 0, "alice", "data1", "read") 606 | e.UpdateFilteredPolicies([][]string{{"bob", "data2", "read"}}, 0, "bob", "data2", "write") 607 | e.LoadPolicy() 608 | testGetPolicyWithoutOrder(t, e, [][]string{{"alice", "data1", "write"}, {"bob", "data2", "read"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 609 | } 610 | 611 | func TestUpdateFilteredPoliciesTxn(t *testing.T) { 612 | initPolicy(t, getReplicaSetURL()) 613 | 614 | a, err := NewAdapter(getReplicaSetURL()) 615 | if err != nil { 616 | panic(err) 617 | } 618 | 619 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 620 | if err != nil { 621 | panic(err) 622 | } 623 | 624 | testGetPolicy(t, e, [][]string{ 625 | {"alice", "data1", "read"}, 626 | {"bob", "data2", "write"}, 627 | {"data2_admin", "data2", "read"}, 628 | {"data2_admin", "data2", "write"}, 629 | }, 630 | ) 631 | 632 | e.EnableAutoSave(true) 633 | e.UpdateFilteredPolicies([][]string{{"alice", "data1", "write"}}, 0, "alice", "data1", "read") 634 | e.UpdateFilteredPolicies([][]string{{"bob", "data2", "read"}}, 0, "bob", "data2", "write") 635 | e.LoadPolicy() 636 | testGetPolicyWithoutOrder(t, e, [][]string{{"alice", "data1", "write"}, {"bob", "data2", "read"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) 637 | } 638 | 639 | func TestSavePolicyPreservesIndexes(t *testing.T) { 640 | // Initialize with some policies 641 | initPolicy(t, getDbURL()) 642 | 643 | a, err := NewAdapter(getDbURL()) 644 | if err != nil { 645 | panic(err) 646 | } 647 | 648 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 649 | if err != nil { 650 | panic(err) 651 | } 652 | 653 | // Save policy which should NOT drop indexes 654 | if err := e.SavePolicy(); err != nil { 655 | t.Errorf("Expected SavePolicy() to be successful; got %v", err) 656 | } 657 | 658 | // Try to add a duplicate policy - should fail due to unique index 659 | adapter := a.(*adapter) 660 | line := savePolicyLine("p", []string{"alice", "data1", "read"}) 661 | 662 | ctx, cancel := context.WithTimeout(context.TODO(), adapter.timeout) 663 | defer cancel() 664 | 665 | // Attempting to insert a duplicate should fail 666 | _, err = adapter.collection.InsertOne(ctx, line) 667 | if err == nil { 668 | t.Error("Expected InsertOne of duplicate to fail due to unique index, but it succeeded") 669 | } 670 | } 671 | 672 | func TestSavePolicyPreventsDuplicates(t *testing.T) { 673 | // Initialize with some policies 674 | initPolicy(t, getDbURL()) 675 | 676 | a, err := NewAdapter(getDbURL()) 677 | if err != nil { 678 | panic(err) 679 | } 680 | 681 | e, err := casbin.NewEnforcer("examples/rbac_model.conf", a) 682 | if err != nil { 683 | panic(err) 684 | } 685 | 686 | // Save policy which should NOT drop indexes 687 | if err := e.SavePolicy(); err != nil { 688 | t.Errorf("Expected SavePolicy() to be successful; got %v", err) 689 | } 690 | 691 | // Try to add a duplicate directly via adapter (bypassing enforcer's in-memory check) 692 | // This should be prevented by the unique index 693 | adapter := a.(*adapter) 694 | err = adapter.AddPolicy("p", "p", []string{"alice", "data1", "read"}) 695 | if err == nil { 696 | t.Error("Expected AddPolicy of duplicate to fail due to unique index, but it succeeded") 697 | } 698 | } 699 | --------------------------------------------------------------------------------