casdoor-ruby-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +193 -0
- data/lib/casdoor/api/auth.rb +153 -0
- data/lib/casdoor/api/crud.rb +122 -0
- data/lib/casdoor/api/enforce.rb +44 -0
- data/lib/casdoor/api/users.rb +83 -0
- data/lib/casdoor/client.rb +143 -0
- data/lib/casdoor/entity.rb +135 -0
- data/lib/casdoor/error.rb +37 -0
- data/lib/casdoor/version.rb +20 -0
- data/lib/casdoor-ruby-sdk.rb +18 -0
- data/lib/casdoor.rb +24 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c0b990a9cbd0382d5af8e70e090df98adcacc9c2b4bfd79d15d16f9a40c0688a
|
|
4
|
+
data.tar.gz: 3d09b80f3e1d36b0f582436bb647fa9e1ba297ba4a934c45b4d372585fa5465a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6895b1b555053be338f78204090ecb63f9112830685905d899a868aeb010fd46e822c7438c1f29de75438803f7fd18911ce4a37aa37188b7018f21873181cfc7
|
|
7
|
+
data.tar.gz: 803e4c91c0dfb4c325af1bc22a0300b8eca03762fe465f6934b4d513ba84bbbc5dae15940acce0133e3ebff9e0960e1cabe0ef6acc057c19e64e8ccf2fb18e93
|
data/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# casdoor-ruby-sdk
|
|
2
|
+
|
|
3
|
+
[](https://github.com/casdoor/casdoor-ruby-sdk/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/casdoor-ruby-sdk)
|
|
5
|
+
[](https://rubygems.org/gems/casdoor-ruby-sdk)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
[](https://github.com/rubocop/rubocop)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://discord.gg/5rPsrAzK7S)
|
|
10
|
+
|
|
11
|
+
The Ruby SDK of [Casdoor](https://casdoor.ai/). It lets a Ruby application (Rails, Sinatra, Hanami, ...) sign users in
|
|
12
|
+
with Casdoor, verify the tokens issued by Casdoor, and manage the users, applications, roles, permissions and the
|
|
13
|
+
other objects of Casdoor through its API.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add it to the `Gemfile`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'casdoor-ruby-sdk'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
or install it directly with `gem install casdoor-ruby-sdk`.
|
|
24
|
+
|
|
25
|
+
It requires Ruby 2.7 or later, and depends only on the [jwt](https://github.com/jwt/ruby-jwt) gem.
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'casdoor'
|
|
31
|
+
|
|
32
|
+
client = Casdoor::Client.new(
|
|
33
|
+
endpoint: 'https://door.casdoor.com', # the URL of your Casdoor
|
|
34
|
+
client_id: 'xxx', # "Client ID" of the application in Casdoor
|
|
35
|
+
client_secret: 'xxx', # "Client secret" of the application in Casdoor
|
|
36
|
+
certificate: File.read('cert.pem'), # "Certificate" of the application's cert, only needed to verify tokens
|
|
37
|
+
organization_name: 'casbin', # the organization of the users
|
|
38
|
+
application_name: 'app-example' # the application in Casdoor
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The certificate is the public certificate shown on the edit page of the cert that the application uses
|
|
43
|
+
("Certs" → the application's cert → "Certificate"). A public key in PEM format works too.
|
|
44
|
+
|
|
45
|
+
`Casdoor::Client.new` also accepts `headers:` (added to every request), `open_timeout:` and `read_timeout:`.
|
|
46
|
+
|
|
47
|
+
## Signing users in
|
|
48
|
+
|
|
49
|
+
Casdoor signs users in with the OAuth 2.0 authorization code flow:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# 1. Redirect the browser to the sign-in page of Casdoor
|
|
53
|
+
redirect_to client.get_signin_url('https://your-app.example.com/callback', state: session[:state] = SecureRandom.hex)
|
|
54
|
+
|
|
55
|
+
# 2. Casdoor redirects back to the callback with "code" and "state", exchange the code for the tokens
|
|
56
|
+
raise 'Invalid state' unless params[:state] == session[:state]
|
|
57
|
+
token = client.get_oauth_token(params[:code])
|
|
58
|
+
# => {"access_token" => "...", "id_token" => "...", "refresh_token" => "...", "expires_in" => 604800, ...}
|
|
59
|
+
|
|
60
|
+
# 3. Verify the access token and read the user in it
|
|
61
|
+
claims = client.parse_jwt_token(token['access_token'])
|
|
62
|
+
claims.name # => "alice"
|
|
63
|
+
claims.owner # => "casbin", the organization
|
|
64
|
+
claims.email
|
|
65
|
+
claims.user # => Casdoor::User with the fields of the user
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`parse_jwt_token` verifies the signature with the certificate, the expiration time, and that the token is issued to
|
|
69
|
+
this application (its client ID); otherwise it raises `Casdoor::InvalidTokenError`.
|
|
70
|
+
|
|
71
|
+
Other helpers:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
client.get_signup_url # the sign-up page of the application
|
|
75
|
+
client.get_user_profile_url('alice', access_token) # the profile page of a user
|
|
76
|
+
client.get_my_profile_url(access_token) # the profile page of the signed-in user
|
|
77
|
+
client.refresh_oauth_token(token['refresh_token'])
|
|
78
|
+
client.get_oauth_token_by_password('alice', 'password') # needs the "password" grant type in the application
|
|
79
|
+
client.get_oauth_token_by_client_credentials # needs the "client_credentials" grant type
|
|
80
|
+
client.introspect_token(token['access_token']) # => {"active" => true, ...}
|
|
81
|
+
client.logout(token['access_token']) # signs the user out of Casdoor
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Calling the API as a user
|
|
85
|
+
|
|
86
|
+
By default the client calls the API as the application, with the client ID and secret, which has the permissions of
|
|
87
|
+
an admin of the application's organization. To call the API as the signed-in user instead:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
user_client = client.with_access_token(token['access_token'])
|
|
91
|
+
user = user_client.get_account
|
|
92
|
+
user.display_name = 'Alice'
|
|
93
|
+
user_client.update_user(user)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Managing users
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
user = client.get_user('alice') # in the client's organization, or client.get_user('org/alice')
|
|
100
|
+
user.display_name = 'Alice Smith'
|
|
101
|
+
user.email = 'alice@example.com'
|
|
102
|
+
user.properties = (user.properties || {}).merge('department' => 'R&D')
|
|
103
|
+
client.update_user(user) # => true when something changed
|
|
104
|
+
|
|
105
|
+
client.update_user(user, columns: %i[display_name email]) # only update these fields
|
|
106
|
+
|
|
107
|
+
client.add_user(Casdoor::User.new(name: 'bob', display_name: 'Bob', password: '123456'))
|
|
108
|
+
client.delete_user('bob')
|
|
109
|
+
|
|
110
|
+
client.get_users # => [Casdoor::User]
|
|
111
|
+
users, total = client.get_pagination_users(1, 20, sort_field: 'created_time', sort_order: 'descend')
|
|
112
|
+
client.get_user_by_email('alice@example.com')
|
|
113
|
+
client.get_user_by_phone('13800000000')
|
|
114
|
+
client.get_user_by_user_id('uuid') # by the "id" field of the user
|
|
115
|
+
client.get_user_count
|
|
116
|
+
client.set_password('casbin', 'alice', 'new-password')
|
|
117
|
+
client.check_user_password(name: 'alice', password: 'password') # => true or false
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The objects (`Casdoor::User`, `Casdoor::Application`, ...) keep all the fields returned by Casdoor, so an object that is
|
|
121
|
+
read, changed and updated never loses the fields this SDK doesn't know about. The fields can be read and written in
|
|
122
|
+
snake_case (`user.display_name`, `user.is_admin?`), or with their names in Casdoor (`user['displayName']`). The fields
|
|
123
|
+
whose names are taken by Ruby (e.g. `hash` of a user) can only be accessed with `[]`.
|
|
124
|
+
|
|
125
|
+
## Managing the other objects
|
|
126
|
+
|
|
127
|
+
The same methods exist for all the objects of Casdoor: adapters, applications, certs, enforcers, groups,
|
|
128
|
+
invitations, models, orders, organizations, payments, permissions, plans, pricings, products, providers, resources,
|
|
129
|
+
roles, subscriptions, syncers, tokens, transactions, users and webhooks.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
client.get_roles # => [Casdoor::Role]
|
|
133
|
+
client.get_pagination_roles(1, 20) # => [[Casdoor::Role], total]
|
|
134
|
+
client.get_role('admin') # => Casdoor::Role or nil
|
|
135
|
+
client.add_role(Casdoor::Role.new(name: 'admin', users: ['casbin/alice']))
|
|
136
|
+
client.update_role(role, columns: %i[users])
|
|
137
|
+
client.delete_role('admin') # by name, or by the object
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Organizations, applications and tokens belong to `admin`, the other objects belong to the client's organization.
|
|
141
|
+
A different owner can be given by `"owner/name"`, by the `owner` field of the object, or by `owner:` in the list
|
|
142
|
+
methods. Extra query parameters of the list methods can be given in snake_case, e.g.
|
|
143
|
+
`client.get_applications(organization: 'casbin')`.
|
|
144
|
+
|
|
145
|
+
## Checking permissions
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
client.enforce(%w[alice data1 read], permission_id: 'casbin/permission-1') # => true or false
|
|
149
|
+
client.batch_enforce([%w[alice data1 read], %w[bob data2 write]], model_id: 'casbin/model-1')
|
|
150
|
+
# => [[true, false]], the results for each permission
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
One of `permission_id:`, `model_id:`, `resource_id:`, `enforcer_id:` or `owner:` chooses the policies to check.
|
|
154
|
+
|
|
155
|
+
## Errors
|
|
156
|
+
|
|
157
|
+
- `Casdoor::ApiError`: Casdoor rejected the request, e.g. "Unauthorized operation", or an OAuth error
|
|
158
|
+
- `Casdoor::HttpError`: an unexpected HTTP status, with `status` and `body`
|
|
159
|
+
- `Casdoor::InvalidTokenError`: `parse_jwt_token` can't verify the token
|
|
160
|
+
- All of them inherit from `Casdoor::Error`.
|
|
161
|
+
|
|
162
|
+
## Other APIs
|
|
163
|
+
|
|
164
|
+
The SDK covers the common APIs. Any other API of Casdoor (see [the Swagger docs](https://door.casdoor.com/swagger))
|
|
165
|
+
can be called with the authentication of the client:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
client.get_data('get-organization-applications', owner: 'admin', organization: 'casbin') # GET, returns "data"
|
|
169
|
+
client.get_response('get-sessions', owner: 'casbin') # GET, returns the whole response, e.g. "data2"
|
|
170
|
+
client.post_json('add-ldap', {}, ldap) # POST with a JSON body
|
|
171
|
+
client.post_form('set-password', {}, form) # POST with a form body
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```shell
|
|
177
|
+
bundle install
|
|
178
|
+
bundle exec rubocop
|
|
179
|
+
bundle exec rspec
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The integration tests run against a real Casdoor, they are skipped unless `CASDOOR_TEST_ENDPOINT` is set. Start a
|
|
183
|
+
Casdoor with the test data, then run them:
|
|
184
|
+
|
|
185
|
+
```shell
|
|
186
|
+
docker run -d -p 8000:8000 -e driverName=sqlite -e dataSourceName='file:casdoor.db?cache=shared' \
|
|
187
|
+
-e initDataFile=/init_data.json -v "$PWD/.ci/casdoor/init_data.json:/init_data.json:ro" casbin/casdoor-all-in-one
|
|
188
|
+
CASDOOR_TEST_ENDPOINT=http://localhost:8000 bundle exec rspec
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
[Apache-2.0](LICENSE)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'jwt'
|
|
18
|
+
require 'openssl'
|
|
19
|
+
|
|
20
|
+
module Casdoor
|
|
21
|
+
module Api
|
|
22
|
+
# Signing users in with OAuth 2.0 / OIDC, and verifying the JWT tokens issued by Casdoor.
|
|
23
|
+
#
|
|
24
|
+
# The OAuth tokens are returned as the hashes of the Casdoor token API, e.g.
|
|
25
|
+
# {"access_token" => "...", "id_token" => "...", "refresh_token" => "...", "token_type" => "Bearer",
|
|
26
|
+
# "expires_in" => 604800, "scope" => "read"}
|
|
27
|
+
module Auth
|
|
28
|
+
JWT_ALGORITHMS = %w[RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512].freeze
|
|
29
|
+
|
|
30
|
+
# The URL of the sign-in page that the browser is redirected to. After signing in, Casdoor redirects back to
|
|
31
|
+
# redirect_uri with "code" and "state" in the query, and the code is exchanged with get_oauth_token.
|
|
32
|
+
def get_signin_url(redirect_uri, state: application_name, scope: 'read')
|
|
33
|
+
query = { 'client_id' => client_id, 'response_type' => 'code', 'redirect_uri' => redirect_uri,
|
|
34
|
+
'scope' => scope, 'state' => state }
|
|
35
|
+
"#{endpoint}/login/oauth/authorize?#{URI.encode_www_form(query)}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The URL of the sign-up page. Without redirect_uri, it's the plain sign-up page of the application, otherwise
|
|
39
|
+
# the user is signed in and redirected back to redirect_uri like get_signin_url.
|
|
40
|
+
def get_signup_url(redirect_uri = nil, state: application_name, scope: 'read')
|
|
41
|
+
return "#{endpoint}/signup/#{application_name}" if redirect_uri.nil?
|
|
42
|
+
|
|
43
|
+
url = get_signin_url(redirect_uri, state: state, scope: scope)
|
|
44
|
+
url.sub('/login/oauth/authorize', '/signup/oauth/authorize')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_user_profile_url(user_name, access_token = nil)
|
|
48
|
+
url = "#{endpoint}/users/#{organization_name}/#{user_name}"
|
|
49
|
+
access_token ? "#{url}?#{URI.encode_www_form('access_token' => access_token)}" : url
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def get_my_profile_url(access_token = nil)
|
|
53
|
+
url = "#{endpoint}/account"
|
|
54
|
+
access_token ? "#{url}?#{URI.encode_www_form('access_token' => access_token)}" : url
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Exchanges the code that Casdoor passed to the redirect URI for the OAuth tokens
|
|
58
|
+
def get_oauth_token(code)
|
|
59
|
+
request_oauth_token('authorization_code', 'code' => code)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def refresh_oauth_token(refresh_token, scope: nil)
|
|
63
|
+
request_oauth_token('refresh_token', 'refresh_token' => refresh_token, 'scope' => scope)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The "password" grant type, it must be enabled in the "Grant types" of the application in Casdoor.
|
|
67
|
+
# username is the user's name in the application's organization, like "alice".
|
|
68
|
+
def get_oauth_token_by_password(username, password)
|
|
69
|
+
request_oauth_token('password', 'username' => username, 'password' => password)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A token of the application itself, the "client_credentials" grant type must be enabled in the application
|
|
73
|
+
def get_oauth_token_by_client_credentials
|
|
74
|
+
request_oauth_token('client_credentials')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Verifies a JWT token (the access token or the ID token) with the certificate of the client, and returns its
|
|
78
|
+
# claims. The token must not be expired, and must be issued to this application: pass audience: to check a
|
|
79
|
+
# different audience (e.g. the resource of RFC 8707), or audience: false to skip the check.
|
|
80
|
+
#
|
|
81
|
+
# claims = client.parse_jwt_token(token["access_token"])
|
|
82
|
+
# claims.name # => "alice"
|
|
83
|
+
# claims.owner # => "casbin"
|
|
84
|
+
# claims.user # => Casdoor::User
|
|
85
|
+
def parse_jwt_token(token, audience: nil)
|
|
86
|
+
# The expiration and the "not before" time are verified by default
|
|
87
|
+
payload, = JWT.decode(token, jwt_public_key, true, algorithms: JWT_ALGORITHMS)
|
|
88
|
+
check_audience(payload['aud'], audience) unless audience == false
|
|
89
|
+
Claims.new(payload)
|
|
90
|
+
rescue JWT::DecodeError => e
|
|
91
|
+
raise InvalidTokenError, e.message
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Asks Casdoor whether the token is active, see RFC 7662. Returns a hash like {"active" => true, ...}.
|
|
95
|
+
def introspect_token(token, token_type_hint: 'access_token')
|
|
96
|
+
request = Net::HTTP::Post.new(get_url('login/oauth/introspect'))
|
|
97
|
+
request.set_form_data('token' => token, 'token_type_hint' => token_type_hint)
|
|
98
|
+
check_oauth_response(send_request(request))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Signs the user out of Casdoor, from all their sessions or only the session of the access token
|
|
102
|
+
def logout(access_token, all_sessions: true)
|
|
103
|
+
with_access_token(access_token).post_form('sso-logout', { 'logoutAll' => all_sessions }, {})
|
|
104
|
+
true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def request_oauth_token(grant_type, params = {})
|
|
110
|
+
form = { 'grant_type' => grant_type, 'client_id' => client_id, 'client_secret' => client_secret }
|
|
111
|
+
form.merge!(params.compact)
|
|
112
|
+
|
|
113
|
+
request = Net::HTTP::Post.new(get_url('login/oauth/access_token'))
|
|
114
|
+
request.set_form_data(form)
|
|
115
|
+
token = check_oauth_response(send_request(request, auth: false))
|
|
116
|
+
|
|
117
|
+
# Older Casdoor versions put the error into the access token
|
|
118
|
+
if token['access_token'].to_s.start_with?('error:')
|
|
119
|
+
raise ApiError, token['access_token'].delete_prefix('error:').strip
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
token
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def check_oauth_response(body)
|
|
126
|
+
if body['error']
|
|
127
|
+
message = [body['error'], body['error_description']].compact.reject(&:empty?).join(': ')
|
|
128
|
+
raise ApiError, message
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
check_response(body)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def jwt_public_key
|
|
135
|
+
pem = certificate.to_s
|
|
136
|
+
raise Error, 'The certificate of the client is required to parse JWT tokens' if pem.strip.empty?
|
|
137
|
+
|
|
138
|
+
pem.include?('CERTIFICATE') ? OpenSSL::X509::Certificate.new(pem).public_key : OpenSSL::PKey.read(pem)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Casdoor issues the tokens to the client ID, or to "<client ID>-org-<organization>" for shared applications
|
|
142
|
+
def check_audience(aud, audience)
|
|
143
|
+
auds = Array(aud)
|
|
144
|
+
valid = if audience
|
|
145
|
+
auds.include?(audience)
|
|
146
|
+
else
|
|
147
|
+
auds.any? { |item| item == client_id || item.to_s.start_with?("#{client_id}-org-") }
|
|
148
|
+
end
|
|
149
|
+
raise InvalidTokenError, "The token is issued to #{auds.inspect}, not this application" unless valid
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Casdoor
|
|
18
|
+
module Api
|
|
19
|
+
# The add/get/update/delete APIs that all the Casdoor objects share. For each object, e.g. the user, it defines:
|
|
20
|
+
#
|
|
21
|
+
# get_users(**query) # => [User]
|
|
22
|
+
# get_pagination_users(page, page_size, **query) # => [[User], total count]
|
|
23
|
+
# get_user(name) # => User or nil, name can be "name" or "owner/name"
|
|
24
|
+
# add_user(user) # => true if added
|
|
25
|
+
# update_user(user, columns: nil) # => true if changed, columns limits the updated fields
|
|
26
|
+
# delete_user(user_or_name) # => true if deleted
|
|
27
|
+
#
|
|
28
|
+
# The objects belong to the client's organization, except the ones in ADMIN_OWNED, which belong to "admin".
|
|
29
|
+
# A different owner can be passed with "owner/name", the owner field of the object, or owner: in the query.
|
|
30
|
+
# Extra query parameters of the list APIs can be passed in snake_case, e.g. get_users(sort_field: "name").
|
|
31
|
+
module Crud
|
|
32
|
+
OBJECTS = {
|
|
33
|
+
adapter: Adapter,
|
|
34
|
+
application: Application,
|
|
35
|
+
cert: Cert,
|
|
36
|
+
enforcer: Enforcer,
|
|
37
|
+
group: Group,
|
|
38
|
+
invitation: Invitation,
|
|
39
|
+
model: Model,
|
|
40
|
+
order: Order,
|
|
41
|
+
organization: Organization,
|
|
42
|
+
payment: Payment,
|
|
43
|
+
permission: Permission,
|
|
44
|
+
plan: Plan,
|
|
45
|
+
pricing: Pricing,
|
|
46
|
+
product: Product,
|
|
47
|
+
provider: Provider,
|
|
48
|
+
resource: Resource,
|
|
49
|
+
role: Role,
|
|
50
|
+
subscription: Subscription,
|
|
51
|
+
syncer: Syncer,
|
|
52
|
+
token: Token,
|
|
53
|
+
transaction: Transaction,
|
|
54
|
+
user: User,
|
|
55
|
+
webhook: Webhook
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
ADMIN_OWNED = %i[application organization token].freeze
|
|
59
|
+
|
|
60
|
+
OBJECTS.each do |object, klass|
|
|
61
|
+
plural = "#{object}s"
|
|
62
|
+
|
|
63
|
+
define_method("get_#{plural}") do |**query|
|
|
64
|
+
data = get_data("get-#{plural}", list_query(object, query)) || []
|
|
65
|
+
data.map { |item| klass.new(item) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
define_method("get_pagination_#{plural}") do |page, page_size, **query|
|
|
69
|
+
query = list_query(object, query).merge('p' => page, 'pageSize' => page_size)
|
|
70
|
+
response = get_response("get-#{plural}", query)
|
|
71
|
+
[(response['data'] || []).map { |item| klass.new(item) }, response['data2'].to_i]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
define_method("get_#{object}") do |name|
|
|
75
|
+
data = get_data("get-#{object}", 'id' => object_id_of(object, name))
|
|
76
|
+
data && klass.new(data)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
define_method("add_#{object}") do |entity|
|
|
80
|
+
modify_object("add-#{object}", object, entity)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
define_method("update_#{object}") do |entity, columns: nil|
|
|
84
|
+
modify_object("update-#{object}", object, entity, columns: columns)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Casdoor deletes an object by the whole object, e.g. an application is only deleted when its organization
|
|
88
|
+
# matches too, so the object is fetched first when only its name is given
|
|
89
|
+
define_method("delete_#{object}") do |entity|
|
|
90
|
+
entity = send("get_#{object}", entity) unless entity.is_a?(Entity)
|
|
91
|
+
return false if entity.nil?
|
|
92
|
+
|
|
93
|
+
modify_object("delete-#{object}", object, entity)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def default_owner(object)
|
|
100
|
+
ADMIN_OWNED.include?(object) ? 'admin' : organization_name
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def object_id_of(object, name)
|
|
104
|
+
name.to_s.include?('/') ? name.to_s : "#{default_owner(object)}/#{name}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def list_query(object, query)
|
|
108
|
+
query = query.each_with_object({}) { |(key, value), hash| hash[Entity.camelize(key)] = value }
|
|
109
|
+
{ 'owner' => default_owner(object) }.merge(query)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def modify_object(action, object, entity, columns: nil, query: {})
|
|
113
|
+
entity = OBJECTS.fetch(object).new(entity) if entity.is_a?(Hash)
|
|
114
|
+
entity.owner = default_owner(object) if entity.owner.to_s.empty?
|
|
115
|
+
|
|
116
|
+
query = { 'id' => "#{entity.owner}/#{entity.name}" }.merge(query)
|
|
117
|
+
query['columns'] = Array(columns).map { |column| Entity.camelize(column) }.join(',') if columns
|
|
118
|
+
post_json(action, query, entity)['data'] == 'Affected'
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Casdoor
|
|
18
|
+
module Api
|
|
19
|
+
# The permission checks of Casdoor. Pass one of permission_id, model_id, resource_id, enforcer_id or owner
|
|
20
|
+
# ("owner/name" for the IDs) to choose which policies to check against.
|
|
21
|
+
module Enforce
|
|
22
|
+
# request is a Casbin request like ["alice", "data1", "read"]. Returns true if any of the checked
|
|
23
|
+
# permissions allows it.
|
|
24
|
+
def enforce(request, permission_id: nil, model_id: nil, resource_id: nil, enforcer_id: nil, owner: nil)
|
|
25
|
+
query = enforce_query(permission_id, model_id, resource_id, enforcer_id, owner)
|
|
26
|
+
Array(post_json('enforce', query, request)['data']).any?(true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# requests is a list of Casbin requests. Returns a list of results for each checked permission, like
|
|
30
|
+
# [[true, false]] for two requests checked against one permission.
|
|
31
|
+
def batch_enforce(requests, permission_id: nil, model_id: nil, resource_id: nil, enforcer_id: nil, owner: nil)
|
|
32
|
+
query = enforce_query(permission_id, model_id, resource_id, enforcer_id, owner)
|
|
33
|
+
post_json('batch-enforce', query, requests)['data'] || []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def enforce_query(permission_id, model_id, resource_id, enforcer_id, owner)
|
|
39
|
+
{ 'permissionId' => permission_id, 'modelId' => model_id, 'resourceId' => resource_id,
|
|
40
|
+
'enforcerId' => enforcer_id, 'owner' => owner }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Casdoor
|
|
18
|
+
module Api
|
|
19
|
+
# The user APIs besides the ones in Crud
|
|
20
|
+
module Users
|
|
21
|
+
# The user that the client is authenticated as, meant to be used with with_access_token:
|
|
22
|
+
#
|
|
23
|
+
# user = client.with_access_token(access_token).get_account
|
|
24
|
+
def get_account
|
|
25
|
+
data = get_data('get-account')
|
|
26
|
+
data && User.new(data)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The users of all the organizations, only allowed for global admins
|
|
30
|
+
def get_global_users
|
|
31
|
+
(get_data('get-global-users') || []).map { |item| User.new(item) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_sorted_users(sorter, limit)
|
|
35
|
+
query = { 'owner' => organization_name, 'sorter' => sorter, 'limit' => limit }
|
|
36
|
+
(get_data('get-sorted-users', query) || []).map { |item| User.new(item) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# is_online: nil counts all the users, true or false counts the online or offline ones
|
|
40
|
+
def get_user_count(is_online: nil)
|
|
41
|
+
online = { nil => '', true => '1', false => '0' }.fetch(is_online)
|
|
42
|
+
query = { 'owner' => organization_name, 'isOnline' => online }
|
|
43
|
+
get_data('get-user-count', query).to_i
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def get_user_by_email(email)
|
|
47
|
+
find_user('email' => email)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get_user_by_phone(phone)
|
|
51
|
+
find_user('phone' => phone)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# user_id is the "id" field of the user (a UUID), not "owner/name"
|
|
55
|
+
def get_user_by_user_id(user_id)
|
|
56
|
+
find_user('userId' => user_id)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Sets the user's password. old_password can be empty when an admin sets the password of another user.
|
|
60
|
+
def set_password(owner, name, new_password, old_password: '')
|
|
61
|
+
form = { 'userOwner' => owner, 'userName' => name, 'oldPassword' => old_password,
|
|
62
|
+
'newPassword' => new_password }
|
|
63
|
+
post_form('set-password', {}, form)['status'] == 'ok'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Checks the password of a user, e.g. User.new(owner: "casbin", name: "alice", password: "123")
|
|
67
|
+
def check_user_password(user)
|
|
68
|
+
user = User.new(user) if user.is_a?(Hash)
|
|
69
|
+
user.owner = organization_name if user.owner.to_s.empty?
|
|
70
|
+
post_json('check-user-password', { 'id' => "#{user.owner}/#{user.name}" }, user)['status'] == 'ok'
|
|
71
|
+
rescue ApiError
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def find_user(query)
|
|
78
|
+
data = get_data('get-user', { 'owner' => organization_name }.merge(query))
|
|
79
|
+
data && User.new(data)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'json'
|
|
18
|
+
require 'net/http'
|
|
19
|
+
require 'uri'
|
|
20
|
+
|
|
21
|
+
module Casdoor
|
|
22
|
+
# The client of the Casdoor API. By default it calls the API as the application, authenticated by the client ID
|
|
23
|
+
# and the client secret. Use with_access_token to call the API as a user instead.
|
|
24
|
+
#
|
|
25
|
+
# client = Casdoor::Client.new(
|
|
26
|
+
# endpoint: "https://door.casdoor.com",
|
|
27
|
+
# client_id: "...",
|
|
28
|
+
# client_secret: "...",
|
|
29
|
+
# certificate: File.read("token_jwt_key.pem"),
|
|
30
|
+
# organization_name: "casbin",
|
|
31
|
+
# application_name: "app-example"
|
|
32
|
+
# )
|
|
33
|
+
class Client
|
|
34
|
+
include Api::Auth
|
|
35
|
+
include Api::Crud
|
|
36
|
+
include Api::Users
|
|
37
|
+
include Api::Enforce
|
|
38
|
+
|
|
39
|
+
attr_reader :endpoint, :client_id, :client_secret, :certificate, :organization_name, :application_name,
|
|
40
|
+
:access_token, :headers, :open_timeout, :read_timeout
|
|
41
|
+
|
|
42
|
+
# certificate is the public certificate (or public key) of the application's cert in Casdoor, in PEM format.
|
|
43
|
+
# It's only needed by parse_jwt_token.
|
|
44
|
+
# headers are added to all the requests, e.g. to pass a gateway.
|
|
45
|
+
def initialize(endpoint:, client_id:, client_secret:, organization_name:, application_name:, certificate: nil,
|
|
46
|
+
headers: {}, access_token: nil, open_timeout: 10, read_timeout: 30)
|
|
47
|
+
@endpoint = endpoint.to_s.chomp('/')
|
|
48
|
+
@client_id = client_id
|
|
49
|
+
@client_secret = client_secret
|
|
50
|
+
@certificate = certificate
|
|
51
|
+
@organization_name = organization_name
|
|
52
|
+
@application_name = application_name
|
|
53
|
+
@headers = headers.dup.freeze
|
|
54
|
+
@access_token = access_token
|
|
55
|
+
@open_timeout = open_timeout
|
|
56
|
+
@read_timeout = read_timeout
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns a copy of the client that calls the API as the user who owns the access token, instead of as the
|
|
60
|
+
# application. The API is still subject to the permission check of Casdoor, so a normal user can only access
|
|
61
|
+
# their own data:
|
|
62
|
+
#
|
|
63
|
+
# token = client.get_oauth_token(code)
|
|
64
|
+
# user = client.with_access_token(token["access_token"]).get_account
|
|
65
|
+
def with_access_token(access_token)
|
|
66
|
+
Client.new(endpoint: endpoint, client_id: client_id, client_secret: client_secret,
|
|
67
|
+
organization_name: organization_name, application_name: application_name, certificate: certificate,
|
|
68
|
+
headers: headers, access_token: access_token, open_timeout: open_timeout, read_timeout: read_timeout)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def get_url(action, query = {})
|
|
72
|
+
query = query.compact
|
|
73
|
+
url = "#{endpoint}/api/#{action}"
|
|
74
|
+
url += "?#{URI.encode_www_form(query)}" unless query.empty?
|
|
75
|
+
URI(url)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Calls a GET API, and returns the whole response like {"status" => "ok", "data" => ..., "data2" => ...}
|
|
79
|
+
def get_response(action, query = {})
|
|
80
|
+
request = Net::HTTP::Get.new(get_url(action, query))
|
|
81
|
+
check_response(send_request(request))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Calls a GET API, and returns the "data" of the response
|
|
85
|
+
def get_data(action, query = {})
|
|
86
|
+
get_response(action, query)['data']
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Calls a POST API with a JSON body, and returns the whole response
|
|
90
|
+
def post_json(action, query, body)
|
|
91
|
+
request = Net::HTTP::Post.new(get_url(action, query), 'Content-Type' => 'application/json')
|
|
92
|
+
request.body = body.to_json
|
|
93
|
+
check_response(send_request(request))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Calls a POST API with a form body, and returns the whole response
|
|
97
|
+
def post_form(action, query, form)
|
|
98
|
+
request = Net::HTTP::Post.new(get_url(action, query))
|
|
99
|
+
request.set_form_data(form)
|
|
100
|
+
check_response(send_request(request))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Sends the request and returns the parsed JSON body. Errors of the HTTP layer are raised, while the
|
|
106
|
+
# {"status": "error"} responses are left to the caller.
|
|
107
|
+
def send_request(request, auth: true)
|
|
108
|
+
if auth
|
|
109
|
+
if access_token
|
|
110
|
+
request['Authorization'] = "Bearer #{access_token}"
|
|
111
|
+
else
|
|
112
|
+
request.basic_auth(client_id.to_s, client_secret.to_s)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
request['Accept'] = 'application/json'
|
|
116
|
+
headers.each { |key, value| request[key] = value }
|
|
117
|
+
|
|
118
|
+
uri = request.uri
|
|
119
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
|
120
|
+
open_timeout: open_timeout, read_timeout: read_timeout) do |http|
|
|
121
|
+
http.request(request)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
begin
|
|
125
|
+
body = JSON.parse(response.body.to_s)
|
|
126
|
+
rescue JSON::ParserError
|
|
127
|
+
raise HttpError.new(response.code.to_i, response.body)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
return body if response.is_a?(Net::HTTPSuccess)
|
|
131
|
+
# Casdoor answers some errors with a non-2xx status and a JSON body that explains the error
|
|
132
|
+
return body if body.is_a?(Hash) && (body['status'] == 'error' || body['error'])
|
|
133
|
+
|
|
134
|
+
raise HttpError.new(response.code.to_i, response.body)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def check_response(body)
|
|
138
|
+
raise ApiError, body['msg'].to_s if body.is_a?(Hash) && body['status'] == 'error'
|
|
139
|
+
|
|
140
|
+
body
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'json'
|
|
18
|
+
|
|
19
|
+
module Casdoor
|
|
20
|
+
# An object of Casdoor, like a user or an application. It keeps all the JSON fields returned by Casdoor, so that
|
|
21
|
+
# updating an object never clears the fields that the SDK doesn't know about, and the fields added by newer
|
|
22
|
+
# Casdoor versions work without updating the SDK.
|
|
23
|
+
#
|
|
24
|
+
# The fields can be read and written in snake_case, which is converted to the camelCase name used by Casdoor:
|
|
25
|
+
#
|
|
26
|
+
# user.display_name # => user["displayName"]
|
|
27
|
+
# user.email = "alice@example.com"
|
|
28
|
+
# user.is_admin? # => true or false
|
|
29
|
+
# user[:display_name] # Symbol keys are converted too
|
|
30
|
+
# user["displayName"] # String keys are used as is
|
|
31
|
+
#
|
|
32
|
+
# The fields whose names are taken by Ruby's own methods (e.g. "hash" of a user, "method" of a provider) can
|
|
33
|
+
# only be accessed with [], like user["hash"].
|
|
34
|
+
#
|
|
35
|
+
# Nested objects (e.g. application.providers) are plain hashes with the camelCase keys of Casdoor.
|
|
36
|
+
class Entity
|
|
37
|
+
def self.camelize(key)
|
|
38
|
+
key.to_s.gsub(/_([a-z\d])/) { Regexp.last_match(1).upcase }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(attributes = {})
|
|
42
|
+
@attributes = {}
|
|
43
|
+
(attributes || {}).each { |key, value| self[key] = value }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](key)
|
|
47
|
+
@attributes[field_name(key)]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def []=(key, value)
|
|
51
|
+
@attributes[field_name(key)] = value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def key?(key)
|
|
55
|
+
@attributes.key?(field_name(key))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h
|
|
59
|
+
@attributes.dup
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_json(*args)
|
|
63
|
+
@attributes.to_json(*args)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ==(other)
|
|
67
|
+
other.class == self.class && other.to_h == to_h
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def inspect
|
|
71
|
+
"#<#{self.class.name} #{@attributes.inspect}>"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def method_missing(method_name, *args)
|
|
75
|
+
field = method_name.to_s
|
|
76
|
+
if field.end_with?('=') && args.length == 1
|
|
77
|
+
self[Entity.camelize(field.chomp('='))] = args.first
|
|
78
|
+
elsif field.end_with?('?') && args.empty?
|
|
79
|
+
self[Entity.camelize(field.chomp('?'))] ? true : false
|
|
80
|
+
elsif args.empty? && field.match?(/\A[a-z][a-z\d_]*\z/)
|
|
81
|
+
self[Entity.camelize(field)]
|
|
82
|
+
else
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
88
|
+
method_name.to_s.match?(/\A[a-z][a-z\d_]*[=?]?\z/) || super
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# String keys are the exact names of Casdoor, while Symbol keys are snake_case
|
|
94
|
+
def field_name(key)
|
|
95
|
+
key.is_a?(Symbol) ? Entity.camelize(key) : key.to_s
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Adapter < Entity; end
|
|
100
|
+
class Application < Entity; end
|
|
101
|
+
class Cert < Entity; end
|
|
102
|
+
class Enforcer < Entity; end
|
|
103
|
+
class Group < Entity; end
|
|
104
|
+
class Invitation < Entity; end
|
|
105
|
+
class Model < Entity; end
|
|
106
|
+
class Order < Entity; end
|
|
107
|
+
class Organization < Entity; end
|
|
108
|
+
class Payment < Entity; end
|
|
109
|
+
class Permission < Entity; end
|
|
110
|
+
class Plan < Entity; end
|
|
111
|
+
class Pricing < Entity; end
|
|
112
|
+
class Product < Entity; end
|
|
113
|
+
class Provider < Entity; end
|
|
114
|
+
class Resource < Entity; end
|
|
115
|
+
class Role < Entity; end
|
|
116
|
+
class Session < Entity; end
|
|
117
|
+
class Subscription < Entity; end
|
|
118
|
+
class Syncer < Entity; end
|
|
119
|
+
class Token < Entity; end
|
|
120
|
+
class Transaction < Entity; end
|
|
121
|
+
class User < Entity; end
|
|
122
|
+
class Webhook < Entity; end
|
|
123
|
+
|
|
124
|
+
# The claims of a JWT token issued by Casdoor: the fields of the user, plus the standard claims like "exp" and "aud"
|
|
125
|
+
class Claims < Entity
|
|
126
|
+
def user
|
|
127
|
+
User.new(to_h)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Casdoor names the claim "tokenType", or "TokenType" for the "JWT-Custom" token format
|
|
131
|
+
def refresh_token?
|
|
132
|
+
(self['tokenType'] || self['TokenType']) == 'refresh-token'
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Casdoor
|
|
18
|
+
# Base class of all the errors raised by the SDK
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
# Casdoor answered the request with an error, e.g. {"status": "error", "msg": "..."} or an OAuth error
|
|
22
|
+
class ApiError < Error; end
|
|
23
|
+
|
|
24
|
+
# Casdoor answered with an unexpected HTTP status or a body that isn't JSON
|
|
25
|
+
class HttpError < Error
|
|
26
|
+
attr_reader :status, :body
|
|
27
|
+
|
|
28
|
+
def initialize(status, body)
|
|
29
|
+
@status = status
|
|
30
|
+
@body = body
|
|
31
|
+
super("Casdoor returned HTTP #{status}: #{body.to_s[0, 500]}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The JWT token can't be verified with the certificate, or it has expired, or it's issued to another application
|
|
36
|
+
class InvalidTokenError < Error; end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module Casdoor
|
|
18
|
+
# Set by semantic-release in CI when a new version is released, see .releaserc.json
|
|
19
|
+
VERSION = '1.0.0'
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
# So that both `require "casdoor-ruby-sdk"` (the gem name) and `require "casdoor"` work
|
|
18
|
+
require_relative 'casdoor'
|
data/lib/casdoor.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require_relative 'casdoor/version'
|
|
18
|
+
require_relative 'casdoor/error'
|
|
19
|
+
require_relative 'casdoor/entity'
|
|
20
|
+
require_relative 'casdoor/api/auth'
|
|
21
|
+
require_relative 'casdoor/api/crud'
|
|
22
|
+
require_relative 'casdoor/api/users'
|
|
23
|
+
require_relative 'casdoor/api/enforce'
|
|
24
|
+
require_relative 'casdoor/client'
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: casdoor-ruby-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yang Luo
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: jwt
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.5'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '2.5'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
32
|
+
description: 'Ruby SDK for Casdoor: sign users in with OAuth 2.0 / OIDC, verify the
|
|
33
|
+
JWT tokens, and manage users, organizations, applications, roles, permissions and
|
|
34
|
+
the other objects of Casdoor.'
|
|
35
|
+
email:
|
|
36
|
+
- hsluoyz@qq.com
|
|
37
|
+
executables: []
|
|
38
|
+
extensions: []
|
|
39
|
+
extra_rdoc_files: []
|
|
40
|
+
files:
|
|
41
|
+
- LICENSE
|
|
42
|
+
- README.md
|
|
43
|
+
- lib/casdoor-ruby-sdk.rb
|
|
44
|
+
- lib/casdoor.rb
|
|
45
|
+
- lib/casdoor/api/auth.rb
|
|
46
|
+
- lib/casdoor/api/crud.rb
|
|
47
|
+
- lib/casdoor/api/enforce.rb
|
|
48
|
+
- lib/casdoor/api/users.rb
|
|
49
|
+
- lib/casdoor/client.rb
|
|
50
|
+
- lib/casdoor/entity.rb
|
|
51
|
+
- lib/casdoor/error.rb
|
|
52
|
+
- lib/casdoor/version.rb
|
|
53
|
+
homepage: https://github.com/casdoor/casdoor-ruby-sdk
|
|
54
|
+
licenses:
|
|
55
|
+
- Apache-2.0
|
|
56
|
+
metadata:
|
|
57
|
+
rubygems_mfa_required: 'true'
|
|
58
|
+
source_code_uri: https://github.com/casdoor/casdoor-ruby-sdk
|
|
59
|
+
changelog_uri: https://github.com/casdoor/casdoor-ruby-sdk/releases
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 2.7.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 3.6.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Ruby SDK for Casdoor
|
|
77
|
+
test_files: []
|