mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-14 16:13:24 +08:00
Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
be74cb621f | |||
13404d6035 | |||
afa9c530ad | |||
1600615aca | |||
2bb8491499 | |||
293283ed25 | |||
9cb519d1e9 | |||
fb9b8f1662 | |||
2fec3f72ae | |||
11695220a8 | |||
155660b0d7 | |||
1c72f5300c | |||
3dd56195d9 | |||
8865244262 | |||
3400fa1e9c | |||
bdc5c92ef0 | |||
4e3eedf246 | |||
8e98fc5a9f | |||
6f6159be07 | |||
3e4dbc2dcb | |||
48b5b27982 | |||
1839252c30 | |||
1fff1db6a7 | |||
a0b0e186b7 | |||
8c7f235ee1 | |||
a0a762aa6f | |||
2eec53a6d0 | |||
117dec4542 | |||
895cdd024d | |||
f0b0891ac9 | |||
10449e89ab | |||
6e70f0fc58 | |||
2bca424370 | |||
de49a45e19 | |||
f7243f879b | |||
7f3b2500b3 |
@ -78,6 +78,7 @@ p, *, *, POST, /api/get-email-and-phone, *, *
|
||||
p, *, *, POST, /api/login, *, *
|
||||
p, *, *, GET, /api/get-app-login, *, *
|
||||
p, *, *, POST, /api/logout, *, *
|
||||
p, *, *, GET, /api/logout, *, *
|
||||
p, *, *, GET, /api/get-account, *, *
|
||||
p, *, *, GET, /api/userinfo, *, *
|
||||
p, *, *, *, /api/login/oauth, *, *
|
||||
@ -92,6 +93,7 @@ p, *, *, GET, /api/get-payment, *, *
|
||||
p, *, *, POST, /api/update-payment, *, *
|
||||
p, *, *, POST, /api/invoice-payment, *, *
|
||||
p, *, *, GET, /api/get-providers, *, *
|
||||
p, *, *, POST, /api/notify-payment, *, *
|
||||
p, *, *, POST, /api/unlink, *, *
|
||||
p, *, *, POST, /api/set-password, *, *
|
||||
p, *, *, POST, /api/send-verification-code, *, *
|
||||
@ -105,6 +107,7 @@ p, *, *, GET, /api/get-saml-login, *, *
|
||||
p, *, *, POST, /api/acs, *, *
|
||||
p, *, *, GET, /api/saml/metadata, *, *
|
||||
p, *, *, *, /cas, *, *
|
||||
p, *, *, *, /api/webauthn, *, *
|
||||
`
|
||||
|
||||
sa := stringadapter.NewAdapter(ruleText)
|
||||
|
@ -217,7 +217,7 @@ func (c *ApiController) Signup() {
|
||||
record.User = user.Name
|
||||
util.SafeGoroutine(func() { object.AddRecord(record) })
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", user.Owner, user.Name)
|
||||
userId := user.GetId()
|
||||
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
|
||||
|
||||
c.ResponseOk(userId)
|
||||
@ -228,7 +228,7 @@ func (c *ApiController) Signup() {
|
||||
// @Tag Login API
|
||||
// @Description logout the current user
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /logout [post]
|
||||
// @router /logout [get,post]
|
||||
func (c *ApiController) Logout() {
|
||||
user := c.GetSessionUsername()
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
|
@ -50,6 +50,17 @@ func tokenToResponse(token *object.Token) *Response {
|
||||
// HandleLoggedIn ...
|
||||
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
|
||||
userId := user.GetId()
|
||||
|
||||
allowed, err := object.CheckAccessPermission(userId, application)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
c.ResponseError("Unauthorized operation")
|
||||
return
|
||||
}
|
||||
|
||||
if form.Type == ResponseTypeLogin {
|
||||
c.SetSessionUsername(userId)
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
|
@ -21,7 +21,8 @@ import (
|
||||
)
|
||||
|
||||
type LinkForm struct {
|
||||
ProviderType string `json:"providerType"`
|
||||
ProviderType string `json:"providerType"`
|
||||
User object.User `json:"user"`
|
||||
}
|
||||
|
||||
// Unlink ...
|
||||
@ -40,16 +41,55 @@ func (c *ApiController) Unlink() {
|
||||
}
|
||||
providerType := form.ProviderType
|
||||
|
||||
// the user will be unlinked from the provider
|
||||
unlinkedUser := form.User
|
||||
user := object.GetUser(userId)
|
||||
value := object.GetUserField(user, providerType)
|
||||
|
||||
if user.Id != unlinkedUser.Id && !user.IsGlobalAdmin {
|
||||
// if the user is not the same as the one we are unlinking, we need to make sure the user is the global admin.
|
||||
c.ResponseError("You are not the global admin, you can't unlink other users")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Id == unlinkedUser.Id && !user.IsGlobalAdmin {
|
||||
// if the user is unlinking themselves, should check the provider can be unlinked, if not, we should return an error.
|
||||
application := object.GetApplicationByUser(user)
|
||||
if application == nil {
|
||||
c.ResponseError("You can't unlink yourself, you are not a member of any application")
|
||||
return
|
||||
}
|
||||
|
||||
if len(application.Providers) == 0 {
|
||||
c.ResponseError("This application has no providers")
|
||||
return
|
||||
}
|
||||
|
||||
provider := application.GetProviderItemByType(providerType)
|
||||
if provider == nil {
|
||||
c.ResponseError("This application has no providers of type " + providerType)
|
||||
return
|
||||
}
|
||||
|
||||
if !provider.CanUnlink {
|
||||
c.ResponseError("This provider can't be unlinked")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// only two situations can happen here
|
||||
// 1. the user is the global admin
|
||||
// 2. the user is unlinking themselves and provider can be unlinked
|
||||
|
||||
value := object.GetUserField(&unlinkedUser, providerType)
|
||||
|
||||
if value == "" {
|
||||
c.ResponseError("Please link first", value)
|
||||
return
|
||||
}
|
||||
|
||||
object.ClearUserOAuthProperties(user, providerType)
|
||||
object.ClearUserOAuthProperties(&unlinkedUser, providerType)
|
||||
|
||||
object.LinkUserAccount(user, providerType, "")
|
||||
object.LinkUserAccount(&unlinkedUser, providerType, "")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
@ -80,19 +80,27 @@ func (c *ApiController) GetUsers() {
|
||||
// @Title GetUser
|
||||
// @Tag User API
|
||||
// @Description get user
|
||||
// @Param id query string true "The id of the user"
|
||||
// @Param id query string true "The id of the user"
|
||||
// @Param owner query string false "The owner of the user"
|
||||
// @Param email query string false "The email of the user"
|
||||
// @Param phone query string false "The phone of the user"
|
||||
// @Success 200 {object} object.User The Response object
|
||||
// @router /get-user [get]
|
||||
func (c *ApiController) GetUser() {
|
||||
id := c.Input().Get("id")
|
||||
owner := c.Input().Get("owner")
|
||||
email := c.Input().Get("email")
|
||||
userOwner, _ := util.GetOwnerAndNameFromId(id)
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", userOwner))
|
||||
phone := c.Input().Get("phone")
|
||||
userId := c.Input().Get("userId")
|
||||
|
||||
owner := c.Input().Get("owner")
|
||||
if owner == "" {
|
||||
owner, _ = util.GetOwnerAndNameFromId(id)
|
||||
}
|
||||
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner))
|
||||
if !organization.IsProfilePublic {
|
||||
requestUserId := c.GetSessionUsername()
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, id, false)
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, id, owner, false)
|
||||
if !hasPermission {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -100,10 +108,22 @@ func (c *ApiController) GetUser() {
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
if email == "" {
|
||||
user = object.GetUser(id)
|
||||
} else {
|
||||
switch {
|
||||
case email != "":
|
||||
user = object.GetUserByEmail(owner, email)
|
||||
case phone != "":
|
||||
user = object.GetUserByPhone(owner, phone)
|
||||
case userId != "":
|
||||
user = object.GetUserByUserId(owner, userId)
|
||||
default:
|
||||
user = object.GetUser(id)
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
roles := object.GetRolesByUser(user.GetId())
|
||||
user.Roles = roles
|
||||
permissions := object.GetPermissionsByUser(user.GetId())
|
||||
user.Permissions = permissions
|
||||
}
|
||||
|
||||
c.Data["json"] = object.GetMaskedUser(user)
|
||||
@ -246,7 +266,7 @@ func (c *ApiController) SetPassword() {
|
||||
requestUserId := c.GetSessionUsername()
|
||||
userId := fmt.Sprintf("%s/%s", userOwner, userName)
|
||||
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true)
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, userId, userOwner, true)
|
||||
if !hasPermission {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
@ -49,8 +49,24 @@ func (c *ApiController) SendVerificationCode() {
|
||||
applicationId := c.Ctx.Request.Form.Get("applicationId")
|
||||
remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
|
||||
|
||||
if destType == "" || dest == "" || applicationId == "" || !strings.Contains(applicationId, "/") || checkType == "" {
|
||||
c.ResponseError("Missing parameter.")
|
||||
if destType == "" {
|
||||
c.ResponseError("Missing parameter: type.")
|
||||
return
|
||||
}
|
||||
if dest == "" {
|
||||
c.ResponseError("Missing parameter: dest.")
|
||||
return
|
||||
}
|
||||
if applicationId == "" {
|
||||
c.ResponseError("Missing parameter: applicationId.")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(applicationId, "/") {
|
||||
c.ResponseError("Wrong parameter: applicationId.")
|
||||
return
|
||||
}
|
||||
if checkType == "" {
|
||||
c.ResponseError("Missing parameter: checkType.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -152,13 +168,35 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
}
|
||||
|
||||
checkDest := dest
|
||||
org := object.GetOrganizationByUser(user)
|
||||
if destType == "phone" {
|
||||
org := object.GetOrganizationByUser(user)
|
||||
phoneItem := object.GetAccountItemByName("Phone", org)
|
||||
if phoneItem == nil {
|
||||
c.ResponseError("Unable to get the phone modify rule.")
|
||||
return
|
||||
}
|
||||
|
||||
if pass, errMsg := object.CheckAccountItemModifyRule(phoneItem, user); !pass {
|
||||
c.ResponseError(errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
phonePrefix := "86"
|
||||
if org != nil && org.PhonePrefix != "" {
|
||||
phonePrefix = org.PhonePrefix
|
||||
}
|
||||
checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest)
|
||||
} else if destType == "email" {
|
||||
emailItem := object.GetAccountItemByName("Email", org)
|
||||
if emailItem == nil {
|
||||
c.ResponseError("Unable to get the email modify rule.")
|
||||
return
|
||||
}
|
||||
|
||||
if pass, errMsg := object.CheckAccountItemModifyRule(emailItem, user); !pass {
|
||||
c.ResponseError(errMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
if ret := object.CheckVerificationCode(checkDest, code); len(ret) != 0 {
|
||||
c.ResponseError(ret)
|
||||
|
138
controllers/webauthn.go
Normal file
138
controllers/webauthn.go
Normal file
@ -0,0 +1,138 @@
|
||||
// Copyright 2022 The Casdoor 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 (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/duo-labs/webauthn/protocol"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
)
|
||||
|
||||
// @Title WebAuthnSignupBegin
|
||||
// @Tag User API
|
||||
// @Description WebAuthn Registration Flow 1st stage
|
||||
// @Success 200 {object} protocol.CredentialCreation The CredentialCreationOptions object
|
||||
// @router /webauthn/signup/begin [get]
|
||||
func (c *ApiController) WebAuthnSignupBegin() {
|
||||
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||
user := c.getCurrentUser()
|
||||
if user == nil {
|
||||
c.ResponseError("Please login first.")
|
||||
return
|
||||
}
|
||||
|
||||
registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
|
||||
credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
|
||||
}
|
||||
options, sessionData, err := webauthnObj.BeginRegistration(
|
||||
user,
|
||||
registerOptions,
|
||||
)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.SetSession("registration", *sessionData)
|
||||
c.Data["json"] = options
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title WebAuthnSignupFinish
|
||||
// @Tag User API
|
||||
// @Description WebAuthn Registration Flow 2nd stage
|
||||
// @Param body body protocol.CredentialCreationResponse true "authenticator attestation Response"
|
||||
// @Success 200 {object} Response "The Response object"
|
||||
// @router /webauthn/signup/finish [post]
|
||||
func (c *ApiController) WebAuthnSignupFinish() {
|
||||
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||
user := c.getCurrentUser()
|
||||
if user == nil {
|
||||
c.ResponseError("Please login first.")
|
||||
return
|
||||
}
|
||||
sessionObj := c.GetSession("registration")
|
||||
sessionData, ok := sessionObj.(webauthn.SessionData)
|
||||
if !ok {
|
||||
c.ResponseError("Please call WebAuthnSignupBegin first")
|
||||
return
|
||||
}
|
||||
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||
|
||||
credential, err := webauthnObj.FinishRegistration(user, sessionData, c.Ctx.Request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
isGlobalAdmin := c.IsGlobalAdmin()
|
||||
user.AddCredentials(*credential, isGlobalAdmin)
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// @Title WebAuthnSigninBegin
|
||||
// @Tag Login API
|
||||
// @Description WebAuthn Login Flow 1st stage
|
||||
// @Param owner query string true "owner"
|
||||
// @Param name query string true "name"
|
||||
// @Success 200 {object} protocol.CredentialAssertion The CredentialAssertion object
|
||||
// @router /webauthn/signin/begin [get]
|
||||
func (c *ApiController) WebAuthnSigninBegin() {
|
||||
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||
userOwner := c.Input().Get("owner")
|
||||
userName := c.Input().Get("name")
|
||||
user := object.GetUserByFields(userOwner, userName)
|
||||
if user == nil {
|
||||
c.ResponseError("Please Giveout Owner and Username.")
|
||||
return
|
||||
}
|
||||
options, sessionData, err := webauthnObj.BeginLogin(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.SetSession("authentication", *sessionData)
|
||||
c.Data["json"] = options
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title WebAuthnSigninBegin
|
||||
// @Tag Login API
|
||||
// @Description WebAuthn Login Flow 2nd stage
|
||||
// @Param body body protocol.CredentialAssertionResponse true "authenticator assertion Response"
|
||||
// @Success 200 {object} Response "The Response object"
|
||||
// @router /webauthn/signin/finish [post]
|
||||
func (c *ApiController) WebAuthnSigninFinish() {
|
||||
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||
sessionObj := c.GetSession("authentication")
|
||||
sessionData, ok := sessionObj.(webauthn.SessionData)
|
||||
if !ok {
|
||||
c.ResponseError("Please call WebAuthnSigninBegin first")
|
||||
return
|
||||
}
|
||||
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||
userId := string(sessionData.UserID)
|
||||
user := object.GetUser(userId)
|
||||
_, err := webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.SetSessionUsername(userId)
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
c.ResponseOk(userId)
|
||||
}
|
1
go.mod
1
go.mod
@ -14,6 +14,7 @@ require (
|
||||
github.com/casdoor/goth v1.69.0-FIX2
|
||||
github.com/casdoor/oss v1.2.0
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
||||
github.com/go-ldap/ldap/v3 v3.3.0
|
||||
github.com/go-pay/gopay v1.5.72
|
||||
|
14
go.sum
14
go.sum
@ -111,6 +111,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1CUzYdAZXijuvLuRMirgiXdf3zsM2Ig=
|
||||
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
|
||||
@ -124,6 +126,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
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/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b h1:L63RATZFZuFMXy6ixnKmv3eNAXwYQF6HW1vd4IYsQqQ=
|
||||
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b/go.mod h1:EYSpSkwoEcryMmQGfhol2IiB3IMN9IIIaNd/wcAQMGQ=
|
||||
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=
|
||||
@ -135,6 +139,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ=
|
||||
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
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=
|
||||
@ -164,6 +170,7 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
||||
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-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
@ -201,6 +208,8 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
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=
|
||||
@ -298,6 +307,8 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
|
||||
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/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
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=
|
||||
@ -397,6 +408,8 @@ github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnD
|
||||
github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
|
||||
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -413,6 +426,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
@ -39,6 +39,7 @@ func readI18nFile(language string) *I18nData {
|
||||
func writeI18nFile(language string, data *I18nData) {
|
||||
s := util.StructToJsonFormatted(data)
|
||||
s = strings.ReplaceAll(s, "\\u0026", "&")
|
||||
s += "\n"
|
||||
println(s)
|
||||
|
||||
util.WriteStringToPath(s, getI18nFilePath(language))
|
||||
|
@ -144,7 +144,7 @@ func (idp *BilibiliIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
type BilibiliUserInfo struct {
|
||||
Name string `json:"name"`
|
||||
Face string `json:"face"`
|
||||
OpenId string `json:"openid`
|
||||
OpenId string `json:"openid"`
|
||||
}
|
||||
|
||||
type BilibiliUserInfoResponse struct {
|
||||
|
@ -139,7 +139,7 @@
|
||||
"cryptoAlgorithm": "RS256",
|
||||
"bitSize": 4096,
|
||||
"expireInYears": 20,
|
||||
"publicKey": "",
|
||||
"certificate": "",
|
||||
"privateKey": ""
|
||||
}
|
||||
],
|
||||
|
@ -22,8 +22,9 @@ import (
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
//_ "github.com/denisenkom/go-mssqldb" // db = mssql
|
||||
_ "github.com/go-sql-driver/mysql" // db = mysql
|
||||
_ "github.com/go-sql-driver/mysql" // db = mysql
|
||||
//_ "github.com/lib/pq" // db = postgres
|
||||
//_ "github.com/mattn/go-sqlite3" // db = sqlite3
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@ -203,6 +204,11 @@ func (a *Adapter) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(PermissionRule))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {
|
||||
|
@ -47,6 +47,7 @@ type Application struct {
|
||||
EnableSigninSession bool `json:"enableSigninSession"`
|
||||
EnableCodeSignin bool `json:"enableCodeSignin"`
|
||||
EnableSamlCompress bool `json:"enableSamlCompress"`
|
||||
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
|
||||
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
||||
|
@ -33,7 +33,7 @@ type Cert struct {
|
||||
BitSize int `json:"bitSize"`
|
||||
ExpireInYears int `json:"expireInYears"`
|
||||
|
||||
PublicKey string `xorm:"mediumtext" json:"publicKey"`
|
||||
Certificate string `xorm:"mediumtext" json:"certificate"`
|
||||
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
|
||||
AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"`
|
||||
AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"`
|
||||
@ -123,9 +123,9 @@ func UpdateCert(id string, cert *Cert) bool {
|
||||
}
|
||||
|
||||
func AddCert(cert *Cert) bool {
|
||||
if cert.PublicKey == "" || cert.PrivateKey == "" {
|
||||
publicKey, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
|
||||
cert.PublicKey = publicKey
|
||||
if cert.Certificate == "" || cert.PrivateKey == "" {
|
||||
certificate, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
|
||||
cert.Certificate = certificate
|
||||
cert.PrivateKey = privateKey
|
||||
}
|
||||
|
||||
|
@ -197,14 +197,18 @@ func filterField(field string) bool {
|
||||
return reFieldWhiteList.MatchString(field)
|
||||
}
|
||||
|
||||
func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error) {
|
||||
func CheckUserPermission(requestUserId, userId, userOwner string, strict bool) (bool, error) {
|
||||
if requestUserId == "" {
|
||||
return false, fmt.Errorf("please login first")
|
||||
}
|
||||
|
||||
targetUser := GetUser(userId)
|
||||
if targetUser == nil {
|
||||
return false, fmt.Errorf("the user: %s doesn't exist", userId)
|
||||
if userId != "" {
|
||||
targetUser := GetUser(userId)
|
||||
if targetUser == nil {
|
||||
return false, fmt.Errorf("the user: %s doesn't exist", userId)
|
||||
}
|
||||
|
||||
userOwner = targetUser.Owner
|
||||
}
|
||||
|
||||
hasPermission := false
|
||||
@ -219,7 +223,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error
|
||||
hasPermission = true
|
||||
} else if requestUserId == userId {
|
||||
hasPermission = true
|
||||
} else if targetUser.Owner == requestUser.Owner {
|
||||
} else if userOwner == requestUser.Owner {
|
||||
if strict {
|
||||
hasPermission = requestUser.IsAdmin
|
||||
} else {
|
||||
@ -229,4 +233,30 @@ func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error
|
||||
}
|
||||
|
||||
return hasPermission, fmt.Errorf("you don't have the permission to do this")
|
||||
}
|
||||
}
|
||||
|
||||
func CheckAccessPermission(userId string, application *Application) (bool, error) {
|
||||
permissions := GetPermissions(application.Organization)
|
||||
allowed := true
|
||||
var err error
|
||||
for _, permission := range permissions {
|
||||
if !permission.IsEnabled || len(permission.Users) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
isHit := false
|
||||
for _, resource := range permission.Resources {
|
||||
if application.Name == resource {
|
||||
isHit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isHit {
|
||||
enforcer := getEnforcer(permission)
|
||||
allowed, err = enforcer.Enforce(userId, application.Name, "read")
|
||||
break
|
||||
}
|
||||
}
|
||||
return allowed, err
|
||||
}
|
||||
|
@ -15,20 +15,25 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
)
|
||||
|
||||
func InitDb() {
|
||||
existed := initBuiltInOrganization()
|
||||
if !existed {
|
||||
initBuiltInPermission()
|
||||
initBuiltInProvider()
|
||||
initBuiltInUser()
|
||||
initBuiltInApplication()
|
||||
initBuiltInCert()
|
||||
initBuiltInLdap()
|
||||
}
|
||||
|
||||
initWebAuthn()
|
||||
}
|
||||
|
||||
func initBuiltInOrganization() bool {
|
||||
@ -66,12 +71,15 @@ func initBuiltInOrganization() bool {
|
||||
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
},
|
||||
}
|
||||
AddOrganization(organization)
|
||||
@ -162,7 +170,7 @@ func readTokenFromFile() (string, string) {
|
||||
}
|
||||
|
||||
func initBuiltInCert() {
|
||||
tokenJwtPublicKey, tokenJwtPrivateKey := readTokenFromFile()
|
||||
tokenJwtCertificate, tokenJwtPrivateKey := readTokenFromFile()
|
||||
cert := getCert("admin", "cert-built-in")
|
||||
if cert != nil {
|
||||
return
|
||||
@ -178,7 +186,7 @@ func initBuiltInCert() {
|
||||
CryptoAlgorithm: "RS256",
|
||||
BitSize: 4096,
|
||||
ExpireInYears: 20,
|
||||
PublicKey: tokenJwtPublicKey,
|
||||
Certificate: tokenJwtCertificate,
|
||||
PrivateKey: tokenJwtPrivateKey,
|
||||
}
|
||||
AddCert(cert)
|
||||
@ -221,3 +229,29 @@ func initBuiltInProvider() {
|
||||
}
|
||||
AddProvider(provider)
|
||||
}
|
||||
|
||||
func initWebAuthn() {
|
||||
gob.Register(webauthn.SessionData{})
|
||||
}
|
||||
|
||||
func initBuiltInPermission() {
|
||||
permission := GetPermission("built-in/permission-built-in")
|
||||
if permission != nil {
|
||||
return
|
||||
}
|
||||
|
||||
permission = &Permission{
|
||||
Owner: "built-in",
|
||||
Name: "permission-built-in",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: "Built-in Permission",
|
||||
Users: []string{"built-in/admin"},
|
||||
Roles: []string{},
|
||||
ResourceType: "Application",
|
||||
Resources: []string{"app-built-in"},
|
||||
Actions: []string{"Read", "Write", "Admin"},
|
||||
Effect: "Allow",
|
||||
IsEnabled: true,
|
||||
}
|
||||
AddPermission(permission)
|
||||
}
|
||||
|
@ -89,6 +89,8 @@ func initDefinedOrganization(organization *Organization) {
|
||||
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
|
@ -97,7 +97,7 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
|
||||
//link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
|
||||
//or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
|
||||
for _, cert := range certs {
|
||||
certPemBlock := []byte(cert.PublicKey)
|
||||
certPemBlock := []byte(cert.Certificate)
|
||||
certDerBlock, _ := pem.Decode(certPemBlock)
|
||||
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
|
||||
|
||||
|
@ -15,6 +15,8 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/cred"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"xorm.io/core"
|
||||
@ -186,3 +188,31 @@ func DeleteOrganization(organization *Organization) bool {
|
||||
func GetOrganizationByUser(user *User) *Organization {
|
||||
return getOrganization("admin", user.Owner)
|
||||
}
|
||||
|
||||
func GetAccountItemByName(name string, organization *Organization) *AccountItem {
|
||||
if organization == nil {
|
||||
return nil
|
||||
}
|
||||
for _, accountItem := range organization.AccountItems {
|
||||
if accountItem.Name == name {
|
||||
return accountItem
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckAccountItemModifyRule(accountItem *AccountItem, user *User) (bool, string) {
|
||||
switch accountItem.ModifyRule {
|
||||
case "Admin":
|
||||
if !(user.IsAdmin || user.IsGlobalAdmin) {
|
||||
return false, fmt.Sprintf("Only admin can modify the %s.", accountItem.Name)
|
||||
}
|
||||
case "Immutable":
|
||||
return false, fmt.Sprintf("The %s is immutable.", accountItem.Name)
|
||||
case "Self":
|
||||
break
|
||||
default:
|
||||
return false, fmt.Sprintf("Unknown modify rule %s.", accountItem.ModifyRule)
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
@ -16,7 +16,12 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
xormadapter "github.com/casbin/xorm-adapter/v2"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"xorm.io/core"
|
||||
)
|
||||
@ -39,6 +44,16 @@ type Permission struct {
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
}
|
||||
|
||||
type PermissionRule struct {
|
||||
PType string `xorm:"varchar(100) index not null default ''"`
|
||||
V0 string `xorm:"varchar(100) index not null default ''"`
|
||||
V1 string `xorm:"varchar(100) index not null default ''"`
|
||||
V2 string `xorm:"varchar(100) index not null default ''"`
|
||||
V3 string `xorm:"varchar(100) index not null default ''"`
|
||||
V4 string `xorm:"varchar(100) index not null default ''"`
|
||||
V5 string `xorm:"varchar(100) index not null default ''"`
|
||||
}
|
||||
|
||||
func GetPermissionCount(owner, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Permission{})
|
||||
@ -95,7 +110,8 @@ func GetPermission(id string) *Permission {
|
||||
|
||||
func UpdatePermission(id string, permission *Permission) bool {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if getPermission(owner, name) == nil {
|
||||
oldPermission := getPermission(owner, name)
|
||||
if oldPermission == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -104,6 +120,11 @@ func UpdatePermission(id string, permission *Permission) bool {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
removePolicies(oldPermission)
|
||||
addPolicies(permission)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
@ -113,6 +134,10 @@ func AddPermission(permission *Permission) bool {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
addPolicies(permission)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
@ -122,9 +147,95 @@ func DeletePermission(permission *Permission) bool {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
removePolicies(permission)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func (permission *Permission) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", permission.Owner, permission.Name)
|
||||
}
|
||||
|
||||
func getEnforcer(permission *Permission) *casbin.Enforcer {
|
||||
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
||||
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), "permission_rule", tableNamePrefix, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
modelText := `
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = permission, sub, obj, act
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act`
|
||||
permissionModel := getModel(permission.Owner, permission.Model)
|
||||
if permissionModel != nil {
|
||||
modelText = permissionModel.ModelText
|
||||
}
|
||||
m, err := model.NewModelFromString(modelText)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
enforcer, err := casbin.NewEnforcer(m, adapter)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = enforcer.LoadFilteredPolicy(xormadapter.Filter{V0: []string{permission.GetId()}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return enforcer
|
||||
}
|
||||
|
||||
func getPolicies(permission *Permission) [][]string {
|
||||
var policies [][]string
|
||||
for _, user := range permission.Users {
|
||||
for _, resource := range permission.Resources {
|
||||
for _, action := range permission.Actions {
|
||||
policies = append(policies, []string{permission.GetId(), user, resource, strings.ToLower(action)})
|
||||
}
|
||||
}
|
||||
}
|
||||
return policies
|
||||
}
|
||||
|
||||
func addPolicies(permission *Permission) {
|
||||
enforcer := getEnforcer(permission)
|
||||
policies := getPolicies(permission)
|
||||
|
||||
_, err := enforcer.AddPolicies(policies)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func removePolicies(permission *Permission) {
|
||||
enforcer := getEnforcer(permission)
|
||||
|
||||
_, err := enforcer.RemoveFilteredPolicy(0, permission.GetId())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetPermissionsByUser(userId string) []*Permission {
|
||||
permissions := []*Permission{}
|
||||
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&permissions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return permissions
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ func TestProduct(t *testing.T) {
|
||||
product := GetProduct("admin/product_123")
|
||||
provider := getProvider(product.Owner, "provider_pay_alipay")
|
||||
cert := getCert(product.Owner, "cert-pay-alipay")
|
||||
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
|
||||
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
|
||||
|
||||
paymentName := util.GenerateTimeId()
|
||||
returnUrl := ""
|
||||
|
@ -214,7 +214,7 @@ func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
|
||||
}
|
||||
}
|
||||
|
||||
pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
|
||||
pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
|
||||
if pProvider == nil {
|
||||
return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
|
||||
}
|
||||
|
@ -33,6 +33,15 @@ func (application *Application) GetProviderItem(providerName string) *ProviderIt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (application *Application) GetProviderItemByType(providerType string) *ProviderItem {
|
||||
for _, item := range application.Providers {
|
||||
if item.Provider.Type == providerType {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pi *ProviderItem) IsProviderVisible() bool {
|
||||
if pi.Provider == nil {
|
||||
return false
|
||||
|
@ -121,3 +121,13 @@ func DeleteRole(role *Role) bool {
|
||||
func (role *Role) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", role.Owner, role.Name)
|
||||
}
|
||||
|
||||
func GetRolesByUser(userId string) []*Role {
|
||||
roles := []*Role{}
|
||||
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&roles)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ import (
|
||||
)
|
||||
|
||||
//returns a saml2 response
|
||||
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
|
||||
func NewSamlResponse(user *User, host string, certificate string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
Tag: "Response",
|
||||
@ -177,8 +177,8 @@ type Attribute struct {
|
||||
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
|
||||
//_, originBackend := getOriginFromHost(host)
|
||||
cert := getCertByApplication(application)
|
||||
block, _ := pem.Decode([]byte(cert.PublicKey))
|
||||
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
block, _ := pem.Decode([]byte(cert.Certificate))
|
||||
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
|
||||
origin := beego.AppConfig.String("origin")
|
||||
originFrontend, originBackend := getOriginFromHost(host)
|
||||
@ -199,7 +199,7 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
|
||||
KeyInfo: KeyInfo{
|
||||
X509Data: X509Data{
|
||||
X509Certificate: X509Certificate{
|
||||
Cert: publicKey,
|
||||
Cert: certificate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -248,18 +248,18 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
return "", "", fmt.Errorf("err: invalid issuer url")
|
||||
}
|
||||
|
||||
// get public key string
|
||||
// get certificate string
|
||||
cert := getCertByApplication(application)
|
||||
block, _ := pem.Decode([]byte(cert.PublicKey))
|
||||
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
block, _ := pem.Decode([]byte(cert.Certificate))
|
||||
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
|
||||
_, originBackend := getOriginFromHost(host)
|
||||
|
||||
// build signedResponse
|
||||
samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
|
||||
samlResponse, _ := NewSamlResponse(user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
|
||||
randomKeyStore := &X509Key{
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Certificate: publicKey,
|
||||
X509Certificate: certificate,
|
||||
}
|
||||
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
|
||||
ctx.Hash = crypto.SHA1
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
@ -42,8 +43,19 @@ func getProviderEndpoint(provider *Provider) string {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func escapePath(path string) string {
|
||||
tokens := strings.Split(path, "/")
|
||||
if len(tokens) > 0 {
|
||||
tokens[len(tokens)-1] = url.QueryEscape(tokens[len(tokens)-1])
|
||||
}
|
||||
|
||||
res := strings.Join(tokens, "/")
|
||||
return res
|
||||
}
|
||||
|
||||
func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
|
||||
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), fullFilePath)
|
||||
escapedPath := escapePath(fullFilePath)
|
||||
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
|
||||
|
||||
host := ""
|
||||
if provider.Type != "Local File System" {
|
||||
@ -60,9 +72,9 @@ func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
|
||||
host = fmt.Sprintf("%s/%s", host, provider.Bucket)
|
||||
}
|
||||
|
||||
fileUrl := util.UrlJoin(host, objectKey)
|
||||
fileUrl := util.UrlJoin(host, escapePath(objectKey))
|
||||
if hasTimestamp {
|
||||
fileUrl = fmt.Sprintf("%s?t=%s", util.UrlJoin(host, objectKey), util.GetCurrentUnixTime())
|
||||
fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime())
|
||||
}
|
||||
|
||||
return fileUrl, objectKey
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
func (syncer *Syncer) syncUsers() {
|
||||
fmt.Printf("Running syncUsers()..\n")
|
||||
|
||||
users, userMap := syncer.getUserMap()
|
||||
users, userMap, userNameMap := syncer.getUserMap()
|
||||
oUsers, oUserMap, err := syncer.getOriginalUserMap()
|
||||
if err != nil {
|
||||
fmt.Printf(err.Error())
|
||||
@ -44,9 +44,11 @@ func (syncer *Syncer) syncUsers() {
|
||||
for _, oUser := range oUsers {
|
||||
id := oUser.Id
|
||||
if _, ok := userMap[id]; !ok {
|
||||
newUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
||||
fmt.Printf("New user: %v\n", newUser)
|
||||
newUsers = append(newUsers, newUser)
|
||||
if _, ok := userNameMap[oUser.Name]; !ok {
|
||||
newUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
||||
fmt.Printf("New user: %v\n", newUser)
|
||||
newUsers = append(newUsers, newUser)
|
||||
}
|
||||
} else {
|
||||
user := userMap[id]
|
||||
oHash := syncer.calculateHash(oUser)
|
||||
|
@ -151,6 +151,8 @@ func (syncer *Syncer) initAdapter() {
|
||||
var dataSourceName string
|
||||
if syncer.DatabaseType == "mssql" {
|
||||
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
|
||||
} else if syncer.DatabaseType == "postgres" {
|
||||
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
|
||||
} else {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
|
||||
}
|
||||
|
@ -173,16 +173,21 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
|
||||
}
|
||||
|
||||
for _, tableColumn := range syncer.TableColumns {
|
||||
tableColumnName := tableColumn.Name
|
||||
if syncer.Type == "Keycloak" && syncer.DatabaseType == "postgres" {
|
||||
tableColumnName = strings.ToLower(tableColumnName)
|
||||
}
|
||||
|
||||
value := ""
|
||||
if strings.Contains(tableColumn.Name, "+") {
|
||||
names := strings.Split(tableColumn.Name, "+")
|
||||
if strings.Contains(tableColumnName, "+") {
|
||||
names := strings.Split(tableColumnName, "+")
|
||||
var values []string
|
||||
for _, name := range names {
|
||||
values = append(values, result[strings.Trim(name, " ")])
|
||||
}
|
||||
value = strings.Join(values, " ")
|
||||
} else {
|
||||
value = result[tableColumn.Name]
|
||||
value = result[tableColumnName]
|
||||
}
|
||||
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value)
|
||||
}
|
||||
@ -198,7 +203,7 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
|
||||
originalUser.PasswordSalt = credential.Salt
|
||||
}
|
||||
// query and set signup application from user group table
|
||||
sql = fmt.Sprintf("select name from keycloak_group where id = " +
|
||||
sql = fmt.Sprintf("select name from keycloak_group where id = "+
|
||||
"(select group_id as gid from user_group_membership where user_id = '%s')", originalUser.Id)
|
||||
groupResult, _ := syncer.Adapter.Engine.QueryString(sql)
|
||||
if len(groupResult) > 0 {
|
||||
@ -209,7 +214,12 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
|
||||
tm := time.Unix(i/int64(1000), 0)
|
||||
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
|
||||
// enable
|
||||
originalUser.IsForbidden = !(result["ENABLED"] == "\x01")
|
||||
value, ok := result["ENABLED"]
|
||||
if ok {
|
||||
originalUser.IsForbidden = !util.ParseBool(value)
|
||||
} else {
|
||||
originalUser.IsForbidden = !util.ParseBool(result["enabled"])
|
||||
}
|
||||
}
|
||||
|
||||
users = append(users, originalUser)
|
||||
|
@ -19,12 +19,15 @@ func (syncer *Syncer) getUsers() []*User {
|
||||
return users
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getUserMap() ([]*User, map[string]*User) {
|
||||
func (syncer *Syncer) getUserMap() ([]*User, map[string]*User, map[string]*User) {
|
||||
users := syncer.getUsers()
|
||||
|
||||
m := map[string]*User{}
|
||||
m1 := map[string]*User{}
|
||||
m2 := map[string]*User{}
|
||||
for _, user := range users {
|
||||
m[user.Id] = user
|
||||
m1[user.Id] = user
|
||||
m2[user.Name] = user
|
||||
}
|
||||
return users, m
|
||||
|
||||
return users, m1, m2
|
||||
}
|
||||
|
@ -241,11 +241,11 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
|
||||
samlResponse := NewSamlResponse11(user, request.RequestID, host)
|
||||
|
||||
cert := getCertByApplication(application)
|
||||
block, _ := pem.Decode([]byte(cert.PublicKey))
|
||||
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
block, _ := pem.Decode([]byte(cert.Certificate))
|
||||
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
randomKeyStore := &X509Key{
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Certificate: publicKey,
|
||||
X509Certificate: certificate,
|
||||
}
|
||||
|
||||
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
|
||||
|
@ -129,13 +129,13 @@ func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
// RSA public key
|
||||
publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.PublicKey))
|
||||
// RSA certificate
|
||||
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return publicKey, nil
|
||||
return certificate, nil
|
||||
})
|
||||
|
||||
if t != nil {
|
||||
|
@ -23,10 +23,10 @@ import (
|
||||
|
||||
func TestGenerateRsaKeys(t *testing.T) {
|
||||
fileId := "token_jwt_key"
|
||||
publicKey, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
|
||||
certificate, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
|
||||
|
||||
// Write certificate (aka public key) to file.
|
||||
util.WriteStringToPath(publicKey, fmt.Sprintf("%s.pem", fileId))
|
||||
// Write certificate (aka certificate) to file.
|
||||
util.WriteStringToPath(certificate, fmt.Sprintf("%s.pem", fileId))
|
||||
|
||||
// Write private key to file.
|
||||
util.WriteStringToPath(privateKey, fmt.Sprintf("%s.key", fileId))
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"xorm.io/core"
|
||||
)
|
||||
|
||||
@ -72,7 +73,7 @@ type User struct {
|
||||
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
|
||||
LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"`
|
||||
|
||||
Github string `xorm:"varchar(100)" json:"github"`
|
||||
GitHub string `xorm:"github 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"`
|
||||
@ -96,11 +97,16 @@ type User struct {
|
||||
Steam string `xorm:"steam varchar(100)" json:"steam"`
|
||||
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
||||
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
||||
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
|
||||
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
|
||||
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
||||
|
||||
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
|
||||
|
||||
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
||||
Properties map[string]string `json:"properties"`
|
||||
|
||||
Roles []*Role `json:"roles"`
|
||||
Permissions []*Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
type Userinfo struct {
|
||||
@ -267,6 +273,42 @@ func GetUserByEmail(owner string, email string) *User {
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByPhone(owner string, phone string) *User {
|
||||
if owner == "" || phone == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
user := User{Owner: owner, Phone: phone}
|
||||
existed, err := adapter.Engine.Get(&user)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &user
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByUserId(owner string, userId string) *User {
|
||||
if owner == "" || userId == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
user := User{Owner: owner, Id: userId}
|
||||
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)
|
||||
@ -326,9 +368,11 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
|
||||
}
|
||||
|
||||
if len(columns) == 0 {
|
||||
columns = []string{"owner", "display_name", "avatar",
|
||||
columns = []string{
|
||||
"owner", "display_name", "avatar",
|
||||
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
|
||||
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties"}
|
||||
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials",
|
||||
}
|
||||
}
|
||||
if isGlobalAdmin {
|
||||
columns = append(columns, "name", "email", "phone")
|
||||
@ -395,10 +439,10 @@ func AddUsers(users []*User) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//organization := GetOrganizationByUser(users[0])
|
||||
// organization := GetOrganizationByUser(users[0])
|
||||
for _, user := range users {
|
||||
// this function is only used for syncer or batch upload, so no need to encrypt the password
|
||||
//user.UpdateUserPassword(organization)
|
||||
// user.UpdateUserPassword(organization)
|
||||
|
||||
user.UpdateUserHash()
|
||||
user.PreHash = user.Hash
|
||||
|
@ -37,11 +37,11 @@ func TestSyncAvatarsFromGitHub(t *testing.T) {
|
||||
|
||||
users := GetGlobalUsers()
|
||||
for _, user := range users {
|
||||
if user.Github == "" {
|
||||
if user.GitHub == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
user.Avatar = fmt.Sprintf("https://avatars.githubusercontent.com/%s", user.Github)
|
||||
user.Avatar = fmt.Sprintf("https://avatars.githubusercontent.com/%s", user.GitHub)
|
||||
updateUserColumn("avatar", user)
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,10 @@ func setUserProperty(user *User, field string, value string) {
|
||||
if value == "" {
|
||||
delete(user.Properties, field)
|
||||
} else {
|
||||
if user.Properties == nil {
|
||||
user.Properties = make(map[string]string)
|
||||
}
|
||||
|
||||
user.Properties[field] = value
|
||||
}
|
||||
}
|
||||
|
102
object/user_webauthn.go
Normal file
102
object/user_webauthn.go
Normal file
@ -0,0 +1,102 @@
|
||||
// Copyright 2022 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 (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/duo-labs/webauthn/protocol"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
)
|
||||
|
||||
func GetWebAuthnObject(host string) *webauthn.WebAuthn {
|
||||
var err error
|
||||
|
||||
origin := beego.AppConfig.String("origin")
|
||||
if origin == "" {
|
||||
_, origin = getOriginFromHost(host)
|
||||
}
|
||||
|
||||
localUrl, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
panic("error when parsing origin:" + err.Error())
|
||||
}
|
||||
|
||||
webAuthn, err := webauthn.New(&webauthn.Config{
|
||||
RPDisplayName: beego.AppConfig.String("appname"), // Display Name for your site
|
||||
RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array
|
||||
RPOrigin: origin, // The origin URL for WebAuthn requests
|
||||
// RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return webAuthn
|
||||
}
|
||||
|
||||
// implementation of webauthn.User interface
|
||||
func (u *User) WebAuthnID() []byte {
|
||||
return []byte(u.GetId())
|
||||
}
|
||||
|
||||
func (u *User) WebAuthnName() string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
func (u *User) WebAuthnDisplayName() string {
|
||||
return u.DisplayName
|
||||
}
|
||||
|
||||
func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||
return u.WebauthnCredentials
|
||||
}
|
||||
|
||||
func (u *User) WebAuthnIcon() string {
|
||||
return u.Avatar
|
||||
}
|
||||
|
||||
// CredentialExcludeList returns a CredentialDescriptor array filled with all the user's credentials
|
||||
func (u *User) CredentialExcludeList() []protocol.CredentialDescriptor {
|
||||
credentials := u.WebAuthnCredentials()
|
||||
credentialExcludeList := []protocol.CredentialDescriptor{}
|
||||
for _, cred := range credentials {
|
||||
descriptor := protocol.CredentialDescriptor{
|
||||
Type: protocol.PublicKeyCredentialType,
|
||||
CredentialID: cred.ID,
|
||||
}
|
||||
credentialExcludeList = append(credentialExcludeList, descriptor)
|
||||
}
|
||||
|
||||
return credentialExcludeList
|
||||
}
|
||||
|
||||
func (u *User) AddCredentials(credential webauthn.Credential, isGlobalAdmin bool) bool {
|
||||
u.WebauthnCredentials = append(u.WebauthnCredentials, credential)
|
||||
return UpdateUser(u.GetId(), u, []string{"webauthnCredentials"}, isGlobalAdmin)
|
||||
}
|
||||
|
||||
func (u *User) DeleteCredentials(credentialIdBase64 string) bool {
|
||||
for i, credential := range u.WebauthnCredentials {
|
||||
if base64.StdEncoding.EncodeToString(credential.ID) == credentialIdBase64 {
|
||||
u.WebauthnCredentials = append(u.WebauthnCredentials[0:i], u.WebauthnCredentials[i+1:]...)
|
||||
return UpdateUserForAllFields(u.GetId(), u)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -28,7 +28,7 @@ type AlipayPaymentProvider struct {
|
||||
Client *alipay.Client
|
||||
}
|
||||
|
||||
func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider {
|
||||
func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider {
|
||||
pp := &AlipayPaymentProvider{}
|
||||
|
||||
client, err := alipay.NewClient(appId, appPrivateKey, true)
|
||||
@ -36,7 +36,7 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = client.SetCertSnByContent([]byte(appPublicKey), []byte(authorityRootPublicKey), []byte(authorityPublicKey))
|
||||
err = client.SetCertSnByContent([]byte(appCertificate), []byte(authorityRootPublicKey), []byte(authorityPublicKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -22,9 +22,9 @@ type PaymentProvider interface {
|
||||
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
|
||||
}
|
||||
|
||||
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {
|
||||
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {
|
||||
if typ == "Alipay" {
|
||||
return NewAlipayPaymentProvider(appId, appPublicKey, appPrivateKey, authorityPublicKey, authorityRootPublicKey)
|
||||
return NewAlipayPaymentProvider(appId, appCertificate, appPrivateKey, authorityPublicKey, authorityRootPublicKey)
|
||||
} else if typ == "GC" {
|
||||
return NewGcPaymentProvider(appId, clientSecret, host)
|
||||
}
|
||||
|
@ -109,6 +109,10 @@ func getUrlPath(urlPath string) string {
|
||||
return "/api/login/oauth"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(urlPath, "/api/webauthn") {
|
||||
return "/api/webauthn"
|
||||
}
|
||||
|
||||
return urlPath
|
||||
}
|
||||
|
||||
@ -118,6 +122,10 @@ func AuthzFilter(ctx *context.Context) {
|
||||
urlPath := getUrlPath(ctx.Request.URL.Path)
|
||||
objOwner, objName := getObject(ctx)
|
||||
|
||||
if strings.HasPrefix(urlPath, "/api/notify-payment") {
|
||||
urlPath = "/api/notify-payment"
|
||||
}
|
||||
|
||||
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName)
|
||||
|
||||
result := "deny"
|
||||
|
@ -48,7 +48,7 @@ func initAPI() {
|
||||
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/logout", &controllers.ApiController{}, "GET,POST:Logout")
|
||||
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
|
||||
beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo")
|
||||
beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink")
|
||||
@ -191,4 +191,9 @@ func initAPI() {
|
||||
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
|
||||
|
||||
beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "Get:WebAuthnSignupBegin")
|
||||
beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "Post:WebAuthnSignupFinish")
|
||||
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin")
|
||||
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish")
|
||||
|
||||
}
|
||||
|
@ -2418,6 +2418,21 @@
|
||||
}
|
||||
},
|
||||
"/api/logout": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Login API"
|
||||
],
|
||||
"description": "logout the current user",
|
||||
"operationId": "ApiController.Logout",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Login API"
|
||||
@ -3096,14 +3111,120 @@
|
||||
],
|
||||
"operationId": "ApiController.VerifyCaptcha"
|
||||
}
|
||||
},
|
||||
"/api/webauthn/signin/begin": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Login API"
|
||||
],
|
||||
"description": "WebAuthn Login Flow 1st stage",
|
||||
"operationId": "ApiController.WebAuthnSigninBegin",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "owner",
|
||||
"description": "owner",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "name",
|
||||
"description": "name",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The CredentialAssertion object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/protocol.CredentialAssertion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/webauthn/signin/finish": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Login API"
|
||||
],
|
||||
"description": "WebAuthn Login Flow 2nd stage",
|
||||
"operationId": "ApiController.WebAuthnSigninBegin",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "authenticator assertion Response",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/protocol.CredentialAssertionResponse"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "\"The Response object\"",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/webauthn/signup/begin": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"User API"
|
||||
],
|
||||
"description": "WebAuthn Registration Flow 1st stage",
|
||||
"operationId": "ApiController.WebAuthnSignupBegin",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The CredentialCreationOptions object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/protocol.CredentialCreation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/webauthn/signup/finish": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"User API"
|
||||
],
|
||||
"description": "WebAuthn Registration Flow 2nd stage",
|
||||
"operationId": "ApiController.WebAuthnSignupFinish",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"description": "authenticator attestation Response",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/protocol.CredentialCreationResponse"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "\"The Response object\"",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"2127.0xc000398090.false": {
|
||||
"2127.0xc000427560.false": {
|
||||
"title": "false",
|
||||
"type": "object"
|
||||
},
|
||||
"2161.0xc0003980c0.false": {
|
||||
"2161.0xc000427590.false": {
|
||||
"title": "false",
|
||||
"type": "object"
|
||||
},
|
||||
@ -3221,10 +3342,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/2127.0xc000398090.false"
|
||||
"$ref": "#/definitions/2127.0xc000427560.false"
|
||||
},
|
||||
"data2": {
|
||||
"$ref": "#/definitions/2161.0xc0003980c0.false"
|
||||
"$ref": "#/definitions/2161.0xc000427590.false"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
@ -3329,12 +3450,18 @@
|
||||
"enablePassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableSamlCompress": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableSignUp": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableSigninSession": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableWebAuthn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"expireInHours": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@ -3444,7 +3571,7 @@
|
||||
"privateKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicKey": {
|
||||
"certificate": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
@ -4507,6 +4634,12 @@
|
||||
"updatedTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"webauthnCredentials": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/webauthn.Credential"
|
||||
}
|
||||
},
|
||||
"wechat": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -4596,6 +4729,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"protocol.CredentialAssertion": {
|
||||
"title": "CredentialAssertion",
|
||||
"type": "object"
|
||||
},
|
||||
"protocol.CredentialAssertionResponse": {
|
||||
"title": "CredentialAssertionResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"protocol.CredentialCreation": {
|
||||
"title": "CredentialCreation",
|
||||
"type": "object"
|
||||
},
|
||||
"protocol.CredentialCreationResponse": {
|
||||
"title": "CredentialCreationResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"webauthn.Credential": {
|
||||
"title": "Credential",
|
||||
"type": "object"
|
||||
},
|
||||
"xorm.Engine": {
|
||||
"title": "Engine",
|
||||
"type": "object"
|
||||
|
@ -1584,6 +1584,16 @@ paths:
|
||||
schema:
|
||||
$ref: '#/definitions/object.TokenError'
|
||||
/api/logout:
|
||||
get:
|
||||
tags:
|
||||
- Login API
|
||||
description: logout the current user
|
||||
operationId: ApiController.Logout
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
post:
|
||||
tags:
|
||||
- Login API
|
||||
@ -2028,11 +2038,80 @@ paths:
|
||||
tags:
|
||||
- Verification API
|
||||
operationId: ApiController.VerifyCaptcha
|
||||
/api/webauthn/signin/begin:
|
||||
get:
|
||||
tags:
|
||||
- Login API
|
||||
description: WebAuthn Login Flow 1st stage
|
||||
operationId: ApiController.WebAuthnSigninBegin
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: owner
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: name
|
||||
description: name
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The CredentialAssertion object
|
||||
schema:
|
||||
$ref: '#/definitions/protocol.CredentialAssertion'
|
||||
/api/webauthn/signin/finish:
|
||||
post:
|
||||
tags:
|
||||
- Login API
|
||||
description: WebAuthn Login Flow 2nd stage
|
||||
operationId: ApiController.WebAuthnSigninBegin
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: authenticator assertion Response
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/protocol.CredentialAssertionResponse'
|
||||
responses:
|
||||
"200":
|
||||
description: '"The Response object"'
|
||||
schema:
|
||||
$ref: '#/definitions/Response'
|
||||
/api/webauthn/signup/begin:
|
||||
get:
|
||||
tags:
|
||||
- User API
|
||||
description: WebAuthn Registration Flow 1st stage
|
||||
operationId: ApiController.WebAuthnSignupBegin
|
||||
responses:
|
||||
"200":
|
||||
description: The CredentialCreationOptions object
|
||||
schema:
|
||||
$ref: '#/definitions/protocol.CredentialCreation'
|
||||
/api/webauthn/signup/finish:
|
||||
post:
|
||||
tags:
|
||||
- User API
|
||||
description: WebAuthn Registration Flow 2nd stage
|
||||
operationId: ApiController.WebAuthnSignupFinish
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: authenticator attestation Response
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/protocol.CredentialCreationResponse'
|
||||
responses:
|
||||
"200":
|
||||
description: '"The Response object"'
|
||||
schema:
|
||||
$ref: '#/definitions/Response'
|
||||
definitions:
|
||||
2127.0xc000398090.false:
|
||||
2127.0xc000427560.false:
|
||||
title: "false"
|
||||
type: object
|
||||
2161.0xc0003980c0.false:
|
||||
2161.0xc000427590.false:
|
||||
title: "false"
|
||||
type: object
|
||||
Response:
|
||||
@ -2113,9 +2192,9 @@ definitions:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/2127.0xc000398090.false'
|
||||
$ref: '#/definitions/2127.0xc000427560.false'
|
||||
data2:
|
||||
$ref: '#/definitions/2161.0xc0003980c0.false'
|
||||
$ref: '#/definitions/2161.0xc000427590.false'
|
||||
msg:
|
||||
type: string
|
||||
name:
|
||||
@ -2185,10 +2264,14 @@ definitions:
|
||||
type: boolean
|
||||
enablePassword:
|
||||
type: boolean
|
||||
enableSamlCompress:
|
||||
type: boolean
|
||||
enableSignUp:
|
||||
type: boolean
|
||||
enableSigninSession:
|
||||
type: boolean
|
||||
enableWebAuthn:
|
||||
type: boolean
|
||||
expireInHours:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -2263,7 +2346,7 @@ definitions:
|
||||
type: string
|
||||
privateKey:
|
||||
type: string
|
||||
publicKey:
|
||||
certificate:
|
||||
type: string
|
||||
scope:
|
||||
type: string
|
||||
@ -2977,6 +3060,10 @@ definitions:
|
||||
type: string
|
||||
updatedTime:
|
||||
type: string
|
||||
webauthnCredentials:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/webauthn.Credential'
|
||||
wechat:
|
||||
type: string
|
||||
wecom:
|
||||
@ -3035,6 +3122,21 @@ definitions:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
protocol.CredentialAssertion:
|
||||
title: CredentialAssertion
|
||||
type: object
|
||||
protocol.CredentialAssertionResponse:
|
||||
title: CredentialAssertionResponse
|
||||
type: object
|
||||
protocol.CredentialCreation:
|
||||
title: CredentialCreation
|
||||
type: object
|
||||
protocol.CredentialCreationResponse:
|
||||
title: CredentialCreationResponse
|
||||
type: object
|
||||
webauthn.Credential:
|
||||
title: Credential
|
||||
type: object
|
||||
xorm.Engine:
|
||||
title: Engine
|
||||
type: object
|
||||
|
@ -52,8 +52,10 @@ func ParseFloat(s string) float64 {
|
||||
}
|
||||
|
||||
func ParseBool(s string) bool {
|
||||
if s == "\x01" {
|
||||
if s == "\x01" || s == "true" {
|
||||
return true
|
||||
} else if s == "false" {
|
||||
return false
|
||||
}
|
||||
|
||||
i := ParseInt(s)
|
||||
|
@ -45,6 +45,10 @@
|
||||
"curly": ["error", "all"],
|
||||
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"sort-imports": ["error", {
|
||||
"ignoreDeclarationSort": true
|
||||
}],
|
||||
|
||||
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
@ -86,12 +86,15 @@ class AccountTable extends React.Component {
|
||||
{name: "Bio", displayName: i18next.t("user:Bio")},
|
||||
{name: "Tag", displayName: i18next.t("user:Tag")},
|
||||
{name: "Signup application", displayName: i18next.t("general:Signup application")},
|
||||
{name: "Roles", displayName: i18next.t("general:Roles")},
|
||||
{name: "Permissions", displayName: i18next.t("general:Permissions")},
|
||||
{name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")},
|
||||
{name: "Properties", displayName: i18next.t("user:Properties")},
|
||||
{name: "Is admin", displayName: i18next.t("user:Is admin")},
|
||||
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
|
||||
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
|
||||
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
|
||||
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
|
||||
];
|
||||
|
||||
const getItemDisplayName = (text) => {
|
||||
|
@ -17,7 +17,7 @@ import "./App.less";
|
||||
import {Helmet} from "react-helmet";
|
||||
import * as Setting from "./Setting";
|
||||
import {DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
|
||||
import {Avatar, BackTop, Dropdown, Layout, Menu, Card, Result, Button} from "antd";
|
||||
import {Avatar, BackTop, Button, Card, Dropdown, Layout, Menu, Result} from "antd";
|
||||
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
|
||||
import OrganizationListPage from "./OrganizationListPage";
|
||||
import OrganizationEditPage from "./OrganizationEditPage";
|
||||
@ -235,6 +235,8 @@ class App extends Component {
|
||||
AuthBackend.logout()
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const owner = this.state.account.owner;
|
||||
|
||||
this.setState({
|
||||
account: null
|
||||
});
|
||||
@ -243,7 +245,9 @@ class App extends Component {
|
||||
let redirectUri = res.data2;
|
||||
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
|
||||
Setting.goToLink(redirectUri);
|
||||
}else{
|
||||
} else if (owner !== "built-in") {
|
||||
Setting.goToLink(`${window.location.origin}/login/${owner}`);
|
||||
} else {
|
||||
Setting.goToLinkSoft(this, "/");
|
||||
}
|
||||
} else {
|
||||
@ -287,10 +291,12 @@ class App extends Component {
|
||||
<Menu onClick={this.handleRightDropdownClick.bind(this)}>
|
||||
<Menu.Item key="/account">
|
||||
<SettingOutlined />
|
||||
|
||||
{i18next.t("account:My Account")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/logout">
|
||||
<LogoutOutlined />
|
||||
|
||||
{i18next.t("account:Logout")}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
@ -667,7 +673,9 @@ class App extends Component {
|
||||
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />)} />
|
||||
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
|
||||
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
|
||||
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
|
||||
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
|
||||
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
|
||||
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
|
||||
|
@ -353,6 +353,16 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable WebAuthn signin"), i18next.t("application:Enable WebAuthn signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableWebAuthn} onChange={checked => {
|
||||
this.updateApplicationField("enableWebAuthn", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} :
|
||||
|
@ -30,6 +30,7 @@ class BaseListPage extends React.Component {
|
||||
loading: false,
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
isAuthorized: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -164,25 +164,25 @@ class CertEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("cert:Public key"), i18next.t("cert:Public key - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("cert:Certificate"), i18next.t("cert:Certificate - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={9} >
|
||||
<Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
|
||||
copy(this.state.cert.publicKey);
|
||||
Setting.showMessage("success", i18next.t("cert:Public key copied to clipboard successfully"));
|
||||
copy(this.state.cert.certificate);
|
||||
Setting.showMessage("success", i18next.t("cert:Certificate copied to clipboard successfully"));
|
||||
}}
|
||||
>
|
||||
{i18next.t("cert:Copy public key")}
|
||||
{i18next.t("cert:Copy certificate")}
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => {
|
||||
const blob = new Blob([this.state.cert.publicKey], {type: "text/plain;charset=utf-8"});
|
||||
const blob = new Blob([this.state.cert.certificate], {type: "text/plain;charset=utf-8"});
|
||||
FileSaver.saveAs(blob, "token_jwt_key.pem");
|
||||
}}
|
||||
>
|
||||
{i18next.t("cert:Download public key")}
|
||||
{i18next.t("cert:Download certificate")}
|
||||
</Button>
|
||||
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.publicKey} onChange={e => {
|
||||
this.updateCertField("publicKey", e.target.value);
|
||||
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.certificate} onChange={e => {
|
||||
this.updateCertField("certificate", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
<Col span={1} />
|
||||
|
@ -34,7 +34,7 @@ class CertListPage extends BaseListPage {
|
||||
cryptoAlgorithm: "RS256",
|
||||
bitSize: 4096,
|
||||
expireInYears: 20,
|
||||
publicKey: "",
|
||||
certificate: "",
|
||||
privateKey: "",
|
||||
};
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import React, {useState} from "react";
|
||||
import Cropper from "react-cropper";
|
||||
import "cropperjs/dist/cropper.css";
|
||||
import * as Setting from "./Setting";
|
||||
import {Button, Row, Col, Modal} from "antd";
|
||||
import {Button, Col, Modal, Row} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as ResourceBackend from "./backend/ResourceBackend";
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Col, Row, Table, Popconfirm} from "antd";
|
||||
import {Button, Col, Popconfirm, Row, Table} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import * as LdapBackend from "./backend/LdapBackend";
|
||||
import i18next from "i18next";
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Popconfirm, Switch, Table} from "antd";
|
||||
import {Button, Popconfirm, Result, Switch, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
@ -57,6 +57,8 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"},
|
||||
{name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
|
||||
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
|
||||
@ -235,6 +237,17 @@ class OrganizationListPage extends BaseListPage {
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title="403 Unauthorized"
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={organizations} rowKey="name" size="middle" bordered pagination={paginationProps}
|
||||
@ -272,6 +285,13 @@ class OrganizationListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (res.msg.includes("Unauthorized")) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Button, Col, Modal, Row, Input} from "antd";
|
||||
import {Button, Col, Input, Modal, Row} from "antd";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
|
@ -21,6 +21,7 @@ import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as RoleBackend from "./backend/RoleBackend";
|
||||
import * as ModelBackend from "./backend/ModelBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -36,6 +37,7 @@ class PermissionEditPage extends React.Component {
|
||||
users: [],
|
||||
roles: [],
|
||||
models: [],
|
||||
resources: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
@ -55,6 +57,7 @@ class PermissionEditPage extends React.Component {
|
||||
this.getUsers(permission.owner);
|
||||
this.getRoles(permission.owner);
|
||||
this.getModels(permission.owner);
|
||||
this.getResources(permission.owner);
|
||||
});
|
||||
}
|
||||
|
||||
@ -94,6 +97,15 @@ class PermissionEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getResources(organizationName) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
resources: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parsePermissionField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
@ -131,6 +143,8 @@ class PermissionEditPage extends React.Component {
|
||||
|
||||
this.getUsers(owner);
|
||||
this.getRoles(owner);
|
||||
this.getModels(owner);
|
||||
this.getResources(owner);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
@ -212,6 +226,18 @@ class PermissionEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("permission:Resources"), i18next.t("permission:Resources - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.permission.resources} onChange={(value => {this.updatePermissionField("resources", value);})}>
|
||||
{
|
||||
this.state.resources.map((resource, index) => <Option key={index} value={`${resource.name}`}>{`${resource.name}`}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("permission:Actions"), i18next.t("permission:Actions - Tooltip"))} :
|
||||
|
@ -33,7 +33,7 @@ class PermissionListPage extends BaseListPage {
|
||||
roles: [],
|
||||
resourceType: "Application",
|
||||
resources: ["app-built-in"],
|
||||
action: "Read",
|
||||
actions: ["Read"],
|
||||
effect: "Allow",
|
||||
isEnabled: true,
|
||||
};
|
||||
|
@ -622,7 +622,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:IdP"), i18next.t("provider:IdP public key"))} :
|
||||
{Setting.getLabel(i18next.t("provider:IdP"), i18next.t("provider:IdP certificate"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.idP} onChange={e => {
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Button, Col, Modal, Row, Input} from "antd";
|
||||
import {Button, Col, Input, Modal, Row} from "antd";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import * as Setting from "./Setting";
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import * as Setting from "./Setting";
|
||||
import {Menu, Dropdown} from "antd";
|
||||
import {Dropdown, Menu} from "antd";
|
||||
import {createFromIconfontCN} from "@ant-design/icons";
|
||||
import "./App.less";
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {message, Tag, Tooltip} from "antd";
|
||||
import {Tag, Tooltip, message} from "antd";
|
||||
import {QuestionCircleTwoTone} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {isMobile as isMobileDevice} from "react-device-detect";
|
||||
@ -662,8 +662,8 @@ export function goToLogin(ths, application) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!application.enablePassword && window.location.pathname.includes("/signup/oauth/authorize")) {
|
||||
const link = window.location.href.replace("/signup/oauth/authorize", "/login/oauth/authorize");
|
||||
if (!application.enablePassword && window.location.pathname.includes("/auto-signup/oauth/authorize")) {
|
||||
const link = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
|
||||
goToLink(link);
|
||||
return;
|
||||
}
|
||||
@ -685,7 +685,7 @@ export function goToSignup(ths, application) {
|
||||
}
|
||||
|
||||
if (!application.enablePassword && window.location.pathname.includes("/login/oauth/authorize")) {
|
||||
const link = window.location.href.replace("/login/oauth/authorize", "/signup/oauth/authorize");
|
||||
const link = window.location.href.replace("/login/oauth/authorize", "/auto-signup/oauth/authorize");
|
||||
goToLink(link);
|
||||
return;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Input, Row, Select, Switch, Table, Tooltip} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DownOutlined, DeleteOutlined, UpOutlined, LinkOutlined} from "@ant-design/icons";
|
||||
import {DeleteOutlined, DownOutlined, LinkOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Input, Row, Table, Tooltip} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
@ -15,6 +15,7 @@
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from "antd";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as UserWebauthnBackend from "./backend/UserWebauthnBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
@ -27,6 +28,7 @@ import AffiliationSelect from "./common/AffiliationSelect";
|
||||
import OAuthWidget from "./common/OAuthWidget";
|
||||
import SamlWidget from "./common/SamlWidget";
|
||||
import SelectRegionBox from "./SelectRegionBox";
|
||||
import WebAuthnCredentialTable from "./WebauthnCredentialTable";
|
||||
|
||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
@ -289,7 +291,7 @@ class UserEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
<Col span={11} >
|
||||
{this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null}
|
||||
{this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
@ -307,7 +309,7 @@ class UserEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
<Col span={11} >
|
||||
{this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
|
||||
{this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
@ -425,6 +427,32 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Roles") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px", alignItems: "center"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Roles"), i18next.t("general:Roles - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
Setting.getTags(this.state.user.roles.map(role => role.name))
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Permissions") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px", alignItems: "center"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Permissions"), i18next.t("general:Permissions - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
Setting.getTags(this.state.user.permissions.map(permission => permission.name))
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "3rd-party logins") {
|
||||
return (
|
||||
!this.isSelfOrAdmin() ? null : (
|
||||
@ -438,7 +466,7 @@ class UserEditPage extends React.Component {
|
||||
(this.state.application === null || this.state.user === null) ? null : (
|
||||
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) =>
|
||||
(providerItem.provider.category === "OAuth") ? (
|
||||
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />
|
||||
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />
|
||||
) : (
|
||||
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />
|
||||
)
|
||||
@ -516,6 +544,17 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if(accountItem.name === "WebAuthn credentials") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:WebAuthn credentials"), i18next.t("user:WebAuthn credentials"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<WebAuthnCredentialTable table={this.state.user.webauthnCredentials} updateTable={(table) => {this.updateUserField("webauthnCredentials", table);}} refresh={this.getUser.bind(this)} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
76
web/src/WebauthnCredentialTable.js
Normal file
76
web/src/WebauthnCredentialTable.js
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2022 The Casdoor 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.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Table} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as UserWebauthnBackend from "./backend/UserWebauthnBackend";
|
||||
import * as Setting from "./Setting";
|
||||
|
||||
class WebAuthnCredentialTable extends React.Component {
|
||||
render() {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("user:WebAuthn credentials"),
|
||||
dataIndex: "ID",
|
||||
key: "ID",
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Button style={{marginTop: "5px", marginBottom: "5px", marginRight: "5px"}} type="danger" onClick={() => {this.deleteRow(this.props.table, index);}}>
|
||||
{i18next.t("general:Delete")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Table scroll={{x: "max-content"}} rowKey={"ID"} columns={columns} dataSource={this.props.table} size="middle" bordered pagination={false}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("user:WebAuthn credentials")}
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => {this.registerWebAuthn();}}>
|
||||
{i18next.t("general:Add")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
deleteRow(table, i) {
|
||||
table = Setting.deleteRow(table, i);
|
||||
this.props.updateTable(table);
|
||||
}
|
||||
|
||||
registerWebAuthn() {
|
||||
UserWebauthnBackend.registerWebauthnCredential().then((res) => {
|
||||
if (res.msg === "") {
|
||||
Setting.showMessage("success", "Successfully added webauthn credentials");
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
|
||||
this.props.refresh();
|
||||
}).catch(error => {
|
||||
Setting.showMessage("error", `Failed to connect to server: ${error}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default WebAuthnCredentialTable;
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Input, Row, Table, Tooltip} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
@ -62,7 +62,7 @@ class ForgetPage extends React.Component {
|
||||
} else {
|
||||
Util.showMessage(
|
||||
"error",
|
||||
i18next.t("forget:Unknown forgot type: ") + this.state.type
|
||||
i18next.t("forget:Unknown forget type: ") + this.state.type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,9 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin} from "antd";
|
||||
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd";
|
||||
import {LockOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import * as Provider from "./Provider";
|
||||
@ -49,6 +50,8 @@ import CustomGithubCorner from "../CustomGithubCorner";
|
||||
import {CountDownInput} from "../common/CountDownInput";
|
||||
import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -65,7 +68,9 @@ class LoginPage extends React.Component {
|
||||
validEmailOrPhone: false,
|
||||
validEmail: false,
|
||||
validPhone: false,
|
||||
loginMethod: "password"
|
||||
};
|
||||
|
||||
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
||||
this.state.owner = props.match?.params.owner;
|
||||
this.state.applicationName = props.match?.params.casApplicationName;
|
||||
@ -186,6 +191,10 @@ class LoginPage extends React.Component {
|
||||
values["type"] = "saml";
|
||||
}
|
||||
|
||||
if (this.state.owner != null) {
|
||||
values["organization"] = this.state.owner;
|
||||
}
|
||||
|
||||
AuthBackend.login(values, oAuthParams)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
@ -198,6 +207,7 @@ class LoginPage extends React.Component {
|
||||
} else if (responseType === "code") {
|
||||
const code = res.data;
|
||||
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
|
||||
const noRedirect = oAuthParams.noRedirect;
|
||||
|
||||
if (Setting.hasPromptPage(application)) {
|
||||
AuthBackend.getAccount("")
|
||||
@ -219,7 +229,19 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
if (noRedirect === "true") {
|
||||
window.close();
|
||||
const newWindow = window.open(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
if (newWindow) {
|
||||
setInterval(() => {
|
||||
if (!newWindow.closed) {
|
||||
newWindow.close();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Util.showMessage("success", `Authorization code: ${res.data}`);
|
||||
@ -407,6 +429,7 @@ class LoginPage extends React.Component {
|
||||
]}
|
||||
>
|
||||
</Form.Item>
|
||||
{this.renderMethodChoiceBox()}
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
@ -421,13 +444,15 @@ class LoginPage extends React.Component {
|
||||
this.setState({validEmailOrPhone: false});
|
||||
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
|
||||
}
|
||||
|
||||
if (Setting.isValidPhone(this.state.username)) {
|
||||
this.setState({validPhone: true});
|
||||
}
|
||||
if (Setting.isValidEmail(this.state.username)) {
|
||||
this.setState({validEmail: true});
|
||||
}
|
||||
}
|
||||
if (Setting.isValidPhone(this.state.username)) {
|
||||
this.setState({validPhone: true});
|
||||
}
|
||||
if (Setting.isValidEmail(this.state.username)) {
|
||||
this.setState({validEmail: true});
|
||||
}
|
||||
|
||||
this.setState({validEmailOrPhone: true});
|
||||
return Promise.resolve();
|
||||
}
|
||||
@ -446,29 +471,7 @@ class LoginPage extends React.Component {
|
||||
/>
|
||||
</Form.Item>
|
||||
{
|
||||
this.state.isCodeSignin ? (
|
||||
<Form.Item
|
||||
name="code"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||
>
|
||||
<CountDownInput
|
||||
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
||||
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
||||
>
|
||||
<Input
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder={i18next.t("login:Password")}
|
||||
disabled={!application.enablePassword}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
this.renderPasswordOrCodeInput()
|
||||
}
|
||||
<Form.Item>
|
||||
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
|
||||
@ -483,16 +486,26 @@ class LoginPage extends React.Component {
|
||||
</a>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{width: "100%", marginBottom: "5px"}}
|
||||
disabled={!application.enablePassword}
|
||||
>
|
||||
{i18next.t("login:Sign In")}
|
||||
</Button>
|
||||
{
|
||||
!application.enableSignUp ? null : this.renderFooter(application)
|
||||
this.state.loginMethod === "password" ?
|
||||
(
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{width: "100%", marginBottom: "5px"}}
|
||||
disabled={!application.enablePassword}
|
||||
>
|
||||
{i18next.t("login:Sign In")}
|
||||
</Button>
|
||||
) :
|
||||
(
|
||||
<Button type="primary" style={{width: "100%", marginBottom: "5px"}} onClick={() => this.signInWithWebAuthn()}>
|
||||
{i18next.t("login:Sign in with WebAuthn")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.renderFooter(application)
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
@ -522,16 +535,12 @@ class LoginPage extends React.Component {
|
||||
return this.renderProviderLogo(providerItem.provider, application, 40, 10, "big");
|
||||
})
|
||||
}
|
||||
{
|
||||
!application.enableSignUp ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{
|
||||
this.renderFooter(application)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<br />
|
||||
{
|
||||
this.renderFooter(application)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -566,13 +575,19 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
</span>
|
||||
<span style={{float: "right"}}>
|
||||
{i18next.t("login:No account?")}
|
||||
<a onClick={() => {
|
||||
sessionStorage.setItem("signinUrl", window.location.href);
|
||||
Setting.goToSignup(this, application);
|
||||
}}>
|
||||
{i18next.t("login:sign up now")}
|
||||
</a>
|
||||
{
|
||||
!application.enableSignUp ? null : (
|
||||
<>
|
||||
{i18next.t("login:No account?")}
|
||||
<a onClick={() => {
|
||||
sessionStorage.setItem("signinUrl", window.location.href);
|
||||
Setting.goToSignup(this, application);
|
||||
}}>
|
||||
{i18next.t("login:sign up now")}
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
@ -624,6 +639,111 @@ class LoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
signInWithWebAuthn() {
|
||||
if (this.state.username === null || this.state.username === "") {
|
||||
Setting.showMessage("error", "username is required for webauthn login");
|
||||
return;
|
||||
}
|
||||
|
||||
let application = this.getApplicationObj();
|
||||
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}&name=${this.state.username}`, {
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then((credentialRequestOptions) => {
|
||||
if ("status" in credentialRequestOptions) {
|
||||
Setting.showMessage("error", credentialRequestOptions.msg);
|
||||
throw credentialRequestOptions.status.msg;
|
||||
}
|
||||
|
||||
credentialRequestOptions.certificate.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.certificate.challenge);
|
||||
credentialRequestOptions.certificate.allowCredentials.forEach(function(listItem) {
|
||||
listItem.id = UserWebauthnBackend.webAuthnBufferDecode(listItem.id);
|
||||
});
|
||||
|
||||
return navigator.credentials.get({
|
||||
certificate: credentialRequestOptions.certificate
|
||||
});
|
||||
})
|
||||
.then((assertion) => {
|
||||
let authData = assertion.response.authenticatorData;
|
||||
let clientDataJSON = assertion.response.clientDataJSON;
|
||||
let rawId = assertion.rawId;
|
||||
let sig = assertion.response.signature;
|
||||
let userHandle = assertion.response.userHandle;
|
||||
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/finish`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
id: assertion.id,
|
||||
rawId: UserWebauthnBackend.webAuthnBufferEncode(rawId),
|
||||
type: assertion.type,
|
||||
response: {
|
||||
authenticatorData: UserWebauthnBackend.webAuthnBufferEncode(authData),
|
||||
clientDataJSON: UserWebauthnBackend.webAuthnBufferEncode(clientDataJSON),
|
||||
signature: UserWebauthnBackend.webAuthnBufferEncode(sig),
|
||||
userHandle: UserWebauthnBackend.webAuthnBufferEncode(userHandle),
|
||||
},
|
||||
})
|
||||
})
|
||||
.then(res => res.json()).then((res) => {
|
||||
if (res.msg === "") {
|
||||
Setting.showMessage("success", "Successfully logged in with webauthn credentials");
|
||||
Setting.goToLink("/");
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Failed to connect to server: ${error}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderPasswordOrCodeInput() {
|
||||
let application = this.getApplicationObj();
|
||||
if (this.state.loginMethod === "password") {
|
||||
return this.state.isCodeSignin ? (
|
||||
<Form.Item
|
||||
name="code"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||
>
|
||||
<CountDownInput
|
||||
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
||||
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
||||
>
|
||||
<Input
|
||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||
type="password"
|
||||
placeholder={i18next.t("login:Password")}
|
||||
disabled={!application.enablePassword}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderMethodChoiceBox() {
|
||||
let application = this.getApplicationObj();
|
||||
if (application.enableWebAuthn) {
|
||||
return (
|
||||
<div>
|
||||
<Tabs defaultActiveKey="password" onChange={(key) => {this.setState({loginMethod: key});}} centered>
|
||||
<TabPane tab={i18next.t("login:Password")} key="password" />
|
||||
<TabPane tab={"WebAuthn"} key="webAuthn" />
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const application = this.getApplicationObj();
|
||||
if (application === null) {
|
||||
|
@ -117,7 +117,7 @@ class PromptPage extends React.Component {
|
||||
<div>
|
||||
{
|
||||
(application === null || this.state.user === null) ? null : (
|
||||
application?.providers.filter(providerItem => Setting.isProviderPrompted(providerItem)).map((providerItem, index) => <OAuthWidget key={providerItem.name} labelSpan={6} user={this.state.user} application={application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />)
|
||||
application?.providers.filter(providerItem => Setting.isProviderPrompted(providerItem)).map((providerItem, index) => <OAuthWidget key={providerItem.name} labelSpan={6} user={this.state.user} application={application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Result, Button} from "antd";
|
||||
import {Button, Result} from "antd";
|
||||
import i18next from "i18next";
|
||||
import {authConfig} from "./Auth";
|
||||
import * as Util from "./Util";
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Form, Input, Checkbox, Button, Row, Col, Result, Modal} from "antd";
|
||||
import {Button, Checkbox, Col, Form, Input, Modal, Result, Row} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import i18next from "i18next";
|
||||
@ -79,19 +79,28 @@ class SignupPage extends React.Component {
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
if (this.state.applicationName !== undefined) {
|
||||
this.getApplication();
|
||||
let applicationName = this.state.applicationName;
|
||||
const oAuthParams = Util.getOAuthGetParameters();
|
||||
if (oAuthParams !== null) {
|
||||
applicationName = oAuthParams.state;
|
||||
this.setState({applicationName: oAuthParams.state});
|
||||
const signinUrl = window.location.href.replace("/signup/oauth/authorize", "/login/oauth/authorize");
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
}
|
||||
|
||||
if (applicationName !== undefined) {
|
||||
this.getApplication(applicationName);
|
||||
} else {
|
||||
Util.showMessage("error", `Unknown application name: ${this.state.applicationName}`);
|
||||
Util.showMessage("error", `Unknown application name: ${applicationName}`);
|
||||
}
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
if (this.state.applicationName === undefined) {
|
||||
getApplication(applicationName) {
|
||||
if (applicationName === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
ApplicationBackend.getApplication("admin", applicationName)
|
||||
.then((application) => {
|
||||
this.setState({
|
||||
application: application,
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Alert, Button, message, Result} from "antd";
|
||||
import {Alert, Button, Result, message} from "antd";
|
||||
|
||||
export function showMessage(type, text) {
|
||||
if (type === "success") {
|
||||
@ -103,6 +103,7 @@ export function getOAuthGetParameters(params) {
|
||||
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
|
||||
const samlRequest = getRefinedValue(queries.get("SAMLRequest"));
|
||||
const relayState = getRefinedValue(queries.get("RelayState"));
|
||||
const noRedirect = getRefinedValue(queries.get("noRedirect"));
|
||||
|
||||
if ((clientId === undefined || clientId === null || clientId === "") && (samlRequest === "" || samlRequest === undefined)) {
|
||||
// login
|
||||
@ -120,6 +121,7 @@ export function getOAuthGetParameters(params) {
|
||||
codeChallenge: codeChallenge,
|
||||
samlRequest: samlRequest,
|
||||
relayState: relayState,
|
||||
noRedirect: noRedirect,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
79
web/src/backend/UserWebauthnBackend.js
Normal file
79
web/src/backend/UserWebauthnBackend.js
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2022 The Casdoor 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.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function registerWebauthnCredential() {
|
||||
return fetch(`${Setting.ServerUrl}/api/webauthn/signup/begin`, {
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then((credentialCreationOptions) => {
|
||||
credentialCreationOptions.certificate.challenge = webAuthnBufferDecode(credentialCreationOptions.certificate.challenge);
|
||||
credentialCreationOptions.certificate.user.id = webAuthnBufferDecode(credentialCreationOptions.certificate.user.id);
|
||||
if (credentialCreationOptions.certificate.excludeCredentials) {
|
||||
for (var i = 0; i < credentialCreationOptions.certificate.excludeCredentials.length; i++) {
|
||||
credentialCreationOptions.certificate.excludeCredentials[i].id = webAuthnBufferDecode(credentialCreationOptions.certificate.excludeCredentials[i].id);
|
||||
}
|
||||
}
|
||||
return navigator.credentials.create({
|
||||
certificate: credentialCreationOptions.certificate
|
||||
});
|
||||
})
|
||||
.then((credential) => {
|
||||
let attestationObject = credential.response.attestationObject;
|
||||
let clientDataJSON = credential.response.clientDataJSON;
|
||||
let rawId = credential.rawId;
|
||||
return fetch(`${Setting.ServerUrl}/api/webauthn/signup/finish`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
id: credential.id,
|
||||
rawId: webAuthnBufferEncode(rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
attestationObject: webAuthnBufferEncode(attestationObject),
|
||||
clientDataJSON: webAuthnBufferEncode(clientDataJSON),
|
||||
},
|
||||
})
|
||||
})
|
||||
.then(res => res.json());
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteUserWebAuthnCredential(credentialID) {
|
||||
let form = new FormData();
|
||||
form.append("credentialID", credentialID);
|
||||
|
||||
return fetch(`${Setting.ServerUrl}/api/webauthn/delete-credential`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: form,
|
||||
dataType: "text"
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
// Base64 to ArrayBuffer
|
||||
export function webAuthnBufferDecode(value) {
|
||||
return Uint8Array.from(atob(value), c => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
// ArrayBuffer to URLBase64
|
||||
export function webAuthnBufferEncode(value) {
|
||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
@ -91,6 +91,8 @@ class OAuthWidget extends React.Component {
|
||||
unlinkUser(providerType) {
|
||||
const body = {
|
||||
providerType: providerType,
|
||||
// should add the unlink user's info, cause the user may not be logged in, but a admin want to unlink the user.
|
||||
user: this.props.user,
|
||||
};
|
||||
AuthBackend.unlink(body)
|
||||
.then((res) => {
|
||||
@ -113,6 +115,8 @@ class OAuthWidget extends React.Component {
|
||||
const displayName = this.getUserProperty(user, provider.type, "displayName");
|
||||
const email = this.getUserProperty(user, provider.type, "email");
|
||||
let avatarUrl = this.getUserProperty(user, provider.type, "avatarUrl");
|
||||
// the account user
|
||||
const account = this.props.account;
|
||||
|
||||
if (avatarUrl === "" || avatarUrl === undefined) {
|
||||
avatarUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAQAAACROWYpAAAAHElEQVR42mNkoAAwjmoe1TyqeVTzqOZRzcNZMwB18wAfEFQkPQAAAABJRU5ErkJggg==";
|
||||
@ -161,10 +165,10 @@ class OAuthWidget extends React.Component {
|
||||
{
|
||||
linkedValue === "" ? (
|
||||
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "link")}>
|
||||
<Button style={{marginLeft: "20px", width: "80px"}} type="primary">{i18next.t("user:Link")}</Button>
|
||||
<Button style={{marginLeft: "20px", width: "80px"}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
|
||||
</a>
|
||||
) : (
|
||||
<Button disabled={!providerItem.canUnlink} style={{marginLeft: "20px", width: "80px"}} onClick={() => this.unlinkUser(provider.type)}>{i18next.t("user:Unlink")}</Button>
|
||||
<Button disabled={!providerItem.canUnlink && !account.isGlobalAdmin} style={{marginLeft: "20px", width: "80px"}} onClick={() => this.unlinkUser(provider.type)}>{i18next.t("user:Unlink")}</Button>
|
||||
)
|
||||
}
|
||||
</Col>
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Edit Application": "Anwendung bearbeiten",
|
||||
"Enable SAML compress": "Enable SAML compress",
|
||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||
"Enable code signin": "Code-Anmeldung aktivieren",
|
||||
"Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip",
|
||||
"Enable signin session - Tooltip": "Aktiviere Anmeldesession - Tooltip",
|
||||
@ -49,11 +51,11 @@
|
||||
"Bit size": "Bitgröße",
|
||||
"Bit size - Tooltip": "Bit Größe - Tooltip",
|
||||
"Copy private key": "Privaten Schlüssel kopieren",
|
||||
"Copy public key": "Öffentlichen Schlüssel kopieren",
|
||||
"Copy certificate": "Kopieren des Zertifikats",
|
||||
"Crypto algorithm": "Crypto-Algorithmus",
|
||||
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
|
||||
"Download private key": "Privaten Schlüssel herunterladen",
|
||||
"Download public key": "Öffentlichen Schlüssel herunterladen",
|
||||
"Download certificate": "Zertifikat herunterladen",
|
||||
"Edit Cert": "Zitat bearbeiten",
|
||||
"Expire in years": "Gültig in Jahren",
|
||||
"Expire in years - Tooltip": "Verfällt in Jahren - Tooltip",
|
||||
@ -61,9 +63,9 @@
|
||||
"Private key": "Privater Schlüssel",
|
||||
"Private key - Tooltip": "Privater Schlüssel - Tooltip",
|
||||
"Private key copied to clipboard successfully": "Privater Schlüssel erfolgreich in die Zwischenablage kopiert",
|
||||
"Public key": "Öffentlicher Schlüssel",
|
||||
"Public key - Tooltip": "Öffentlicher Schlüssel - Tooltip",
|
||||
"Public key copied to clipboard successfully": "Öffentlicher Schlüssel erfolgreich in die Zwischenablage kopiert",
|
||||
"Certificate": "Zertifikat",
|
||||
"Certificate - Tooltip": "Zertifikat - Tooltip",
|
||||
"Certificate copied to clipboard successfully": "Das Zertifikat wurde erfolgreich in die Zwischenablage kopiert",
|
||||
"Scope": "Bereich",
|
||||
"Scope - Tooltip": "Bereich - Tooltip",
|
||||
"Type": "Typ",
|
||||
@ -91,6 +93,7 @@
|
||||
"Please input your username!": "Bitte geben Sie Ihren Benutzernamen ein!",
|
||||
"Reset": "Reset",
|
||||
"Retrieve password": "Passwort abrufen",
|
||||
"Unknown forget type": "Unknown forget type",
|
||||
"Verify": "Überprüfen"
|
||||
},
|
||||
"general": {
|
||||
@ -185,6 +188,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
@ -248,6 +252,7 @@
|
||||
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!",
|
||||
"Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!",
|
||||
"Sign In": "Anmelden",
|
||||
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||
"Sign in with code": "Mit Code anmelden",
|
||||
"Sign in with password": "Mit Passwort anmelden",
|
||||
"Sign in with {type}": "Mit {type} anmelden",
|
||||
@ -342,7 +347,8 @@
|
||||
"New Permission": "New Permission",
|
||||
"Resource type": "Ressourcentyp",
|
||||
"Resource type - Tooltip": "Ressourcentyp - Tooltip",
|
||||
"Resources": "Ressourcen"
|
||||
"Resources": "Ressourcen",
|
||||
"Resources - Tooltip": "Resources - Tooltip"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "Alipay",
|
||||
@ -420,7 +426,7 @@
|
||||
"Host": "Host",
|
||||
"Host - Tooltip": "Unique string-style identifier",
|
||||
"IdP": "IdP",
|
||||
"IdP public key": "IdP-öffentlicher Schlüssel",
|
||||
"IdP certificate": "IdP-öffentlicher Schlüssel",
|
||||
"Issuer URL": "Ausgabe-URL",
|
||||
"Issuer URL - Tooltip": "Ausgabe-URL - Tooltip",
|
||||
"Link copied to clipboard successfully": "Link erfolgreich in die Zwischenablage kopiert",
|
||||
@ -639,6 +645,7 @@
|
||||
"Unlink": "Link aufheben",
|
||||
"Upload (.xlsx)": "Upload (.xlsx)",
|
||||
"Upload a photo": "Foto hochladen",
|
||||
"WebAuthn credentials": "WebAuthn credentials",
|
||||
"input password": "Passwort eingeben"
|
||||
},
|
||||
"webhook": {
|
||||
@ -659,4 +666,4 @@
|
||||
"URL - Tooltip": "URL - Tooltip",
|
||||
"Value": "Wert"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable SAML compress": "Enable SAML compress",
|
||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||
"Enable code signin": "Enable code signin",
|
||||
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
||||
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
||||
@ -49,11 +51,11 @@
|
||||
"Bit size": "Bit size",
|
||||
"Bit size - Tooltip": "Bit size - Tooltip",
|
||||
"Copy private key": "Copy private key",
|
||||
"Copy public key": "Copy public key",
|
||||
"Copy certificate": "Copy certificate",
|
||||
"Crypto algorithm": "Crypto algorithm",
|
||||
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
|
||||
"Download private key": "Download private key",
|
||||
"Download public key": "Download public key",
|
||||
"Download certificate": "Download certificate",
|
||||
"Edit Cert": "Edit Cert",
|
||||
"Expire in years": "Expire in years",
|
||||
"Expire in years - Tooltip": "Expire in years - Tooltip",
|
||||
@ -61,9 +63,9 @@
|
||||
"Private key": "Private key",
|
||||
"Private key - Tooltip": "Private key - Tooltip",
|
||||
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
|
||||
"Public key": "Public key",
|
||||
"Public key - Tooltip": "Public key - Tooltip",
|
||||
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
|
||||
"Certificate": "Certificate",
|
||||
"Certificate - Tooltip": "Certificate - Tooltip",
|
||||
"Certificate copied to clipboard successfully": "Certificate copied to clipboard successfully",
|
||||
"Scope": "Scope",
|
||||
"Scope - Tooltip": "Scope - Tooltip",
|
||||
"Type": "Type",
|
||||
@ -91,6 +93,7 @@
|
||||
"Please input your username!": "Please input your username!",
|
||||
"Reset": "Reset",
|
||||
"Retrieve password": "Retrieve password",
|
||||
"Unknown forget type": "Unknown forget type",
|
||||
"Verify": "Verify"
|
||||
},
|
||||
"general": {
|
||||
@ -185,6 +188,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
@ -248,6 +252,7 @@
|
||||
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
|
||||
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
|
||||
"Sign In": "Sign In",
|
||||
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||
"Sign in with code": "Sign in with code",
|
||||
"Sign in with password": "Sign in with password",
|
||||
"Sign in with {type}": "Sign in with {type}",
|
||||
@ -342,7 +347,8 @@
|
||||
"New Permission": "New Permission",
|
||||
"Resource type": "Resource type",
|
||||
"Resource type - Tooltip": "Resource type - Tooltip",
|
||||
"Resources": "Resources"
|
||||
"Resources": "Resources",
|
||||
"Resources - Tooltip": "Resources - Tooltip"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "Alipay",
|
||||
@ -420,7 +426,7 @@
|
||||
"Host": "Host",
|
||||
"Host - Tooltip": "Host - Tooltip",
|
||||
"IdP": "IdP",
|
||||
"IdP public key": "IdP public key",
|
||||
"IdP certificate": "IdP certificate",
|
||||
"Issuer URL": "Issuer URL",
|
||||
"Issuer URL - Tooltip": "Issuer URL - Tooltip",
|
||||
"Link copied to clipboard successfully": "Link copied to clipboard successfully",
|
||||
@ -639,6 +645,7 @@
|
||||
"Unlink": "Unlink",
|
||||
"Upload (.xlsx)": "Upload (.xlsx)",
|
||||
"Upload a photo": "Upload a photo",
|
||||
"WebAuthn credentials": "WebAuthn credentials",
|
||||
"input password": "input password"
|
||||
},
|
||||
"webhook": {
|
||||
@ -659,4 +666,4 @@
|
||||
"URL - Tooltip": "URL - Tooltip",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Edit Application": "Modifier l'application",
|
||||
"Enable SAML compress": "Enable SAML compress",
|
||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||
"Enable code signin": "Activer la connexion au code",
|
||||
"Enable code signin - Tooltip": "Activer la connexion au code - infobulle",
|
||||
"Enable signin session - Tooltip": "Activer la session de connexion - infobulle",
|
||||
@ -49,21 +51,21 @@
|
||||
"Bit size": "Taille du bit",
|
||||
"Bit size - Tooltip": "Taille du bit - Infobulle",
|
||||
"Copy private key": "Copier la clé privée",
|
||||
"Copy public key": "Copier la clé publique",
|
||||
"Copy certificate": "Copier le certificate",
|
||||
"Crypto algorithm": "Algorithme de cryptomonnaie",
|
||||
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
|
||||
"Download private key": "Télécharger la clé privée",
|
||||
"Download public key": "Télécharger la clé publique",
|
||||
"Edit Cert": "Modifier le certificat",
|
||||
"Download certificate": "Télécharger le certificate",
|
||||
"Edit Cert": "Modifier le certificate",
|
||||
"Expire in years": "Expire dans les années",
|
||||
"Expire in years - Tooltip": "Expire dans les années - infobulle",
|
||||
"New Cert": "New Cert",
|
||||
"Private key": "Clé privée",
|
||||
"Private key - Tooltip": "Clé privée - Infobulle",
|
||||
"Private key copied to clipboard successfully": "Clé privée copiée dans le presse-papiers avec succès",
|
||||
"Public key": "Clé publique",
|
||||
"Public key - Tooltip": "Clé publique - Infobulle",
|
||||
"Public key copied to clipboard successfully": "Clé publique copiée dans le presse-papiers avec succès",
|
||||
"Certificate": "certificate",
|
||||
"Certificate - Tooltip": "certificate - Infobulle",
|
||||
"Certificate copied to clipboard successfully": "Le certificate a été copié avec succès dans le presse-papiers",
|
||||
"Scope": "Périmètre d'application",
|
||||
"Scope - Tooltip": "Scope - Infobulle",
|
||||
"Type": "Type de texte",
|
||||
@ -91,6 +93,7 @@
|
||||
"Please input your username!": "Veuillez entrer votre nom d'utilisateur !",
|
||||
"Reset": "Reset",
|
||||
"Retrieve password": "Récupérer le mot de passe",
|
||||
"Unknown forget type": "Unknown forget type",
|
||||
"Verify": "Vérifier"
|
||||
},
|
||||
"general": {
|
||||
@ -185,6 +188,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Désolé, vous n'avez pas la permission d'accéder à cette page.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
@ -248,6 +252,7 @@
|
||||
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !",
|
||||
"Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!",
|
||||
"Sign In": "Se connecter",
|
||||
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||
"Sign in with code": "Se connecter avec le code",
|
||||
"Sign in with password": "Se connecter avec le mot de passe",
|
||||
"Sign in with {type}": "Se connecter avec {type}",
|
||||
@ -342,7 +347,8 @@
|
||||
"New Permission": "New Permission",
|
||||
"Resource type": "Type de ressource",
|
||||
"Resource type - Tooltip": "Type de ressource - infobulle",
|
||||
"Resources": "Ressource"
|
||||
"Resources": "Ressource",
|
||||
"Resources - Tooltip": "Resources - Tooltip"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "Alipay",
|
||||
@ -420,7 +426,7 @@
|
||||
"Host": "Hôte",
|
||||
"Host - Tooltip": "Unique string-style identifier",
|
||||
"IdP": "IDP",
|
||||
"IdP public key": "Clé publique IdP",
|
||||
"IdP certificate": "Clé publique IdP",
|
||||
"Issuer URL": "URL de l'émetteur",
|
||||
"Issuer URL - Tooltip": "URL de l'émetteur - infobulle",
|
||||
"Link copied to clipboard successfully": "Lien copié dans le presse-papiers avec succès",
|
||||
@ -639,6 +645,7 @@
|
||||
"Unlink": "Délier",
|
||||
"Upload (.xlsx)": "Télécharger (.xlsx)",
|
||||
"Upload a photo": "Télécharger une photo",
|
||||
"WebAuthn credentials": "WebAuthn credentials",
|
||||
"input password": "saisir le mot de passe"
|
||||
},
|
||||
"webhook": {
|
||||
@ -659,4 +666,4 @@
|
||||
"URL - Tooltip": "URL - Info-bulle",
|
||||
"Value": "Valeur"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Edit Application": "アプリケーションを編集",
|
||||
"Enable SAML compress": "Enable SAML compress",
|
||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||
"Enable code signin": "コードサインインを有効にする",
|
||||
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
||||
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
||||
@ -49,11 +51,11 @@
|
||||
"Bit size": "ビットサイズ",
|
||||
"Bit size - Tooltip": "ビットサイズ - ツールチップ",
|
||||
"Copy private key": "秘密鍵をコピー",
|
||||
"Copy public key": "公開鍵をコピー",
|
||||
"Copy certificate": "証明書をコピーします",
|
||||
"Crypto algorithm": "暗号化アルゴリズム",
|
||||
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
|
||||
"Download private key": "秘密鍵をダウンロード",
|
||||
"Download public key": "公開鍵をダウンロード",
|
||||
"Download certificate": "証明書をダウンロードします",
|
||||
"Edit Cert": "Certを編集",
|
||||
"Expire in years": "有効期限",
|
||||
"Expire in years - Tooltip": "年間有効期限 - ツールチップ",
|
||||
@ -61,9 +63,9 @@
|
||||
"Private key": "Private key",
|
||||
"Private key - Tooltip": "Private key - Tooltip",
|
||||
"Private key copied to clipboard successfully": "秘密鍵を正常にクリップボードにコピーしました",
|
||||
"Public key": "公開キー",
|
||||
"Public key - Tooltip": "Public key - Tooltip",
|
||||
"Public key copied to clipboard successfully": "公開鍵を正常にクリップボードにコピーしました",
|
||||
"Certificate": "Certificate",
|
||||
"Certificate - Tooltip": "Certificate - Tooltip",
|
||||
"Certificate copied to clipboard successfully": "証明書はクリップボードに正常にコピーされました",
|
||||
"Scope": "スコープ",
|
||||
"Scope - Tooltip": "スコープ → ツールチップ",
|
||||
"Type": "タイプ",
|
||||
@ -91,6 +93,7 @@
|
||||
"Please input your username!": "ユーザー名を入力してください!",
|
||||
"Reset": "Reset",
|
||||
"Retrieve password": "パスワードの取得",
|
||||
"Unknown forget type": "Unknown forget type",
|
||||
"Verify": "確認する"
|
||||
},
|
||||
"general": {
|
||||
@ -185,6 +188,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "申し訳ありませんが、このページにアクセスする権限がありません。",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
@ -248,6 +252,7 @@
|
||||
"Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください!",
|
||||
"Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。",
|
||||
"Sign In": "サインイン",
|
||||
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||
"Sign in with code": "コードでサインイン",
|
||||
"Sign in with password": "パスワードでサインイン",
|
||||
"Sign in with {type}": "{type} でサインイン",
|
||||
@ -342,7 +347,8 @@
|
||||
"New Permission": "New Permission",
|
||||
"Resource type": "リソースタイプ",
|
||||
"Resource type - Tooltip": "リソースタイプ - ツールチップ",
|
||||
"Resources": "リソース"
|
||||
"Resources": "リソース",
|
||||
"Resources - Tooltip": "Resources - Tooltip"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "Alipay",
|
||||
@ -420,7 +426,7 @@
|
||||
"Host": "ホスト",
|
||||
"Host - Tooltip": "Unique string-style identifier",
|
||||
"IdP": "IdP",
|
||||
"IdP public key": "IdP public key",
|
||||
"IdP certificate": "IdP certificate",
|
||||
"Issuer URL": "Issuer URL",
|
||||
"Issuer URL - Tooltip": "Issuer URL - ツールチップ",
|
||||
"Link copied to clipboard successfully": "リンクをクリップボードにコピーしました",
|
||||
@ -639,6 +645,7 @@
|
||||
"Unlink": "リンクを解除",
|
||||
"Upload (.xlsx)": "アップロード (.xlsx)",
|
||||
"Upload a photo": "写真をアップロード",
|
||||
"WebAuthn credentials": "WebAuthn credentials",
|
||||
"input password": "パスワードを入力"
|
||||
},
|
||||
"webhook": {
|
||||
@ -659,4 +666,4 @@
|
||||
"URL - Tooltip": "URL → ツールチップ",
|
||||
"Value": "値"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable SAML compress": "Enable SAML compress",
|
||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||
"Enable code signin": "Enable code signin",
|
||||
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
||||
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
||||
@ -49,11 +51,11 @@
|
||||
"Bit size": "Bit size",
|
||||
"Bit size - Tooltip": "Bit size - Tooltip",
|
||||
"Copy private key": "Copy private key",
|
||||
"Copy public key": "Copy public key",
|
||||
"Copy certificate": "Copy certificate",
|
||||
"Crypto algorithm": "Crypto algorithm",
|
||||
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
|
||||
"Download private key": "Download private key",
|
||||
"Download public key": "Download public key",
|
||||
"Download certificate": "Download certificate",
|
||||
"Edit Cert": "Edit Cert",
|
||||
"Expire in years": "Expire in years",
|
||||
"Expire in years - Tooltip": "Expire in years - Tooltip",
|
||||
@ -61,9 +63,9 @@
|
||||
"Private key": "Private key",
|
||||
"Private key - Tooltip": "Private key - Tooltip",
|
||||
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
|
||||
"Public key": "Public key",
|
||||
"Public key - Tooltip": "Public key - Tooltip",
|
||||
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully",
|
||||
"Certificate": "Certificate",
|
||||
"Certificate - Tooltip": "Certificate - Tooltip",
|
||||
"Certificate copied to clipboard successfully": "Certificate copied to clipboard successfully",
|
||||
"Scope": "Scope",
|
||||
"Scope - Tooltip": "Scope - Tooltip",
|
||||
"Type": "Type",
|
||||
@ -91,6 +93,7 @@
|
||||
"Please input your username!": "Please input your username!",
|
||||
"Reset": "Reset",
|
||||
"Retrieve password": "Retrieve password",
|
||||
"Unknown forget type": "Unknown forget type",
|
||||
"Verify": "Verify"
|
||||
},
|
||||
"general": {
|
||||
@ -185,6 +188,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
@ -248,6 +252,7 @@
|
||||
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
|
||||
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
|
||||
"Sign In": "Sign In",
|
||||
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||
"Sign in with code": "Sign in with code",
|
||||
"Sign in with password": "Sign in with password",
|
||||
"Sign in with {type}": "Sign in with {type}",
|
||||
@ -342,7 +347,8 @@
|
||||
"New Permission": "New Permission",
|
||||
"Resource type": "Resource type",
|
||||
"Resource type - Tooltip": "Resource type - Tooltip",
|
||||
"Resources": "Resources"
|
||||
"Resources": "Resources",
|
||||
"Resources - Tooltip": "Resources - Tooltip"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "Alipay",
|
||||
@ -420,7 +426,7 @@
|
||||
"Host": "Host",
|
||||
"Host - Tooltip": "Unique string-style identifier",
|
||||
"IdP": "IdP",
|
||||
"IdP public key": "IdP public key",
|
||||
"IdP certificate": "IdP certificate",
|
||||
"Issuer URL": "Issuer URL",
|
||||
"Issuer URL - Tooltip": "Issuer URL - Tooltip",
|
||||
"Link copied to clipboard successfully": "Link copied to clipboard successfully",
|
||||
@ -639,6 +645,7 @@
|
||||
"Unlink": "Unlink",
|
||||
"Upload (.xlsx)": "Upload (.xlsx)",
|
||||
"Upload a photo": "Upload a photo",
|
||||
"WebAuthn credentials": "WebAuthn credentials",
|
||||
"input password": "input password"
|
||||
},
|
||||
"webhook": {
|
||||
@ -659,4 +666,4 @@
|
||||
"URL - Tooltip": "URL - Tooltip",
|
||||
"Value": "Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Edit Application": "Изменить приложение",
|
||||
"Enable SAML compress": "Enable SAML compress",
|
||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||
"Enable code signin": "Включить кодовый вход",
|
||||
"Enable code signin - Tooltip": "Включить вход с кодом - Tooltip",
|
||||
"Enable signin session - Tooltip": "Включить сеанс входа - Подсказка",
|
||||
@ -49,11 +51,11 @@
|
||||
"Bit size": "Размер бита",
|
||||
"Bit size - Tooltip": "Размер бита - Подсказка",
|
||||
"Copy private key": "Копировать закрытый ключ",
|
||||
"Copy public key": "Копировать открытый ключ",
|
||||
"Copy certificate": "Копирование сертификата",
|
||||
"Crypto algorithm": "Алгоритм крипто",
|
||||
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
|
||||
"Download private key": "Скачать закрытый ключ",
|
||||
"Download public key": "Скачать открытый ключ",
|
||||
"Download certificate": "Скачать сертификат",
|
||||
"Edit Cert": "Изменить сертификат",
|
||||
"Expire in years": "Истекает через годы",
|
||||
"Expire in years - Tooltip": "Истекает через годы - Подсказка",
|
||||
@ -61,9 +63,9 @@
|
||||
"Private key": "Приватный ключ",
|
||||
"Private key - Tooltip": "Приватный ключ - Подсказка",
|
||||
"Private key copied to clipboard successfully": "Приватный ключ скопирован в буфер обмена",
|
||||
"Public key": "Публичный ключ",
|
||||
"Public key - Tooltip": "Открытый ключ - Подсказка",
|
||||
"Public key copied to clipboard successfully": "Открытый ключ успешно скопирован в буфер обмена",
|
||||
"Certificate": "сертификат",
|
||||
"Certificate - Tooltip": "сертификат - Подсказка",
|
||||
"Certificate copied to clipboard successfully": "Сертификат успешно скопирован в буфер обмена",
|
||||
"Scope": "Сфера охвата",
|
||||
"Scope - Tooltip": "Область применения - Подсказка",
|
||||
"Type": "Тип",
|
||||
@ -91,6 +93,7 @@
|
||||
"Please input your username!": "Пожалуйста, введите ваше имя пользователя!",
|
||||
"Reset": "Reset",
|
||||
"Retrieve password": "Получить пароль",
|
||||
"Unknown forget type": "Unknown forget type",
|
||||
"Verify": "Подтвердить"
|
||||
},
|
||||
"general": {
|
||||
@ -185,6 +188,7 @@
|
||||
"Signup application - Tooltip": "Signup application - Tooltip",
|
||||
"Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"Sorry, you do not have permission to access this page.": "Извините, вы не имеете права доступа к этой странице.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
@ -248,6 +252,7 @@
|
||||
"Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!",
|
||||
"Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!",
|
||||
"Sign In": "Войти",
|
||||
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||
"Sign in with code": "Войти с помощью кода",
|
||||
"Sign in with password": "Войти с помощью пароля",
|
||||
"Sign in with {type}": "Войти с помощью {type}",
|
||||
@ -342,7 +347,8 @@
|
||||
"New Permission": "New Permission",
|
||||
"Resource type": "Тип ресурса",
|
||||
"Resource type - Tooltip": "Тип ресурса - Подсказка",
|
||||
"Resources": "Ресурсы"
|
||||
"Resources": "Ресурсы",
|
||||
"Resources - Tooltip": "Resources - Tooltip"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "Alipay",
|
||||
@ -420,7 +426,7 @@
|
||||
"Host": "Хост",
|
||||
"Host - Tooltip": "Unique string-style identifier",
|
||||
"IdP": "ИдП",
|
||||
"IdP public key": "Публичный ключ IdP",
|
||||
"IdP certificate": "Публичный ключ IdP",
|
||||
"Issuer URL": "URL эмитента",
|
||||
"Issuer URL - Tooltip": "URL эмитента - Tooltip",
|
||||
"Link copied to clipboard successfully": "Ссылка скопирована в буфер обмена",
|
||||
@ -639,6 +645,7 @@
|
||||
"Unlink": "Отвязать",
|
||||
"Upload (.xlsx)": "Загрузить (.xlsx)",
|
||||
"Upload a photo": "Загрузить фото",
|
||||
"WebAuthn credentials": "WebAuthn credentials",
|
||||
"input password": "пароль для ввода"
|
||||
},
|
||||
"webhook": {
|
||||
@ -659,4 +666,4 @@
|
||||
"URL - Tooltip": "URL - Подсказка",
|
||||
"Value": "Значение"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Edit Application": "编辑应用",
|
||||
"Enable SAML compress": "压缩SAML响应",
|
||||
"Enable SAML compress - Tooltip": "Casdoor作为SAML idp时,是否压缩SAML响应信息",
|
||||
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||
"Enable code signin": "启用验证码登录",
|
||||
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
|
||||
"Enable signin session - Tooltip": "从应用登录Casdoor后,Casdoor是否保持会话",
|
||||
@ -49,11 +51,11 @@
|
||||
"Bit size": "位大小",
|
||||
"Bit size - Tooltip": "位大小 - 工具提示",
|
||||
"Copy private key": "复制私钥",
|
||||
"Copy public key": "复制公钥",
|
||||
"Copy certificate": "复制证书",
|
||||
"Crypto algorithm": "加密算法",
|
||||
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
|
||||
"Download private key": "下载私钥",
|
||||
"Download public key": "下载公钥",
|
||||
"Download certificate": "下载证书",
|
||||
"Edit Cert": "编辑证书",
|
||||
"Expire in years": "有效期(年)",
|
||||
"Expire in years - Tooltip": "到期年份-工具提示",
|
||||
@ -61,9 +63,9 @@
|
||||
"Private key": "私钥",
|
||||
"Private key - Tooltip": "私钥 - 工具提示",
|
||||
"Private key copied to clipboard successfully": "私钥已成功复制到剪贴板",
|
||||
"Public key": "公钥",
|
||||
"Public key - Tooltip": "公钥 - 工具提示",
|
||||
"Public key copied to clipboard successfully": "公钥已成功复制到剪贴板",
|
||||
"Certificate": "证书",
|
||||
"Certificate - Tooltip": "证书 - 工具提示",
|
||||
"Certificate copied to clipboard successfully": "证书已成功复制到剪贴板",
|
||||
"Scope": "用途",
|
||||
"Scope - Tooltip": "范围 - 工具提示",
|
||||
"Type": "类型",
|
||||
@ -91,6 +93,7 @@
|
||||
"Please input your username!": "请输入您的用户名!",
|
||||
"Reset": "重置",
|
||||
"Retrieve password": "找回密码",
|
||||
"Unknown forget type": "未知的忘记密码类型",
|
||||
"Verify": "验证"
|
||||
},
|
||||
"general": {
|
||||
@ -185,6 +188,7 @@
|
||||
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
|
||||
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "抱歉,您访问的用户不存在或您无权访问该用户",
|
||||
"Sorry, you do not have permission to access this page.": "抱歉,您无权访问该页面",
|
||||
"State": "状态",
|
||||
"State - Tooltip": "状态",
|
||||
"Swagger": "API文档",
|
||||
@ -248,6 +252,7 @@
|
||||
"Please input your password, at least 6 characters!": "请输入您的密码,不少于6位",
|
||||
"Please input your username, Email or phone!": "请输入您的用户名、Email或手机号!",
|
||||
"Sign In": "登录",
|
||||
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||
"Sign in with code": "验证码登录",
|
||||
"Sign in with password": "密码登录",
|
||||
"Sign in with {type}": "{type}登录",
|
||||
@ -342,7 +347,8 @@
|
||||
"New Permission": "添加权限",
|
||||
"Resource type": "资源类型",
|
||||
"Resource type - Tooltip": "授权资源的类型",
|
||||
"Resources": "资源"
|
||||
"Resources": "资源",
|
||||
"Resources - Tooltip": "被授权的资源"
|
||||
},
|
||||
"product": {
|
||||
"Alipay": "支付宝",
|
||||
@ -420,7 +426,7 @@
|
||||
"Host": "主机",
|
||||
"Host - Tooltip": "主机",
|
||||
"IdP": "IdP",
|
||||
"IdP public key": "IdP 公钥",
|
||||
"IdP certificate": "IdP 公钥",
|
||||
"Issuer URL": "发行者网址",
|
||||
"Issuer URL - Tooltip": "发行者URL - 工具提示",
|
||||
"Link copied to clipboard successfully": "链接已成功复制到剪贴板",
|
||||
@ -639,6 +645,7 @@
|
||||
"Unlink": "解绑",
|
||||
"Upload (.xlsx)": "上传(.xlsx)",
|
||||
"Upload a photo": "上传头像",
|
||||
"WebAuthn credentials": "WebAuthn credentials",
|
||||
"input password": "输入密码"
|
||||
},
|
||||
"webhook": {
|
||||
@ -659,4 +666,4 @@
|
||||
"URL - Tooltip": "URL",
|
||||
"Value": "值"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user