diff --git a/casdoor/.gitignore b/casdoor/.gitignore new file mode 100644 index 0000000..da28697 --- /dev/null +++ b/casdoor/.gitignore @@ -0,0 +1,26 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.swp + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +.idea/ +*.iml + +tmp/ +tmpFiles/ +*.tmp +logs/ +lastupdate.tmp +commentsRouter*.go diff --git a/casdoor/LICENSE b/casdoor/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/casdoor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/casdoor/README.md b/casdoor/README.md new file mode 100644 index 0000000..c0e9543 --- /dev/null +++ b/casdoor/README.md @@ -0,0 +1,161 @@ +Casdoor +==== + +Casdoor is a UI-first centralized authentication / Single-Sign-On (SSO) platform based on OAuth 2.0 / OIDC. + +## Online demo + +### Casdoor + +Casdoor is the authentication server. It serves both the web UI and the login requests from the application users. + +- Deployed site: https://door.casbin.com/ +- Source code: https://github.com/casbin/casdoor (this repo) + +Global admin login: + +- Username: `admin` +- Password: `123` + +### Web application + +Casbin-OA is one of our applications that use Casdoor as authentication. + +- Deployed site: https://oa.casbin.com/ +- Source code: https://github.com/casbin/casbin-oa + +## Architecture + +Casdoor contains 2 parts: + +Name | Description | Language | Source code +----|------|----|---- +Frontend | Web frontend UI for Casdoor | Javascript + React | https://github.com/casbin/casdoor/tree/master/web +Backend | RESTful API backend for Casdoor | Golang + Beego + MySQL | https://github.com/casbin/casdoor + +## Installation + +- Get code via `go get`: + + ```shell + go get github.com/casbin/casdoor + ``` + + or `git clone`: + + ```shell + git clone https://github.com/casbin/casdoor + ``` + +## Run through Docker +- Install Docker and Docker-compose,you see [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) +- vi casdoor/conf/app.conf +- Modify dataSourceName = root:123@tcp(localhost:3306)/ to dataSourceName = root:123@tcp(db:3306)/ +- Execute the following command + ```shell + docker-compose up + ``` +- Open browser: + + http://localhost:8000/ + +## Run (Dev Environment) + +- Run backend (in port 8000): + + ```shell + go run main.go + ``` + +- Run frontend (in the same machine's port 7001): + + ```shell + cd web + ## npm + npm install + npm run start + ## yarn + yarn install + yarn run start + ``` + +- Open browser: + + http://localhost:7001/ + +## Run (Production Environment) + +- build static pages: + + ``` + cd web + ## npm + npm run build + ## yarn + yarn run build + ## back to casdoor directory + cd .. + ``` + +- build and run go code: + + ``` + go build + ./casdoor + ``` + +Now, Casdoor is running on port 8000. You can access Casdoor pages directly in your browser, or you can setup a reverse proxy to hold your domain name, SSL, etc. + +## Config + +- Setup database (MySQL): + + Casdoor will store its users, nodes and topics informations in a MySQL database named: `casdoor`, will create it if not existed. The DB connection string can be specified at: https://github.com/casbin/casdoor/blob/master/conf/app.conf + + ```ini + db = mysql + dataSourceName = root:123@tcp(localhost:3306)/ + dbName = casdoor + ``` + +- Setup database (Postgres): + + Since we must choose a database when opening Postgres with xorm, you should prepare a database manually before running Casdoor. Let's assume that you have already prepared a database called `casdoor`, then you should specify `app.conf` like this: + + ``` ini + db = postgres + dataSourceName = "user=postgres password=xxx sslmode=disable dbname=" + dbName = casdoor + ``` + + **Please notice:** You can add Postgres parameters in `dataSourceName`, but please make sure that `dataSourceName` ends with `dbname=`. Or database adapter may crash when you launch Casdoor. + + Casdoor uses XORM to connect to DB, so all DBs supported by XORM can also be used. + +- Github corner + + We added a Github icon in the upper right corner, linking to your Github repository address. + You could set `ShowGithubCorner` to hidden it. + + Configuration (`web/src/commo/Conf.js`): + + ```javascript + export const ShowGithubCorner = true + + export const GithubRepo = "https://github.com/casbin/casdoor" //your github repository + ``` + +- OSS conf + + We use an OSS to store and provide user avatars. You must modify the file `conf/oss.conf` to tell the backend your OSS info. For OSS providers, we support Aliyun(`[aliyun]`), awss3(`[s3]`) now. + + ``` + [provider] + accessId = id + accessKey = key + bucket = bucket + endpoint = endpoint + ``` + + Please fill out this conf correctly, or the avatar server won't work! + diff --git a/casdoor/authz/authz.go b/casdoor/authz/authz.go new file mode 100644 index 0000000..3287dac --- /dev/null +++ b/casdoor/authz/authz.go @@ -0,0 +1,119 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "github.com/astaxie/beego" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + xormadapter "github.com/casbin/xorm-adapter/v2" + stringadapter "github.com/qiangmzsx/string-adapter/v2" +) + +var Enforcer *casbin.Enforcer + +func InitAuthz() { + var err error + + a, err := xormadapter.NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName")+beego.AppConfig.String("dbName"), true) + if err != nil { + panic(err) + } + + modelText := ` +[request_definition] +r = subOwner, subName, method, urlPath, objOwner, objName + +[policy_definition] +p = subOwner, subName, method, urlPath, objOwner, objName + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = (r.subOwner == p.subOwner || p.subOwner == "*") && \ + (r.subName == p.subName || p.subName == "*" || r.subName != "anonymous" && p.subName == "!anonymous") && \ + (r.method == p.method || p.method == "*") && \ + (r.urlPath == p.urlPath || p.urlPath == "*") && \ + (r.objOwner == p.objOwner || p.objOwner == "*") && \ + (r.objName == p.objName || p.objName == "*") || \ + (r.urlPath == "/api/update-user" && r.subOwner == r.objOwner && r.subName == r.objName) +` + + m, err := model.NewModelFromString(modelText) + if err != nil { + panic(err) + } + + Enforcer, err = casbin.NewEnforcer(m, a) + if err != nil { + panic(err) + } + + Enforcer.ClearPolicy() + + //if len(Enforcer.GetPolicy()) == 0 { + if true { + ruleText := ` +p, built-in, *, *, *, *, * +p, *, *, POST, /api/signup, *, * +p, *, *, POST, /api/get-email-and-phone, *, * +p, *, *, POST, /api/login, *, * +p, *, *, GET, /api/get-app-login, *, * +p, *, *, POST, /api/logout, *, * +p, *, *, GET, /api/get-account, *, * +p, *, *, POST, /api/login/oauth/access_token, *, * +p, *, *, GET, /api/get-application, *, * +p, *, *, GET, /api/get-users, *, * +p, *, *, GET, /api/get-user, *, * +p, *, *, GET, /api/get-organizations, *, * +p, *, *, GET, /api/get-user-application, *, * +p, *, *, GET, /api/get-default-providers, *, * +p, *, *, POST, /api/upload-avatar, *, * +p, *, *, POST, /api/unlink, *, * +p, *, *, POST, /api/set-password, *, * +p, *, *, POST, /api/send-verification-code, *, * +p, *, *, GET, /api/get-human-check, *, * +p, *, *, POST, /api/reset-email-or-phone, *, * +` + + sa := stringadapter.NewAdapter(ruleText) + // load all rules from string adapter to enforcer's memory + err := sa.LoadPolicy(Enforcer.GetModel()) + if err != nil { + panic(err) + } + + // save all rules from enforcer's memory to Xorm adapter (DB) + // same as: + // a.SavePolicy(Enforcer.GetModel()) + err = Enforcer.SavePolicy() + if err != nil { + panic(err) + } + } +} + +func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool { + res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName) + if err != nil { + panic(err) + } + + return res +} diff --git a/casdoor/casdoor b/casdoor/casdoor new file mode 100755 index 0000000..0259b8b Binary files /dev/null and b/casdoor/casdoor differ diff --git a/casdoor/conf/app.conf b/casdoor/conf/app.conf new file mode 100644 index 0000000..015c778 --- /dev/null +++ b/casdoor/conf/app.conf @@ -0,0 +1,11 @@ +appname = casdoor +httpport = 8000 +runmode = dev +SessionOn = true +copyrequestbody = true +driverName = mysql +dataSourceName = root:cappuccino@tcp(localhost:3306)/ +dbName = casdoor +authState = "casdoor" +httpProxy = "127.0.0.1:10808" +verificationCodeTimeout = 10 \ No newline at end of file diff --git a/casdoor/conf/oss.conf b/casdoor/conf/oss.conf new file mode 100644 index 0000000..a7949f8 --- /dev/null +++ b/casdoor/conf/oss.conf @@ -0,0 +1,6 @@ +[provider] +endpoint = endpoint +accessId = id +accessKey = key +domain = domain +bucket = bucket \ No newline at end of file diff --git a/casdoor/controllers/account.go b/casdoor/controllers/account.go new file mode 100644 index 0000000..6d4c97c --- /dev/null +++ b/casdoor/controllers/account.go @@ -0,0 +1,277 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/original" + "github.com/casdoor/casdoor/util" +) + +const ( + ResponseTypeLogin = "login" + ResponseTypeCode = "code" +) + +type RequestForm struct { + Type string `json:"type"` + + Organization string `json:"organization"` + Username string `json:"username"` + Password string `json:"password"` + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + Affiliation string `json:"affiliation"` + + Application string `json:"application"` + Provider string `json:"provider"` + Code string `json:"code"` + State string `json:"state"` + RedirectUri string `json:"redirectUri"` + Method string `json:"method"` + + EmailCode string `json:"emailCode"` + PhoneCode string `json:"phoneCode"` + PhonePrefix string `json:"phonePrefix"` + + AutoSignin bool `json:"autoSignin"` +} + +type Response struct { + Status string `json:"status"` + Msg string `json:"msg"` + Data interface{} `json:"data"` + Data2 interface{} `json:"data2"` +} + +type HumanCheck struct { + Type string `json:"type"` + AppKey string `json:"appKey"` + Scene string `json:"scene"` + CaptchaId string `json:"captchaId"` + CaptchaImage interface{} `json:"captchaImage"` +} + +// @Title Signup +// @Description sign up a new user +// @Param username formData string true "The username to sign up" +// @Param password formData string true "The password" +// @Success 200 {object} controllers.Response The Response object +// @router /signup [post] +func (c *ApiController) Signup() { + var resp Response + + if c.GetSessionUsername() != "" { + c.ResponseErrorWithData("Please sign out first before signing up", c.GetSessionUsername()) + return + } + + var form RequestForm + err := json.Unmarshal(c.Ctx.Input.RequestBody, &form) + if err != nil { + panic(err) + } + + application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) + if !application.EnableSignUp { + c.ResponseError("The application does not allow to sign up new account") + return + } + + if application.IsSignupItemEnabled("Email") { + checkResult := object.CheckVerificationCode(form.Email, form.EmailCode) + if len(checkResult) != 0 { + c.ResponseError(fmt.Sprintf("Email%s", checkResult)) + return + } + } + + var checkPhone string + if application.IsSignupItemEnabled("Phone") { + checkPhone = fmt.Sprintf("+%s%s", form.PhonePrefix, form.Phone) + checkResult := object.CheckVerificationCode(checkPhone, form.PhoneCode) + if len(checkResult) != 0 { + c.ResponseError(fmt.Sprintf("Phone%s", checkResult)) + return + } + } + + userId := fmt.Sprintf("%s/%s", form.Organization, form.Username) + + organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", form.Organization)) + msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.Email, form.Phone, form.Affiliation) + if msg != "" { + c.ResponseError(msg) + return + } + + id := util.GenerateId() + if application.GetSignupItemRule("ID") == "Incremental" { + lastUser := object.GetLastUser(form.Organization) + lastIdInt := util.ParseInt(lastUser.Id) + id = strconv.Itoa(lastIdInt + 1) + } + + username := form.Username + if !application.IsSignupItemVisible("Username") { + username = id + } + + user := &object.User{ + Owner: form.Organization, + Name: username, + CreatedTime: util.GetCurrentTime(), + Id: id, + Type: "normal-user", + Password: form.Password, + DisplayName: form.Name, + Avatar: organization.DefaultAvatar, + Email: form.Email, + Phone: form.Phone, + Address: []string{}, + Affiliation: form.Affiliation, + IsAdmin: false, + IsGlobalAdmin: false, + IsForbidden: false, + SignupApplication: application.Name, + Properties: map[string]string{}, + } + + affected := object.AddUser(user) + if affected { + original.AddUserToOriginalDatabase(user) + } + + if application.HasPromptPage() { + // The prompt page needs the user to be signed in + c.SetSessionUsername(user.GetId()) + } + + object.DisableVerificationCode(form.Email) + object.DisableVerificationCode(checkPhone) + + util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId) + + resp = Response{Status: "ok", Msg: "", Data: userId} + c.Data["json"] = resp + c.ServeJSON() +} + +// @Title Logout +// @Description logout the current user +// @Success 200 {object} controllers.Response The Response object +// @router /logout [post] +func (c *ApiController) Logout() { + var resp Response + + user := c.GetSessionUsername() + util.LogInfo(c.Ctx, "API: [%s] logged out", user) + + c.SetSessionUsername("") + c.SetSessionData(nil) + + resp = Response{Status: "ok", Msg: "", Data: user} + + c.Data["json"] = resp + c.ServeJSON() +} + +// @Title GetAccount +// @Description get the details of the current account +// @Success 200 {object} controllers.Response The Response object +// @router /get-account [get] +func (c *ApiController) GetAccount() { + userId, ok := c.RequireSignedIn() + if !ok { + return + } + + var resp Response + + user := object.GetUser(userId) + if user == nil { + resp := Response{Status: "error", Msg: fmt.Sprintf("The user: %s doesn't exist", userId)} + c.Data["json"] = resp + c.ServeJSON() + return + } + + organization := object.GetOrganizationByUser(user) + resp = Response{Status: "ok", Msg: "", Data: user, Data2: organization} + + c.Data["json"] = resp + c.ServeJSON() +} + +// @Title UploadAvatar +// @Description upload avatar +// @Param avatarfile formData string true "The base64 encode of avatarfile" +// @Param password formData string true "The password" +// @Success 200 {object} controllers.Response The Response object +// @router /upload-avatar [post] +func (c *ApiController) UploadAvatar() { + userId, ok := c.RequireSignedIn() + if !ok { + return + } + + var resp Response + + user := object.GetUser(userId) + + avatarBase64 := c.Ctx.Request.Form.Get("avatarfile") + index := strings.Index(avatarBase64, ",") + if index < 0 || avatarBase64[0:index] != "data:image/png;base64" { + resp = Response{Status: "error", Msg: "File encoding error"} + c.Data["json"] = resp + c.ServeJSON() + return + } + + dist, _ := base64.StdEncoding.DecodeString(avatarBase64[index+1:]) + msg := object.UploadAvatar(user.GetId(), dist) + if msg != "" { + resp = Response{Status: "error", Msg: msg} + c.Data["json"] = resp + c.ServeJSON() + return + } + user.Avatar = fmt.Sprintf("%s%s.png?time=%s", object.GetAvatarPath(), user.GetId(), util.GetCurrentUnixTime()) + object.UpdateUser(user.GetId(), user) + resp = Response{Status: "ok", Msg: ""} + c.Data["json"] = resp + c.ServeJSON() +} + +func (c *ApiController) GetHumanCheck() { + c.Data["json"] = HumanCheck{Type: "none"} + + provider := object.GetDefaultHumanCheckProvider() + if provider == nil { + id, img := object.GetCaptcha() + c.Data["json"] = HumanCheck{Type: "captcha", CaptchaId: id, CaptchaImage: img} + c.ServeJSON() + return + } + + c.ServeJSON() +} diff --git a/casdoor/controllers/application.go b/casdoor/controllers/application.go new file mode 100644 index 0000000..36bb394 --- /dev/null +++ b/casdoor/controllers/application.go @@ -0,0 +1,113 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/casdoor/casdoor/object" +) + +// @Title GetApplications +// @Description get all applications +// @Param owner query string true "The owner of applications." +// @Success 200 {array} object.Application The Response object +// @router /get-applications [get] +func (c *ApiController) GetApplications() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetApplications(owner) + c.ServeJSON() +} + +// @Title GetApplication +// @Description get the detail of an application +// @Param id query string true "The id of the application." +// @Success 200 {object} object.Application The Response object +// @router /get-application [get] +func (c *ApiController) GetApplication() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetApplication(id) + c.ServeJSON() +} + +// @Title GetUserApplication +// @Description get the detail of the user's application +// @Param id query string true "The id of the user" +// @Success 200 {object} object.Application The Response object +// @router /get-user-application [get] +func (c *ApiController) GetUserApplication() { + id := c.Input().Get("id") + user := object.GetUser(id) + if user == nil { + c.ResponseError("No such user.") + return + } + + c.Data["json"] = object.GetApplicationByUser(user) + c.ServeJSON() +} + +// @Title UpdateApplication +// @Description update an application +// @Param id query string true "The id of the application" +// @Param body body object.Application true "The details of the application" +// @Success 200 {object} controllers.Response The Response object +// @router /update-application [post] +func (c *ApiController) UpdateApplication() { + id := c.Input().Get("id") + + var application object.Application + err := json.Unmarshal(c.Ctx.Input.RequestBody, &application) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application)) + c.ServeJSON() +} + +// @Title AddApplication +// @Description add an application +// @Param body body object.Application true "The details of the application" +// @Success 200 {object} controllers.Response The Response object +// @router /add-application [post] +func (c *ApiController) AddApplication() { + var application object.Application + err := json.Unmarshal(c.Ctx.Input.RequestBody, &application) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.AddApplication(&application)) + c.ServeJSON() +} + +// @Title DeleteApplication +// @Description delete an application +// @Param body body object.Application true "The details of the application" +// @Success 200 {object} controllers.Response The Response object +// @router /delete-application [post] +func (c *ApiController) DeleteApplication() { + var application object.Application + err := json.Unmarshal(c.Ctx.Input.RequestBody, &application) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.DeleteApplication(&application)) + c.ServeJSON() +} diff --git a/casdoor/controllers/auth.go b/casdoor/controllers/auth.go new file mode 100644 index 0000000..ac2fe9a --- /dev/null +++ b/casdoor/controllers/auth.go @@ -0,0 +1,370 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/astaxie/beego" + "github.com/casdoor/casdoor/idp" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +func codeToResponse(code *object.Code) *Response { + if code.Code == "" { + return &Response{Status: "error", Msg: code.Message, Data: code.Code} + } else { + return &Response{Status: "ok", Msg: "", Data: code.Code} + } +} + +func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) *Response { + userId := user.GetId() + resp := &Response{} + if form.Type == ResponseTypeLogin { + c.SetSessionUsername(userId) + util.LogInfo(c.Ctx, "API: [%s] signed in", userId) + resp = &Response{Status: "ok", Msg: "", Data: userId} + } else if form.Type == ResponseTypeCode { + clientId := c.Input().Get("clientId") + responseType := c.Input().Get("responseType") + redirectUri := c.Input().Get("redirectUri") + scope := c.Input().Get("scope") + state := c.Input().Get("state") + + code := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state) + resp = codeToResponse(code) + + if application.HasPromptPage() { + // The prompt page needs the user to be signed in + c.SetSessionUsername(userId) + } + } else { + resp = &Response{Status: "error", Msg: fmt.Sprintf("Unknown response type: %s", form.Type)} + } + + // if user did not check auto signin + if resp.Status == "ok" && !form.AutoSignin { + timestamp := time.Now().Unix() + timestamp += 3600 * 24 + c.SetSessionData(&SessionData{ + ExpireTime: timestamp, + }) + } + + return resp +} + +// @Title GetApplicationLogin +// @Description get application login +// @Param clientId query string true "client id" +// @Param responseType query string true "response type" +// @Param redirectUri query string true "redirect uri" +// @Param scope query string true "scope" +// @Param state query string true "state" +// @Success 200 {object} controllers.api_controller.Response The Response object +// @router /update-application [get] +func (c *ApiController) GetApplicationLogin() { + var resp Response + + clientId := c.Input().Get("clientId") + responseType := c.Input().Get("responseType") + redirectUri := c.Input().Get("redirectUri") + scope := c.Input().Get("scope") + state := c.Input().Get("state") + + msg, application := object.CheckOAuthLogin(clientId, responseType, redirectUri, scope, state) + if msg != "" { + resp = Response{Status: "error", Msg: msg, Data: application} + } else { + resp = Response{Status: "ok", Msg: "", Data: application} + } + c.Data["json"] = resp + c.ServeJSON() +} + +// @Title Login +// @Description login +// @Param oAuthParams query string true "oAuth parameters" +// @Param body body RequestForm true "Login information" +// @Success 200 {object} controllers.api_controller.Response The Response object +// @router /login [post] +func (c *ApiController) Login() { + resp := &Response{Status: "null", Msg: ""} + var form RequestForm + err := json.Unmarshal(c.Ctx.Input.RequestBody, &form) + if err != nil { + resp = &Response{Status: "error", Msg: err.Error()} + c.Data["json"] = resp + c.ServeJSON() + return + } + + if form.Username != "" { + if form.Type == ResponseTypeLogin { + if c.GetSessionUsername() != "" { + resp = &Response{Status: "error", Msg: "Please log out first before signing in", Data: c.GetSessionUsername()} + c.Data["json"] = resp + c.ServeJSON() + return + } + } + + var user *object.User + var msg string + + if form.Password == "" { + var verificationCodeType string + + // check result through Email or Phone + if strings.Contains(form.Email, "@") { + verificationCodeType = "email" + checkResult := object.CheckVerificationCode(form.Email, form.EmailCode) + if len(checkResult) != 0 { + responseText := fmt.Sprintf("Email%s", checkResult) + c.ResponseError(responseText) + return + } + } else { + verificationCodeType = "phone" + checkPhone := fmt.Sprintf("+%s%s", form.PhonePrefix, form.Email) + checkResult := object.CheckVerificationCode(checkPhone, form.EmailCode) + if len(checkResult) != 0 { + responseText := fmt.Sprintf("Phone%s", checkResult) + c.ResponseError(responseText) + return + } + } + + // get user + var userId string + if form.Username == "" { + userId, _ = c.RequireSignedIn() + } else { + userId = fmt.Sprintf("%s/%s", form.Organization, form.Username) + } + + user = object.GetUser(userId) + if user == nil { + c.ResponseError("No such user.") + return + } + + // disable the verification code + switch verificationCodeType { + case "email": + if user.Email != form.Email { + c.ResponseError("wrong email!") + } + object.DisableVerificationCode(form.Email) + break + case "phone": + if user.Phone != form.Email { + c.ResponseError("wrong phone!") + } + object.DisableVerificationCode(form.Email) + break + } + } else { + password := form.Password + user, msg = object.CheckUserLogin(form.Organization, form.Username, password) + } + + if msg != "" { + resp = &Response{Status: "error", Msg: msg, Data: ""} + } else { + application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) + resp = c.HandleLoggedIn(application, user, &form) + + record := util.Records(c.Ctx) + record.Organization = application.Organization + record.Username = user.Name + + object.AddRecord(record) + } + } else if form.Provider != "" { + application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) + organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", application.Organization)) + provider := object.GetProvider(fmt.Sprintf("admin/%s", form.Provider)) + providerItem := application.GetProviderItem(provider.Name) + + idProvider := idp.GetIdProvider(provider.Type, provider.ClientId, provider.ClientSecret, form.RedirectUri) + if idProvider == nil { + resp = &Response{Status: "error", Msg: fmt.Sprintf("provider: %s does not exist", provider.Type)} + c.Data["json"] = resp + c.ServeJSON() + return + } + + idProvider.SetHttpClient(httpClient) + + if form.State != beego.AppConfig.String("authState") && form.State != application.Name { + resp = &Response{Status: "error", Msg: fmt.Sprintf("state expected: \"%s\", but got: \"%s\"", beego.AppConfig.String("authState"), form.State)} + c.Data["json"] = resp + c.ServeJSON() + return + } + + // https://github.com/golang/oauth2/issues/123#issuecomment-103715338 + token, err := idProvider.GetToken(form.Code) + if err != nil { + resp = &Response{Status: "error", Msg: err.Error()} + c.Data["json"] = resp + c.ServeJSON() + return + } + + if !token.Valid() { + resp = &Response{Status: "error", Msg: "Invalid token"} + c.Data["json"] = resp + c.ServeJSON() + return + } + + userInfo, err := idProvider.GetUserInfo(token) + if err != nil { + resp = &Response{Status: "error", Msg: fmt.Sprintf("Failed to login in: %s", err.Error())} + c.Data["json"] = resp + c.ServeJSON() + return + } + + if form.Method == "signup" { + user := object.GetUserByField(application.Organization, provider.Type, userInfo.Id) + if user == nil { + user = object.GetUserByField(application.Organization, provider.Type, userInfo.Username) + } + if user == nil { + user = object.GetUserByField(application.Organization, "name", userInfo.Username) + } + + if user != nil { + // Sign in via OAuth + + //if object.IsForbidden(userId) { + // c.forbiddenAccountResp(userId) + // return + //} + + //if len(object.GetMemberAvatar(userId)) == 0 { + // avatar := UploadAvatarToOSS(res.Avatar, userId) + // object.LinkMemberAccount(userId, "avatar", avatar) + //} + + resp = c.HandleLoggedIn(application, user, &form) + + record := util.Records(c.Ctx) + record.Organization = application.Organization + record.Username = user.Name + + object.AddRecord(record) + } else { + // Sign up via OAuth + if !application.EnableSignUp { + resp = &Response{Status: "error", Msg: fmt.Sprintf("The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support", provider.Type, userInfo.Username, userInfo.DisplayName)} + c.Data["json"] = resp + c.ServeJSON() + return + } + + if !providerItem.CanSignUp { + resp = &Response{Status: "error", Msg: fmt.Sprintf("The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up", provider.Type, userInfo.Username, userInfo.DisplayName, provider.Type)} + c.Data["json"] = resp + c.ServeJSON() + return + } + + properties := map[string]string{} + properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2) + user := &object.User{ + Owner: application.Organization, + Name: userInfo.Username, + CreatedTime: util.GetCurrentTime(), + Id: util.GenerateId(), + Type: "normal-user", + DisplayName: userInfo.DisplayName, + Avatar: userInfo.AvatarUrl, + Email: userInfo.Email, + Score: 200, + IsAdmin: false, + IsGlobalAdmin: false, + IsForbidden: false, + SignupApplication: application.Name, + Properties: properties, + } + object.AddUser(user) + + // sync info from 3rd-party if possible + object.SetUserOAuthProperties(organization, user, provider.Type, userInfo) + + object.LinkUserAccount(user, provider.Type, userInfo.Id) + + resp = c.HandleLoggedIn(application, user, &form) + + record := util.Records(c.Ctx) + record.Organization = application.Organization + record.Username = user.Name + + object.AddRecord(record) + } + //resp = &Response{Status: "ok", Msg: "", Data: res} + } else { // form.Method != "signup" + userId := c.GetSessionUsername() + if userId == "" { + resp = &Response{Status: "error", Msg: "The account does not exist", Data: userInfo} + c.Data["json"] = resp + c.ServeJSON() + return + } + + oldUser := object.GetUserByField(application.Organization, provider.Type, userInfo.Id) + if oldUser == nil { + oldUser = object.GetUserByField(application.Organization, provider.Type, userInfo.Username) + } + if oldUser != nil { + resp = &Response{Status: "error", Msg: fmt.Sprintf("The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)", provider.Type, userInfo.Username, userInfo.DisplayName, oldUser.Name, oldUser.DisplayName)} + c.Data["json"] = resp + c.ServeJSON() + return + } + + user := object.GetUser(userId) + + // sync info from 3rd-party if possible + object.SetUserOAuthProperties(organization, user, provider.Type, userInfo) + + isLinked := object.LinkUserAccount(user, provider.Type, userInfo.Id) + if isLinked { + resp = &Response{Status: "ok", Msg: "", Data: isLinked} + } else { + resp = &Response{Status: "error", Msg: "Failed to link user account", Data: isLinked} + } + //if len(object.GetMemberAvatar(userId)) == 0 { + // avatar := UploadAvatarToOSS(tempUserAccount.AvatarUrl, userId) + // object.LinkUserAccount(userId, "avatar", avatar) + //} + } + } else { + panic("unknown authentication type (not password or provider), form = " + util.StructToJson(form)) + } + + c.Data["json"] = resp + c.ServeJSON() +} diff --git a/casdoor/controllers/base.go b/casdoor/controllers/base.go new file mode 100644 index 0000000..4824a20 --- /dev/null +++ b/casdoor/controllers/base.go @@ -0,0 +1,85 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "time" + + "github.com/astaxie/beego" + "github.com/casdoor/casdoor/util" +) + +type ApiController struct { + beego.Controller +} + +type SessionData struct { + ExpireTime int64 +} + +func (c *ApiController) GetSessionUsername() string { + // check if user session expired + sessionData := c.GetSessionData() + if sessionData != nil && + sessionData.ExpireTime != 0 && + sessionData.ExpireTime < time.Now().Unix() { + c.SetSessionUsername("") + c.SetSessionData(nil) + return "" + } + + user := c.GetSession("username") + if user == nil { + return "" + } + + return user.(string) +} + +func (c *ApiController) SetSessionUsername(user string) { + c.SetSession("username", user) +} + +func (c *ApiController) GetSessionData() *SessionData { + session := c.GetSession("SessionData") + if session == nil { + return nil + } + + sessionData := &SessionData{} + err := util.JsonToStruct(session.(string), sessionData) + if err != nil { + panic(err) + } + + return sessionData +} + +func (c *ApiController) SetSessionData(s *SessionData) { + if s == nil { + c.DelSession("SessionData") + return + } + + c.SetSession("SessionData", util.StructToJson(s)) +} + +func wrapActionResponse(affected bool) *Response { + if affected { + return &Response{Status: "ok", Msg: "", Data: "Affected"} + } else { + return &Response{Status: "ok", Msg: "", Data: "Unaffected"} + } +} diff --git a/casdoor/controllers/ldap.go b/casdoor/controllers/ldap.go new file mode 100644 index 0000000..334e3ca --- /dev/null +++ b/casdoor/controllers/ldap.go @@ -0,0 +1,209 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +type LdapServer struct { + Host string `json:"host"` + Port int `json:"port"` + Admin string `json:"admin"` + Passwd string `json:"passwd"` + BaseDn string `json:"baseDn"` +} + +type LdapResp struct { + //Groups []LdapRespGroup `json:"groups"` + Users []object.LdapRespUser `json:"users"` +} + +//type LdapRespGroup struct { +// GroupId string +// GroupName string +//} + +type LdapSyncResp struct { + Exist []object.LdapRespUser `json:"exist"` + Failed []object.LdapRespUser `json:"failed"` +} + +func (c *ApiController) GetLdapUser() { + ldapServer := LdapServer{} + err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldapServer) + if err != nil || util.IsStrsEmpty(ldapServer.Host, ldapServer.Admin, ldapServer.Passwd, ldapServer.BaseDn) { + c.ResponseError("Missing parameter") + return + } + + var resp LdapResp + + conn, err := object.GetLdapConn(ldapServer.Host, ldapServer.Port, ldapServer.Admin, ldapServer.Passwd) + if err != nil { + c.Data["json"] = Response{Status: "error", Msg: err.Error()} + c.ServeJSON() + return + } + + //groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn) + //if err != nil { + // c.Data["json"] = Response{Status: "error", Msg: err.Error()} + // c.ServeJSON() + // return + //} + + //for _, group := range groupsMap { + // resp.Groups = append(resp.Groups, LdapRespGroup{ + // GroupId: group.GidNumber, + // GroupName: group.Cn, + // }) + //} + + users, err := conn.GetLdapUsers(ldapServer.BaseDn) + if err != nil { + c.Data["json"] = Response{Status: "error", Msg: err.Error()} + c.ServeJSON() + return + } + + for _, user := range users { + resp.Users = append(resp.Users, object.LdapRespUser{ + UidNumber: user.UidNumber, + Uid: user.Uid, + Cn: user.Cn, + GroupId: user.GidNumber, + //GroupName: groupsMap[user.GidNumber].Cn, + Uuid: user.Uuid, + Email: util.GetMaxLenStr(user.Mail, user.Email, user.EmailAddress), + Phone: util.GetMaxLenStr(user.TelephoneNumber, user.Mobile, user.MobileTelephoneNumber), + Address: util.GetMaxLenStr(user.RegisteredAddress, user.PostalAddress), + }) + } + + c.Data["json"] = Response{Status: "ok", Data: resp} + c.ServeJSON() + return +} + +func (c *ApiController) GetLdaps() { + owner := c.Input().Get("owner") + + c.Data["json"] = Response{Status: "ok", Data: object.GetLdaps(owner)} + c.ServeJSON() +} + +func (c *ApiController) GetLdap() { + id := c.Input().Get("id") + + if util.IsStrsEmpty(id) { + c.ResponseError("Missing parameter") + return + } + + c.Data["json"] = Response{Status: "ok", Data: object.GetLdap(id)} + c.ServeJSON() +} + +func (c *ApiController) AddLdap() { + var ldap object.Ldap + err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap) + if err != nil { + c.ResponseError("Missing parameter") + return + } + + if util.IsStrsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) { + c.ResponseError("Missing parameter") + return + } + + if object.CheckLdapExist(&ldap) { + c.ResponseError("Ldap server exist") + return + } + + affected := object.AddLdap(&ldap) + resp := wrapActionResponse(affected) + if affected { + resp.Data2 = ldap + } + + c.Data["json"] = resp + c.ServeJSON() +} + +func (c *ApiController) UpdateLdap() { + var ldap object.Ldap + err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap) + if err != nil || util.IsStrsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) { + c.ResponseError("Missing parameter") + return + } + + affected := object.UpdateLdap(&ldap) + resp := wrapActionResponse(affected) + if affected { + resp.Data2 = ldap + } + + c.Data["json"] = resp + c.ServeJSON() +} + +func (c *ApiController) DeleteLdap() { + var ldap object.Ldap + err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.DeleteLdap(&ldap)) + c.ServeJSON() +} + +func (c *ApiController) SyncLdapUsers() { + owner := c.Input().Get("owner") + ldapId := c.Input().Get("ldapId") + var users []object.LdapRespUser + err := json.Unmarshal(c.Ctx.Input.RequestBody, &users) + if err != nil { + panic(err) + } + + object.UpdateLdapSyncTime(ldapId) + + exist, failed := object.SyncLdapUsers(owner, users) + c.Data["json"] = &Response{Status: "ok", Data: &LdapSyncResp{ + Exist: *exist, + Failed: *failed, + }} + c.ServeJSON() +} + +func (c *ApiController) CheckLdapUsersExist() { + owner := c.Input().Get("owner") + var uuids []string + err := json.Unmarshal(c.Ctx.Input.RequestBody, &uuids) + if err != nil { + panic(err) + } + + exist := object.CheckLdapUuidExist(owner, uuids) + c.Data["json"] = &Response{Status: "ok", Data: exist} + c.ServeJSON() +} diff --git a/casdoor/controllers/link.go b/casdoor/controllers/link.go new file mode 100644 index 0000000..6f2654c --- /dev/null +++ b/casdoor/controllers/link.go @@ -0,0 +1,58 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/casdoor/casdoor/object" +) + +type LinkForm struct { + ProviderType string `json:"providerType"` +} + +func (c *ApiController) Unlink() { + userId, ok := c.RequireSignedIn() + if !ok { + return + } + + var resp Response + + var form LinkForm + err := json.Unmarshal(c.Ctx.Input.RequestBody, &form) + if err != nil { + panic(err) + } + providerType := form.ProviderType + + user := object.GetUser(userId) + value := object.GetUserField(user, providerType) + + if value == "" { + resp = Response{Status: "error", Msg: "Please link first", Data: value} + c.Data["json"] = resp + c.ServeJSON() + return + } + + object.ClearUserOAuthProperties(user, providerType) + + object.LinkUserAccount(user, providerType, "") + resp = Response{Status: "ok", Msg: ""} + c.Data["json"] = resp + c.ServeJSON() +} diff --git a/casdoor/controllers/organization.go b/casdoor/controllers/organization.go new file mode 100644 index 0000000..abaee89 --- /dev/null +++ b/casdoor/controllers/organization.go @@ -0,0 +1,96 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/casdoor/casdoor/object" +) + +// @Title GetOrganizations +// @Description get organizations +// @Param owner query string true "owner" +// @Success 200 {array} object.Organization The Response object +// @router /get-organizations [get] +func (c *ApiController) GetOrganizations() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetOrganizations(owner) + c.ServeJSON() +} + +// @Title GetOrganization +// @Description get organization +// @Param id query string true "organization id" +// @Success 200 {object} object.Organization The Response object +// @router /get-organization [get] +func (c *ApiController) GetOrganization() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetOrganization(id) + c.ServeJSON() +} + +// @Title UpdateOrganization +// @Description update organization +// @Param id query string true "The id of the organization" +// @Param body body object.Organization true "The details of the organization" +// @Success 200 {object} controllers.Response The Response object +// @router /update-organization [post] +func (c *ApiController) UpdateOrganization() { + id := c.Input().Get("id") + + var organization object.Organization + err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization)) + c.ServeJSON() +} + +// @Title AddOrganization +// @Description add organization +// @Param body body object.Organization true "The details of the organization" +// @Success 200 {object} controllers.Response The Response object +// @router /add-organization [post] +func (c *ApiController) AddOrganization() { + var organization object.Organization + err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization)) + c.ServeJSON() +} + +// @Title DeleteOrganization +// @Description delete organization +// @Param body body object.Organization true "The details of the organization" +// @Success 200 {object} controllers.Response The Response object +// @router /delete-organization [post] +func (c *ApiController) DeleteOrganization() { + var organization object.Organization + err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.DeleteOrganization(&organization)) + c.ServeJSON() +} diff --git a/casdoor/controllers/provider.go b/casdoor/controllers/provider.go new file mode 100644 index 0000000..2acb4e4 --- /dev/null +++ b/casdoor/controllers/provider.go @@ -0,0 +1,96 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/casdoor/casdoor/object" +) + +// @Title GetProviders +// @Description get providers +// @Param owner query string true "The owner of providers" +// @Success 200 {array} object.Provider The Response object +// @router /get-providers [get] +func (c *ApiController) GetProviders() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetProviders(owner) + c.ServeJSON() +} + +// @Title GetProvider +// @Description get provider +// @Param id query string true "The id of the provider" +// @Success 200 {object} object.Provider The Response object +// @router /get-provider [get] +func (c *ApiController) GetProvider() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetProvider(id) + c.ServeJSON() +} + +// @Title UpdateProvider +// @Description update provider +// @Param id query string true "The id of the provider" +// @Param body body object.Provider true "The details of the provider" +// @Success 200 {object} controllers.Response The Response object +// @router /update-provider [post] +func (c *ApiController) UpdateProvider() { + id := c.Input().Get("id") + + var provider object.Provider + err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.UpdateProvider(id, &provider)) + c.ServeJSON() +} + +// @Title AddProvider +// @Description add provider +// @Param body body object.Provider true "The details of the provider" +// @Success 200 {object} controllers.Response The Response object +// @router /add-provider [post] +func (c *ApiController) AddProvider() { + var provider object.Provider + err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.AddProvider(&provider)) + c.ServeJSON() +} + +// @Title DeleteProvider +// @Description delete provider +// @Param body body object.Provider true "The details of the provider" +// @Success 200 {object} controllers.Response The Response object +// @router /delete-provider [post] +func (c *ApiController) DeleteProvider() { + var provider object.Provider + err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.DeleteProvider(&provider)) + c.ServeJSON() +} diff --git a/casdoor/controllers/record.go b/casdoor/controllers/record.go new file mode 100644 index 0000000..177bde1 --- /dev/null +++ b/casdoor/controllers/record.go @@ -0,0 +1,46 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/casdoor/casdoor/object" +) + +// @Title GetRecords +// @Description get all records +// @Success 200 {array} object.Records The Response object +// @router /get-records [get] +func (c *ApiController) GetRecords() { + c.Data["json"] = object.GetRecords() + c.ServeJSON() +} + +// @Title GetRecordsByFilter +// @Description get records by filter +// @Param body body object.Records true "filter Record message" +// @Success 200 {array} object.Records The Response object +// @router /get-records-filter [post] +func (c *ApiController) GetRecordsByFilter() { + var record object.Records + err := json.Unmarshal(c.Ctx.Input.RequestBody, &record) + if err != nil { + panic(err) + } + + c.Data["json"] = object.GetRecordsByField(&record) + c.ServeJSON() +} diff --git a/casdoor/controllers/token.go b/casdoor/controllers/token.go new file mode 100644 index 0000000..45110fe --- /dev/null +++ b/casdoor/controllers/token.go @@ -0,0 +1,114 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/casdoor/casdoor/object" +) + +// @Title GetTokens +// @Description get tokens +// @Param owner query string true "The owner of tokens" +// @Success 200 {array} object.Token The Response object +// @router /get-tokens [get] +func (c *ApiController) GetTokens() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetTokens(owner) + c.ServeJSON() +} + +// @Title GetToken +// @Description get token +// @Param id query string true "The id of token" +// @Success 200 {object} object.Token The Response object +// @router /get-token [get] +func (c *ApiController) GetToken() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetToken(id) + c.ServeJSON() +} + +// @Title UpdateToken +// @Description update token +// @Param id query string true "The id of token" +// @Param body body object.Token true "Details of the token" +// @Success 200 {object} controllers.Response The Response object +// @router /update-token [post] +func (c *ApiController) UpdateToken() { + id := c.Input().Get("id") + + var token object.Token + err := json.Unmarshal(c.Ctx.Input.RequestBody, &token) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token)) + c.ServeJSON() +} + +// @Title AddToken +// @Description add token +// @Param body body object.Token true "Details of the token" +// @Success 200 {object} controllers.Response The Response object +// @router /add-token [post] +func (c *ApiController) AddToken() { + var token object.Token + err := json.Unmarshal(c.Ctx.Input.RequestBody, &token) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.AddToken(&token)) + c.ServeJSON() +} + +// @Title DeleteToken +// @Description delete token +// @Param body body object.Token true "Details of the token" +// @Success 200 {object} controllers.Response The Response object +// @router /delete-token [post] +func (c *ApiController) DeleteToken() { + var token object.Token + err := json.Unmarshal(c.Ctx.Input.RequestBody, &token) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.DeleteToken(&token)) + c.ServeJSON() +} + +// @Title GetOAuthToken +// @Description get oAuth token +// @Param grant_type query string true "oAuth grant type" +// @Param client_id query string true "oAuth client id" +// @Param client_secret query string true "oAuth client secret" +// @Param code query string true "oAuth code" +// @Success 200 {object} object.TokenWrapper The Response object +// @router /login/oauth/access_token [post] +func (c *ApiController) GetOAuthToken() { + grantType := c.Input().Get("grant_type") + clientId := c.Input().Get("client_id") + clientSecret := c.Input().Get("client_secret") + code := c.Input().Get("code") + + c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code) + c.ServeJSON() +} diff --git a/casdoor/controllers/user.go b/casdoor/controllers/user.go new file mode 100644 index 0000000..ed1f6a8 --- /dev/null +++ b/casdoor/controllers/user.go @@ -0,0 +1,232 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/original" +) + +// @Title GetGlobalUsers +// @Description get global users +// @Success 200 {array} object.User The Response object +// @router /get-global-users [get] +func (c *ApiController) GetGlobalUsers() { + c.Data["json"] = object.GetMaskedUsers(object.GetGlobalUsers()) + c.ServeJSON() +} + +// @Title GetUsers +// @Description +// @Param owner query string true "The owner of users" +// @Success 200 {array} object.User The Response object +// @router /get-users [get] +func (c *ApiController) GetUsers() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetMaskedUsers(object.GetUsers(owner)) + c.ServeJSON() +} + +// @Title GetUser +// @Description get user +// @Param id query string true "The id of the user" +// @Success 200 {object} object.User The Response object +// @router /get-user [get] +func (c *ApiController) GetUser() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetMaskedUser(object.GetUser(id)) + c.ServeJSON() +} + +// @Title UpdateUser +// @Description update user +// @Param id query string true "The id of the user" +// @Param body body object.User true "The details of the user" +// @Success 200 {object} controllers.Response The Response object +// @router /update-user [post] +func (c *ApiController) UpdateUser() { + id := c.Input().Get("id") + + var user object.User + err := json.Unmarshal(c.Ctx.Input.RequestBody, &user) + if err != nil { + panic(err) + } + + if user.DisplayName == "" { + c.ResponseError("Display name cannot be empty") + return + } + + affected := object.UpdateUser(id, &user) + if affected { + newUser := object.GetUser(user.GetId()) + original.UpdateUserToOriginalDatabase(newUser) + } + + c.Data["json"] = wrapActionResponse(affected) + c.ServeJSON() +} + +// @Title AddUser +// @Description add user +// @Param body body object.User true "The details of the user" +// @Success 200 {object} controllers.Response The Response object +// @router /add-user [post] +func (c *ApiController) AddUser() { + var user object.User + err := json.Unmarshal(c.Ctx.Input.RequestBody, &user) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.AddUser(&user)) + c.ServeJSON() +} + +// @Title DeleteUser +// @Description delete user +// @Param body body object.User true "The details of the user" +// @Success 200 {object} controllers.Response The Response object +// @router /delete-user [post] +func (c *ApiController) DeleteUser() { + var user object.User + err := json.Unmarshal(c.Ctx.Input.RequestBody, &user) + if err != nil { + panic(err) + } + + c.Data["json"] = wrapActionResponse(object.DeleteUser(&user)) + c.ServeJSON() +} + +// @Title GetEmailAndPhone +// @Description get email and phone by username +// @Param username formData string true "The username of the user" +// @Param organization formData string true "The organization of the user" +// @Success 200 {object} controllers.Response The Response object +// @router /get-email-and-phone [post] +func (c *ApiController) GetEmailAndPhone() { + var resp Response + + var form RequestForm + err := json.Unmarshal(c.Ctx.Input.RequestBody, &form) + if err != nil { + panic(err) + } + + user := object.GetUserByFields(form.Organization, form.Username) + if user == nil { + c.ResponseError("No such user.") + return + } + + respUser := object.User{Email: user.Email, Phone: user.Phone, Name: user.Name} + var contentType string + switch form.Username { + case user.Email: + contentType = "email" + case user.Phone: + contentType = "phone" + case user.Name: + contentType = "username" + } + + resp = Response{Status: "ok", Msg: "", Data: respUser, Data2: contentType} + + c.Data["json"] = resp + c.ServeJSON() +} + +// @Title SetPassword +// @Description set password +// @Param userOwner formData string true "The owner of the user" +// @Param userName formData string true "The name of the user" +// @Param oldPassword formData string true "The old password of the user" +// @Param newPassword formData string true "The new password of the user" +// @Success 200 {object} controllers.Response The Response object +// @router /set-password [post] +func (c *ApiController) SetPassword() { + userOwner := c.Ctx.Request.Form.Get("userOwner") + userName := c.Ctx.Request.Form.Get("userName") + oldPassword := c.Ctx.Request.Form.Get("oldPassword") + newPassword := c.Ctx.Request.Form.Get("newPassword") + + requestUserId := c.GetSessionUsername() + if requestUserId == "" { + c.ResponseError("Please login first.") + return + } + requestUser := object.GetUser(requestUserId) + if requestUser == nil { + c.ResponseError("Session outdated. Please login again.") + return + } + + userId := fmt.Sprintf("%s/%s", userOwner, userName) + targetUser := object.GetUser(userId) + if targetUser == nil { + c.ResponseError("Invalid user id.") + return + } + + hasPermission := false + + if requestUser.IsGlobalAdmin { + hasPermission = true + } else if requestUserId == userId { + hasPermission = true + } else if targetUser.Owner == requestUser.Owner && requestUser.IsAdmin { + hasPermission = true + } + + if !hasPermission { + c.ResponseError("You don't have the permission to do this.") + return + } + + if oldPassword != "" { + msg := object.CheckPassword(targetUser, oldPassword) + if msg != "" { + c.ResponseError(msg) + return + } + } else { + + } + + if strings.Index(newPassword, " ") >= 0 { + c.ResponseError("New password cannot contain blank space.") + return + } + + if len(newPassword) <= 5 { + c.ResponseError("New password must have at least 6 characters") + return + } + + c.SetSessionUsername("") + + targetUser.Password = newPassword + object.SetUserField(targetUser, "password", targetUser.Password) + c.Data["json"] = Response{Status: "ok"} + c.ServeJSON() +} diff --git a/casdoor/controllers/util.go b/casdoor/controllers/util.go new file mode 100644 index 0000000..ffbedb5 --- /dev/null +++ b/casdoor/controllers/util.go @@ -0,0 +1,71 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "net/http" + + "github.com/astaxie/beego" + "golang.org/x/net/proxy" +) + +var httpClient *http.Client + +func InitHttpClient() { + httpProxy := beego.AppConfig.String("httpProxy") + if httpProxy == "" { + httpClient = &http.Client{} + return + } + + // https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client + dialer, err := proxy.SOCKS5("tcp", httpProxy, nil, proxy.Direct) + if err != nil { + panic(err) + } + + tr := &http.Transport{Dial: dialer.Dial} + httpClient = &http.Client{ + Transport: tr, + } + + //resp, err2 := httpClient.Get("https://google.com") + //if err2 != nil { + // panic(err2) + //} + //defer resp.Body.Close() + //println("Response status: %s", resp.Status) +} + +func (c *ApiController) ResponseError(error string) { + c.Data["json"] = Response{Status: "error", Msg: error} + c.ServeJSON() +} + +func (c *ApiController) ResponseErrorWithData(error string, data interface{}) { + c.Data["json"] = Response{Status: "error", Msg: error, Data: data} + c.ServeJSON() +} + +func (c *ApiController) RequireSignedIn() (string, bool) { + userId := c.GetSessionUsername() + if userId == "" { + resp := Response{Status: "error", Msg: "Please sign in first"} + c.Data["json"] = resp + c.ServeJSON() + return "", false + } + return userId, true +} diff --git a/casdoor/controllers/verification.go b/casdoor/controllers/verification.go new file mode 100644 index 0000000..8d99f33 --- /dev/null +++ b/casdoor/controllers/verification.go @@ -0,0 +1,150 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "fmt" + "strings" + + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +func (c *ApiController) getCurrentUser() *object.User { + var user *object.User + userId := c.GetSessionUsername() + if userId == "" { + user = nil + } else { + user = object.GetUser(userId) + } + return user +} + +func (c *ApiController) SendVerificationCode() { + destType := c.Ctx.Request.Form.Get("type") + dest := c.Ctx.Request.Form.Get("dest") + orgId := c.Ctx.Request.Form.Get("organizationId") + checkType := c.Ctx.Request.Form.Get("checkType") + checkId := c.Ctx.Request.Form.Get("checkId") + checkKey := c.Ctx.Request.Form.Get("checkKey") + remoteAddr := c.Ctx.Request.RemoteAddr + remoteAddr = remoteAddr[:strings.LastIndex(remoteAddr, ":")] + + if len(destType) == 0 || len(dest) == 0 || len(orgId) == 0 || strings.Index(orgId, "/") < 0 || len(checkType) == 0 || len(checkId) == 0 || len(checkKey) == 0 { + c.ResponseError("Missing parameter.") + return + } + + isHuman := false + captchaProvider := object.GetDefaultHumanCheckProvider() + if captchaProvider == nil { + isHuman = object.VerifyCaptcha(checkId, checkKey) + } + + if !isHuman { + c.ResponseError("Turing test failed.") + return + } + + user := c.getCurrentUser() + organization := object.GetOrganization(orgId) + application := object.GetApplicationByOrganizationName(organization.Name) + + msg := "Invalid dest type." + switch destType { + case "email": + if !util.IsEmailValid(dest) { + c.ResponseError("Invalid Email address") + return + } + + provider := application.GetEmailProvider() + msg = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest) + case "phone": + if !util.IsPhoneCnValid(dest) { + c.ResponseError("Invalid phone number") + return + } + org := object.GetOrganization(orgId) + if org == nil { + c.ResponseError("Missing parameter.") + return + } + + dest = fmt.Sprintf("+%s%s", org.PhonePrefix, dest) + provider := application.GetSmsProvider() + msg = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, dest) + } + + status := "ok" + if msg != "" { + status = "error" + } + + c.Data["json"] = Response{Status: status, Msg: msg} + c.ServeJSON() +} + +func (c *ApiController) ResetEmailOrPhone() { + userId, ok := c.RequireSignedIn() + if !ok { + return + } + + user := object.GetUser(userId) + if user == nil { + c.ResponseError("No such user.") + return + } + + destType := c.Ctx.Request.Form.Get("type") + dest := c.Ctx.Request.Form.Get("dest") + code := c.Ctx.Request.Form.Get("code") + if len(dest) == 0 || len(code) == 0 || len(destType) == 0 { + c.ResponseError("Missing parameter.") + return + } + + checkDest := dest + if destType == "phone" { + org := object.GetOrganizationByUser(user) + phonePrefix := "86" + if org != nil && org.PhonePrefix != "" { + phonePrefix = org.PhonePrefix + } + checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest) + } + if ret := object.CheckVerificationCode(checkDest, code); len(ret) != 0 { + c.ResponseError(ret) + return + } + + switch destType { + case "email": + user.Email = dest + object.SetUserField(user, "email", user.Email) + case "phone": + user.Phone = dest + object.SetUserField(user, "phone", user.Phone) + default: + c.ResponseError("Unknown type.") + return + } + + object.DisableVerificationCode(checkDest) + c.Data["json"] = Response{Status: "ok"} + c.ServeJSON() +} diff --git a/casdoor/docker-compose.yml b/casdoor/docker-compose.yml new file mode 100644 index 0000000..63c1d98 --- /dev/null +++ b/casdoor/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.1' +services: + casdoor: + build: + context: ./ + dockerfile: go-dockerfile + ports: + - 8000:8000 + depends_on: + - db + db: + restart: always + image: mysql:8.0.25 + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: 123 + volumes: + - /usr/local/docker/mysql:/var/lib/mysql diff --git a/casdoor/go-dockerfile b/casdoor/go-dockerfile new file mode 100644 index 0000000..8cb65a4 --- /dev/null +++ b/casdoor/go-dockerfile @@ -0,0 +1,25 @@ +FROM golang:1.17-rc-buster +WORKDIR /casdoor +COPY ./ /casdoor +RUN go env -w CGO_ENABLED=0 GOPROXY=https://goproxy.io,direct GOOS=linux GOARCH=amd64 \ + && apt update && apt install sudo \ + && wget https://nodejs.org/dist/v12.22.0/node-v12.22.0-linux-x64.tar.gz \ + && sudo tar xf node-v12.22.0-linux-x64.tar.gz \ + && sudo apt install wait-for-it +ENV PATH=$PATH:/casdoor/node-v12.22.0-linux-x64/bin +RUN npm install -g yarn \ + && cd web \ + && yarn install \ + && yarn run build \ + && rm -rf node_modules \ + && cd /casdoor \ + && go build main.go +FROM alpine:3.7 +COPY --from=0 /casdoor / +COPY --from=0 /usr/bin/wait-for-it / +RUN set -eux \ + && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \ + && apk update \ + && apk upgrade \ + && apk add bash +CMD ./wait-for-it db:3306 && ./main diff --git a/casdoor/go.mod b/casdoor/go.mod new file mode 100644 index 0000000..3eaf865 --- /dev/null +++ b/casdoor/go.mod @@ -0,0 +1,34 @@ +module github.com/casdoor/casdoor + +go 1.15 + +require ( + github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible // indirect + github.com/astaxie/beego v1.12.3 + github.com/aws/aws-sdk-go v1.37.30 + github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect + github.com/casbin/casbin/v2 v2.30.1 + github.com/casbin/xorm-adapter/v2 v2.3.1 + github.com/casdoor/go-sms-sender v0.0.1 + github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df + github.com/go-ldap/ldap/v3 v3.3.0 + github.com/go-sql-driver/mysql v1.5.0 + github.com/google/uuid v1.2.0 + github.com/jinzhu/configor v1.2.1 // indirect + github.com/mileusna/crontab v1.0.1 + github.com/qiangmzsx/string-adapter/v2 v2.1.0 + github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 + github.com/satori/go.uuid v1.2.0 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect + github.com/thanhpk/randstr v1.0.4 + golang.org/x/net v0.0.0-20201110031124-69a78807bb2b + golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect + gopkg.in/ini.v1 v1.62.0 + xorm.io/core v0.7.2 + xorm.io/xorm v1.0.3 +) diff --git a/casdoor/go.sum b/casdoor/go.sum new file mode 100644 index 0000000..75dd984 --- /dev/null +++ b/casdoor/go.sum @@ -0,0 +1,314 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1075 h1:Z0SzZttfYI/raZ5O9WF3cezZJTSW4Yz4Kow9uWdyRwg= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1075/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= +github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible h1:Ft+KeWIJxFP76LqgJbvtOA1qBIoC8vGkTV3QeCOeJC4= +github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ= +github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA= +github.com/aws/aws-sdk-go v1.37.30 h1:fZeVg3QuTkWE/dEvPQbK6AL32+3G9ofJfGFSPS1XLH0= +github.com/aws/aws-sdk-go v1.37.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/casbin/casbin v1.7.0 h1:PuzlE8w0JBg/DhIqnkF1Dewf3z+qmUZMVN07PonvVUQ= +github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= +github.com/casbin/casbin/v2 v2.1.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/casbin/casbin/v2 v2.25.5/go.mod h1:wUgota0cQbTXE6Vd+KWpg41726jFRi7upxio0sR+Xd0= +github.com/casbin/casbin/v2 v2.30.1 h1:P5HWadDL7olwUXNdcuKUBk+x75Y2eitFxYTcLNKeKF0= +github.com/casbin/casbin/v2 v2.30.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/casbin/xorm-adapter/v2 v2.3.1 h1:RVGsM6KYFP9s4OQJXrP/gv56Wmt5P40mzvcyXgv5xeg= +github.com/casbin/xorm-adapter/v2 v2.3.1/go.mod h1:GZ+nlIdasVFunQ71SlvkL/HcQQBvFncphDf+2Yl167c= +github.com/casdoor/go-sms-sender v0.0.1 h1:n/r6fGgXsV+6uMxXvb0XLZnUCjmbUB1uSB817Ej0/gI= +github.com/casdoor/go-sms-sender v0.0.1/go.mod h1:rr4na8Zc+0vgPVY5JPB0LZkRVuj5AhNVhE1G7W8lDk8= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= +github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= +github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M= +github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY= +github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= +github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= +github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= +github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ= +github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= +github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mileusna/crontab v1.0.1 h1:YrDLc7l3xOiznmXq2FtAgg+1YQ3yC6pfFVPe+ywXNtg= +github.com/mileusna/crontab v1.0.1/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4U1ZwExmc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U= +github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/qiangmzsx/string-adapter/v2 v2.1.0 h1:q0y8TPa/sTwtriJPRe8gWL++PuZ+XbOUuvKU+hvtTYs= +github.com/qiangmzsx/string-adapter/v2 v2.1.0/go.mod h1:PElPB7b7HnGKTsuADAffFpOQXHqjEGJz1+U1a6yR5wA= +github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 h1:J2Xj92efYLxPl3BiibgEDEUiMsCBzwTurE/8JjD8CG4= +github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76/go.mod h1:JhtPzUhP5KGtCB2yksmxuYAD4hEWw4qGQJpucjsm3U0= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= +github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= +github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tencentcloud/tencentcloud-sdk-go v1.0.154 h1:THBgwGwUQtsw6L53cSSA2wwL3sLrm+HJ3Dk+ye/lMCI= +github.com/tencentcloud/tencentcloud-sdk-go v1.0.154/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI= +github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= +github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= +github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= +github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw= +google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI= +xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw= +xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= +xorm.io/xorm v1.0.3 h1:3dALAohvINu2mfEix5a5x5ZmSVGSljinoSGgvGbaZp0= +xorm.io/xorm v1.0.3/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4= diff --git a/casdoor/idp/dingtalk.go b/casdoor/idp/dingtalk.go new file mode 100644 index 0000000..c8aa08b --- /dev/null +++ b/casdoor/idp/dingtalk.go @@ -0,0 +1,364 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "golang.org/x/oauth2" +) + +// A total of three steps are required: +// +// 1. Construct the link and get the temporary authorization code +// tmp_auth_code through the code at the end of the url. +// +// 2. Use hmac256 to calculate the signature, and then submit it together with timestamp, +// tmp_auth_code, accessKey to obtain unionid, userid, accessKey. +// +// 3. Get detailed information through userid. + +type DingTalkIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewDingTalkIdProvider(clientId string, clientSecret string, redirectUrl string) *DingTalkIdProvider { + idp := &DingTalkIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *DingTalkIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *DingTalkIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + AuthURL: "https://oapi.dingtalk.com/sns/getuserinfo_bycode", + TokenURL: "https://oapi.dingtalk.com/gettoken", + } + + var config = &oauth2.Config{ + // DingTalk not allow to set scopes,here it is just a placeholder, + // convenient to use later + Scopes: []string{"", ""}, + + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type DingTalkAccessToken struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + AccessToken string `json:"access_token"` // Interface call credentials + ExpiresIn int64 `json:"expires_in"` // access_token interface call credential timeout time, unit (seconds) +} + +type DingTalkIds struct { + UserId string `json:"user_id"` + UnionId string `json:"union_id"` +} + +type InfoResp struct { + Errcode int `json:"errcode"` + UserInfo struct { + Nick string `json:"nick"` + Unionid string `json:"unionid"` + Openid string `json:"openid"` + MainOrgAuthHighLevel bool `json:"main_org_auth_high_level"` + } `json:"user_info"` + Errmsg string `json:"errmsg"` +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://developers.dingtalk.com/document/app/dingtalk-retrieve-user-information?spm=ding_open_doc.document.0.0.51b91a31wWV3tY#doc-api-dingtalk-GetUser +func (idp *DingTalkIdProvider) GetToken(code string) (*oauth2.Token, error) { + timestamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) + signature := EncodeSHA256(timestamp, idp.Config.ClientSecret) + u := fmt.Sprintf( + "%s?accessKey=%s×tamp=%s&signature=%s", idp.Config.Endpoint.AuthURL, + idp.Config.ClientID, timestamp, signature) + + tmpCode := struct { + TmpAuthCode string `json:"tmp_auth_code"` + }{code} + bs, _ := json.Marshal(tmpCode) + r := strings.NewReader(string(bs)) + resp, err := http.Post(u, "application/json;charset=UTF-8", r) + if err != nil { + return nil, err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + body, _ := ioutil.ReadAll(resp.Body) + info := InfoResp{} + _ = json.Unmarshal(body, &info) + errCode := info.Errcode + if errCode != 0 { + return nil, errors.New(fmt.Sprintf("%d: %s", errCode, info.Errmsg)) + } + + u2 := fmt.Sprintf("%s?appkey=%s&appsecret=%s", idp.Config.Endpoint.TokenURL, idp.Config.ClientID, idp.Config.ClientSecret) + resp, _ = http.Get(u2) + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + body, _ = ioutil.ReadAll(resp.Body) + tokenResp := DingTalkAccessToken{} + _ = json.Unmarshal(body, &tokenResp) + if tokenResp.ErrCode != 0 { + return nil, errors.New(fmt.Sprintf("%d: %s", tokenResp.ErrCode, tokenResp.ErrMsg)) + } + + // use unionid to get userid + unionid := info.UserInfo.Unionid + userid, err := idp.GetUseridByUnionid(tokenResp.AccessToken, unionid) + if err != nil { + return nil, err + } + + // Since DingTalk does not require scopes, put userid and unionid into + // idp.config.scopes to facilitate GetUserInfo() to obtain these two parameters. + idp.Config.Scopes = []string{unionid, userid} + + token := &oauth2.Token{ + AccessToken: tokenResp.AccessToken, + Expiry: time.Unix(time.Now().Unix()+tokenResp.ExpiresIn, 0), + } + + return token, nil +} + +type UnionIdResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + Result struct { + ContactType string `json:"contact_type"` + Userid string `json:"userid"` + } `json:"result"` + RequestId string `json:"request_id"` +} + +func (idp *DingTalkIdProvider) GetUseridByUnionid(accesstoken, unionid string) (userid string, err error) { + u := fmt.Sprintf("https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=%s&unionid=%s", + accesstoken, unionid) + useridInfo, err := idp.GetUrlResp(u) + if err != nil { + return "", err + } + + uresp := UnionIdResponse{} + _ = json.Unmarshal([]byte(useridInfo), &uresp) + errcode := uresp.Errcode + if errcode != 0 { + return "", errors.New(fmt.Sprintf("%d: %s", errcode, uresp.Errmsg)) + } + return uresp.Result.Userid, nil +} + +/* +{ + "errcode":0, + "result":{ + "boss":false, + "unionid":"5M6zgZBKQPCxdiPdANeJ6MgiEiE", + "role_list":[ + { + "group_name":"默认", + "name":"主管理员", + "id":2062489174 + } + ], + "exclusive_account":false, + "mobile":"15236176076", + "active":true, + "admin":true, + "avatar":"https://static-legacy.dingtalk.com/media/lALPDeRETW9WAnnNAyDNAyA_800_800.png", + "hide_mobile":false, + "userid":"manager4713", + "senior":false, + "dept_order_list":[ + { + "dept_id":1, + "order":176294576350761512 + } + ], + "real_authed":true, + "name":"刘继坤", + "dept_id_list":[ + 1 + ], + "state_code":"86", + "email":"", + "leader_in_dept":[ + { + "leader":false, + "dept_id":1 + } + ] + }, + "errmsg":"ok", + "request_id":"3sug9d2exsla" +} +*/ + +type DingTalkUserResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + Result struct { + Extension string `json:"extension"` + Unionid string `json:"unionid"` + Boss bool `json:"boss"` + UnionEmpExt struct { + CorpId string `json:"corpId"` + Userid string `json:"userid"` + UnionEmpMapList []struct { + CorpId string `json:"corpId"` + Userid string `json:"userid"` + } `json:"unionEmpMapList"` + } `json:"unionEmpExt"` + RoleList []struct { + GroupName string `json:"group_name"` + Id int `json:"id"` + Name string `json:"name"` + } `json:"role_list"` + Admin bool `json:"admin"` + Remark string `json:"remark"` + Title string `json:"title"` + HiredDate int64 `json:"hired_date"` + Userid string `json:"userid"` + WorkPlace string `json:"work_place"` + DeptOrderList []struct { + DeptId int `json:"dept_id"` + Order int64 `json:"order"` + } `json:"dept_order_list"` + RealAuthed bool `json:"real_authed"` + DeptIdList []int `json:"dept_id_list"` + JobNumber string `json:"job_number"` + Email string `json:"email"` + LeaderInDept []struct { + DeptId int `json:"dept_id"` + Leader bool `json:"leader"` + } `json:"leader_in_dept"` + ManagerUserid string `json:"manager_userid"` + Mobile string `json:"mobile"` + Active bool `json:"active"` + Telephone string `json:"telephone"` + Avatar string `json:"avatar"` + HideMobile bool `json:"hide_mobile"` + Senior bool `json:"senior"` + Name string `json:"name"` + StateCode string `json:"state_code"` + } `json:"result"` + RequestId string `json:"request_id"` +} + +// GetUserInfo Use userid and access_token to get UserInfo +// get more detail via: https://developers.dingtalk.com/document/app/query-user-details +func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var dtUserInfo DingTalkUserResponse + accessToken := token.AccessToken + + u := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/user/get?access_token=%s&userid=%s", + accessToken, idp.Config.Scopes[1]) + + userinfoResp, err := idp.GetUrlResp(u) + if err != nil { + return nil, err + } + + if err = json.Unmarshal([]byte(userinfoResp), &dtUserInfo); err != nil { + return nil, err + } + + userInfo := UserInfo{ + Id: strconv.Itoa(dtUserInfo.Result.RoleList[0].Id), + Username: dtUserInfo.Result.RoleList[0].Name, + DisplayName: dtUserInfo.Result.Name, + Email: dtUserInfo.Result.Email, + AvatarUrl: dtUserInfo.Result.Avatar, + } + + return &userInfo, nil +} + +func (idp *DingTalkIdProvider) GetUrlResp(url string) (string, error) { + resp, err := idp.Client.Get(url) + if err != nil { + return "", err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// EncodeSHA256 Use the HmacSHA256 algorithm to sign, the signature data is the current timestamp, +// and the key is the appSecret corresponding to the appId. Use this key to calculate the timestamp signature value. +// get more detail via: https://developers.dingtalk.com/document/app/signature-calculation-for-logon-free-scenarios-1?spm=ding_open_doc.document.0.0.63262ea7l6iEm1#topic-2021698 +func EncodeSHA256(message, secret string) string { + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(message)) + sum := h.Sum(nil) + msg1 := base64.StdEncoding.EncodeToString(sum) + + uv := url.Values{} + uv.Add("0", msg1) + msg2 := uv.Encode()[2:] + return msg2 +} diff --git a/casdoor/idp/facebook.go b/casdoor/idp/facebook.go new file mode 100644 index 0000000..8814032 --- /dev/null +++ b/casdoor/idp/facebook.go @@ -0,0 +1,194 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "golang.org/x/oauth2" +) + +type FacebookIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewFacebookIdProvider(clientId string, clientSecret string, redirectUrl string) *FacebookIdProvider { + idp := &FacebookIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *FacebookIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *FacebookIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://graph.facebook.com/oauth/access_token", + } + + var config = &oauth2.Config{ + Scopes: []string{"email,public_profile"}, + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type FacebookAccessToken struct { + AccessToken string `json:"access_token"` //Interface call credentials + TokenType string `json:"token_type"` //Access token type + ExpiresIn int64 `json:"expires_in"` //access_token interface call credential timeout time, unit (seconds) +} + +type FacebookCheckToken struct { + Data string `json:"data"` +} + +// Get more detail via: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#checktoken +type FacebookCheckTokenData struct { + UserId string `json:"user_id"` +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#confirm +func (idp *FacebookIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("client_id", idp.Config.ClientID) + params.Add("redirect_uri", idp.Config.RedirectURL) + params.Add("client_secret", idp.Config.ClientSecret) + params.Add("code", code) + + accessTokenUrl := fmt.Sprintf("https://graph.facebook.com/oauth/access_token?%s", params.Encode()) + + accessTokenResp, err := idp.GetUrlResp(accessTokenUrl) + if err != nil { + return nil, err + } + + var facebookAccessToken FacebookAccessToken + if err = json.Unmarshal([]byte(accessTokenResp), &facebookAccessToken); err != nil { + return nil, err + } + + token := oauth2.Token{ + AccessToken: facebookAccessToken.AccessToken, + TokenType: "FacebookAccessToken", + Expiry: time.Time{}, + } + + return &token, nil +} + +//{ +// "id": "123456789", +// "name": "Example Name", +// "name_format": "{first} {last}", +// "picture": { +// "data": { +// "height": 50, +// "is_silhouette": false, +// "url": "https://example.com", +// "width": 50 +// } +// }, +// "email": "test@example.com" +//} + +type FacebookUserInfo struct { + Id string `json:"id"` // The app user's App-Scoped User ID. This ID is unique to the app and cannot be used by other apps. + Name string `json:"name"` // The person's full name. + NameFormat string `json:"name_format"` // The person's name formatted to correctly handle Chinese, Japanese, or Korean ordering. + Picture struct { // The person's profile picture. + Data struct { // This struct is different as https://developers.facebook.com/docs/graph-api/reference/user/picture/ + Height int `json:"height"` + IsSilhouette bool `json:"is_silhouette"` + Url string `json:"url"` + Width int `json:"width"` + } `json:"data"` + } `json:"picture"` + Email string `json:"email"` // The User's primary email address listed on their profile. This field will not be returned if no valid email address is available. +} + +// GetUserInfo use FacebookAccessToken gotten before return FacebookUserInfo +// get more detail via: https://developers.facebook.com/docs/graph-api/reference/user +func (idp *FacebookIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var facebookUserInfo FacebookUserInfo + accessToken := token.AccessToken + + userIdUrl := fmt.Sprintf("https://graph.facebook.com/me?access_token=%s", accessToken) + userIdResp, err := idp.GetUrlResp(userIdUrl) + if err != nil { + return nil, err + } + + if err = json.Unmarshal([]byte(userIdResp), &facebookUserInfo); err != nil { + return nil, err + } + + userInfoUrl := fmt.Sprintf("https://graph.facebook.com/%s?fields=id,name,name_format,picture,email&access_token=%s", facebookUserInfo.Id, accessToken) + userInfoResp, err := idp.GetUrlResp(userInfoUrl) + if err != nil { + return nil, err + } + + if err = json.Unmarshal([]byte(userInfoResp), &facebookUserInfo); err != nil { + return nil, err + } + + userInfo := UserInfo{ + Id: facebookUserInfo.Id, + DisplayName: facebookUserInfo.Name, + Email: facebookUserInfo.Email, + AvatarUrl: facebookUserInfo.Picture.Data.Url, + } + return &userInfo, nil +} + +func (idp *FacebookIdProvider) GetUrlResp(url string) (string, error) { + resp, err := idp.Client.Get(url) + if err != nil { + return "", err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/casdoor/idp/gitee.go b/casdoor/idp/gitee.go new file mode 100644 index 0000000..a150a6f --- /dev/null +++ b/casdoor/idp/gitee.go @@ -0,0 +1,230 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type GiteeIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewGiteeIdProvider(clientId string, clientSecret string, redirectUrl string) *GiteeIdProvider { + idp := &GiteeIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *GiteeIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *GiteeIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://gitee.com/oauth/token", + } + + var config = &oauth2.Config{ + Scopes: []string{"user_info emails"}, + + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type GiteeAccessToken struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + CreatedAt int `json:"created_at"` +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// The POST Url format of submission is: https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret} +// get more detail via: https://gitee.com/api/v5/oauth_doc#/ +func (idp *GiteeIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("client_id", idp.Config.ClientID) + params.Add("client_secret", idp.Config.ClientSecret) + params.Add("code", code) + params.Add("redirect_uri", idp.Config.RedirectURL) + + accessTokenUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.TokenURL, params.Encode()) + bs, _ := json.Marshal(params.Encode()) + req, _ := http.NewRequest("POST", accessTokenUrl, strings.NewReader(string(bs))) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + rbs, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + tokenResp := GiteeAccessToken{} + if err = json.Unmarshal(rbs, &tokenResp); err != nil { + return nil, err + } + + token := &oauth2.Token{ + AccessToken: tokenResp.AccessToken, + TokenType: tokenResp.TokenType, + RefreshToken: tokenResp.RefreshToken, + Expiry: time.Unix(time.Now().Unix()+int64(tokenResp.ExpiresIn), 0), + } + + return token, nil +} + +/* +{ + "id": 999999, + "login": "xxx", + "name": "xxx", + "avatar_url": "https://gitee.com/assets/no_portrait.png", + "url": "https://gitee.com/api/v5/users/xxx", + "html_url": "https://gitee.com/xxx", + "followers_url": "https://gitee.com/api/v5/users/xxx/followers", + "following_url": "https://gitee.com/api/v5/users/xxx/following_url{/other_user}", + "gists_url": "https://gitee.com/api/v5/users/xxx/gists{/gist_id}", + "starred_url": "https://gitee.com/api/v5/users/xxx/starred{/owner}{/repo}", + "subscriptions_url": "https://gitee.com/api/v5/users/xxx/subscriptions", + "organizations_url": "https://gitee.com/api/v5/users/xxx/orgs", + "repos_url": "https://gitee.com/api/v5/users/xxx/repos", + "events_url": "https://gitee.com/api/v5/users/xxx/events{/privacy}", + "received_events_url": "https://gitee.com/api/v5/users/xxx/received_events", + "type": "User", + "blog": null, + "weibo": null, + "bio": "个人博客:https://gitee.com/xxx/xxx/pages", + "public_repos": 2, + "public_gists": 0, + "followers": 0, + "following": 0, + "stared": 0, + "watched": 2, + "created_at": "2019-08-03T23:21:16+08:00", + "updated_at": "2021-06-14T12:47:09+08:00", + "email": null +} +*/ + +type GiteeUserResponse struct { + AvatarUrl string `json:"avatar_url"` + Bio string `json:"bio"` + Blog string `json:"blog"` + CreatedAt string `json:"created_at"` + Email string `json:"email"` + EventsUrl string `json:"events_url"` + Followers int `json:"followers"` + FollowersUrl string `json:"followers_url"` + Following int `json:"following"` + FollowingUrl string `json:"following_url"` + GistsUrl string `json:"gists_url"` + HtmlUrl string `json:"html_url"` + Id int `json:"id"` + Login string `json:"login"` + MemberRole string `json:"member_role"` + Name string `json:"name"` + OrganizationsUrl string `json:"organizations_url"` + PublicGists int `json:"public_gists"` + PublicRepos int `json:"public_repos"` + ReceivedEventsUrl string `json:"received_events_url"` + ReposUrl string `json:"repos_url"` + Stared int `json:"stared"` + StarredUrl string `json:"starred_url"` + SubscriptionsUrl string `json:"subscriptions_url"` + Type string `json:"type"` + UpdatedAt string `json:"updated_at"` + Url string `json:"url"` + Watched int `json:"watched"` + Weibo string `json:"weibo"` +} + +// GetUserInfo Use userid and access_token to get UserInfo +// get more detail via: https://gitee.com/api/v5/swagger#/getV5User +func (idp *GiteeIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var gtUserInfo GiteeUserResponse + accessToken := token.AccessToken + + u := fmt.Sprintf("https://gitee.com/api/v5/user?access_token=%s", + accessToken) + + userinfoResp, err := idp.GetUrlResp(u) + if err != nil { + return nil, err + } + + if err = json.Unmarshal([]byte(userinfoResp), >UserInfo); err != nil { + return nil, err + } + + userInfo := UserInfo{ + Id: strconv.Itoa(gtUserInfo.Id), + Username: gtUserInfo.Name, + DisplayName: gtUserInfo.Name, + Email: gtUserInfo.Email, + AvatarUrl: gtUserInfo.AvatarUrl, + } + + return &userInfo, nil +} + +func (idp *GiteeIdProvider) GetUrlResp(url string) (string, error) { + resp, err := idp.Client.Get(url) + if err != nil { + return "", err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/casdoor/idp/github.go b/casdoor/idp/github.go new file mode 100644 index 0000000..81b6b52 --- /dev/null +++ b/casdoor/idp/github.go @@ -0,0 +1,194 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "strconv" + "time" + + "golang.org/x/oauth2" +) + +type GithubIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewGithubIdProvider(clientId string, clientSecret string, redirectUrl string) *GithubIdProvider { + idp := &GithubIdProvider{} + + config := idp.getConfig() + config.ClientID = clientId + config.ClientSecret = clientSecret + config.RedirectURL = redirectUrl + idp.Config = config + + return idp +} + +func (idp *GithubIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +func (idp *GithubIdProvider) getConfig() *oauth2.Config { + var endpoint = oauth2.Endpoint{ + AuthURL: "https://github.com/login/oauth/authorize", + TokenURL: "https://github.com/login/oauth/access_token", + } + + var config = &oauth2.Config{ + Scopes: []string{"user:email", "read:user"}, + Endpoint: endpoint, + } + + return config +} + +func (idp *GithubIdProvider) GetToken(code string) (*oauth2.Token, error) { + ctx := context.WithValue(oauth2.NoContext, oauth2.HTTPClient, idp.Client) + return idp.Config.Exchange(ctx, code) +} + +//{ +// "login": "jimgreen", +// "id": 3781234, +// "node_id": "MDQ6VXNlcjM3O123456=", +// "avatar_url": "https://avatars.githubusercontent.com/u/3781234?v=4", +// "gravatar_id": "", +// "url": "https://api.github.com/users/jimgreen", +// "html_url": "https://github.com/jimgreen", +// "followers_url": "https://api.github.com/users/jimgreen/followers", +// "following_url": "https://api.github.com/users/jimgreen/following{/other_user}", +// "gists_url": "https://api.github.com/users/jimgreen/gists{/gist_id}", +// "starred_url": "https://api.github.com/users/jimgreen/starred{/owner}{/repo}", +// "subscriptions_url": "https://api.github.com/users/jimgreen/subscriptions", +// "organizations_url": "https://api.github.com/users/jimgreen/orgs", +// "repos_url": "https://api.github.com/users/jimgreen/repos", +// "events_url": "https://api.github.com/users/jimgreen/events{/privacy}", +// "received_events_url": "https://api.github.com/users/jimgreen/received_events", +// "type": "User", +// "site_admin": false, +// "name": "Jim Green", +// "company": "Casbin", +// "blog": "https://casbin.org", +// "location": "Bay Area", +// "email": "jimgreen@gmail.com", +// "hireable": true, +// "bio": "My bio", +// "twitter_username": null, +// "public_repos": 45, +// "public_gists": 3, +// "followers": 123, +// "following": 31, +// "created_at": "2016-03-06T13:16:13Z", +// "updated_at": "2020-05-30T12:15:29Z", +// "private_gists": 0, +// "total_private_repos": 12, +// "owned_private_repos": 12, +// "disk_usage": 46331, +// "collaborators": 5, +// "two_factor_authentication": true, +// "plan": { +// "name": "free", +// "space": 976562499, +// "collaborators": 0, +// "private_repos": 10000 +// } +//} + +type GitHubUserInfo struct { + Login string `json:"login"` + Id int `json:"id"` + NodeId string `json:"node_id"` + AvatarUrl string `json:"avatar_url"` + GravatarId string `json:"gravatar_id"` + Url string `json:"url"` + HtmlUrl string `json:"html_url"` + FollowersUrl string `json:"followers_url"` + FollowingUrl string `json:"following_url"` + GistsUrl string `json:"gists_url"` + StarredUrl string `json:"starred_url"` + SubscriptionsUrl string `json:"subscriptions_url"` + OrganizationsUrl string `json:"organizations_url"` + ReposUrl string `json:"repos_url"` + EventsUrl string `json:"events_url"` + ReceivedEventsUrl string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + Name string `json:"name"` + Company string `json:"company"` + Blog string `json:"blog"` + Location string `json:"location"` + Email string `json:"email"` + Hireable bool `json:"hireable"` + Bio string `json:"bio"` + TwitterUsername interface{} `json:"twitter_username"` + PublicRepos int `json:"public_repos"` + PublicGists int `json:"public_gists"` + Followers int `json:"followers"` + Following int `json:"following"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PrivateGists int `json:"private_gists"` + TotalPrivateRepos int `json:"total_private_repos"` + OwnedPrivateRepos int `json:"owned_private_repos"` + DiskUsage int `json:"disk_usage"` + Collaborators int `json:"collaborators"` + TwoFactorAuthentication bool `json:"two_factor_authentication"` + Plan struct { + Name string `json:"name"` + Space int `json:"space"` + Collaborators int `json:"collaborators"` + PrivateRepos int `json:"private_repos"` + } `json:"plan"` +} + +func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + req, err := http.NewRequest("GET", "https://api.github.com/user", nil) + if err != nil { + panic(err) + } + req.Header.Add("Authorization", "token "+token.AccessToken) + resp, err := idp.Client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var githubUserInfo GitHubUserInfo + err = json.Unmarshal(body, &githubUserInfo) + if err != nil { + return nil, err + } + + userInfo := UserInfo{ + Id: strconv.Itoa(githubUserInfo.Id), + Username: githubUserInfo.Login, + DisplayName: githubUserInfo.Name, + Email: githubUserInfo.Email, + AvatarUrl: githubUserInfo.AvatarUrl, + } + return &userInfo, nil +} diff --git a/casdoor/idp/google.go b/casdoor/idp/google.go new file mode 100644 index 0000000..3875cab --- /dev/null +++ b/casdoor/idp/google.go @@ -0,0 +1,121 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + + "golang.org/x/oauth2" +) + +type GoogleIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewGoogleIdProvider(clientId string, clientSecret string, redirectUrl string) *GoogleIdProvider { + idp := &GoogleIdProvider{} + + config := idp.getConfig() + config.ClientID = clientId + config.ClientSecret = clientSecret + config.RedirectURL = redirectUrl + idp.Config = config + + return idp +} + +func (idp *GoogleIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +func (idp *GoogleIdProvider) getConfig() *oauth2.Config { + var endpoint = oauth2.Endpoint{ + AuthURL: "https://accounts.google.com/o/oauth2/auth", + TokenURL: "https://accounts.google.com/o/oauth2/token", + } + + var config = &oauth2.Config{ + Scopes: []string{"profile", "email"}, + Endpoint: endpoint, + } + + return config +} + +func (idp *GoogleIdProvider) GetToken(code string) (*oauth2.Token, error) { + ctx := context.WithValue(oauth2.NoContext, oauth2.HTTPClient, idp.Client) + return idp.Config.Exchange(ctx, code) +} + +//{ +// "id": "110613473084924141234", +// "email": "jimgreen@gmail.com", +// "verified_email": true, +// "name": "Jim Green", +// "given_name": "Jim", +// "family_name": "Green", +// "picture": "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", +// "locale": "en" +//} + +type GoogleUserInfo struct { + Id string `json:"id"` + Email string `json:"email"` + VerifiedEmail bool `json:"verified_email"` + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + Picture string `json:"picture"` + Locale string `json:"locale"` +} + +func (idp *GoogleIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + url := fmt.Sprintf("https://www.googleapis.com/oauth2/v2/userinfo?alt=json&access_token=%s", token.AccessToken) + resp, err := idp.Client.Get(url) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var googleUserInfo GoogleUserInfo + err = json.Unmarshal(body, &googleUserInfo) + if err != nil { + return nil, err + } + + if googleUserInfo.Email == "" { + return nil, errors.New("google email is empty") + } + + userInfo := UserInfo{ + Id: googleUserInfo.Id, + Username: googleUserInfo.Email, + DisplayName: googleUserInfo.Name, + Email: googleUserInfo.Email, + AvatarUrl: googleUserInfo.Picture, + } + return &userInfo, nil +} diff --git a/casdoor/idp/linkedin.go b/casdoor/idp/linkedin.go new file mode 100644 index 0000000..077a7e2 --- /dev/null +++ b/casdoor/idp/linkedin.go @@ -0,0 +1,331 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type LinkedInIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewLinkedInIdProvider(clientId string, clientSecret string, redirectUrl string) *LinkedInIdProvider { + idp := &LinkedInIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *LinkedInIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *LinkedInIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://www.linkedIn.com/oauth/v2/accessToken", + } + + var config = &oauth2.Config{ + Scopes: []string{"email,public_profile"}, + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type LinkedInAccessToken struct { + AccessToken string `json:"access_token"` //Interface call credentials + ExpiresIn int64 `json:"expires_in"` //access_token interface call credential timeout time, unit (seconds) +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://docs.microsoft.com/en-us/linkedIn/shared/authentication/authorization-code-flow?context=linkedIn%2Fcontext&tabs=HTTPS +func (idp *LinkedInIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("redirect_uri", idp.Config.RedirectURL) + params.Add("client_id", idp.Config.ClientID) + params.Add("client_secret", idp.Config.ClientSecret) + params.Add("code", code) + + accessTokenUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.TokenURL, params.Encode()) + bs, _ := json.Marshal(params.Encode()) + req, _ := http.NewRequest("POST", accessTokenUrl, strings.NewReader(string(bs))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + rbs, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + tokenResp := LinkedInAccessToken{} + if err = json.Unmarshal(rbs, &tokenResp); err != nil { + return nil, err + } + + token := &oauth2.Token{ + AccessToken: tokenResp.AccessToken, + TokenType: "Bearer", + Expiry: time.Unix(time.Now().Unix()+tokenResp.ExpiresIn, 0), + } + + return token, nil +} + +/* +{ + "firstName": { + "localized": { + "zh_CN": "继坤" + }, + "preferredLocale": { + "country": "CN", + "language": "zh" + } + }, + "lastName": { + "localized": { + "zh_CN": "刘" + }, + "preferredLocale": { + "country": "CN", + "language": "zh" + } + }, + "profilePicture": { + "displayImage": "urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg", + "displayImage~": { + "paging": { + "count": 10, + "start": 0, + "links": [] + }, + "elements": [ + { + "artifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100)", + "authorizationMethod": "PUBLIC", + "data": { + "com.linkedin.digitalmedia.mediaartifact.StillImage": { + "mediaType": "image/jpeg", + "rawCodecSpec": { + "name": "jpeg", + "type": "image" + }, + "displaySize": { + "width": 100.0, + "uom": "PX", + "height": 100.0 + }, + "storageSize": { + "width": 100, + "height": 100 + }, + "storageAspectRatio": { + "widthAspect": 1.0, + "heightAspect": 1.0, + "formatted": "1.00:1.00" + }, + "displayAspectRatio": { + "widthAspect": 1.0, + "heightAspect": 1.0, + "formatted": "1.00:1.00" + } + } + }, + "identifiers": [ + { + "identifier": "https://media.licdn.cn/dms/image/C5603AQHbdR8RkG62yg/profile-displayphoto-shrink_100_100/0/1625279434135?e=1630540800&v=beta&t=Z-bQKf_jFv8L1uwr6X5AJLoTQRWZrueT7qrITDSvxWM", + "index": 0, + "mediaType": "image/jpeg", + "file": "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100,0)", + "identifierType": "EXTERNAL_URL", + "identifierExpiresInSeconds": 1630540800 + } + ] + }, + // ... + } + ] + } + }, + "id": "vvMfLsLIRs" +} +*/ + +type LinkedInUserInfo struct { + FirstName struct { + Localized map[string]string `json:"localized"` + PreferredLocale struct { + Country string `json:"country"` + Language string `json:"language"` + } `json:"preferredLocale"` + } `json:"firstName"` + LastName struct { + Localized map[string]string `json:"localized"` + PreferredLocale struct { + Country string `json:"country"` + Language string `json:"language"` + } `json:"preferredLocale"` + } `json:"lastName"` + ProfilePicture struct { + DisplayImage string `json:"displayImage"` + DisplayImage1 struct { + Paging struct { + Count int `json:"count"` + Start int `json:"start"` + Links []interface{} `json:"links"` + } `json:"paging"` + Elements []struct { + Artifact string `json:"artifact"` + AuthorizationMethod string `json:"authorizationMethod"` + Data struct { + ComLinkedinDigitalmediaMediaartifactStillImage struct { + MediaType string `json:"mediaType"` + RawCodecSpec struct { + Name string `json:"name"` + Type string `json:"type"` + } `json:"rawCodecSpec"` + DisplaySize struct { + Width float64 `json:"width"` + Uom string `json:"uom"` + Height float64 `json:"height"` + } `json:"displaySize"` + StorageSize struct { + Width int `json:"width"` + Height int `json:"height"` + } `json:"storageSize"` + StorageAspectRatio struct { + WidthAspect float64 `json:"widthAspect"` + HeightAspect float64 `json:"heightAspect"` + Formatted string `json:"formatted"` + } `json:"storageAspectRatio"` + DisplayAspectRatio struct { + WidthAspect float64 `json:"widthAspect"` + HeightAspect float64 `json:"heightAspect"` + Formatted string `json:"formatted"` + } `json:"displayAspectRatio"` + } `json:"com.linkedin.digitalmedia.mediaartifact.StillImage"` + } `json:"data"` + Identifiers []struct { + Identifier string `json:"identifier"` + Index int `json:"index"` + MediaType string `json:"mediaType"` + File string `json:"file"` + IdentifierType string `json:"identifierType"` + IdentifierExpiresInSeconds int `json:"identifierExpiresInSeconds"` + } `json:"identifiers"` + } `json:"elements"` + } `json:"displayImage~"` + } `json:"profilePicture"` + Id string `json:"id"` +} + +/* +{ + "handle": "urn:li:emailAddress:3775708763", + "handle~": { + "emailAddress": "hsimpson@linkedin.com" + } +} +*/ + +type LinkedInUserEmail struct { + Elements []struct { + Handle struct { + EmailAddress string `json:"emailAddress"` + } `json:"handle~"` + Handle1 string `json:"handle"` + } `json:"elements"` +} + +// GetUserInfo use LinkedInAccessToken gotten before return LinkedInUserInfo +// get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context +func (idp *LinkedInIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var linkedInUserInfo LinkedInUserInfo + bs, err := idp.GetUrlRespWithAuthorization("https://api.linkedIn.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))", token.AccessToken) + if err != nil { + return nil, err + } + if err = json.Unmarshal(bs, &linkedInUserInfo); err != nil { + return nil, err + } + + var linkedInUserEmail LinkedInUserEmail + bs, err = idp.GetUrlRespWithAuthorization("https://api.linkedIn.com/v2/emailAddress?q=members&projection=(elements*(handle~))", token.AccessToken) + if err != nil { + return nil, err + } + if err = json.Unmarshal(bs, &linkedInUserEmail); err != nil { + return nil, err + } + + username := "" + for _, name := range linkedInUserInfo.FirstName.Localized { + username += name + } + for _, name := range linkedInUserInfo.LastName.Localized { + username += name + } + userInfo := UserInfo{ + Id: linkedInUserInfo.Id, + DisplayName: username, + Username: username, + Email: linkedInUserEmail.Elements[0].Handle.EmailAddress, + AvatarUrl: linkedInUserInfo.ProfilePicture.DisplayImage1.Elements[0].Identifiers[0].Identifier, + } + return &userInfo, nil +} + +func (idp *LinkedInIdProvider) GetUrlRespWithAuthorization(url, token string) ([]byte, error) { + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + bs, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return bs, nil +} diff --git a/casdoor/idp/provider.go b/casdoor/idp/provider.go new file mode 100644 index 0000000..7b69e68 --- /dev/null +++ b/casdoor/idp/provider.go @@ -0,0 +1,59 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "net/http" + + "golang.org/x/oauth2" +) + +type UserInfo struct { + Id string + Username string + DisplayName string + Email string + AvatarUrl string +} + +type IdProvider interface { + SetHttpClient(client *http.Client) + GetToken(code string) (*oauth2.Token, error) + GetUserInfo(token *oauth2.Token) (*UserInfo, error) +} + +func GetIdProvider(providerType string, clientId string, clientSecret string, redirectUrl string) IdProvider { + if providerType == "GitHub" { + return NewGithubIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "Google" { + return NewGoogleIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "QQ" { + return NewQqIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "WeChat" { + return NewWeChatIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "Facebook" { + return NewFacebookIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "DingTalk" { + return NewDingTalkIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "Weibo" { + return NewWeiBoIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "Gitee" { + return NewGiteeIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "LinkedIn" { + return NewLinkedInIdProvider(clientId, clientSecret, redirectUrl) + } + + return nil +} diff --git a/casdoor/idp/qq.go b/casdoor/idp/qq.go new file mode 100644 index 0000000..25c3c8e --- /dev/null +++ b/casdoor/idp/qq.go @@ -0,0 +1,185 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "regexp" + + "golang.org/x/oauth2" +) + +type QqIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewQqIdProvider(clientId string, clientSecret string, redirectUrl string) *QqIdProvider { + idp := &QqIdProvider{} + + config := idp.getConfig() + config.ClientID = clientId + config.ClientSecret = clientSecret + config.RedirectURL = redirectUrl + idp.Config = config + + return idp +} + +func (idp *QqIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +func (idp *QqIdProvider) getConfig() *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://graph.qq.com/oauth2.0/token", + } + + var config = &oauth2.Config{ + Scopes: []string{"get_user_info"}, + Endpoint: endpoint, + } + + return config +} + +func (idp *QqIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("client_id", idp.Config.ClientID) + params.Add("client_secret", idp.Config.ClientSecret) + params.Add("code", code) + params.Add("redirect_uri", idp.Config.RedirectURL) + + accessTokenUrl := fmt.Sprintf("https://graph.qq.com/oauth2.0/token?%s", params.Encode()) + resp, err := idp.Client.Get(accessTokenUrl) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + tokenContent, err := ioutil.ReadAll(resp.Body) + + re := regexp.MustCompile("token=(.*?)&") + matched := re.FindAllStringSubmatch(string(tokenContent), -1) + accessToken := matched[0][1] + token := &oauth2.Token{ + AccessToken: accessToken, + TokenType: "Bearer", + } + return token, nil +} + +//{ +// "ret": 0, +// "msg": "", +// "is_lost": 0, +// "nickname": "飞翔的企鹅", +// "gender": "男", +// "gender_type": 1, +// "province": "", +// "city": "安道尔城", +// "year": "1968", +// "constellation": "", +// "figureurl": "http:\/\/qzapp.qlogo.cn\/qzapp\/101896710\/C0D022F92B604AA4B1CDF92CC79463A4\/30", +// "figureurl_1": "http:\/\/qzapp.qlogo.cn\/qzapp\/101896710\/C0D022F92B604AA4B1CDF92CC79463A4\/50", +// "figureurl_2": "http:\/\/qzapp.qlogo.cn\/qzapp\/101896710\/C0D022F92B604AA4B1CDF92CC79463A4\/100", +// "figureurl_qq_1": "http://thirdqq.qlogo.cn/g?b=oidb&k=QtAu5OiaSfqGD0kfclwvxJA&s=40&t=1557635654", +// "figureurl_qq_2": "http://thirdqq.qlogo.cn/g?b=oidb&k=QtAu5OiaSfqGD0kfclwvxJA&s=100&t=1557635654", +// "figureurl_qq": "http://thirdqq.qlogo.cn/g?b=oidb&k=QtAu5OiaSfqGD0kfclwvxJA&s=640&t=1557635654", +// "figureurl_type": "1", +// "is_yellow_vip": "0", +// "vip": "0", +// "yellow_vip_level": "0", +// "level": "0", +// "is_yellow_year_vip": "0" +//} + +type QqUserInfo struct { + Ret int `json:"ret"` + Msg string `json:"msg"` + IsLost int `json:"is_lost"` + Nickname string `json:"nickname"` + Gender string `json:"gender"` + GenderType int `json:"gender_type"` + Province string `json:"province"` + City string `json:"city"` + Year string `json:"year"` + Constellation string `json:"constellation"` + Figureurl string `json:"figureurl"` + Figureurl1 string `json:"figureurl_1"` + Figureurl2 string `json:"figureurl_2"` + FigureurlQq1 string `json:"figureurl_qq_1"` + FigureurlQq2 string `json:"figureurl_qq_2"` + FigureurlQq string `json:"figureurl_qq"` + FigureurlType string `json:"figureurl_type"` + IsYellowVip string `json:"is_yellow_vip"` + Vip string `json:"vip"` + YellowVipLevel string `json:"yellow_vip_level"` + Level string `json:"level"` + IsYellowYearVip string `json:"is_yellow_year_vip"` +} + +func (idp *QqIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + openIdUrl := fmt.Sprintf("https://graph.qq.com/oauth2.0/me?access_token=%s", token.AccessToken) + resp, err := idp.Client.Get(openIdUrl) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + openIdBody, err := ioutil.ReadAll(resp.Body) + + re := regexp.MustCompile("\"openid\":\"(.*?)\"}") + matched := re.FindAllStringSubmatch(string(openIdBody), -1) + openId := matched[0][1] + if openId == "" { + return nil, errors.New("openId is empty") + } + + userInfoUrl := fmt.Sprintf("https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s", token.AccessToken, idp.Config.ClientID, openId) + resp, err = idp.Client.Get(userInfoUrl) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + userInfoBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var qqUserInfo QqUserInfo + err = json.Unmarshal(userInfoBody, &qqUserInfo) + if err != nil { + return nil, err + } + + if qqUserInfo.Ret != 0 { + return nil, errors.New(fmt.Sprintf("ret expected 0, got %d", qqUserInfo.Ret)) + } + + userInfo := UserInfo{ + Id: openId, + DisplayName: qqUserInfo.Nickname, + AvatarUrl: qqUserInfo.FigureurlQq1, + } + return &userInfo, nil +} diff --git a/casdoor/idp/wechat.go b/casdoor/idp/wechat.go new file mode 100644 index 0000000..f2aa134 --- /dev/null +++ b/casdoor/idp/wechat.go @@ -0,0 +1,186 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "golang.org/x/oauth2" +) + +type WeChatIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewWeChatIdProvider(clientId string, clientSecret string, redirectUrl string) *WeChatIdProvider { + idp := &WeChatIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *WeChatIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *WeChatIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://graph.qq.com/oauth2.0/token", + } + + var config = &oauth2.Config{ + Scopes: []string{"snsapi_login"}, + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type WechatAccessToken struct { + AccessToken string `json:"access_token"` //Interface call credentials + ExpiresIn int64 `json:"expires_in"` //access_token interface call credential timeout time, unit (seconds) + RefreshToken string `json:"refresh_token"` //User refresh access_token + Openid string `json:"openid"` //Unique ID of authorized user + Scope string `json:"scope"` //The scope of user authorization, separated by commas. (,) + Unionid string `json:"unionid"` //This field will appear if and only if the website application has been authorized by the user's UserInfo. +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html +func (idp *WeChatIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("appid", idp.Config.ClientID) + params.Add("secret", idp.Config.ClientSecret) + params.Add("code", code) + + accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?%s", params.Encode()) + tokenResponse, err := idp.Client.Get(accessTokenUrl) + if err != nil { + return nil, err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(tokenResponse.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(tokenResponse.Body) + if err != nil { + return nil, err + } + + var wechatAccessToken WechatAccessToken + if err = json.Unmarshal([]byte(buf.String()), &wechatAccessToken); err != nil { + return nil, err + } + + token := oauth2.Token{ + AccessToken: wechatAccessToken.AccessToken, + TokenType: "WeChatAccessToken", + RefreshToken: wechatAccessToken.RefreshToken, + Expiry: time.Time{}, + } + + raw := make(map[string]string) + raw["Openid"] = wechatAccessToken.Openid + token.WithExtra(raw) + + return &token, nil +} + +//{ +// "openid": "of_Hl5zVpyj0vwzIlAyIlnXe1234", +// "nickname": "飞翔的企鹅", +// "sex": 1, +// "language": "zh_CN", +// "city": "Shanghai", +// "province": "Shanghai", +// "country": "CN", +// "headimgurl": "https:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/Q0j4TwGTfTK6xc7vGca4KtibJib5dslRianc9VHt9k2N7fewYOl8fak7grRM7nS5V6HcvkkIkGThWUXPjDbXkQFYA\/132", +// "privilege": [], +// "unionid": "oxW9O1VAL8x-zfWP2hrqW9c81234" +//} + +type WechatUserInfo struct { + Openid string `json:"openid"` // The ID of an ordinary user, which is unique to the current developer account + Nickname string `json:"nickname"` // Ordinary user nickname + Sex int `json:"sex"` // Ordinary user gender, 1 is male, 2 is female + Language string `json:"language"` + City string `json:"city"` // City filled in by general user's personal data + Province string `json:"province"` // Province filled in by ordinary user's personal information + Country string `json:"country"` // Country, such as China is CN + Headimgurl string `json:"headimgurl"` // User avatar, the last value represents the size of the square avatar (there are optional values of 0, 46, 64, 96, 132, 0 represents a 640*640 square avatar), this item is empty when the user does not have a avatar + Privilege []string `json:"privilege"` // User Privilege information, json array, such as Wechat Woka user (chinaunicom) + Unionid string `json:"unionid"` // Unified user identification. For an application under a WeChat open platform account, the unionid of the same user is unique. +} + +// GetUserInfo use WechatAccessToken gotten before return WechatUserInfo +// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html +func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var wechatUserInfo WechatUserInfo + accessToken := token.AccessToken + openid := token.Extra("Openid") + + userInfoUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s", accessToken, openid) + resp, err := idp.Client.Get(userInfoUrl) + if err != nil { + return nil, err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return nil, err + } + if err = json.Unmarshal([]byte(buf.String()), &wechatUserInfo); err != nil { + return nil, err + } + + id := wechatUserInfo.Unionid + if id == "" { + id = wechatUserInfo.Openid + } + + userInfo := UserInfo{ + Id: id, + DisplayName: wechatUserInfo.Nickname, + AvatarUrl: wechatUserInfo.Headimgurl, + } + return &userInfo, nil +} diff --git a/casdoor/idp/weibo.go b/casdoor/idp/weibo.go new file mode 100644 index 0000000..191642b --- /dev/null +++ b/casdoor/idp/weibo.go @@ -0,0 +1,267 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "time" + + "golang.org/x/oauth2" +) + +type WeiBoIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewWeiBoIdProvider(clientId string, clientSecret string, redirectUrl string) *WeiBoIdProvider { + idp := &WeiBoIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *WeiBoIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *WeiBoIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://api.weibo.com/oauth2/access_token", + } + + var config = &oauth2.Config{ + Scopes: []string{""}, + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type WeiboAccessToken struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RemindIn string `json:"remind_in"` // This parameter is about to be obsolete, developers please use expires_in + Uid string `json:"uid"` +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html +func (idp *WeiBoIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("client_id", idp.Config.ClientID) + params.Add("client_secret", idp.Config.ClientSecret) + params.Add("code", code) + params.Add("redirect_uri", idp.Config.RedirectURL) + + // accessTokenUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.TokenURL, params.Encode()) + resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, params) + // resp, err := idp.GetUrlResp(accessTokenUrl) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + bs, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var weiboAccessToken WeiboAccessToken + if err = json.Unmarshal(bs, &weiboAccessToken); err != nil { + return nil, err + } + + token := oauth2.Token{ + AccessToken: weiboAccessToken.AccessToken, + TokenType: "WeiboAccessToken", + Expiry: time.Unix(time.Now().Unix()+int64(weiboAccessToken.ExpiresIn), 0), + } + + idp.Config.Scopes[0] = weiboAccessToken.Uid + return &token, nil +} + +/* +{ + "id": 1404376560, + "screen_name": "zaku", + "name": "zaku", + "province": "11", + "city": "5", + "location": "北京 朝阳区", + "description": "人生五十年,乃如梦如幻;有生斯有死,壮士复何憾。", + "url": "http://blog.sina.com.cn/zaku", + "profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1", + "domain": "zaku", + "gender": "m", + "followers_count": 1204, + "friends_count": 447, + "statuses_count": 2908, + "favourites_count": 0, + "created_at": "Fri Aug 28 00:00:00 +0800 2009", + "following": false, + "allow_all_act_msg": false, + "geo_enabled": true, + "verified": false, + "status": { + "created_at": "Tue May 24 18:04:53 +0800 2011", + "id": 11142488790, + "text": "我的相机到了。", + "source": "新浪微博", + "favorited": false, + "truncated": false, + "in_reply_to_status_id": "", + "in_reply_to_user_id": "", + "in_reply_to_screen_name": "", + "geo": null, + "mid": "5610221544300749636", + "annotations": [], + "reposts_count": 5, + "comments_count": 8 + }, + "allow_all_comment": true, + "avatar_large": "http://tp1.sinaimg.cn/1404376560/180/0/1", + "verified_reason": "", + "follow_me": false, + "online_status": 0, + "bi_followers_count": 215 +} +*/ + +type WeiboUserinfo struct { + Id int `json:"id"` + ScreenName string `json:"screen_name"` + Name string `json:"name"` + Province string `json:"province"` + City string `json:"city"` + Location string `json:"location"` + Description string `json:"description"` + Url string `json:"url"` + ProfileImageUrl string `json:"profile_image_url"` + Domain string `json:"domain"` + Gender string `json:"gender"` + FollowersCount int `json:"followers_count"` + FriendsCount int `json:"friends_count"` + StatusesCount int `json:"statuses_count"` + FavouritesCount int `json:"favourites_count"` + CreatedAt string `json:"created_at"` + Following bool `json:"following"` + AllowAllActMsg bool `json:"allow_all_act_msg"` + GeoEnabled bool `json:"geo_enabled"` + Verified bool `json:"verified"` + Status struct { + CreatedAt string `json:"created_at"` + Id int64 `json:"id"` + Text string `json:"text"` + Source string `json:"source"` + Favorited bool `json:"favorited"` + Truncated bool `json:"truncated"` + InReplyToStatusId string `json:"in_reply_to_status_id"` + InReplyToUserId string `json:"in_reply_to_user_id"` + InReplyToScreenName string `json:"in_reply_to_screen_name"` + Geo interface{} `json:"geo"` + Mid string `json:"mid"` + Annotations []interface{} `json:"annotations"` + RepostsCount int `json:"reposts_count"` + CommentsCount int `json:"comments_count"` + } `json:"status"` + AllowAllComment bool `json:"allow_all_comment"` + AvatarLarge string `json:"avatar_large"` + VerifiedReason string `json:"verified_reason"` + FollowMe bool `json:"follow_me"` + OnlineStatus int `json:"online_status"` + BiFollowersCount int `json:"bi_followers_count"` +} + +// GetUserInfo use WeiboAccessToken gotten before return UserInfo +// get more detail via: https://open.weibo.com/wiki/2/users/show +func (idp *WeiBoIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var weiboUserInfo WeiboUserinfo + accessToken := token.AccessToken + uid := idp.Config.Scopes[0] + id, _ := strconv.Atoi(uid) + + userInfoUrl := fmt.Sprintf("https://api.weibo.com/2/users/show.json?access_token=%s&uid=%d", accessToken, id) + resp, err := idp.GetUrlResp(userInfoUrl) + if err != nil { + return nil, err + } + if err = json.Unmarshal([]byte(resp), &weiboUserInfo); err != nil { + return nil, err + } + + // weibo user email need to get separately through this url, need user authorization. + e := struct { + Email string `json:"email"` + }{} + emailUrl := fmt.Sprintf("https://api.weibo.com/2/account/profile/email.json?access_token=%s", accessToken) + resp, err = idp.GetUrlResp(emailUrl) + if err != nil { + return nil, err + } + if err = json.Unmarshal([]byte(resp), &e); err != nil { + return nil, err + } + + userInfo := UserInfo{ + Id: strconv.Itoa(weiboUserInfo.Id), + Username: weiboUserInfo.Name, + DisplayName: weiboUserInfo.Name, + AvatarUrl: weiboUserInfo.AvatarLarge, + Email: e.Email, + } + return &userInfo, nil +} + +func (idp *WeiBoIdProvider) GetUrlResp(url string) (string, error) { + resp, err := idp.Client.Get(url) + if err != nil { + return "", err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/casdoor/main.go b/casdoor/main.go new file mode 100644 index 0000000..e113e4f --- /dev/null +++ b/casdoor/main.go @@ -0,0 +1,67 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/astaxie/beego" + "github.com/astaxie/beego/logs" + "github.com/astaxie/beego/plugins/cors" + "github.com/casdoor/casdoor/authz" + "github.com/casdoor/casdoor/controllers" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/routers" + + _ "github.com/casdoor/casdoor/routers" +) + +func main() { + object.InitAdapter() + object.InitDb() + controllers.InitHttpClient() + authz.InitAuthz() + + beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "PUT", "PATCH"}, + AllowHeaders: []string{"Origin"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + })) + + //beego.DelStaticPath("/static") + beego.SetStaticPath("/static", "web/build/static") + beego.BConfig.WebConfig.DirectoryIndex = true + beego.SetStaticPath("/swagger", "swagger") + // https://studygolang.com/articles/2303 + beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter) + beego.InsertFilter("*", beego.BeforeRouter, routers.AutoLoginFilter) + beego.InsertFilter("*", beego.BeforeRouter, routers.AuthzFilter) + beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage) + + beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id" + beego.BConfig.WebConfig.Session.SessionProvider = "file" + beego.BConfig.WebConfig.Session.SessionProviderConfig = "./tmp" + beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 365 + //beego.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteNoneMode + + err := logs.SetLogger("file", `{"filename":"logs/casdoor.log","maxdays":99999}`) + if err != nil { + panic(err) + } + logs.SetLevel(logs.LevelInformational) + logs.SetLogFuncCall(false) + + beego.Run() +} diff --git a/casdoor/object/adapter.go b/casdoor/object/adapter.go new file mode 100644 index 0000000..05c8ac8 --- /dev/null +++ b/casdoor/object/adapter.go @@ -0,0 +1,145 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + "runtime" + + "github.com/astaxie/beego" + _ "github.com/go-sql-driver/mysql" // db = mysql + //_ "github.com/lib/pq" // db = postgres + "xorm.io/xorm" +) + +var adapter *Adapter + +func InitConfig() { + err := beego.LoadAppConfig("ini", "../conf/app.conf") + if err != nil { + panic(err) + } + + InitAdapter() +} + +func InitAdapter() { + adapter = NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"), beego.AppConfig.String("dbName")) + adapter.createTable() +} + +// Adapter represents the MySQL adapter for policy storage. +type Adapter struct { + driverName string + dataSourceName string + dbName string + Engine *xorm.Engine +} + +// finalizer is the destructor for Adapter. +func finalizer(a *Adapter) { + err := a.Engine.Close() + if err != nil { + panic(err) + } +} + +// NewAdapter is the constructor for Adapter. +func NewAdapter(driverName string, dataSourceName string, dbName string) *Adapter { + a := &Adapter{} + a.driverName = driverName + a.dataSourceName = dataSourceName + a.dbName = dbName + + // Open the DB, create it if not existed. + a.open() + + // Call the destructor when the object is released. + runtime.SetFinalizer(a, finalizer) + + return a +} + +func (a *Adapter) createDatabase() error { + engine, err := xorm.NewEngine(a.driverName, a.dataSourceName) + if err != nil { + return err + } + defer engine.Close() + + _, err = engine.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s default charset utf8 COLLATE utf8_general_ci", a.dbName)) + return err +} + +func (a *Adapter) open() { + if a.driverName != "postgres" { + if err := a.createDatabase(); err != nil { + panic(err) + } + } + + engine, err := xorm.NewEngine(a.driverName, a.dataSourceName+a.dbName) + if err != nil { + panic(err) + } + + a.Engine = engine +} + +func (a *Adapter) close() { + a.Engine.Close() + a.Engine = nil +} + +func (a *Adapter) createTable() { + err := a.Engine.Sync2(new(Organization)) + if err != nil { + panic(err) + } + + err = a.Engine.Sync2(new(User)) + if err != nil { + panic(err) + } + + err = a.Engine.Sync2(new(Provider)) + if err != nil { + panic(err) + } + + err = a.Engine.Sync2(new(Application)) + if err != nil { + panic(err) + } + + err = a.Engine.Sync2(new(Token)) + if err != nil { + panic(err) + } + + err = a.Engine.Sync2(new(VerificationRecord)) + if err != nil { + panic(err) + } + err = a.Engine.Sync2(new(Records)) + if err != nil { + panic(err) + } + + err = a.Engine.Sync2(new(Ldap)) + if err != nil { + panic(err) + } +} diff --git a/casdoor/object/application.go b/casdoor/object/application.go new file mode 100644 index 0000000..2c46433 --- /dev/null +++ b/casdoor/object/application.go @@ -0,0 +1,190 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +type Application struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + DisplayName string `xorm:"varchar(100)" json:"displayName"` + Logo string `xorm:"varchar(100)" json:"logo"` + HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"` + Description string `xorm:"varchar(100)" json:"description"` + Organization string `xorm:"varchar(100)" json:"organization"` + EnablePassword bool `json:"enablePassword"` + EnableSignUp bool `json:"enableSignUp"` + Providers []*ProviderItem `xorm:"varchar(10000)" json:"providers"` + SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` + OrganizationObj *Organization `xorm:"-" json:"organizationObj"` + + ClientId string `xorm:"varchar(100)" json:"clientId"` + ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` + RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"` + ExpireInHours int `json:"expireInHours"` + SignupUrl string `xorm:"varchar(100)" json:"signupUrl"` + SigninUrl string `xorm:"varchar(100)" json:"signinUrl"` + ForgetUrl string `xorm:"varchar(100)" json:"forgetUrl"` + AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"` +} + +func GetApplications(owner string) []*Application { + applications := []*Application{} + err := adapter.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner}) + if err != nil { + panic(err) + } + + return applications +} + +func getProviderMap(owner string) map[string]*Provider { + providers := GetProviders(owner) + m := map[string]*Provider{} + for _, provider := range providers { + //if provider.Category != "OAuth" { + // continue + //} + + m[provider.Name] = getMaskedProvider(provider) + } + return m +} + +func extendApplicationWithProviders(application *Application) { + m := getProviderMap(application.Owner) + for _, providerItem := range application.Providers { + if provider, ok := m[providerItem.Name]; ok { + providerItem.Provider = provider + } + } +} + +func extendApplicationWithOrg(application *Application) { + organization := getOrganization(application.Owner, application.Organization) + application.OrganizationObj = organization +} + +func getApplication(owner string, name string) *Application { + if owner == "" || name == "" { + return nil + } + + application := Application{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&application) + if err != nil { + panic(err) + } + + if existed { + extendApplicationWithProviders(&application) + extendApplicationWithOrg(&application) + return &application + } else { + return nil + } +} + +func GetApplicationByOrganizationName(organization string) *Application { + application := Application{} + existed, err := adapter.Engine.Where("organization=?", organization).Get(&application) + if err != nil { + panic(err) + } + + if existed { + extendApplicationWithProviders(&application) + extendApplicationWithOrg(&application) + return &application + } else { + return nil + } +} + +func GetApplicationByUser(user *User) *Application { + if user.SignupApplication != "" { + return getApplication("admin", user.SignupApplication) + } else { + return GetApplicationByOrganizationName(user.Owner) + } +} + +func GetApplicationByClientId(clientId string) *Application { + application := Application{} + existed, err := adapter.Engine.Where("client_id=?", clientId).Get(&application) + if err != nil { + panic(err) + } + + if existed { + extendApplicationWithProviders(&application) + extendApplicationWithOrg(&application) + return &application + } else { + return nil + } +} + +func GetApplication(id string) *Application { + owner, name := util.GetOwnerAndNameFromId(id) + return getApplication(owner, name) +} + +func UpdateApplication(id string, application *Application) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getApplication(owner, name) == nil { + return false + } + + for _, providerItem := range application.Providers { + providerItem.Provider = nil + } + + affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(application) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddApplication(application *Application) bool { + application.ClientId = util.GenerateClientId() + application.ClientSecret = util.GenerateClientSecret() + for _, providerItem := range application.Providers { + providerItem.Provider = nil + } + + affected, err := adapter.Engine.Insert(application) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteApplication(application *Application) bool { + affected, err := adapter.Engine.ID(core.PK{application.Owner, application.Name}).Delete(&Application{}) + if err != nil { + panic(err) + } + + return affected != 0 +} diff --git a/casdoor/object/application_item.go b/casdoor/object/application_item.go new file mode 100644 index 0000000..daee72b --- /dev/null +++ b/casdoor/object/application_item.go @@ -0,0 +1,102 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +func (application *Application) getProviderByCategory(category string) *Provider { + providers := GetProviders(application.Owner) + m := map[string]*Provider{} + for _, provider := range providers { + if provider.Category != category { + continue + } + + m[provider.Name] = provider + } + + for _, providerItem := range application.Providers { + if provider, ok := m[providerItem.Name]; ok { + return provider + } + } + + return nil +} + +func (application *Application) GetEmailProvider() *Provider { + return application.getProviderByCategory("Email") +} + +func (application *Application) GetSmsProvider() *Provider { + return application.getProviderByCategory("SMS") +} + +func (application *Application) getSignupItem(itemName string) *SignupItem { + for _, signupItem := range application.SignupItems { + if signupItem.Name == itemName { + return signupItem + } + } + return nil +} + +func (application *Application) IsSignupItemEnabled(itemName string) bool { + return application.getSignupItem(itemName) != nil +} + +func (application *Application) IsSignupItemVisible(itemName string) bool { + signupItem := application.getSignupItem(itemName) + if signupItem == nil { + return false + } + + return signupItem.Visible +} + +func (application *Application) GetSignupItemRule(itemName string) string { + signupItem := application.getSignupItem(itemName) + if signupItem == nil { + return "" + } + + return signupItem.Rule +} + +func (application *Application) getAllPromptedProviderItems() []*ProviderItem { + res := []*ProviderItem{} + for _, providerItem := range application.Providers { + if providerItem.isProviderPrompted() { + res = append(res, providerItem) + } + } + return res +} + +func (application *Application) isAffiliationPrompted() bool { + signupItem := application.getSignupItem("Affiliation") + if signupItem == nil { + return false + } + + return signupItem.Prompted +} + +func (application *Application) HasPromptPage() bool { + providerItems := application.getAllPromptedProviderItems() + if len(providerItems) != 0 { + return true + } + + return application.isAffiliationPrompted() +} diff --git a/casdoor/object/captcha.go b/casdoor/object/captcha.go new file mode 100644 index 0000000..11ef925 --- /dev/null +++ b/casdoor/object/captcha.go @@ -0,0 +1,40 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "bytes" + + "github.com/dchest/captcha" +) + +func GetCaptcha() (string, []byte) { + id := captcha.NewLen(5) + + var buffer bytes.Buffer + + err := captcha.WriteImage(&buffer, id, 200, 80) + if err != nil { + panic(err) + } + + return id, buffer.Bytes() +} + +func VerifyCaptcha(id string, digits string) bool { + res := captcha.VerifyString(id, digits) + + return res +} diff --git a/casdoor/object/check.go b/casdoor/object/check.go new file mode 100644 index 0000000..40d222e --- /dev/null +++ b/casdoor/object/check.go @@ -0,0 +1,120 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + "regexp" + + "github.com/casdoor/casdoor/util" +) + +var reWhiteSpace *regexp.Regexp + +func init() { + reWhiteSpace, _ = regexp.Compile("\\s") +} + +func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, email string, phone string, affiliation string) string { + if organization == nil { + return "organization does not exist" + } + + if application.IsSignupItemVisible("Username") { + if len(username) <= 1 { + return "username must have at least 2 characters" + } else if reWhiteSpace.MatchString(username) { + return "username cannot contain white spaces" + } else if HasUserByField(organization.Name, "name", username) { + return "username already exists" + } + } + + if len(password) <= 5 { + return "password must have at least 6 characters" + } + + if application.IsSignupItemVisible("Email") { + if HasUserByField(organization.Name, "email", email) { + return "email already exists" + } else if !util.IsEmailValid(email) { + return "email is invalid" + } + } + + if application.IsSignupItemVisible("Phone") { + if HasUserByField(organization.Name, "phone", phone) { + return "phone already exists" + } else if organization.PhonePrefix == "86" && !util.IsPhoneCnValid(phone) { + return "phone number is invalid" + } + } + + if application.IsSignupItemVisible("Display name") { + if displayName == "" { + return "displayName cannot be blank" + } else if application.GetSignupItemRule("Display name") == "Personal" { + if !isValidPersonalName(displayName) { + return "displayName is not valid personal name" + } + } + } + + if application.IsSignupItemVisible("Affiliation") { + if affiliation == "" { + return "affiliation cannot be blank" + } + } + + return "" +} + +func CheckPassword(user *User, password string) string { + organization := GetOrganizationByUser(user) + + if organization.PasswordType == "plain" { + if password == user.Password { + return "" + } else { + return "password incorrect" + } + } else if organization.PasswordType == "salt" { + if password == user.Password || getSaltedPassword(password, organization.PasswordSalt) == user.Password { + return "" + } else { + return "password incorrect" + } + } else { + return fmt.Sprintf("unsupported password type: %s", organization.PasswordType) + } +} + +func CheckUserLogin(organization string, username string, password string) (*User, string) { + user := GetUserByFields(organization, username) + if user == nil { + return nil, "the user does not exist, please sign up first" + } + + if user.IsForbidden { + return nil, "the user is forbidden to sign in, please contact the administrator" + } + + msg := CheckPassword(user, password) + if msg != "" { + return nil, msg + } + + return user, "" +} diff --git a/casdoor/object/check_util.go b/casdoor/object/check_util.go new file mode 100644 index 0000000..a746d71 --- /dev/null +++ b/casdoor/object/check_util.go @@ -0,0 +1,31 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import "regexp" + +var rePersonalName *regexp.Regexp + +func init() { + var err error + rePersonalName, err = regexp.Compile("^[\u4E00-\u9FA5]{2,3}(?:·[\u4E00-\u9FA5]{2,3})*$") + if err != nil { + panic(err) + } +} + +func isValidPersonalName(s string) bool { + return rePersonalName.MatchString(s) +} diff --git a/casdoor/object/email.go b/casdoor/object/email.go new file mode 100644 index 0000000..cbefd25 --- /dev/null +++ b/casdoor/object/email.go @@ -0,0 +1,36 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// modified from https://github.com/casbin/casnode/blob/master/service/mail.go + +package object + +import "github.com/go-gomail/gomail" + +func SendEmail(provider *Provider, title, content, dest, sender string) string { + dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret) + + message := gomail.NewMessage() + message.SetAddressHeader("From", provider.ClientId, sender) + message.SetHeader("To", dest) + message.SetHeader("Subject", title) + message.SetBody("text/html", content) + + err := dialer.DialAndSend(message) + if err == nil { + return "" + } else { + return err.Error() + } +} diff --git a/casdoor/object/init.go b/casdoor/object/init.go new file mode 100644 index 0000000..55f1c3c --- /dev/null +++ b/casdoor/object/init.go @@ -0,0 +1,117 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import "github.com/casdoor/casdoor/util" + +func InitDb() { + initBuiltInOrganization() + initBuiltInUser() + initBuiltInApplication() + initBuiltInLdap() +} + +func initBuiltInOrganization() { + organization := getOrganization("admin", "built-in") + if organization != nil { + return + } + + organization = &Organization{ + Owner: "admin", + Name: "built-in", + CreatedTime: util.GetCurrentTime(), + DisplayName: "Built-in Organization", + WebsiteUrl: "https://example.com", + Favicon: "https://cdn.casbin.com/static/favicon.ico", + PhonePrefix: "86", + DefaultAvatar: "https://casbin.org/img/casbin.svg", + PasswordType: "plain", + } + AddOrganization(organization) +} + +func initBuiltInUser() { + user := getUser("built-in", "admin") + if user != nil { + return + } + + user = &User{ + Owner: "built-in", + Name: "admin", + CreatedTime: util.GetCurrentTime(), + Id: util.GenerateId(), + Type: "normal-user", + Password: "123", + DisplayName: "Admin", + Avatar: "https://casbin.org/img/casbin.svg", + Email: "admin@example.com", + Phone: "12345678910", + Address: []string{}, + Affiliation: "Example Inc.", + Tag: "staff", + IsAdmin: true, + IsGlobalAdmin: true, + IsForbidden: false, + Properties: make(map[string]string), + } + AddUser(user) +} + +func initBuiltInApplication() { + application := getApplication("admin", "app-built-in") + if application != nil { + return + } + + application = &Application{ + Owner: "admin", + Name: "app-built-in", + CreatedTime: util.GetCurrentTime(), + DisplayName: "Casdoor", + Logo: "https://cdn.casbin.com/logo/logo_1024x256.png", + HomepageUrl: "https://casdoor.org", + Organization: "built-in", + EnablePassword: true, + EnableSignUp: true, + Providers: []*ProviderItem{}, + SignupItems: []*SignupItem{}, + RedirectUris: []string{}, + ExpireInHours: 168, + } + AddApplication(application) +} + +func initBuiltInLdap() { + ldap := GetLdap("ldap-built-in") + if ldap != nil { + return + } + + ldap = &Ldap{ + Id: "ldap-built-in", + Owner: "built-in", + ServerName: "BuildIn LDAP Server", + Host: "example.com", + Port: 389, + Admin: "cn=buildin,dc=example,dc=com", + Passwd: "123", + BaseDn: "ou=BuildIn,dc=example,dc=com", + AutoSync: 0, + LastSync: "", + } + AddLdap(ldap) +} diff --git a/casdoor/object/ldap.go b/casdoor/object/ldap.go new file mode 100644 index 0000000..31d68fc --- /dev/null +++ b/casdoor/object/ldap.go @@ -0,0 +1,381 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "errors" + "fmt" + "github.com/casdoor/casdoor/util" + goldap "github.com/go-ldap/ldap/v3" + "github.com/thanhpk/randstr" + "strings" +) + +type Ldap struct { + Id string `xorm:"varchar(100) notnull pk" json:"id"` + Owner string `xorm:"varchar(100)" json:"owner"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + ServerName string `xorm:"varchar(100)" json:"serverName"` + Host string `xorm:"varchar(100)" json:"host"` + Port int `json:"port"` + Admin string `xorm:"varchar(100)" json:"admin"` + Passwd string `xorm:"varchar(100)" json:"passwd"` + BaseDn string `xorm:"varchar(100)" json:"baseDn"` + + AutoSync int `json:"autoSync"` + LastSync string `xorm:"varchar(100)" json:"lastSync"` +} + +type ldapConn struct { + Conn *goldap.Conn +} + +//type ldapGroup struct { +// GidNumber string +// Cn string +//} + +type ldapUser struct { + UidNumber string + Uid string + Cn string + GidNumber string + //Gcn string + Uuid string + Mail string + Email string + EmailAddress string + TelephoneNumber string + Mobile string + MobileTelephoneNumber string + RegisteredAddress string + PostalAddress string +} + +type LdapRespUser struct { + UidNumber string `json:"uidNumber"` + Uid string `json:"uid"` + Cn string `json:"cn"` + GroupId string `json:"groupId"` + //GroupName string `json:"groupName"` + Uuid string `json:"uuid"` + Email string `json:"email"` + Phone string `json:"phone"` + Address string `json:"address"` +} + +func GetLdapConn(host string, port int, adminUser string, adminPasswd string) (*ldapConn, error) { + conn, err := goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) + if err != nil { + return nil, err + } + + err = conn.Bind(adminUser, adminPasswd) + if err != nil { + return nil, fmt.Errorf("fail to login Ldap server with [%s]", adminUser) + } + + return &ldapConn{Conn: conn}, nil +} + +//FIXME: The Base DN does not necessarily contain the Group +//func (l *ldapConn) GetLdapGroups(baseDn string) (map[string]ldapGroup, error) { +// SearchFilter := "(objectClass=posixGroup)" +// SearchAttributes := []string{"cn", "gidNumber"} +// groupMap := make(map[string]ldapGroup) +// +// searchReq := goldap.NewSearchRequest(baseDn, +// goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false, +// SearchFilter, SearchAttributes, nil) +// searchResult, err := l.Conn.Search(searchReq) +// if err != nil { +// return nil, err +// } +// +// if len(searchResult.Entries) == 0 { +// return nil, errors.New("no result") +// } +// +// for _, entry := range searchResult.Entries { +// var ldapGroupItem ldapGroup +// for _, attribute := range entry.Attributes { +// switch attribute.Name { +// case "gidNumber": +// ldapGroupItem.GidNumber = attribute.Values[0] +// break +// case "cn": +// ldapGroupItem.Cn = attribute.Values[0] +// break +// } +// } +// groupMap[ldapGroupItem.GidNumber] = ldapGroupItem +// } +// +// return groupMap, nil +//} + +func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) { + SearchFilter := "(objectClass=posixAccount)" + SearchAttributes := []string{"uidNumber", "uid", "cn", "gidNumber", "entryUUID", "mail", "email", + "emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"} + + searchReq := goldap.NewSearchRequest(baseDn, + goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false, + SearchFilter, SearchAttributes, nil) + searchResult, err := l.Conn.Search(searchReq) + if err != nil { + return nil, err + } + + if len(searchResult.Entries) == 0 { + return nil, errors.New("no result") + } + + var ldapUsers []ldapUser + + for _, entry := range searchResult.Entries { + var ldapUserItem ldapUser + for _, attribute := range entry.Attributes { + switch attribute.Name { + case "uidNumber": + ldapUserItem.UidNumber = attribute.Values[0] + break + case "uid": + ldapUserItem.Uid = attribute.Values[0] + break + case "cn": + ldapUserItem.Cn = attribute.Values[0] + break + case "gidNumber": + ldapUserItem.GidNumber = attribute.Values[0] + break + case "entryUUID": + ldapUserItem.Uuid = attribute.Values[0] + break + case "mail": + ldapUserItem.Mail = attribute.Values[0] + break + case "email": + ldapUserItem.Email = attribute.Values[0] + break + case "emailAddress": + ldapUserItem.EmailAddress = attribute.Values[0] + break + case "telephoneNumber": + ldapUserItem.TelephoneNumber = attribute.Values[0] + break + case "mobile": + ldapUserItem.Mobile = attribute.Values[0] + break + case "mobileTelephoneNumber": + ldapUserItem.MobileTelephoneNumber = attribute.Values[0] + break + case "registeredAddress": + ldapUserItem.RegisteredAddress = attribute.Values[0] + break + case "postalAddress": + ldapUserItem.PostalAddress = attribute.Values[0] + break + } + } + ldapUsers = append(ldapUsers, ldapUserItem) + } + + return ldapUsers, nil +} + +func AddLdap(ldap *Ldap) bool { + if len(ldap.Id) == 0 { + ldap.Id = util.GenerateId() + } + + if len(ldap.CreatedTime) == 0 { + ldap.CreatedTime = util.GetCurrentTime() + } + + affected, err := adapter.Engine.Insert(ldap) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func CheckLdapExist(ldap *Ldap) bool { + var result []*Ldap + err := adapter.Engine.Find(&result, &Ldap{ + Owner: ldap.Owner, + Host: ldap.Host, + Port: ldap.Port, + Admin: ldap.Admin, + Passwd: ldap.Passwd, + BaseDn: ldap.BaseDn, + }) + if err != nil { + panic(err) + } + + if len(result) > 0 { + return true + } + + return false +} + +func GetLdaps(owner string) []*Ldap { + var ldaps []*Ldap + err := adapter.Engine.Desc("created_time").Find(&ldaps, &Ldap{Owner: owner}) + if err != nil { + panic(err) + } + + return ldaps +} + +func GetLdap(id string) *Ldap { + if util.IsStrsEmpty(id) { + return nil + } + + ldap := Ldap{Id: id} + existed, err := adapter.Engine.Get(&ldap) + if err != nil { + panic(err) + } + + if existed { + return &ldap + } else { + return nil + } +} + +func UpdateLdap(ldap *Ldap) bool { + if GetLdap(ldap.Id) == nil { + return false + } + + affected, err := adapter.Engine.ID(ldap.Id).Cols("owner", "server_name", "host", + "port", "admin", "passwd", "base_dn", "auto_sync").Update(ldap) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteLdap(ldap *Ldap) bool { + affected, err := adapter.Engine.ID(ldap.Id).Delete(&Ldap{}) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]LdapRespUser) { + var existUsers []LdapRespUser + var failedUsers []LdapRespUser + var uuids []string + + for _, user := range users { + uuids = append(uuids, user.Uuid) + } + + existUuids := CheckLdapUuidExist(owner, uuids) + + for _, user := range users { + if len(existUuids) > 0 { + for index, existUuid := range existUuids { + if user.Uuid == existUuid { + existUsers = append(existUsers, user) + existUuids = append(existUuids[:index], existUuids[index+1:]...) + } + } + } + if !AddUser(&User{ + Owner: owner, + Name: buildLdapUserName(user.Uid, user.UidNumber), + CreatedTime: util.GetCurrentTime(), + Password: "123", + DisplayName: user.Cn, + Avatar: "https://casbin.org/img/casbin.svg", + Email: user.Email, + Phone: user.Phone, + Address: []string{user.Address}, + Affiliation: "Example Inc.", + Tag: "staff", + Ldap: user.Uuid, + }) { + failedUsers = append(failedUsers, user) + continue + } + } + + return &existUsers, &failedUsers +} + +func UpdateLdapSyncTime(ldapId string) { + _, err := adapter.Engine.ID(ldapId).Update(&Ldap{LastSync: util.GetCurrentTime()}) + if err != nil { + panic(err) + } +} + +func CheckLdapUuidExist(owner string, uuids []string) []string { + var results []User + var existUuids []string + + //whereStr := "" + //for i, uuid := range uuids { + // if i == 0 { + // whereStr = fmt.Sprintf("'%s'", uuid) + // } else { + // whereStr = fmt.Sprintf(",'%s'", uuid) + // } + //} + + err := adapter.Engine.Where(fmt.Sprintf("ldap IN (%s) AND owner = ?", "'" + strings.Join(uuids, "','") + "'"), owner).Find(&results) + if err != nil { + panic(err) + } + + if len(results) > 0 { + for _, result := range results { + existUuids = append(existUuids, result.Ldap) + } + } + return existUuids +} + +func buildLdapUserName(uid, uidNum string) string { + var result User + uidWithNumber := fmt.Sprintf("%s_%s", uid, uidNum) + + has, err := adapter.Engine.Where("name = ? or name = ?", uid, uidWithNumber).Get(&result) + if err != nil { + panic(err) + } + + if has { + if result.Name == uid { + return uidWithNumber + } + return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6)) + } + + return uid +} diff --git a/casdoor/object/organization.go b/casdoor/object/organization.go new file mode 100644 index 0000000..b048120 --- /dev/null +++ b/casdoor/object/organization.go @@ -0,0 +1,103 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +type Organization struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + DisplayName string `xorm:"varchar(100)" json:"displayName"` + WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"` + Favicon string `xorm:"varchar(100)" json:"favicon"` + PasswordType string `xorm:"varchar(100)" json:"passwordType"` + PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"` + PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"` + DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"` +} + +func GetOrganizations(owner string) []*Organization { + organizations := []*Organization{} + err := adapter.Engine.Desc("created_time").Find(&organizations, &Organization{Owner: owner}) + if err != nil { + panic(err) + } + + return organizations +} + +func getOrganization(owner string, name string) *Organization { + if owner == "" || name == "" { + return nil + } + + organization := Organization{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&organization) + if err != nil { + panic(err) + } + + if existed { + return &organization + } else { + return nil + } +} + +func GetOrganization(id string) *Organization { + owner, name := util.GetOwnerAndNameFromId(id) + return getOrganization(owner, name) +} + +func UpdateOrganization(id string, organization *Organization) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getOrganization(owner, name) == nil { + return false + } + + affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(organization) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddOrganization(organization *Organization) bool { + affected, err := adapter.Engine.Insert(organization) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteOrganization(organization *Organization) bool { + affected, err := adapter.Engine.ID(core.PK{organization.Owner, organization.Name}).Delete(&Organization{}) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func GetOrganizationByUser(user *User) *Organization { + return getOrganization("admin", user.Owner) +} diff --git a/casdoor/object/oss.go b/casdoor/object/oss.go new file mode 100644 index 0000000..a1bd7f0 --- /dev/null +++ b/casdoor/object/oss.go @@ -0,0 +1,130 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "bytes" + "fmt" + + awss3 "github.com/aws/aws-sdk-go/service/s3" + "github.com/qor/oss" + "github.com/qor/oss/aliyun" + //"github.com/qor/oss/qiniu" + "github.com/qor/oss/s3" + "gopkg.in/ini.v1" +) + +var storage oss.StorageInterface +var domain string + +func AliyunInit(section *ini.Section) string { + endpoint := section.Key("endpoint").String() + accessId := section.Key("accessId").String() + accessKey := section.Key("accessKey").String() + domain = section.Key("domain").String() + bucket := section.Key("bucket").String() + if accessId == "" || accessKey == "" || bucket == "" || endpoint == "" { + return "Config oss.conf wrong" + } + storage = aliyun.New(&aliyun.Config{ + AccessID: accessId, + AccessKey: accessKey, + Bucket: bucket, + Endpoint: endpoint, + }) + return "" +} + +//func QiniuInit(section *ini.Section) string { +// endpoint := section.Key("endpoint").String() +// accessId := section.Key("accessId").String() +// accessKey := section.Key("accessKey").String() +// domain = section.Key("domain").String() +// bucket := section.Key("bucket").String() +// region := section.Key("region").String() +// if accessId == "" || accessKey == "" || bucket == "" || endpoint == "" || region == "" { +// return "Config oss.conf wrong" +// } +// storage = qiniu.New(&qiniu.Config{ +// AccessID: accessId, +// AccessKey: accessKey, +// Bucket: bucket, +// Region: region, +// Endpoint: endpoint, +// }) +// return "" +//} + +func Awss3Init(section *ini.Section) string { + endpoint := section.Key("endpoint").String() + accessId := section.Key("accessId").String() + accessKey := section.Key("accessKey").String() + domain = section.Key("domain").String() + bucket := section.Key("bucket").String() + region := section.Key("region").String() + if accessId == "" || accessKey == "" || bucket == "" || endpoint == "" || region == "" { + return "Config oss.conf wrong" + } + storage = s3.New(&s3.Config{ + AccessID: accessId, + AccessKey: accessKey, + Region: region, + Bucket: bucket, + Endpoint: endpoint, + ACL: awss3.BucketCannedACLPublicRead, + }) + return "" +} + +func InitOssClient() { + if storage != nil { + return + } + ossConf, err := ini.Load("./conf/oss.conf") + if err != nil { + panic(err) + return + } + aliyunSection, _ := ossConf.GetSection("aliyun") + qiniuSection, _ := ossConf.GetSection("qiniu") + awss3Section, _ := ossConf.GetSection("s3") + if aliyunSection != nil { + AliyunInit(aliyunSection) + } else if qiniuSection != nil { + //QiniuInit(qiniuSection) + } else { + Awss3Init(awss3Section) + } +} + +func UploadAvatar(username string, avatar []byte) string { + if storage == nil { + InitOssClient() + if storage == nil { + return "oss error" + } + } + + path := fmt.Sprintf("/casdoor/avatar/%s.png", username) + _, err := storage.Put(path, bytes.NewReader(avatar)) + if err != nil { + panic(err) + } + return "" +} + +func GetAvatarPath() string { + return fmt.Sprintf("https://%s/casdoor/avatar/", domain) +} diff --git a/casdoor/object/password.go b/casdoor/object/password.go new file mode 100644 index 0000000..31d477a --- /dev/null +++ b/casdoor/object/password.go @@ -0,0 +1,37 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "crypto/sha256" + "encoding/hex" +) + +func getSha256(data []byte) []byte { + hash := sha256.Sum256(data) + return hash[:] +} + +func getSha256HexDigest(s string) string { + b := getSha256([]byte(s)) + res := hex.EncodeToString(b) + return res +} + +func getSaltedPassword(password string, salt string) string { + hash1 := getSha256HexDigest(password) + res := getSha256HexDigest(hash1 + salt) + return res +} diff --git a/casdoor/object/provider.go b/casdoor/object/provider.go new file mode 100644 index 0000000..6c272c0 --- /dev/null +++ b/casdoor/object/provider.go @@ -0,0 +1,136 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +type Provider struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + DisplayName string `xorm:"varchar(100)" json:"displayName"` + Category string `xorm:"varchar(100)" json:"category"` + Type string `xorm:"varchar(100)" json:"type"` + ClientId string `xorm:"varchar(100)" json:"clientId"` + ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` + + Host string `xorm:"varchar(100)" json:"host"` + Port int `json:"port"` + Title string `xorm:"varchar(100)" json:"title"` + Content string `xorm:"varchar(1000)" json:"content"` + + RegionId string `xorm:"varchar(100)" json:"regionId"` + SignName string `xorm:"varchar(100)" json:"signName"` + TemplateCode string `xorm:"varchar(100)" json:"templateCode"` + AppId string `xorm:"varchar(100)" json:"appId"` + + ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"` +} + +func getMaskedProvider(provider *Provider) *Provider { + p := &Provider{ + Owner: provider.Owner, + Name: provider.Name, + CreatedTime: provider.CreatedTime, + DisplayName: provider.DisplayName, + Category: provider.Category, + Type: provider.Type, + ClientId: provider.ClientId, + } + return p +} + +func GetProviders(owner string) []*Provider { + providers := []*Provider{} + err := adapter.Engine.Desc("created_time").Find(&providers, &Provider{Owner: owner}) + if err != nil { + panic(err) + } + + return providers +} + +func getProvider(owner string, name string) *Provider { + if owner == "" || name == "" { + return nil + } + + provider := Provider{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&provider) + if err != nil { + panic(err) + } + + if existed { + return &provider + } else { + return nil + } +} + +func GetProvider(id string) *Provider { + owner, name := util.GetOwnerAndNameFromId(id) + return getProvider(owner, name) +} + +func GetDefaultHumanCheckProvider() *Provider { + provider := Provider{Owner: "admin", Category: "HumanCheck"} + existed, err := adapter.Engine.Get(&provider) + if err != nil { + panic(err) + } + + if !existed { + return nil + } + + return &provider +} + +func UpdateProvider(id string, provider *Provider) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getProvider(owner, name) == nil { + return false + } + + affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(provider) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddProvider(provider *Provider) bool { + affected, err := adapter.Engine.Insert(provider) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteProvider(provider *Provider) bool { + affected, err := adapter.Engine.ID(core.PK{provider.Owner, provider.Name}).Delete(&Provider{}) + if err != nil { + panic(err) + } + + return affected != 0 +} diff --git a/casdoor/object/provider_item.go b/casdoor/object/provider_item.go new file mode 100644 index 0000000..c74ddc3 --- /dev/null +++ b/casdoor/object/provider_item.go @@ -0,0 +1,42 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +type ProviderItem struct { + Name string `json:"name"` + CanSignUp bool `json:"canSignUp"` + CanSignIn bool `json:"canSignIn"` + CanUnlink bool `json:"canUnlink"` + Prompted bool `json:"prompted"` + AlertType string `json:"alertType"` + Provider *Provider `json:"provider"` +} + +func (application *Application) GetProviderItem(providerName string) *ProviderItem { + for _, providerItem := range application.Providers { + if providerItem.Name == providerName { + return providerItem + } + } + return nil +} + +func (pi *ProviderItem) isProviderVisible() bool { + return pi.Provider.Category == "OAuth" +} + +func (pi *ProviderItem) isProviderPrompted() bool { + return pi.isProviderVisible() && pi.Prompted +} diff --git a/casdoor/object/record.go b/casdoor/object/record.go new file mode 100644 index 0000000..3058bf7 --- /dev/null +++ b/casdoor/object/record.go @@ -0,0 +1,65 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "github.com/casdoor/casdoor/util" +) + +type Records struct { + Id int `xorm:"int notnull pk autoincr" json:"id"` + Record util.Record `xorm:"extends"` +} + +func AddRecord(record *util.Record) bool { + records := new(Records) + records.Record = *record + + affected, err := adapter.Engine.Insert(records) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func GetRecordCount() int { + count, err := adapter.Engine.Count(&Records{}) + if err != nil { + panic(err) + } + + return int(count) +} + +func GetRecords() []*Records { + records := []*Records{} + err := adapter.Engine.Desc("id").Find(&records) + if err != nil { + panic(err) + } + + return records +} + +func GetRecordsByField(record *Records) []*Records { + records := []*Records{} + err := adapter.Engine.Find(&records, record) + if err != nil { + panic(err) + } + + return records +} diff --git a/casdoor/object/signup_item.go b/casdoor/object/signup_item.go new file mode 100644 index 0000000..67a90e6 --- /dev/null +++ b/casdoor/object/signup_item.go @@ -0,0 +1,23 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +type SignupItem struct { + Name string `json:"name"` + Visible bool `json:"visible"` + Required bool `json:"required"` + Prompted bool `json:"prompted"` + Rule string `json:"rule"` +} diff --git a/casdoor/object/sms.go b/casdoor/object/sms.go new file mode 100644 index 0000000..abfed9b --- /dev/null +++ b/casdoor/object/sms.go @@ -0,0 +1,37 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + + "github.com/casdoor/go-sms-sender" +) + +func SendCodeToPhone(provider *Provider, phone, code string) string { + client := go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.RegionId, provider.TemplateCode, provider.AppId) + if client == nil { + return fmt.Sprintf("Unsupported provide type: %s", provider.Type) + } + + param := make(map[string]string) + if provider.Type == "tencent" { + param["0"] = code + } else { + param["code"] = code + } + client.SendMessage(param, phone) + return "" +} diff --git a/casdoor/object/token.go b/casdoor/object/token.go new file mode 100644 index 0000000..821c2f7 --- /dev/null +++ b/casdoor/object/token.go @@ -0,0 +1,263 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "strings" + + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +type Code struct { + Message string `xorm:"varchar(100)" json:"message"` + Code string `xorm:"varchar(100)" json:"code"` +} + +type Token struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + Application string `xorm:"varchar(100)" json:"application"` + Organization string `xorm:"varchar(100)" json:"organization"` + User string `xorm:"varchar(100)" json:"user"` + + Code string `xorm:"varchar(100)" json:"code"` + AccessToken string `xorm:"mediumtext" json:"accessToken"` + ExpiresIn int `json:"expiresIn"` + Scope string `xorm:"varchar(100)" json:"scope"` + TokenType string `xorm:"varchar(100)" json:"tokenType"` +} + +type TokenWrapper struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` +} + +func GetTokens(owner string) []*Token { + tokens := []*Token{} + err := adapter.Engine.Desc("created_time").Find(&tokens, &Token{Owner: owner}) + if err != nil { + panic(err) + } + + return tokens +} + +func getToken(owner string, name string) *Token { + if owner == "" || name == "" { + return nil + } + + token := Token{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&token) + if err != nil { + panic(err) + } + + if existed { + return &token + } else { + return nil + } +} + +func getTokenByCode(code string) *Token { + token := Token{} + existed, err := adapter.Engine.Where("code=?", code).Get(&token) + if err != nil { + panic(err) + } + + if existed { + return &token + } else { + return nil + } +} + +func GetToken(id string) *Token { + owner, name := util.GetOwnerAndNameFromId(id) + return getToken(owner, name) +} + +func UpdateToken(id string, token *Token) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getToken(owner, name) == nil { + return false + } + + affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(token) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddToken(token *Token) bool { + affected, err := adapter.Engine.Insert(token) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteToken(token *Token) bool { + affected, err := adapter.Engine.ID(core.PK{token.Owner, token.Name}).Delete(&Token{}) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string) (string, *Application) { + if responseType != "code" { + return "response_type should be \"code\"", nil + } + + application := GetApplicationByClientId(clientId) + if application == nil { + return "Invalid client_id", nil + } + + validUri := false + for _, tmpUri := range application.RedirectUris { + if strings.Contains(redirectUri, tmpUri) { + validUri = true + break + } + } + if !validUri { + return "redirect_uri doesn't exist in the allowed Redirect URL list", application + } + + return "", application +} + +func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string) *Code { + user := GetUser(userId) + if user == nil { + return &Code{ + Message: "Invalid user_id", + Code: "", + } + } + + msg, application := CheckOAuthLogin(clientId, responseType, redirectUri, scope, state) + if msg != "" { + return &Code{ + Message: msg, + Code: "", + } + } + + accessToken, err := generateJwtToken(application, user) + if err != nil { + panic(err) + } + + token := &Token{ + Owner: application.Owner, + Name: util.GenerateId(), + CreatedTime: util.GetCurrentTime(), + Application: application.Name, + Organization: user.Owner, + User: user.Name, + Code: util.GenerateClientId(), + AccessToken: accessToken, + ExpiresIn: application.ExpireInHours * 60, + Scope: scope, + TokenType: "Bearer", + } + AddToken(token) + + return &Code{ + Message: "", + Code: token.Code, + } +} + +func GetOAuthToken(grantType string, clientId string, clientSecret string, code string) *TokenWrapper { + application := GetApplicationByClientId(clientId) + if application == nil { + return &TokenWrapper{ + AccessToken: "error: invalid client_id", + TokenType: "", + ExpiresIn: 0, + Scope: "", + } + } + + if grantType != "authorization_code" { + return &TokenWrapper{ + AccessToken: "error: grant_type should be \"authorization_code\"", + TokenType: "", + ExpiresIn: 0, + Scope: "", + } + } + + if code == "" { + return &TokenWrapper{ + AccessToken: "error: code should not be empty", + TokenType: "", + ExpiresIn: 0, + Scope: "", + } + } + + token := getTokenByCode(code) + if token == nil { + return &TokenWrapper{ + AccessToken: "error: invalid code", + TokenType: "", + ExpiresIn: 0, + Scope: "", + } + } + + if application.Name != token.Application { + return &TokenWrapper{ + AccessToken: "error: the token is for wrong application (client_id)", + TokenType: "", + ExpiresIn: 0, + Scope: "", + } + } + + if application.ClientSecret != clientSecret { + return &TokenWrapper{ + AccessToken: "error: invalid client_secret", + TokenType: "", + ExpiresIn: 0, + Scope: "", + } + } + + tokenWrapper := &TokenWrapper{ + AccessToken: token.AccessToken, + TokenType: token.TokenType, + ExpiresIn: token.ExpiresIn, + Scope: token.Scope, + } + + return tokenWrapper +} diff --git a/casdoor/object/token_jwt.go b/casdoor/object/token_jwt.go new file mode 100644 index 0000000..bcf8c59 --- /dev/null +++ b/casdoor/object/token_jwt.go @@ -0,0 +1,87 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "time" + + "github.com/dgrijalva/jwt-go" +) + +var jwtSecret = []byte("CasdoorSecret") + +type Claims struct { + Organization string `json:"organization"` + Username string `json:"username"` + Type string `json:"type"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Email string `json:"email"` + Phone string `json:"phone"` + Affiliation string `json:"affiliation"` + Tag string `json:"tag"` + Language string `json:"language"` + Score int `json:"score"` + IsAdmin bool `json:"isAdmin"` + jwt.StandardClaims +} + +func generateJwtToken(application *Application, user *User) (string, error) { + nowTime := time.Now() + expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour) + + claims := Claims{ + Organization: user.Owner, + Username: user.Name, + Type: user.Type, + Name: user.DisplayName, + Avatar: user.Avatar, + Email: user.Email, + Phone: user.Phone, + Affiliation: user.Affiliation, + Tag: user.Tag, + Language: user.Language, + Score: user.Score, + IsAdmin: user.IsAdmin, + StandardClaims: jwt.StandardClaims{ + Audience: application.ClientId, + ExpiresAt: expireTime.Unix(), + Id: "", + IssuedAt: nowTime.Unix(), + Issuer: "casdoor", + NotBefore: nowTime.Unix(), + Subject: user.Id, + }, + } + + tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := tokenClaims.SignedString(jwtSecret) + + return token, err +} + +func ParseJwtToken(token string) (*Claims, error) { + tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + + if tokenClaims != nil { + if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { + return claims, nil + } + } + + return nil, err +} diff --git a/casdoor/object/user.go b/casdoor/object/user.go new file mode 100644 index 0000000..1804abb --- /dev/null +++ b/casdoor/object/user.go @@ -0,0 +1,261 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +type User struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"` + + Id string `xorm:"varchar(100)" json:"id"` + Type string `xorm:"varchar(100)" json:"type"` + Password string `xorm:"varchar(100)" json:"password"` + DisplayName string `xorm:"varchar(100)" json:"displayName"` + Avatar string `xorm:"varchar(255)" json:"avatar"` + Email string `xorm:"varchar(100)" json:"email"` + Phone string `xorm:"varchar(100)" json:"phone"` + Address []string `json:"address"` + Affiliation string `xorm:"varchar(100)" json:"affiliation"` + Tag string `xorm:"varchar(100)" json:"tag"` + Language string `xorm:"varchar(100)" json:"language"` + Score int `json:"score"` + IsAdmin bool `json:"isAdmin"` + IsGlobalAdmin bool `json:"isGlobalAdmin"` + IsForbidden bool `json:"isForbidden"` + SignupApplication string `xorm:"varchar(100)" json:"signupApplication"` + Hash string `xorm:"varchar(100)" json:"hash"` + PreHash string `xorm:"varchar(100)" json:"preHash"` + + Github string `xorm:"varchar(100)" json:"github"` + Google string `xorm:"varchar(100)" json:"google"` + QQ string `xorm:"qq varchar(100)" json:"qq"` + WeChat string `xorm:"wechat varchar(100)" json:"wechat"` + Facebook string `xorm:"facebook varchar(100)" json:"facebook"` + DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"` + Weibo string `xorm:"weibo varchar(100)" json:"weibo"` + Gitee string `xorm:"gitee varchar(100)" json:"gitee"` + LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"` + + Ldap string `xorm:"ldap varchar(100)" json:"ldap"` + Properties map[string]string `json:"properties"` +} + +func GetGlobalUsers() []*User { + users := []*User{} + err := adapter.Engine.Desc("created_time").Find(&users) + if err != nil { + panic(err) + } + + return users +} + +func GetUsers(owner string) []*User { + users := []*User{} + err := adapter.Engine.Desc("created_time").Find(&users, &User{Owner: owner}) + if err != nil { + panic(err) + } + + return users +} + +func getUser(owner string, name string) *User { + if owner == "" || name == "" { + return nil + } + + user := User{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&user) + if err != nil { + panic(err) + } + + if existed { + return &user + } else { + return nil + } +} + +func GetUser(id string) *User { + owner, name := util.GetOwnerAndNameFromId(id) + return getUser(owner, name) +} + +func GetMaskedUser(user *User) *User { + if user == nil { + return nil + } + + if user.Password != "" { + user.Password = "***" + } + return user +} + +func GetMaskedUsers(users []*User) []*User { + for _, user := range users { + user = GetMaskedUser(user) + } + return users +} + +func GetLastUser(owner string) *User { + user := User{Owner: owner} + existed, err := adapter.Engine.Desc("created_time", "id").Get(&user) + if err != nil { + panic(err) + } + + if existed { + return &user + } else { + return nil + } +} + +func UpdateUser(id string, user *User) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getUser(owner, name) == nil { + return false + } + + user.UpdateUserHash() + + affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols("owner", "display_name", "avatar", + "address", "language", "affiliation", "score", "tag", "is_admin", "is_global_admin", "is_forbidden", + "hash", "properties").Update(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func UpdateUserInternal(id string, user *User) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getUser(owner, name) == nil { + return false + } + + user.UpdateUserHash() + + affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func UpdateUserForOriginal(user *User) bool { + affected, err := adapter.Engine.ID(core.PK{user.Owner, user.Name}).Cols("display_name", "password", "phone", "avatar", "affiliation", "score", "is_forbidden", "hash", "pre_hash").Update(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddUser(user *User) bool { + if user.Id == "" { + user.Id = util.GenerateId() + } + + organization := GetOrganizationByUser(user) + user.UpdateUserPassword(organization) + + user.UpdateUserHash() + user.PreHash = user.Hash + + affected, err := adapter.Engine.Insert(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddUsers(users []*User) bool { + if len(users) == 0 { + return false + } + + organization := GetOrganizationByUser(users[0]) + for _, user := range users { + user.UpdateUserPassword(organization) + + user.UpdateUserHash() + user.PreHash = user.Hash + } + + affected, err := adapter.Engine.Insert(users) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func AddUsersSafe(users []*User) bool { + batchSize := 1000 + + if len(users) == 0 { + return false + } + + affected := false + for i := 0; i < (len(users)-1)/batchSize+1; i++ { + start := i * batchSize + end := (i + 1) * batchSize + if end > len(users) { + end = len(users) + } + + tmp := users[start:end] + fmt.Printf("Add users: [%d - %d].\n", start, end) + if AddUsers(tmp) { + affected = true + } + } + + return affected +} + +func DeleteUser(user *User) bool { + affected, err := adapter.Engine.ID(core.PK{user.Owner, user.Name}).Delete(&User{}) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func LinkUserAccount(user *User, field string, value string) bool { + return SetUserField(user, field, value) +} + +func (user *User) GetId() string { + return fmt.Sprintf("%s/%s", user.Owner, user.Name) +} diff --git a/casdoor/object/user_cred.go b/casdoor/object/user_cred.go new file mode 100644 index 0000000..46a2548 --- /dev/null +++ b/casdoor/object/user_cred.go @@ -0,0 +1,38 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "strconv" + "strings" + + "github.com/casdoor/casdoor/util" +) + +func calculateHash(user *User) string { + s := strings.Join([]string{user.Id, user.Password, user.DisplayName, user.Avatar, user.Phone, strconv.Itoa(user.Score)}, "|") + return util.GetMd5Hash(s) +} + +func (user *User) UpdateUserHash() { + hash := calculateHash(user) + user.Hash = hash +} + +func (user *User) UpdateUserPassword(organization *Organization) { + if organization.PasswordType == "salt" { + user.Password = getSaltedPassword(user.Password, organization.PasswordSalt) + } +} diff --git a/casdoor/object/user_test.go b/casdoor/object/user_test.go new file mode 100644 index 0000000..4f939d4 --- /dev/null +++ b/casdoor/object/user_test.go @@ -0,0 +1,80 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + "testing" + + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +func updateUserColumn(column string, user *User) bool { + affected, err := adapter.Engine.ID(core.PK{user.Owner, user.Name}).Cols(column).Update(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func TestSyncAvatarsFromGitHub(t *testing.T) { + InitConfig() + + users := GetGlobalUsers() + for _, user := range users { + if user.Github == "" { + continue + } + + user.Avatar = fmt.Sprintf("https://avatars.githubusercontent.com/%s", user.Github) + updateUserColumn("avatar", user) + } +} + +func TestSyncIds(t *testing.T) { + InitConfig() + + users := GetGlobalUsers() + for _, user := range users { + if user.Id != "" { + continue + } + + user.Id = util.GenerateId() + updateUserColumn("id", user) + } +} + +func TestSyncHashes(t *testing.T) { + InitConfig() + + users := GetGlobalUsers() + for _, user := range users { + if user.Hash != "" { + continue + } + + user.UpdateUserHash() + updateUserColumn("hash", user) + } +} + +func TestGetSaltedPassword(t *testing.T) { + password := "123456" + salt := "123" + fmt.Printf("%s -> %s\n", password, getSaltedPassword(password, salt)) +} diff --git a/casdoor/object/user_util.go b/casdoor/object/user_util.go new file mode 100644 index 0000000..2d1b9cb --- /dev/null +++ b/casdoor/object/user_util.go @@ -0,0 +1,156 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + "reflect" + "strings" + + "github.com/casdoor/casdoor/idp" + "xorm.io/core" +) + +func GetUserByField(organizationName string, field string, value string) *User { + if field == "" || value == "" { + return nil + } + + user := User{Owner: organizationName} + existed, err := adapter.Engine.Where(fmt.Sprintf("%s=?", field), value).Get(&user) + if err != nil { + panic(err) + } + + if existed { + return &user + } else { + return nil + } +} + +func HasUserByField(organizationName string, field string, value string) bool { + return GetUserByField(organizationName, field, value) != nil +} + +func GetUserByFields(organization string, field string) *User { + // check username + user := GetUserByField(organization, "name", field) + if user != nil { + return user + } + + // check email + user = GetUserByField(organization, "email", field) + if user != nil { + return user + } + + // check phone + user = GetUserByField(organization, "phone", field) + if user != nil { + return user + } + + return nil +} + +func SetUserField(user *User, field string, value string) bool { + if field == "password" { + organization := GetOrganizationByUser(user) + user.UpdateUserPassword(organization) + value = user.Password + } + + affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{field: value}) + if err != nil { + panic(err) + } + + user = getUser(user.Owner, user.Name) + user.UpdateUserHash() + _, err = adapter.Engine.ID(core.PK{user.Owner, user.Name}).Cols("hash").Update(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func GetUserField(user *User, field string) string { + // https://socketloop.com/tutorials/golang-how-to-get-struct-field-and-value-by-name + u := reflect.ValueOf(user) + f := reflect.Indirect(u).FieldByName(field) + return f.String() +} + +func setUserProperty(user *User, field string, value string) { + if value == "" { + delete(user.Properties, field) + } else { + user.Properties[field] = value + } +} + +func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo) bool { + if userInfo.Id != "" { + propertyName := fmt.Sprintf("oauth_%s_id", providerType) + setUserProperty(user, propertyName, userInfo.Id) + } + if userInfo.Username != "" { + propertyName := fmt.Sprintf("oauth_%s_username", providerType) + setUserProperty(user, propertyName, userInfo.Username) + } + if userInfo.DisplayName != "" { + propertyName := fmt.Sprintf("oauth_%s_displayName", providerType) + setUserProperty(user, propertyName, userInfo.DisplayName) + if user.DisplayName == "" { + user.DisplayName = userInfo.DisplayName + } + } + if userInfo.Email != "" { + propertyName := fmt.Sprintf("oauth_%s_email", providerType) + setUserProperty(user, propertyName, userInfo.Email) + if user.Email == "" { + user.Email = userInfo.Email + } + } + if userInfo.AvatarUrl != "" { + propertyName := fmt.Sprintf("oauth_%s_avatarUrl", providerType) + setUserProperty(user, propertyName, userInfo.AvatarUrl) + if user.Avatar == "" || user.Avatar == organization.DefaultAvatar { + user.Avatar = userInfo.AvatarUrl + } + } + + affected := UpdateUserInternal(user.GetId(), user) + return affected +} + +func ClearUserOAuthProperties(user *User, providerType string) bool { + for k := range user.Properties { + prefix := fmt.Sprintf("oauth_%s_", providerType) + if strings.HasPrefix(k, prefix) { + delete(user.Properties, k) + } + } + + affected, err := adapter.Engine.ID(core.PK{user.Owner, user.Name}).Cols("properties").Update(user) + if err != nil { + panic(err) + } + + return affected != 0 +} diff --git a/casdoor/object/verification.go b/casdoor/object/verification.go new file mode 100644 index 0000000..98bf6bb --- /dev/null +++ b/casdoor/object/verification.go @@ -0,0 +1,171 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + "math/rand" + "time" + + "github.com/astaxie/beego" + "github.com/casdoor/casdoor/util" + "xorm.io/core" +) + +type VerificationRecord struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + RemoteAddr string `xorm:"varchar(100)"` + Type string `xorm:"varchar(10)"` + User string `xorm:"varchar(100) notnull"` + Provider string `xorm:"varchar(100) notnull"` + Receiver string `xorm:"varchar(100) notnull"` + Code string `xorm:"varchar(10) notnull"` + Time int64 `xorm:"notnull"` + IsUsed bool +} + +func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) string { + if provider == nil { + return "Please set an Email provider first" + } + + sender := organization.DisplayName + title := provider.Title + code := getRandomCode(5) + // "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes." + content := fmt.Sprintf(provider.Content, code) + + if result := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); len(result) != 0 { + return result + } + + return SendEmail(provider, title, content, dest, sender) +} + +func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) string { + if provider == nil { + return "Please set a SMS provider first" + } + + code := getRandomCode(5) + if result := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); len(result) != 0 { + return result + } + + return SendCodeToPhone(provider, dest, code) +} + +func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordType, dest, code string) string { + var record VerificationRecord + record.RemoteAddr = remoteAddr + record.Type = recordType + if user != nil { + record.User = user.GetId() + } + has, err := adapter.Engine.Desc("created_time").Get(&record) + if err != nil { + panic(err) + } + + now := time.Now().Unix() + if has && now-record.Time < 60 { + return "You can only send one code in 60s." + } + + record.Owner = provider.Owner + record.Name = util.GenerateId() + record.CreatedTime = util.GetCurrentTime() + if user != nil { + record.User = user.GetId() + } + record.Provider = provider.Name + + record.Receiver = dest + record.Code = code + record.Time = now + record.IsUsed = false + + _, err = adapter.Engine.Insert(record) + if err != nil { + panic(err) + } + + return "" +} + +func getVerificationRecord(dest string) *VerificationRecord { + var record VerificationRecord + record.Receiver = dest + has, err := adapter.Engine.Desc("time").Where("is_used = 0").Get(&record) + if err != nil { + panic(err) + } + if !has { + return nil + } + return &record +} + +func CheckVerificationCode(dest, code string) string { + record := getVerificationRecord(dest) + + if record == nil { + return "Code has not been sent yet!" + } + + timeout, err := beego.AppConfig.Int64("verificationCodeTimeout") + if err != nil { + panic(err) + } + + now := time.Now().Unix() + if now-record.Time > timeout*60 { + return fmt.Sprintf("You should verify your code in %d min!", timeout) + } + + if record.Code != code { + return "Wrong code!" + } + + return "" +} + +func DisableVerificationCode(dest string) { + record := getVerificationRecord(dest) + if record == nil { + return + } + + record.IsUsed = true + _, err := adapter.Engine.ID(core.PK{record.Owner, record.Name}).AllCols().Update(record) + if err != nil { + panic(err) + } +} + +// from Casnode/object/validateCode.go line 116 +var stdNums = []byte("0123456789") + +func getRandomCode(length int) string { + var result []byte + r := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < length; i++ { + result = append(result, stdNums[r.Intn(len(stdNums))]) + } + return string(result) +} diff --git a/casdoor/original/adapter.go b/casdoor/original/adapter.go new file mode 100644 index 0000000..39d0409 --- /dev/null +++ b/casdoor/original/adapter.go @@ -0,0 +1,50 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +import ( + "github.com/astaxie/beego" + "github.com/casdoor/casdoor/object" + _ "github.com/go-sql-driver/mysql" // db = mysql + //_ "github.com/lib/pq" // db = postgres +) + +var adapter *object.Adapter + +func initConfig() { + err := beego.LoadAppConfig("ini", "../conf/app.conf") + if err != nil { + panic(err) + } + + initAdapter() +} + +func initAdapter() { + if dbName == "dbName" { + adapter = nil + return + } + + adapter = object.NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"), dbName) + createTable(adapter) +} + +func createTable(a *object.Adapter) { + err := a.Engine.Sync2(new(User)) + if err != nil { + panic(err) + } +} diff --git a/casdoor/original/affiliation.go b/casdoor/original/affiliation.go new file mode 100644 index 0000000..b89a885 --- /dev/null +++ b/casdoor/original/affiliation.go @@ -0,0 +1,44 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +type Affiliation struct { + Id int `xorm:"int notnull pk autoincr" json:"id"` + Name string `xorm:"varchar(128)" json:"name"` +} + +func (Affiliation) TableName() string { + return affiliationTableName +} + +func getAffiliations() []*Affiliation { + affiliations := []*Affiliation{} + err := adapter.Engine.Asc("id").Find(&affiliations) + if err != nil { + panic(err) + } + + return affiliations +} + +func getAffiliationMap() ([]*Affiliation, map[int]string) { + affiliations := getAffiliations() + + m := map[int]string{} + for _, affiliation := range affiliations { + m[affiliation.Id] = affiliation.Name + } + return affiliations, m +} diff --git a/casdoor/original/conf.go b/casdoor/original/conf.go new file mode 100644 index 0000000..c584078 --- /dev/null +++ b/casdoor/original/conf.go @@ -0,0 +1,22 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +var dbName = "dbName" +var userTableName = "userTableName" +var affiliationTableName = "affiliationTableName" +var avatarBaseUrl = "https://cdn.example.com/" + +var orgName = "orgName" diff --git a/casdoor/original/cron.go b/casdoor/original/cron.go new file mode 100644 index 0000000..8af8927 --- /dev/null +++ b/casdoor/original/cron.go @@ -0,0 +1,23 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +import "github.com/mileusna/crontab" + +var ctab *crontab.Crontab + +func init() { + ctab = crontab.New() +} diff --git a/casdoor/original/public_api.go b/casdoor/original/public_api.go new file mode 100644 index 0000000..da68ac5 --- /dev/null +++ b/casdoor/original/public_api.go @@ -0,0 +1,59 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +import ( + "fmt" + + "github.com/casdoor/casdoor/object" +) + +func isEnabled() bool { + if adapter == nil { + initAdapter() + if adapter == nil { + return false + } + } + return true +} + +func AddUserToOriginalDatabase(user *object.User) { + if user.Owner != orgName { + return + } + + if !isEnabled() { + return + } + + updatedOUser := createOriginalUserFromUser(user) + addUser(updatedOUser) + fmt.Printf("Add from user to oUser: %v\n", updatedOUser) +} + +func UpdateUserToOriginalDatabase(user *object.User) { + if user.Owner != orgName { + return + } + + if !isEnabled() { + return + } + + updatedOUser := createOriginalUserFromUser(user) + updateUser(updatedOUser) + fmt.Printf("Update from user to oUser: %v\n", updatedOUser) +} diff --git a/casdoor/original/sync.go b/casdoor/original/sync.go new file mode 100644 index 0000000..6c3dbe8 --- /dev/null +++ b/casdoor/original/sync.go @@ -0,0 +1,151 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +import ( + "fmt" + "strconv" + "strings" + + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +func getFullAvatarUrl(avatar string) string { + if !strings.HasPrefix(avatar, "https://") { + return fmt.Sprintf("%s%s", avatarBaseUrl, avatar) + } + return avatar +} + +func getPartialAvatarUrl(avatar string) string { + if strings.HasPrefix(avatar, avatarBaseUrl) { + return avatar[len(avatarBaseUrl):] + } + return avatar +} + +func createUserFromOriginalUser(originalUser *User, affiliationMap map[int]string) *object.User { + affiliation := "" + if originalUser.SchoolId != 0 { + var ok bool + affiliation, ok = affiliationMap[originalUser.SchoolId] + if !ok { + panic(fmt.Sprintf("SchoolId not found: %d", originalUser.SchoolId)) + } + } + + user := &object.User{ + Owner: orgName, + Name: strconv.Itoa(originalUser.Id), + CreatedTime: util.GetCurrentTime(), + Id: strconv.Itoa(originalUser.Id), + Type: "normal-user", + Password: originalUser.Password, + DisplayName: originalUser.Name, + Avatar: getFullAvatarUrl(originalUser.Avatar), + Email: "", + Phone: originalUser.Cellphone, + Address: []string{}, + Affiliation: affiliation, + Score: originalUser.SchoolId, + IsAdmin: false, + IsGlobalAdmin: false, + IsForbidden: originalUser.Deleted != 0, + Properties: map[string]string{}, + } + return user +} + +func createOriginalUserFromUser(user *object.User) *User { + deleted := 0 + if user.IsForbidden { + deleted = 1 + } + + originalUser := &User{ + Id: util.ParseInt(user.Id), + Name: user.DisplayName, + Password: user.Password, + Cellphone: user.Phone, + SchoolId: user.Score, + Avatar: getPartialAvatarUrl(user.Avatar), + Deleted: deleted, + } + return originalUser +} + +func syncUsers() { + fmt.Printf("Running syncUsers()..\n") + + users, userMap := getUserMap() + oUsers, oUserMap := getUserMapOriginal() + fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers)) + + _, affiliationMap := getAffiliationMap() + + newUsers := []*object.User{} + for _, oUser := range oUsers { + id := strconv.Itoa(oUser.Id) + if _, ok := userMap[id]; !ok { + newUser := createUserFromOriginalUser(oUser, affiliationMap) + fmt.Printf("New user: %v\n", newUser) + newUsers = append(newUsers, newUser) + } else { + user := userMap[id] + oHash := calculateHash(oUser) + + if user.Hash == user.PreHash { + if user.Hash != oHash { + updatedUser := createUserFromOriginalUser(oUser, affiliationMap) + updatedUser.Hash = oHash + updatedUser.PreHash = oHash + object.UpdateUserForOriginal(updatedUser) + fmt.Printf("Update from oUser to user: %v\n", updatedUser) + } + } else { + if user.PreHash == oHash { + updatedOUser := createOriginalUserFromUser(user) + updateUser(updatedOUser) + fmt.Printf("Update from user to oUser: %v\n", updatedOUser) + + // update preHash + user.PreHash = user.Hash + object.SetUserField(user, "pre_hash", user.PreHash) + } else { + if user.Hash == oHash { + // update preHash + user.PreHash = user.Hash + object.SetUserField(user, "pre_hash", user.PreHash) + } else { + updatedUser := createUserFromOriginalUser(oUser, affiliationMap) + updatedUser.Hash = oHash + updatedUser.PreHash = oHash + object.UpdateUserForOriginal(updatedUser) + fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser) + } + } + } + } + } + object.AddUsersSafe(newUsers) + + for _, user := range users { + id := user.Id + if _, ok := oUserMap[id]; !ok { + panic(fmt.Sprintf("New original user: cannot create now, user = %v", user)) + } + } +} diff --git a/casdoor/original/user.go b/casdoor/original/user.go new file mode 100644 index 0000000..8063a2d --- /dev/null +++ b/casdoor/original/user.go @@ -0,0 +1,32 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +import "github.com/casdoor/casdoor/object" + +func getUsers() []*object.User { + users := object.GetUsers(orgName) + return users +} + +func getUserMap() ([]*object.User, map[string]*object.User) { + users := getUsers() + + m := map[string]*object.User{} + for _, user := range users { + m[user.Name] = user + } + return users, m +} diff --git a/casdoor/original/user_original.go b/casdoor/original/user_original.go new file mode 100644 index 0000000..ade91d6 --- /dev/null +++ b/casdoor/original/user_original.go @@ -0,0 +1,79 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +import ( + "strconv" + "strings" + + "github.com/casdoor/casdoor/util" +) + +type User struct { + Id int `xorm:"int notnull pk autoincr" json:"id"` + Name string `xorm:"varchar(128)" json:"name"` + Password string `xorm:"varchar(128)" json:"password"` + Cellphone string `xorm:"varchar(128)" json:"cellphone"` + SchoolId int `json:"schoolId"` + Avatar string `xorm:"varchar(128)" json:"avatar"` + Deleted int `xorm:"tinyint(1)" json:"deleted"` +} + +func (User) TableName() string { + return userTableName +} + +func getUsersOriginal() []*User { + users := []*User{} + err := adapter.Engine.Asc("id").Find(&users) + if err != nil { + panic(err) + } + + return users +} + +func getUserMapOriginal() ([]*User, map[string]*User) { + users := getUsersOriginal() + + m := map[string]*User{} + for _, user := range users { + m[strconv.Itoa(user.Id)] = user + } + return users, m +} + +func addUser(user *User) bool { + affected, err := adapter.Engine.Insert(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func updateUser(user *User) bool { + affected, err := adapter.Engine.ID(user.Id).Cols("name", "password", "cellphone", "school_id", "avatar", "deleted").Update(user) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func calculateHash(user *User) string { + s := strings.Join([]string{strconv.Itoa(user.Id), user.Password, user.Name, getFullAvatarUrl(user.Avatar), user.Cellphone, strconv.Itoa(user.SchoolId)}, "|") + return util.GetMd5Hash(s) +} diff --git a/casdoor/original/user_original_test.go b/casdoor/original/user_original_test.go new file mode 100644 index 0000000..8db0515 --- /dev/null +++ b/casdoor/original/user_original_test.go @@ -0,0 +1,49 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package original + +import ( + "fmt" + "testing" + "time" + + "github.com/casdoor/casdoor/object" +) + +func TestGetUsers(t *testing.T) { + initConfig() + initAdapter() + + users := getUsersOriginal() + for _, user := range users { + fmt.Printf("%v\n", user) + } +} + +func TestSyncUsers(t *testing.T) { + initConfig() + initAdapter() + object.InitAdapter() + + syncUsers() + + // run at every minute + schedule := "* * * * *" + err := ctab.AddJob(schedule, syncUsers) + if err != nil { + panic(err) + } + time.Sleep(time.Duration(1<<63 - 1)) +} diff --git a/casdoor/routers/authz_filter.go b/casdoor/routers/authz_filter.go new file mode 100644 index 0000000..2540b27 --- /dev/null +++ b/casdoor/routers/authz_filter.go @@ -0,0 +1,148 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routers + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/astaxie/beego/context" + "github.com/casdoor/casdoor/authz" + "github.com/casdoor/casdoor/controllers" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +type Object struct { + Owner string `json:"owner"` + Name string `json:"name"` +} + +func getUsernameByClientIdSecret(ctx *context.Context) string { + requestUri := ctx.Request.RequestURI + clientId := parseQuery(requestUri, "clientId") + clientSecret := parseQuery(requestUri, "clientSecret") + if len(clientId) == 0 || len(clientSecret) == 0 { + return "" + } + + app := object.GetApplicationByClientId(clientId) + if app == nil || app.ClientSecret != clientSecret { + return "" + } + return "built-in/service" +} + +func getUsername(ctx *context.Context) (username string) { + defer func() { + if r := recover(); r != nil { + username = getUsernameByClientIdSecret(ctx) + } + }() + + // bug in Beego: this call will panic when file session store is empty + // so we catch the panic + username = ctx.Input.Session("username").(string) + + if len(username) == 0 { + username = getUsernameByClientIdSecret(ctx) + } + + return +} + +func getSubject(ctx *context.Context) (string, string) { + username := getUsername(ctx) + if username == "" { + return "anonymous", "anonymous" + } + + // username == "built-in/admin" + tokens := strings.Split(username, "/") + owner := tokens[0] + name := tokens[1] + return owner, name +} + +func getObject(ctx *context.Context) (string, string) { + method := ctx.Request.Method + if method == http.MethodGet { + query := ctx.Request.URL.RawQuery + // query == "?id=built-in/admin" + idParamValue := parseQuery(query, "id") + if idParamValue == "" { + return "", "" + } + return parseSlash(idParamValue) + } else { + body := ctx.Input.RequestBody + + if len(body) == 0 { + return "", "" + } + + var obj Object + err := json.Unmarshal(body, &obj) + if err != nil { + //panic(err) + return "", "" + } + return obj.Owner, obj.Name + } +} + +func denyRequest(ctx *context.Context) { + w := ctx.ResponseWriter + w.WriteHeader(403) + resp := &controllers.Response{Status: "error", Msg: "Unauthorized operation"} + _, err := w.Write([]byte(util.StructToJson(resp))) + if err != nil { + panic(err) + } +} + +func willLog(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool { + if subOwner == "anonymous" && subName == "anonymous" && method == "GET" && (urlPath == "/api/get-account" || urlPath == "/api/get-app-login") && objOwner == "" && objName == "" { + return false + } + return true +} + +func AuthzFilter(ctx *context.Context) { + subOwner, subName := getSubject(ctx) + method := ctx.Request.Method + urlPath := ctx.Request.URL.Path + objOwner, objName := getObject(ctx) + + isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName) + + result := "deny" + if isAllowed { + result = "allow" + } + + if willLog(subOwner, subName, method, urlPath, objOwner, objName) { + logLine := fmt.Sprintf("subOwner = %s, subName = %s, method = %s, urlPath = %s, obj.Owner = %s, obj.Name = %s, result = %s", + subOwner, subName, method, urlPath, objOwner, objName, result) + fmt.Println(logLine) + util.LogInfo(ctx, logLine) + } + + if !isAllowed { + denyRequest(ctx) + } +} diff --git a/casdoor/routers/auto_login_filter.go b/casdoor/routers/auto_login_filter.go new file mode 100644 index 0000000..a8b9721 --- /dev/null +++ b/casdoor/routers/auto_login_filter.go @@ -0,0 +1,95 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routers + +import ( + "fmt" + "net/url" + + "github.com/astaxie/beego/context" + "github.com/casdoor/casdoor/controllers" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +func getSessionUser(ctx *context.Context) string { + user := ctx.Input.CruSession.Get("username") + if user == nil { + return "" + } + + return user.(string) +} + +func setSessionUser(ctx *context.Context, user string) { + err := ctx.Input.CruSession.Set("username", user) + if err != nil { + panic(err) + } + + // https://github.com/beego/beego/issues/3445#issuecomment-455411915 + ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter) +} + +func returnRequest(ctx *context.Context, msg string) { + w := ctx.ResponseWriter + w.WriteHeader(200) + resp := &controllers.Response{Status: "error", Msg: msg} + _, err := w.Write([]byte(util.StructToJson(resp))) + if err != nil { + panic(err) + } +} + +func AutoLoginFilter(ctx *context.Context) { + //if getSessionUser(ctx) != "" { + // return + //} + + query := ctx.Request.URL.RawQuery + queryMap, err := url.ParseQuery(query) + if err != nil { + panic(err) + } + + // "/page?access_token=123" + accessToken := queryMap.Get("accessToken") + if accessToken != "" { + claims, err := object.ParseJwtToken(accessToken) + if err != nil { + returnRequest(ctx, "Invalid JWT token") + return + } + + userId := fmt.Sprintf("%s/%s", claims.Organization, claims.Username) + setSessionUser(ctx, userId) + return + } + + // "/page?username=abc&password=123" + userId := queryMap.Get("username") + password := queryMap.Get("password") + if userId != "" && password != "" { + owner, name := util.GetOwnerAndNameFromId(userId) + _, msg := object.CheckUserLogin(owner, name, password) + if msg != "" { + returnRequest(ctx, msg) + return + } + + setSessionUser(ctx, userId) + return + } +} diff --git a/casdoor/routers/record.go b/casdoor/routers/record.go new file mode 100644 index 0000000..4cbe53c --- /dev/null +++ b/casdoor/routers/record.go @@ -0,0 +1,70 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routers + +import ( + "strings" + + "github.com/astaxie/beego/context" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +func getUser(ctx *context.Context) (username string) { + defer func() { + if r := recover(); r != nil { + username = getUserByClientIdSecret(ctx) + } + }() + + username = ctx.Input.Session("username").(string) + + if username == "" { + username = getUserByClientIdSecret(ctx) + } + + return +} + +func getUserByClientIdSecret(ctx *context.Context) string { + requestUri := ctx.Request.RequestURI + clientId := parseQuery(requestUri, "clientId") + clientSecret := parseQuery(requestUri, "clientSecret") + if len(clientId) == 0 || len(clientSecret) == 0 { + return "" + } + + app := object.GetApplicationByClientId(clientId) + if app == nil || app.ClientSecret != clientSecret { + return "" + } + return app.Organization+"/"+app.Name +} + +func RecordMessage(ctx *context.Context) { + if ctx.Request.URL.Path != "/api/login" { + user := getUser(ctx) + userinfo := strings.Split(user,"/") + if user == "" { + userinfo = append(userinfo,"") + } + record := util.Records(ctx) + record.Organization = userinfo[0] + record.Username = userinfo[1] + + object.AddRecord(record) + } +} + diff --git a/casdoor/routers/router.go b/casdoor/routers/router.go new file mode 100644 index 0000000..d7379f1 --- /dev/null +++ b/casdoor/routers/router.go @@ -0,0 +1,98 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @APIVersion 1.0.0 +// @Title Casdoor API +// @Description Documentation of Casdoor API +// @Contact admin@casbin.org +package routers + +import ( + "github.com/astaxie/beego" + + "github.com/casdoor/casdoor/controllers" +) + +func init() { + initAPI() +} + +func initAPI() { + ns := + beego.NewNamespace("/api", + beego.NSNamespace("/api", + beego.NSInclude( + &controllers.ApiController{}, + ), + ), + ) + beego.AddNamespace(ns) + + beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup") + beego.Router("/api/login", &controllers.ApiController{}, "POST:Login") + beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin") + beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout") + beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount") + beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink") + + beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations") + beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization") + beego.Router("/api/update-organization", &controllers.ApiController{}, "POST:UpdateOrganization") + beego.Router("/api/add-organization", &controllers.ApiController{}, "POST:AddOrganization") + beego.Router("/api/delete-organization", &controllers.ApiController{}, "POST:DeleteOrganization") + + beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers") + beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers") + beego.Router("/api/get-user", &controllers.ApiController{}, "GET:GetUser") + beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser") + beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser") + beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser") + beego.Router("/api/upload-avatar", &controllers.ApiController{}, "POST:UploadAvatar") + beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") + beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone") + beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode") + beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone") + beego.Router("/api/get-human-check", &controllers.ApiController{}, "GET:GetHumanCheck") + beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser") + beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps") + beego.Router("/api/get-ldap", &controllers.ApiController{}, "POST:GetLdap") + beego.Router("/api/add-ldap", &controllers.ApiController{}, "POST:AddLdap") + beego.Router("/api/update-ldap", &controllers.ApiController{}, "POST:UpdateLdap") + beego.Router("/api/delete-ldap", &controllers.ApiController{}, "POST:DeleteLdap") + beego.Router("/api/check-ldap-users-exist", &controllers.ApiController{}, "POST:CheckLdapUsersExist") + beego.Router("/api/sync-ldap-users", &controllers.ApiController{}, "POST:SyncLdapUsers") + + beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders") + beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider") + beego.Router("/api/update-provider", &controllers.ApiController{}, "POST:UpdateProvider") + beego.Router("/api/add-provider", &controllers.ApiController{}, "POST:AddProvider") + beego.Router("/api/delete-provider", &controllers.ApiController{}, "POST:DeleteProvider") + + beego.Router("/api/get-applications", &controllers.ApiController{}, "GET:GetApplications") + beego.Router("/api/get-application", &controllers.ApiController{}, "GET:GetApplication") + beego.Router("/api/get-user-application", &controllers.ApiController{}, "GET:GetUserApplication") + beego.Router("/api/update-application", &controllers.ApiController{}, "POST:UpdateApplication") + beego.Router("/api/add-application", &controllers.ApiController{}, "POST:AddApplication") + beego.Router("/api/delete-application", &controllers.ApiController{}, "POST:DeleteApplication") + + beego.Router("/api/get-tokens", &controllers.ApiController{}, "GET:GetTokens") + beego.Router("/api/get-token", &controllers.ApiController{}, "GET:GetToken") + beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken") + beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken") + beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken") + beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken") + + beego.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords") + beego.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter") +} diff --git a/casdoor/routers/static_filter.go b/casdoor/routers/static_filter.go new file mode 100644 index 0000000..ea75534 --- /dev/null +++ b/casdoor/routers/static_filter.go @@ -0,0 +1,43 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routers + +import ( + "net/http" + "strings" + + "github.com/astaxie/beego/context" + "github.com/casdoor/casdoor/util" +) + +func StaticFilter(ctx *context.Context) { + urlPath := ctx.Request.URL.Path + if strings.HasPrefix(urlPath, "/api/") { + return + } + + path := "web/build" + if urlPath == "/" { + path += "/index.html" + } else { + path += urlPath + } + + if util.FileExist(path) { + http.ServeFile(ctx.ResponseWriter, ctx.Request, path) + } else { + http.ServeFile(ctx.ResponseWriter, ctx.Request, "web/build/index.html") + } +} diff --git a/casdoor/routers/util.go b/casdoor/routers/util.go new file mode 100644 index 0000000..c4ea364 --- /dev/null +++ b/casdoor/routers/util.go @@ -0,0 +1,34 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routers + +import ( + "net/url" + "strings" +) + +func parseQuery(query string, key string) string { + queryMap, err := url.ParseQuery(query) + if err != nil { + panic(err) + } + + return queryMap.Get(key) +} + +func parseSlash(s string) (string, string) { + tokens := strings.Split(s, "/") + return tokens[0], tokens[1] +} diff --git a/casdoor/swagger/favicon-16x16.png b/casdoor/swagger/favicon-16x16.png new file mode 100644 index 0000000..0f7e13b Binary files /dev/null and b/casdoor/swagger/favicon-16x16.png differ diff --git a/casdoor/swagger/favicon-32x32.png b/casdoor/swagger/favicon-32x32.png new file mode 100644 index 0000000..b0a3352 Binary files /dev/null and b/casdoor/swagger/favicon-32x32.png differ diff --git a/casdoor/swagger/index.html b/casdoor/swagger/index.html new file mode 100644 index 0000000..d425107 --- /dev/null +++ b/casdoor/swagger/index.html @@ -0,0 +1,93 @@ + + + +
+ +>>u&yn;if(_!==h>>>u&yn)break;_&&(l+=(1<i&&(c=c.removeBefore(r,u,a-l)),c&&h
a&&(a=c.size),o(u)||(c=c.map(function(e){return V(e)})),i.push(c)}return a>e.size&&(e=e.setSize(a)),Ie(e,t,i)}function $e(e){return es)return k();var e=i.next();return r||t===xn?e:t===bn?w(t,u-1,void 0,e):w(t,u-1,e.value[1],e)})},c}function dt(e,t,n){var r=Tt(e);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var a=0;return e.__iterate(function(e,i,s){return t.call(n,e,i,s)&&++a&&r(e,i,o)}),a},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var a=e.__iterator(wn,i),s=!0;return new x(function(){if(!s)return k();var e=a.next();if(e.done)return e;var i=e.value,u=i[0],c=i[1];return t.call(n,c,u,o)?r===wn?e:w(r,u,c,e):(s=!1,k())})},r}function mt(e,t,n,r){var i=Tt(e);return i.__iterateUncached=function(i,o){var a=this;if(o)return this.cacheResult().__iterate(i,o);var s=!0,u=0;return e.__iterate(function(e,o,c){if(!s||!(s=t.call(n,e,o,c)))return u++,i(e,r?o:u-1,a)}),u},i.__iteratorUncached=function(i,o){var a=this;if(o)return this.cacheResult().__iterator(i,o);var s=e.__iterator(wn,o),u=!0,c=0;return new x(function(){var e,o,l;do{if(e=s.next(),e.done)return r||i===xn?e:i===bn?w(i,c++,void 0,e):w(i,c++,e.value[1],e);var p=e.value;o=p[0],l=p[1],u&&(u=t.call(n,l,o,a))}while(u);return i===wn?e:w(i,o,l,e)})},i}function yt(e,t){var r=a(e),i=[e].concat(t).map(function(e){return o(e)?r&&(e=n(e)):e=r?L(e):z(Array.isArray(e)?e:[e]),e}).filter(function(e){return 0!==e.size});if(0===i.length)return e;if(1===i.length){var u=i[0];if(u===e||r&&a(u)||s(e)&&s(u))return u}var c=new I(i);return r?c=c.toKeyedSeq():s(e)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce(function(e,t){if(void 0!==e){var n=t.size;if(void 0!==n)return e+n}},0),c}function vt(e,t,n){var r=Tt(e);return r.__iterateUncached=function(r,i){function a(e,c){var l=this;e.__iterate(function(e,i){return(!t||c=Vn)return Oe(e,f,c,s,d);if(l&&!d&&2===f.length&&Ee(f[1^p]))return f[1^p];if(l&&d&&1===f.length&&Ee(d))return d;var m=e&&e===this.ownerID,y=l?d?c:c^u:c|u,v=l?d?Ne(f,p,d,m):Be(f,p,m):Fe(f,p,d,m);return m?(this.bitmap=y,this.nodes=v,this):new de(e,y,v)},me.prototype.get=function(e,t,n,r){void 0===t&&(t=oe(n));var i=(0===e?t:t>>>e)&yn,o=this.nodes[i];return o?o.get(e+dn,t,n,r):r},me.prototype.update=function(e,t,n,r,i,o,a){void 0===n&&(n=oe(r));var s=(0===t?n:n>>>t)&yn,u=i===vn,c=this.nodes,l=c[s];if(u&&!l)return this;var p=Se(l,e,t+dn,n,r,i,o,a);if(p===l)return this;var f=this.count;if(l){if(!p&&(f--,ft)return e.textContent;var o=function(e){for(var t,o,a,s,u,c=e.textContent,l=0,p=c[0],f=1,h=e.innerHTML="",d=0;o=t,t=d<7&&"\\"==t?1:f;){if(f=p,p=c[++l],s=h.length>1,!f||d>8&&"\n"==f||[/\S/[i](f),1,1,!/[$\w]/[i](f),("/"==t||"\n"==t)&&s,'"'==t&&s,"'"==t&&s,c[l-4]+o+t=="-->",o+t=="*/"][d])for(h&&(e[r](u=n.createElement("span")).setAttribute("style",["color: #555; font-weight: bold;","","","color: #555;",""][d?d<3?2:d>6?4:d>3?3:+/^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/[i](h):0]),u[r](n.createTextNode(h))),a=d&&d<7?d:a,h="",d=11;![1,/[\/{}[(\-+*=<>:;|\\.,?!&@~]/[i](f),/[\])]/[i](f),/[$\w]/[i](f),"/"==f&&a<2&&"<"!=t,'"'==f,"'"==f,f+p+c[l+1]+c[l+2]=="':null;var r=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=r[1]}return(0,z.memoizedCreateXMLExample)(e,n)}return JSON.stringify((0,z.memoizedSampleFromSchema)(e,n),null,2)},t.parseSeach=function(){var e={},t=window.location.search;if(""!=t){var n=t.substr(1).split("&");for(var r in n)r=n[r].split("="),e[decodeURIComponent(r[0])]=decodeURIComponent(r[1])}return e},t.btoa=function(t){var n=void 0;return n=t instanceof e?t:new e(t.toString(),"utf-8"),n.toString("base64")},t.sorters={operationsSorter:{alpha:function(e,t){return e.get("path").localeCompare(t.get("path"))},method:function(e,t){return e.get("method").localeCompare(t.get("method"))}}},t.buildFormData=function(e){var t=[];for(var n in e){var r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},t.filterConfigs=function(e,t){var n=void 0,r={};for(n in e)t.indexOf(n)!==-1&&(r[n]=e[n]);return r}}).call(t,n(299).Buffer)},function(e,t,n){"use strict";var r=n(337);e.exports=function(e,t,n,i){var o=n?n.call(i,e,t):void 0;if(void 0!==o)return!!o;if(e===t)return!0;if("object"!=typeof e||null===e||"object"!=typeof t||null===t)return!1;var a=r(e),s=r(t),u=a.length;if(u!==s.length)return!1;i=i||null;for(var c=Object.prototype.hasOwnProperty.bind(t),l=0;l-1&&e%1==0&&e","
"],l=[3,"
"],p=[1,'"],f={"*":[1,"?","
"],legend:[1,""],param:[1,""],tr:[2,"","
"],optgroup:u,option:u,caption:c,colgroup:c,tbody:c,tfoot:c,thead:c,td:l,th:l},h=["circle","clipPath","defs","ellipse","g","image","line","linearGradient","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","text","tspan"];h.forEach(function(e){f[e]=p,s[e]=!0}),e.exports=r},function(e,t,n){"use strict";var r=n(694),i=n(653),o={dangerouslyProcessChildrenUpdates:function(e,t){var n=i.getNodeFromInstance(e);r.processUpdates(n,t)}};e.exports=o},function(e,t,n){"use strict";function r(e){if(e){var t=e._currentElement._owner||null;if(t){var n=t.getName();if(n)return" This DOM node was rendered by `"+n+"`."}}return""}function i(e,t){t&&(G[e._tag]&&(null!=t.children||null!=t.dangerouslySetInnerHTML?m("137",e._tag,e._currentElement._owner?" Check the render method of "+e._currentElement._owner.getName()+".":""):void 0),null!=t.dangerouslySetInnerHTML&&(null!=t.children?m("60"):void 0,"object"==typeof t.dangerouslySetInnerHTML&&U in t.dangerouslySetInnerHTML?void 0:m("61")),null!=t.style&&"object"!=typeof t.style?m("62",r(e)):void 0)}function o(e,t,n,r){if(!(r instanceof I)){var i=e._hostContainerInfo,o=i._node&&i._node.nodeType===K,s=o?i._node:i._ownerDocument;B(t,s),r.getReactMountReady().enqueue(a,{inst:e,registrationName:t,listener:n})}}function a(){var e=this;k.putListener(e.inst,e.registrationName,e.listener)}function s(){var e=this;T.postMountWrapper(e)}function u(){var e=this;M.postMountWrapper(e)}function c(){var e=this;O.postMountWrapper(e)}function l(){var e=this;e._rootNodeID?void 0:m("63");var t=F(e);switch(t?void 0:m("64"),e._tag){case"iframe":case"object":e._wrapperState.listeners=[E.trapBubbledEvent("topLoad","load",t)];break;case"video":case"audio":e._wrapperState.listeners=[];for(var n in V)V.hasOwnProperty(n)&&e._wrapperState.listeners.push(E.trapBubbledEvent(n,V[n],t));break;case"source":e._wrapperState.listeners=[E.trapBubbledEvent("topError","error",t)];break;case"img":e._wrapperState.listeners=[E.trapBubbledEvent("topError","error",t),E.trapBubbledEvent("topLoad","load",t)];break;case"form":e._wrapperState.listeners=[E.trapBubbledEvent("topReset","reset",t),E.trapBubbledEvent("topSubmit","submit",t)];break;case"input":case"select":case"textarea":e._wrapperState.listeners=[E.trapBubbledEvent("topInvalid","invalid",t)]}}function p(){D.postUpdateWrapper(this)}function f(e){$.call(Y,e)||(X.test(e)?void 0:m("65",e),Y[e]=!0)}function h(e,t){return e.indexOf("-")>=0||null!=t.is}function d(e){var t=e.type;f(t),this._currentElement=e,this._tag=t.toLowerCase(),this._namespaceURI=null,this._renderedChildren=null,this._previousStyle=null,this._previousStyleCopy=null,this._hostNode=null,this._hostParent=null,this._rootNodeID=0,this._domID=0,this._hostContainerInfo=null,this._wrapperState=null,this._topLevelWrapper=null,this._flags=0}var m=n(654),y=n(623),v=n(707),g=n(709),_=n(695),b=n(696),x=n(655),w=n(717),k=n(661),S=n(662),E=n(719),C=n(656),A=n(653),T=n(722),O=n(725),D=n(726),M=n(727),P=(n(681),n(728)),I=n(746),j=(n(631),n(700)),R=(n(627),n(684),n(735),n(749),n(630),C),N=k.deleteListener,F=A.getNodeFromInstance,B=E.listenTo,L=S.registrationNameModules,z={string:!0,number:!0},q="style",U="__html",W={children:null,dangerouslySetInnerHTML:null,suppressContentEditableWarning:null},K=11,V={topAbort:"abort",topCanPlay:"canplay",topCanPlayThrough:"canplaythrough",topDurationChange:"durationchange",topEmptied:"emptied",topEncrypted:"encrypted",topEnded:"ended",topError:"error",topLoadedData:"loadeddata",topLoadedMetadata:"loadedmetadata",topLoadStart:"loadstart",topPause:"pause",topPlay:"play",topPlaying:"playing",topProgress:"progress",topRateChange:"ratechange",topSeeked:"seeked",topSeeking:"seeking",topStalled:"stalled",topSuspend:"suspend",topTimeUpdate:"timeupdate",topVolumeChange:"volumechange",topWaiting:"waiting"},H={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0},J={listing:!0,pre:!0,textarea:!0},G=y({menuitem:!0},H),X=/^[a-zA-Z][a-zA-Z:_\.\-\d]*$/,Y={},$={}.hasOwnProperty,Z=1;d.displayName="ReactDOMComponent",d.Mixin={mountComponent:function(e,t,n,r){this._rootNodeID=Z++,this._domID=n._idCounter++,this._hostParent=t,this._hostContainerInfo=n;var o=this._currentElement.props;switch(this._tag){case"audio":case"form":case"iframe":case"img":case"link":case"object":case"source":case"video":this._wrapperState={listeners:null},e.getReactMountReady().enqueue(l,this);break;case"input":T.mountWrapper(this,o,t),o=T.getHostProps(this,o),e.getReactMountReady().enqueue(l,this);break;case"option":O.mountWrapper(this,o,t),o=O.getHostProps(this,o);break;case"select":D.mountWrapper(this,o,t),o=D.getHostProps(this,o),e.getReactMountReady().enqueue(l,this);break;case"textarea":M.mountWrapper(this,o,t),o=M.getHostProps(this,o),e.getReactMountReady().enqueue(l,this)}i(this,o);var a,p;null!=t?(a=t._namespaceURI,p=t._tag):n._tag&&(a=n._namespaceURI,p=n._tag),(null==a||a===b.svg&&"foreignobject"===p)&&(a=b.html),a===b.html&&("svg"===this._tag?a=b.svg:"math"===this._tag&&(a=b.mathml)),this._namespaceURI=a;var f;if(e.useCreateElement){var h,d=n._ownerDocument;if(a===b.html)if("script"===this._tag){var m=d.createElement("div"),y=this._currentElement.type;m.innerHTML="<"+y+">"+y+">",h=m.removeChild(m.firstChild)}else h=o.is?d.createElement(this._currentElement.type,o.is):d.createElement(this._currentElement.type);else h=d.createElementNS(a,this._currentElement.type);A.precacheNode(this,h),this._flags|=R.hasCachedChildNodes,this._hostParent||w.setAttributeForRoot(h),this._updateDOMProperties(null,o,e);var g=_(h);this._createInitialChildren(e,o,r,g),f=g}else{var x=this._createOpenTagMarkupAndPutListeners(e,o),k=this._createContentMarkup(e,o,r);f=!k&&H[this._tag]?x+"/>":x+">"+k+""+this._currentElement.type+">"}switch(this._tag){case"input":e.getReactMountReady().enqueue(s,this),o.autoFocus&&e.getReactMountReady().enqueue(v.focusDOMComponent,this);break;case"textarea":e.getReactMountReady().enqueue(u,this),o.autoFocus&&e.getReactMountReady().enqueue(v.focusDOMComponent,this);break;case"select":o.autoFocus&&e.getReactMountReady().enqueue(v.focusDOMComponent,this);break;case"button":o.autoFocus&&e.getReactMountReady().enqueue(v.focusDOMComponent,this);break;case"option":e.getReactMountReady().enqueue(c,this)}return f},_createOpenTagMarkupAndPutListeners:function(e,t){var n="<"+this._currentElement.type;for(var r in t)if(t.hasOwnProperty(r)){var i=t[r];if(null!=i)if(L.hasOwnProperty(r))i&&o(this,r,i,e);else{r===q&&(i&&(i=this._previousStyleCopy=y({},t.style)),i=g.createMarkupForStyles(i,this));var a=null;null!=this._tag&&h(this._tag,t)?W.hasOwnProperty(r)||(a=w.createMarkupForCustomAttribute(r,i)):a=w.createMarkupForProperty(r,i),a&&(n+=" "+a)}}return e.renderToStaticMarkup?n:(this._hostParent||(n+=" "+w.createMarkupForRoot()),n+=" "+w.createMarkupForID(this._domID))},_createContentMarkup:function(e,t,n){var r="",i=t.dangerouslySetInnerHTML;if(null!=i)null!=i.__html&&(r=i.__html);else{var o=z[typeof t.children]?t.children:null,a=null!=o?null:t.children;if(null!=o)r=j(o);else if(null!=a){var s=this.mountChildren(a,e,n);r=s.join("")}}return J[this._tag]&&"\n"===r.charAt(0)?"\n"+r:r},_createInitialChildren:function(e,t,n,r){var i=t.dangerouslySetInnerHTML;if(null!=i)null!=i.__html&&_.queueHTML(r,i.__html);else{var o=z[typeof t.children]?t.children:null,a=null!=o?null:t.children;if(null!=o)""!==o&&_.queueText(r,o);else if(null!=a)for(var s=this.mountChildren(a,e,n),u=0;u-1&&(s=o?s.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+s.split("\n").map(function(e){return" "+e}).join("\n"))):s=e.stylize("[Circular]","special")),x(a)){if(o&&i.match(/^\d+$/))return s;a=JSON.stringify(""+i),a.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function h(e,t,n){var r=0,i=e.reduce(function(e,t){return r++,t.indexOf("\n")>=0&&r++,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0);return i>60?n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1]:n[0]+t+" "+e.join(", ")+" "+n[1]}function d(e){return Array.isArray(e)}function m(e){return"boolean"==typeof e}function y(e){return null===e}function v(e){return null==e}function g(e){return"number"==typeof e}function _(e){return"string"==typeof e}function b(e){return"symbol"==typeof e}function x(e){return void 0===e}function w(e){return k(e)&&"[object RegExp]"===T(e)}function k(e){return"object"==typeof e&&null!==e}function S(e){return k(e)&&"[object Date]"===T(e)}function E(e){return k(e)&&("[object Error]"===T(e)||e instanceof Error)}function C(e){return"function"==typeof e}function A(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||"undefined"==typeof e}function T(e){return Object.prototype.toString.call(e)}function O(e){return e<10?"0"+e.toString(10):e.toString(10)}function D(){var e=new Date,t=[O(e.getHours()),O(e.getMinutes()),O(e.getSeconds())].join(":");return[e.getDate(),R[e.getMonth()],t].join(" ")}function M(e,t){return Object.prototype.hasOwnProperty.call(e,t)}var P=/%[sdj%]/g;t.format=function(e){if(!_(e)){for(var t=[],n=0;nn?p.push([l,s]):i[s]=this.yaml_path_resolvers[l][s]);else for(d=this.yaml_path_resolvers,a=0,c=d.length;a=h){o.push(x[r.op].call(r,l,i,e));break}if(E(l)){if("-"===i)i=l.length;else{if(n&&!p(i))throw new C("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",a-1,r.path,r);i=parseInt(i,10)}if(f>=h){if(n&&"add"===r.op&&i>l.length)throw new C("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",a-1,r.path,r);o.push(b[r.op].call(r,l,i,e));break}}else if(i&&i.indexOf("~")!=-1&&(i=i.replace(/~1/g,"/").replace(/~0/g,"~")),f>=h){o.push(_[r.op].call(r,l,i,e));break}l=l[i]}}return o}function h(e,t){var n=[];return l(e,t,n,""),n}function d(e,t){function n(){this.constructor=e}for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r]);e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}function m(e){if(void 0===e)return!0;if(e)if(E(e)){for(var t=0,n=e.length;t