Compare commits

...

17 Commits

Author SHA1 Message Date
Yang Luo
b8db07db4d feat: enable GetMaskedSyncers() 2024-01-18 20:59:27 +08:00
Yang Luo
a681c267b3 Refactor code format 2024-01-18 20:53:04 +08:00
Yang Luo
5fb6ea0ab4 Fix "password" tab in SigninMethods 2024-01-18 20:17:05 +08:00
Yang Luo
0f6b7984d4 feat: improve isAllowedInDemoMode() 2024-01-17 13:07:44 +08:00
Yang Luo
ba9d6e5d78 Fix Swagger API version 2024-01-16 00:09:28 +08:00
Yang Luo
a4524e9996 fix: fix Swagger @Tag 2024-01-15 23:35:40 +08:00
Yang Luo
b469928780 Fix Swagger @router 2024-01-15 23:27:42 +08:00
Yang Luo
dc6fe13f75 feat: use signupItem.Regex to check signup page 2024-01-15 18:12:38 +08:00
Yang Luo
8227762988 Support more special chars in password validating 2024-01-15 18:12:38 +08:00
hsluoyz
d92b072ed0 feat: revert PR: "feat: more RFC like LDAP server behaviour" (#2611) 2024-01-15 13:58:33 +08:00
hsluoyz
1161310f81 feat: improve README.md 2024-01-15 10:14:01 +08:00
xiao-kong-long
48ba5f91ed feat: add Synology NAS storage provider (#2605) 2024-01-14 22:38:31 +08:00
Satinder Singh
53df2c2704 fix: add semantic versioning for helm charts (#2603) 2024-01-14 09:44:16 +08:00
Yang Luo
78066da208 Improve setCorsHeaders() for "include" mode 2024-01-13 23:46:05 +08:00
Yang Luo
60096468fe fix: fix CI email 2024-01-13 18:12:52 +08:00
Yang Luo
39d6bc10f7 Fix GetCaptchaStatus() crash if not logged in 2024-01-13 18:04:38 +08:00
Yang Luo
177f2f2f11 Add userId param to GetAllObjects() API 2024-01-13 18:03:40 +08:00
36 changed files with 1232 additions and 1024 deletions

View File

@@ -217,13 +217,9 @@ jobs:
- name: Update Helm Chart
if: steps.should_push.outputs.push=='true'
run: |
# Set the appVersion of the chart to the current tag
# Set the appVersion and version of the chart to the current tag
sed -i "s/appVersion: .*/appVersion: ${{steps.get-current-tag.outputs.tag }}/g" ./charts/casdoor/Chart.yaml
# increase the patch version of the chart
currentChartVersion=$(cat ./charts/casdoor/Chart.yaml | grep ^version | awk '{print $2}')
newChartVersion=$(echo $currentChartVersion | awk -F. -v OFS=. '{$NF++;print}')
sed -i "s/version: .*/version: $newChartVersion/g" ./charts/casdoor/Chart.yaml
sed -i "s/version: .*/version: ${{steps.get-current-tag.outputs.tag }}/g" ./charts/casdoor/Chart.yaml
REGISTRY=oci://registry-1.docker.io/casbin
cd charts/casdoor
@@ -235,7 +231,8 @@ jobs:
# Commit and push the changes back to the repository
git config --global user.name "casbin-bot"
git config --global user.email "casbin-bot@github.com"
git config --global user.email "bot@casbin.org"
git add Chart.yaml index.yaml
git commit -m "chore(helm): bump helm charts appVersion to ${{steps.get-current-tag.outputs.tag }}"
git push origin HEAD:master
git tag ${{steps.get-current-tag.outputs.tag }}
git push origin HEAD:master --follow-tags

View File

@@ -69,6 +69,7 @@ https://casdoor.org
- By source code: https://casdoor.org/docs/basic/server-installation
- By Docker: https://casdoor.org/docs/basic/try-with-docker
- By Kubernetes Helm: https://casdoor.org/docs/basic/try-with-helm
## How to connect to Casdoor?

View File

@@ -150,7 +150,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information

View File

@@ -453,7 +453,7 @@ func (c *ApiController) GetUserinfo2() {
// GetCaptcha ...
// @Tag Login API
// @Title GetCaptcha
// @router /api/get-captcha [get]
// @router /get-captcha [get]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) GetCaptcha() {
applicationId := c.Input().Get("applicationId")

View File

@@ -916,9 +916,9 @@ func (c *ApiController) HandleSamlLogin() {
}
// HandleOfficialAccountEvent ...
// @Tag HandleOfficialAccountEvent API
// @Tag System API
// @Title HandleOfficialAccountEvent
// @router /api/webhook [POST]
// @router /webhook [POST]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) HandleOfficialAccountEvent() {
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body)
@@ -947,9 +947,9 @@ func (c *ApiController) HandleOfficialAccountEvent() {
}
// GetWebhookEventType ...
// @Tag GetWebhookEventType API
// @Tag System API
// @Title GetWebhookEventType
// @router /api/get-webhook-event [GET]
// @router /get-webhook-event [GET]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) GetWebhookEventType() {
lock.Lock()
@@ -970,26 +970,30 @@ func (c *ApiController) GetWebhookEventType() {
// @Description Get Login Error Counts
// @Param id query string true "The id ( owner/name ) of user"
// @Success 200 {object} controllers.Response The Response object
// @router /api/get-captcha-status [get]
// @router /get-captcha-status [get]
func (c *ApiController) GetCaptchaStatus() {
organization := c.Input().Get("organization")
userId := c.Input().Get("user_id")
userId := c.Input().Get("userId")
user, err := object.GetUserByFields(organization, userId)
if err != nil {
c.ResponseError(err.Error())
return
}
failedSigninLimit, _, err := object.GetFailedSigninConfigByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
captchaEnabled := false
if user != nil {
var failedSigninLimit int
failedSigninLimit, _, err = object.GetFailedSigninConfigByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
if user.SigninWrongTimes >= failedSigninLimit {
captchaEnabled = true
}
}
var captchaEnabled bool
if user != nil && user.SigninWrongTimes >= failedSigninLimit {
captchaEnabled = true
}
c.ResponseOk(captchaEnabled)
}
@@ -997,7 +1001,7 @@ func (c *ApiController) GetCaptchaStatus() {
// @Title Callback
// @Tag Callback API
// @Description Get Login Error Counts
// @router /api/Callback [post]
// @router /Callback [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) Callback() {
code := c.GetString("code")

View File

@@ -24,7 +24,7 @@ import (
// Enforce
// @Title Enforce
// @Tag Enforce API
// @Tag Enforcer API
// @Description Call Casbin Enforce API
// @Param body body []string true "Casbin request"
// @Param permissionId query string false "permission id"
@@ -151,7 +151,7 @@ func (c *ApiController) Enforce() {
// BatchEnforce
// @Title BatchEnforce
// @Tag Enforce API
// @Tag Enforcer API
// @Description Call Casbin BatchEnforce API
// @Param body body []string true "array of casbin requests"
// @Param permissionId query string false "permission id"
@@ -264,10 +264,13 @@ func (c *ApiController) BatchEnforce() {
}
func (c *ApiController) GetAllObjects() {
userId := c.GetSessionUsername()
userId := c.Input().Get("userId")
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
userId = c.GetSessionUsername()
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
}
objects, err := object.GetAllObjects(userId)
@@ -280,10 +283,13 @@ func (c *ApiController) GetAllObjects() {
}
func (c *ApiController) GetAllActions() {
userId := c.GetSessionUsername()
userId := c.Input().Get("userId")
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
userId = c.GetSessionUsername()
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
}
actions, err := object.GetAllActions(userId)
@@ -296,10 +302,13 @@ func (c *ApiController) GetAllActions() {
}
func (c *ApiController) GetAllRoles() {
userId := c.GetSessionUsername()
userId := c.Input().Get("userId")
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
userId = c.GetSessionUsername()
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
}
roles, err := object.GetAllRoles(userId)

View File

@@ -39,13 +39,13 @@ func (c *ApiController) GetCerts() {
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
maskedCerts, err := object.GetMaskedCerts(object.GetCerts(owner))
certs, err := object.GetMaskedCerts(object.GetCerts(owner))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedCerts)
c.ResponseOk(certs)
} else {
limit := util.ParseInt(limit)
count, err := object.GetCertCount(owner, field, value)
@@ -80,13 +80,13 @@ func (c *ApiController) GetGlobalCerts() {
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
maskedCerts, err := object.GetMaskedCerts(object.GetGlobalCerts())
certs, err := object.GetMaskedCerts(object.GetGlobalCerts())
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedCerts)
c.ResponseOk(certs)
} else {
limit := util.ParseInt(limit)
count, err := object.GetGlobalCertsCount(field, value)

View File

@@ -18,7 +18,7 @@ import "github.com/casdoor/casdoor/object"
// GetDashboard
// @Title GetDashboard
// @Tag GetDashboard API
// @Tag System API
// @Description get information of dashboard
// @Success 200 {object} controllers.Response The Response object
// @router /get-dashboard [get]

View File

@@ -41,13 +41,12 @@ func (c *ApiController) GetOrganizations() {
isGlobalAdmin := c.IsGlobalAdmin()
if limit == "" || page == "" {
var maskedOrganizations []*object.Organization
var organizations []*object.Organization
var err error
if isGlobalAdmin {
maskedOrganizations, err = object.GetMaskedOrganizations(object.GetOrganizations(owner))
organizations, err = object.GetMaskedOrganizations(object.GetOrganizations(owner))
} else {
maskedOrganizations, err = object.GetMaskedOrganizations(object.GetOrganizations(owner, c.getCurrentUser().Owner))
organizations, err = object.GetMaskedOrganizations(object.GetOrganizations(owner, c.getCurrentUser().Owner))
}
if err != nil {
@@ -55,15 +54,15 @@ func (c *ApiController) GetOrganizations() {
return
}
c.ResponseOk(maskedOrganizations)
c.ResponseOk(organizations)
} else {
if !isGlobalAdmin {
maskedOrganizations, err := object.GetMaskedOrganizations(object.GetOrganizations(owner, c.getCurrentUser().Owner))
organizations, err := object.GetMaskedOrganizations(object.GetOrganizations(owner, c.getCurrentUser().Owner))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedOrganizations)
c.ResponseOk(organizations)
} else {
limit := util.ParseInt(limit)
count, err := object.GetOrganizationCount(owner, field, value)
@@ -93,13 +92,13 @@ func (c *ApiController) GetOrganizations() {
// @router /get-organization [get]
func (c *ApiController) GetOrganization() {
id := c.Input().Get("id")
maskedOrganization, err := object.GetMaskedOrganization(object.GetOrganization(id))
organization, err := object.GetMaskedOrganization(object.GetOrganization(id))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedOrganization)
c.ResponseOk(organization)
}
// UpdateOrganization ...
@@ -190,8 +189,8 @@ func (c *ApiController) GetDefaultApplication() {
return
}
maskedApplication := object.GetMaskedApplication(application, userId)
c.ResponseOk(maskedApplication)
application = object.GetMaskedApplication(application, userId)
c.ResponseOk(application)
}
// GetOrganizationNames ...

View File

@@ -20,7 +20,7 @@ import (
// GetPrometheusInfo
// @Title GetPrometheusInfo
// @Tag Prometheus API
// @Tag System API
// @Description get Prometheus Info
// @Success 200 {object} object.PrometheusInfo The Response object
// @router /get-prometheus-info [get]

View File

@@ -52,7 +52,7 @@ type NotificationForm struct {
// @Param clientSecret query string true "The clientSecret of the application"
// @Param from body controllers.EmailForm true "Details of the email request"
// @Success 200 {object} controllers.Response The Response object
// @router /api/send-email [post]
// @router /send-email [post]
func (c *ApiController) SendEmail() {
userId, ok := c.RequireSignedIn()
if !ok {
@@ -148,7 +148,7 @@ func (c *ApiController) SendEmail() {
// @Param clientSecret query string true "The clientSecret of the application"
// @Param from body controllers.SmsForm true "Details of the sms request"
// @Success 200 {object} controllers.Response The Response object
// @router /api/send-sms [post]
// @router /send-sms [post]
func (c *ApiController) SendSms() {
provider, err := c.GetProviderFromContext("SMS")
if err != nil {
@@ -186,7 +186,7 @@ func (c *ApiController) SendSms() {
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param from body controllers.NotificationForm true "Details of the notification request"
// @Success 200 {object} controllers.Response The Response object
// @router /api/send-notification [post]
// @router /send-notification [post]
func (c *ApiController) SendNotification() {
provider, err := c.GetProviderFromContext("Notification")
if err != nil {

View File

@@ -40,13 +40,13 @@ func (c *ApiController) GetSyncers() {
organization := c.Input().Get("organization")
if limit == "" || page == "" {
organizationSyncers, err := object.GetOrganizationSyncers(owner, organization)
syncers, err := object.GetMaskedSyncers(object.GetOrganizationSyncers(owner, organization))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(organizationSyncers)
c.ResponseOk(syncers)
} else {
limit := util.ParseInt(limit)
count, err := object.GetSyncerCount(owner, organization, field, value)
@@ -56,7 +56,7 @@ func (c *ApiController) GetSyncers() {
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
syncers, err := object.GetPaginationSyncers(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
syncers, err := object.GetMaskedSyncers(object.GetPaginationSyncers(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder))
if err != nil {
c.ResponseError(err.Error())
return
@@ -76,7 +76,7 @@ func (c *ApiController) GetSyncers() {
func (c *ApiController) GetSyncer() {
id := c.Input().Get("id")
syncer, err := object.GetSyncer(id)
syncer, err := object.GetMaskedSyncer(object.GetSyncer(id))
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -156,7 +156,7 @@ func (c *ApiController) DeleteToken() {
// @Success 200 {object} object.TokenWrapper The Response object
// @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object
// @router api/login/oauth/access_token [post]
// @router /login/oauth/access_token [post]
func (c *ApiController) GetOAuthToken() {
clientId := c.Input().Get("client_id")
clientSecret := c.Input().Get("client_secret")
@@ -273,6 +273,7 @@ func (c *ApiController) RefreshToken() {
// IntrospectToken
// @Title IntrospectToken
// @Tag Login API
// @Description The introspection endpoint is an OAuth 2.0 endpoint that takes a
// parameter representing an OAuth 2.0 token and returns a JSON document
// representing the meta information surrounding the

View File

@@ -39,13 +39,13 @@ func (c *ApiController) GetGlobalUsers() {
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
maskedUsers, err := object.GetMaskedUsers(object.GetGlobalUsers())
users, err := object.GetMaskedUsers(object.GetGlobalUsers())
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedUsers)
c.ResponseOk(users)
} else {
limit := util.ParseInt(limit)
count, err := object.GetGlobalUserCount(field, value)
@@ -90,22 +90,22 @@ func (c *ApiController) GetUsers() {
if limit == "" || page == "" {
if groupName != "" {
maskedUsers, err := object.GetMaskedUsers(object.GetGroupUsers(util.GetId(owner, groupName)))
users, err := object.GetMaskedUsers(object.GetGroupUsers(util.GetId(owner, groupName)))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedUsers)
c.ResponseOk(users)
return
}
maskedUsers, err := object.GetMaskedUsers(object.GetUsers(owner))
users, err := object.GetMaskedUsers(object.GetUsers(owner))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedUsers)
c.ResponseOk(users)
} else {
limit := util.ParseInt(limit)
count, err := object.GetUserCount(owner, field, value, groupName)
@@ -223,13 +223,13 @@ func (c *ApiController) GetUser() {
}
isAdminOrSelf := c.IsAdminOrSelf(user)
maskedUser, err := object.GetMaskedUser(user, isAdminOrSelf)
user, err = object.GetMaskedUser(user, isAdminOrSelf)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedUser)
c.ResponseOk(user)
}
// UpdateUser
@@ -541,13 +541,13 @@ func (c *ApiController) GetSortedUsers() {
sorter := c.Input().Get("sorter")
limit := util.ParseInt(c.Input().Get("limit"))
maskedUsers, err := object.GetMaskedUsers(object.GetSortedUsers(owner, sorter, limit))
users, err := object.GetMaskedUsers(object.GetSortedUsers(owner, sorter, limit))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedUsers)
c.ResponseOk(users)
}
// GetUserCount

View File

@@ -272,7 +272,7 @@ func (c *ApiController) VerifyCaptcha() {
// ResetEmailOrPhone ...
// @Tag Account API
// @Title ResetEmailOrPhone
// @router /api/reset-email-or-phone [post]
// @router /reset-email-or-phone [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) ResetEmailOrPhone() {
user, ok := c.RequireSignedInUser()
@@ -367,7 +367,7 @@ func (c *ApiController) ResetEmailOrPhone() {
// VerifyCode
// @Tag Verification API
// @Title VerifyCode
// @router /api/verify-code [post]
// @router /verify-code [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) VerifyCode() {
var authForm form.AuthForm

View File

@@ -14,6 +14,8 @@
package form
import "reflect"
type AuthForm struct {
Type string `json:"type"`
SigninMethod string `json:"signinMethod"`
@@ -60,3 +62,13 @@ type AuthForm struct {
Plan string `json:"plan"`
Pricing string `json:"pricing"`
}
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
val := reflect.ValueOf(*form)
fieldValue := val.FieldByName(fieldName)
if fieldValue.IsValid() && fieldValue.Kind() == reflect.String {
return true, fieldValue.String()
}
return false, ""
}

2
go.mod
View File

@@ -12,7 +12,7 @@ require (
github.com/casdoor/go-sms-sender v0.19.0
github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.4.1
github.com/casdoor/oss v1.5.0
github.com/casdoor/xorm-adapter/v3 v3.1.0
github.com/casvisor/casvisor-go-sdk v1.0.3
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f

2
go.sum
View File

@@ -1091,6 +1091,8 @@ github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.4.1 h1:/P2JCyGzB2TtpJ3LocKocI1VAme2YdvVau2wpMQGt7I=
github.com/casdoor/oss v1.4.1/go.mod h1:rJAWA0hLhtu94t6IRpotLUkXO1NWMASirywQYaGizJE=
github.com/casdoor/oss v1.5.0 h1:mi1htaXR5fynskDry1S3wk+Dd2nRY1z1pVcnGsqMqP4=
github.com/casdoor/oss v1.5.0/go.mod h1:rJAWA0hLhtu94t6IRpotLUkXO1NWMASirywQYaGizJE=
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM=
github.com/casvisor/casvisor-go-sdk v1.0.3 h1:TKJQWKnhtznEBhzLPEdNsp7nJK2GgdD8JsB0lFPMW7U=

View File

@@ -18,7 +18,6 @@ import (
"fmt"
"hash/fnv"
"log"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
@@ -50,17 +49,7 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
res := ldap.NewBindResponse(ldap.LDAPResultSuccess)
if r.AuthenticationChoice() == "simple" {
bindDN := string(r.Name())
bindPassword := string(r.AuthenticationSimple())
if bindDN == "" && bindPassword == "" {
res.SetResultCode(ldap.LDAPResultInappropriateAuthentication)
res.SetDiagnosticMessage("Anonymous bind disallowed")
w.Write(res)
return
}
bindUsername, bindOrg, err := getNameAndOrgFromDN(bindDN)
bindUsername, bindOrg, err := getNameAndOrgFromDN(string(r.Name()))
if err != nil {
log.Printf("getNameAndOrgFromDN() error: %s", err.Error())
res.SetResultCode(ldap.LDAPResultInvalidDNSyntax)
@@ -69,6 +58,7 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
return
}
bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
if err != nil {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
@@ -103,46 +93,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
}
r := m.GetSearchRequest()
// case insensitive match
if strings.EqualFold(r.FilterString(), "(objectClass=*)") {
if len(r.Attributes()) == 0 {
w.Write(res)
return
}
first_attr := string(r.Attributes()[0])
if string(r.BaseObject()) == "" {
// handle special search requests
if first_attr == "namingContexts" {
orgs, code := GetFilteredOrganizations(m)
if code != ldap.LDAPResultSuccess {
res.SetResultCode(code)
w.Write(res)
return
}
e := ldap.NewSearchResultEntry(string(r.BaseObject()))
dnlist := make([]message.AttributeValue, len(orgs))
for i, org := range orgs {
dnlist[i] = message.AttributeValue(fmt.Sprintf("ou=%s", org.Name))
}
e.AddAttribute("namingContexts", dnlist...)
w.Write(e)
} else if first_attr == "subschemaSubentry" {
e := ldap.NewSearchResultEntry(string(r.BaseObject()))
e.AddAttribute("subschemaSubentry", message.AttributeValue("cn=Subschema"))
w.Write(e)
}
} else if strings.EqualFold(first_attr, "objectclasses") && string(r.BaseObject()) == "cn=Subschema" {
e := ldap.NewSearchResultEntry(string(r.BaseObject()))
e.AddAttribute("objectClasses", []message.AttributeValue{
"( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) )",
"( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) )",
}...)
w.Write(e)
}
if r.FilterString() == "(objectClass=*)" {
w.Write(res)
return
}
@@ -155,72 +106,38 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
default:
}
objectClass := searchFilterForEquality(r.Filter(), "objectClass", "posixAccount", "posixGroup")
switch objectClass {
case "posixAccount":
users, code := GetFilteredUsers(m)
if code != ldap.LDAPResultSuccess {
res.SetResultCode(code)
w.Write(res)
return
}
// log.Printf("Handling posixAccount filter=%s", r.FilterString())
for _, user := range users {
dn := fmt.Sprintf("uid=%s,cn=users,%s", user.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn)
attrs := r.Attributes()
for _, attr := range attrs {
if string(attr) == "*" {
attrs = AdditionalLdapUserAttributes
break
}
}
for _, attr := range attrs {
if strings.HasSuffix(string(attr), ";binary") {
// unsupported: userCertificate;binary
continue
}
field, ok := ldapUserAttributesMapping.CaseInsensitiveGet(string(attr))
if ok {
e.AddAttribute(message.AttributeDescription(attr), field.GetAttributeValues(user)...)
}
}
w.Write(e)
}
case "posixGroup":
// log.Printf("Handling posixGroup filter=%s", r.FilterString())
groups, code := GetFilteredGroups(m)
if code != ldap.LDAPResultSuccess {
res.SetResultCode(code)
w.Write(res)
return
}
for _, group := range groups {
dn := fmt.Sprintf("cn=%s,cn=groups,%s", group.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn)
attrs := r.Attributes()
for _, attr := range attrs {
if string(attr) == "*" {
attrs = AdditionalLdapGroupAttributes
break
}
}
for _, attr := range attrs {
field, ok := ldapGroupAttributesMapping.CaseInsensitiveGet(string(attr))
if ok {
e.AddAttribute(message.AttributeDescription(attr), field.GetAttributeValues(group)...)
}
}
w.Write(e)
}
case "":
log.Printf("Unmatched search request. filter=%s", r.FilterString())
users, code := GetFilteredUsers(m)
if code != ldap.LDAPResultSuccess {
res.SetResultCode(code)
w.Write(res)
return
}
for _, user := range users {
dn := fmt.Sprintf("uid=%s,cn=%s,%s", user.Id, user.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn)
uidNumberStr := fmt.Sprintf("%v", hash(user.Name))
e.AddAttribute("uidNumber", message.AttributeValue(uidNumberStr))
e.AddAttribute("gidNumber", message.AttributeValue(uidNumberStr))
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Id))
attrs := r.Attributes()
for _, attr := range attrs {
if string(attr) == "*" {
attrs = AdditionalLdapAttributes
break
}
}
for _, attr := range attrs {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "cn" {
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
}
}
w.Write(e)
}
w.Write(res)
}

View File

@@ -18,7 +18,6 @@ import (
"fmt"
"log"
"strings"
"time"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
@@ -29,259 +28,65 @@ import (
"github.com/xorm-io/builder"
)
type V = message.AttributeValue
type AttributeMapper func(user *object.User) message.AttributeValue
type UserAttributeMapper func(user *object.User) []V
type UserFieldRelation struct {
type FieldRelation struct {
userField string
ldapField string
notSearchable bool
hideOnStarOp bool
fieldMapper UserAttributeMapper
constantValue []V
fieldMapper AttributeMapper
}
func (rel UserFieldRelation) GetField() (string, error) {
func (rel FieldRelation) GetField() (string, error) {
if rel.notSearchable {
return "", fmt.Errorf("attribute %s not supported", rel.userField)
}
return rel.userField, nil
}
func (rel UserFieldRelation) GetAttributeValues(user *object.User) []V {
if rel.constantValue != nil && rel.fieldMapper == nil {
return rel.constantValue
}
func (rel FieldRelation) GetAttributeValue(user *object.User) message.AttributeValue {
return rel.fieldMapper(user)
}
type UserFieldRelationMap map[string]UserFieldRelation
func (m UserFieldRelationMap) CaseInsensitiveGet(key string) (UserFieldRelation, bool) {
lowerKey := strings.ToLower(key)
ret, ok := m[lowerKey]
return ret, ok
}
type GroupAttributeMapper func(group *object.Group) []V
type GroupFieldRelation struct {
groupField string
ldapField string
notSearchable bool
hideOnStarOp bool
fieldMapper GroupAttributeMapper
constantValue []V
}
func (rel GroupFieldRelation) GetField() (string, error) {
if rel.notSearchable {
return "", fmt.Errorf("attribute %s not supported", rel.groupField)
}
return rel.groupField, nil
}
func (rel GroupFieldRelation) GetAttributeValues(group *object.Group) []V {
if rel.constantValue != nil && rel.fieldMapper == nil {
return rel.constantValue
}
return rel.fieldMapper(group)
}
type GroupFieldRelationMap map[string]GroupFieldRelation
func (m GroupFieldRelationMap) CaseInsensitiveGet(key string) (GroupFieldRelation, bool) {
lowerKey := strings.ToLower(key)
ret, ok := m[lowerKey]
return ret, ok
}
var ldapUserAttributesMapping = UserFieldRelationMap{
"cn": {ldapField: "cn", userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) []V {
return []V{V(user.Name)}
var ldapAttributesMapping = map[string]FieldRelation{
"cn": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Name)
}},
"uid": {ldapField: "uid", userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) []V {
return []V{V(user.Name)}
"uid": {userField: "name", hideOnStarOp: true, fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Name)
}},
"displayname": {ldapField: "displayName", userField: "displayName", fieldMapper: func(user *object.User) []V {
return []V{V(user.DisplayName)}
"displayname": {userField: "displayName", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.DisplayName)
}},
"email": {ldapField: "email", userField: "email", fieldMapper: func(user *object.User) []V {
return []V{V(user.Email)}
"email": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Email)
}},
"mail": {ldapField: "mail", userField: "email", fieldMapper: func(user *object.User) []V {
return []V{V(user.Email)}
"mail": {userField: "email", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Email)
}},
"mobile": {ldapField: "mobile", userField: "phone", fieldMapper: func(user *object.User) []V {
return []V{V(user.Phone)}
"mobile": {userField: "phone", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Phone)
}},
"telephonenumber": {ldapField: "telephoneNumber", userField: "phone", fieldMapper: func(user *object.User) []V {
return []V{V(user.Phone)}
"title": {userField: "tag", fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(user.Tag)
}},
"postaladdress": {ldapField: "postalAddress", userField: "address", fieldMapper: func(user *object.User) []V {
return []V{V(strings.Join(user.Address, " "))}
}},
"title": {ldapField: "title", userField: "title", fieldMapper: func(user *object.User) []V {
return []V{V(user.Title)}
}},
"gecos": {ldapField: "gecos", userField: "displayName", fieldMapper: func(user *object.User) []V {
return []V{V(user.DisplayName)}
}},
"description": {ldapField: "description", userField: "displayName", fieldMapper: func(user *object.User) []V {
return []V{V(user.DisplayName)}
}},
"logindisabled": {ldapField: "loginDisabled", userField: "isForbidden", fieldMapper: func(user *object.User) []V {
if user.IsForbidden {
return []V{V("1")}
} else {
return []V{V("0")}
}
}},
"userpassword": {
ldapField: "userPassword",
"userPassword": {
userField: "userPassword",
notSearchable: true,
fieldMapper: func(user *object.User) []V {
return []V{V(getUserPasswordWithType(user))}
fieldMapper: func(user *object.User) message.AttributeValue {
return message.AttributeValue(getUserPasswordWithType(user))
},
},
"uidnumber": {ldapField: "uidNumber", notSearchable: true, fieldMapper: func(user *object.User) []V {
return []V{V(fmt.Sprintf("%v", hash(user.Name)))}
}},
"gidnumber": {ldapField: "gidNumber", notSearchable: true, fieldMapper: func(user *object.User) []V {
if len(user.Groups) == 0 {
return []V{V("")}
}
group, err := object.GetGroup(user.Groups[0])
if err != nil {
log.Printf("gidnumber object.GetGroup error: %s", err)
return []V{V("")}
}
return []V{V(fmt.Sprintf("%v", hash(group.Name)))}
}},
"homedirectory": {ldapField: "homeDirectory", notSearchable: true, fieldMapper: func(user *object.User) []V {
return []V{V("/home/" + user.Name)}
}},
"loginshell": {ldapField: "loginShell", notSearchable: true, fieldMapper: func(user *object.User) []V {
if user.IsForbidden || user.IsDeleted {
return []V{V("/sbin/nologin")}
} else {
return []V{V("/bin/bash")}
}
}},
"shadowlastchange": {ldapField: "shadowLastChange", notSearchable: true, fieldMapper: func(user *object.User) []V {
// "this attribute specifies number of days between January 1, 1970, and the date that the password was last modified"
updatedTime, err := time.Parse(time.RFC3339, user.UpdatedTime)
if err != nil {
log.Printf("shadowlastchange time.Parse error: %s", err)
updatedTime = time.Now()
}
return []V{V(fmt.Sprint(updatedTime.Unix() / 86400))}
}},
"pwdchangedtime": {ldapField: "pwdChangedTime", notSearchable: true, fieldMapper: func(user *object.User) []V {
updatedTime, err := time.Parse(time.RFC3339, user.UpdatedTime)
if err != nil {
log.Printf("pwdchangedtime time.Parse error: %s", err)
updatedTime = time.Now()
}
return []V{V(updatedTime.UTC().Format("20060102030405Z"))}
}},
"shadowmin": {ldapField: "shadowMin", notSearchable: true, constantValue: []V{V("0")}},
"shadowmax": {ldapField: "shadowMax", notSearchable: true, constantValue: []V{V("99999")}},
"shadowwarning": {ldapField: "shadowWarning", notSearchable: true, constantValue: []V{V("7")}},
"shadowexpire": {ldapField: "shadowExpire", notSearchable: true, fieldMapper: func(user *object.User) []V {
if user.IsForbidden {
return []V{V("1")}
} else {
return []V{V("-1")}
}
}},
"shadowinactive": {ldapField: "shadowInactive", notSearchable: true, constantValue: []V{V("0")}},
"shadowflag": {ldapField: "shadowFlag", notSearchable: true, constantValue: []V{V("0")}},
"memberof": {ldapField: "memberOf", notSearchable: true, fieldMapper: func(user *object.User) []V {
var groupdn []V
for _, groupId := range user.Groups {
group, err := object.GetGroup(groupId)
if err != nil {
log.Printf("memberOf object.GetGroup error: %s", err)
continue
}
groupdn = append(groupdn, V(fmt.Sprintf("cn=%s,cn=groups,ou=%s", group.Name, group.Owner)))
}
return groupdn
}},
"objectclass": {ldapField: "objectClass", notSearchable: true, constantValue: []V{
V("top"),
V("posixAccount"),
V("shadowAccount"),
V("person"),
V("organizationalPerson"),
V("inetOrgPerson"),
V("apple-user"),
V("sambaSamAccount"),
V("sambaIdmapEntry"),
V("extensibleObject"),
}},
}
var ldapGroupAttributesMapping = GroupFieldRelationMap{
"cn": {ldapField: "cn", hideOnStarOp: true, fieldMapper: func(group *object.Group) []V {
return []V{V(group.Name)}
}},
"gidnumber": {ldapField: "gidNumber", hideOnStarOp: true, fieldMapper: func(group *object.Group) []V {
return []V{V(fmt.Sprintf("%v", hash(group.Name)))}
}},
"member": {ldapField: "member", fieldMapper: func(group *object.Group) []V {
users, err := object.GetGroupUsers(group.GetId())
if err != nil {
log.Printf("member object.GetGroupUsers error: %s", err)
return []V{V("")}
}
var members []V
for _, user := range users {
members = append(members, V(fmt.Sprintf("uid=%s,cn=users,ou=%s", user.Name, user.Owner)))
}
return members
}},
"memberuid": {ldapField: "memberUid", fieldMapper: func(group *object.Group) []V {
users, err := object.GetGroupUsers(group.GetId())
if err != nil {
log.Printf("member object.GetGroupUsers error: %s", err)
return []V{V("")}
}
var members []message.AttributeValue
for _, user := range users {
members = append(members, message.AttributeValue(user.Name))
}
return members
}},
"description": {ldapField: "description", hideOnStarOp: true, fieldMapper: func(group *object.Group) []V {
return []V{V(group.DisplayName)}
}},
"objectclass": {ldapField: "objectClass", hideOnStarOp: true, constantValue: []V{
V("top"),
V("posixGroup"),
}},
}
var (
AdditionalLdapUserAttributes []message.LDAPString
AdditionalLdapGroupAttributes []message.LDAPString
)
var AdditionalLdapAttributes []message.LDAPString
func init() {
for _, v := range ldapUserAttributesMapping {
for k, v := range ldapAttributesMapping {
if v.hideOnStarOp {
continue
}
AdditionalLdapUserAttributes = append(AdditionalLdapUserAttributes, message.LDAPString(v.ldapField))
}
for _, v := range ldapGroupAttributesMapping {
if v.hideOnStarOp {
continue
}
AdditionalLdapGroupAttributes = append(AdditionalLdapGroupAttributes, message.LDAPString(v.ldapField))
AdditionalLdapAttributes = append(AdditionalLdapAttributes, message.LDAPString(k))
}
}
@@ -502,52 +307,6 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
}
}
func GetFilteredOrganizations(m *ldap.Message) ([]*object.Organization, int) {
if m.Client.IsGlobalAdmin {
organizations, err := object.GetOrganizations("")
if err != nil {
panic(err)
}
return organizations, ldap.LDAPResultSuccess
} else if m.Client.IsOrgAdmin {
requestUserId := util.GetId(m.Client.OrgName, m.Client.UserName)
user, err := object.GetUser(requestUserId)
if err != nil {
panic(err)
}
organization, err := object.GetOrganizationByUser(user)
if err != nil {
panic(err)
}
return []*object.Organization{organization}, ldap.LDAPResultSuccess
} else {
return nil, ldap.LDAPResultInsufficientAccessRights
}
}
func GetFilteredGroups(m *ldap.Message) ([]*object.Group, int) {
if m.Client.IsGlobalAdmin {
groups, err := object.GetGroups("")
if err != nil {
panic(err)
}
return groups, ldap.LDAPResultSuccess
} else if m.Client.IsOrgAdmin {
requestUserId := util.GetId(m.Client.OrgName, m.Client.UserName)
user, err := object.GetUser(requestUserId)
if err != nil {
panic(err)
}
groups, err := object.GetGroups(user.Owner)
if err != nil {
panic(err)
}
return groups, ldap.LDAPResultSuccess
} else {
return nil, ldap.LDAPResultInsufficientAccessRights
}
}
// get user password with hash type prefix
// TODO not handle salt yet
// @return {md5}5f4dcc3b5aa765d61d8327deb882cf99
@@ -571,49 +330,18 @@ func getUserPasswordWithType(user *object.User) string {
return fmt.Sprintf("{%s}%s", prefix, user.Password)
}
func getAttribute(attributeName string, user *object.User) message.AttributeValue {
v, ok := ldapAttributesMapping[attributeName]
if !ok {
return ""
}
return v.GetAttributeValue(user)
}
func getUserFieldFromAttribute(attributeName string) (string, error) {
v, ok := ldapUserAttributesMapping.CaseInsensitiveGet(attributeName)
v, ok := ldapAttributesMapping[attributeName]
if !ok {
return "", fmt.Errorf("attribute %s not supported", attributeName)
}
return v.GetField()
}
func searchFilterForEquality(filter message.Filter, desc string, values ...string) string {
switch f := filter.(type) {
case message.FilterAnd:
for _, child := range f {
if val := searchFilterForEquality(child, desc, values...); val != "" {
return val
}
}
case message.FilterOr:
for _, child := range f {
if val := searchFilterForEquality(child, desc, values...); val != "" {
return val
}
}
case message.FilterNot:
return searchFilterForEquality(f.Filter, desc, values...)
case message.FilterSubstrings:
// Handle FilterSubstrings case if needed
case message.FilterEqualityMatch:
if strings.EqualFold(string(f.AttributeDesc()), desc) {
for _, value := range values {
if val := string(f.AssertionValue()); val == value {
return val
}
}
}
case message.FilterGreaterOrEqual:
// Handle FilterGreaterOrEqual case if needed
case message.FilterLessOrEqual:
// Handle FilterLessOrEqual case if needed
case message.FilterPresent:
// Handle FilterPresent case if needed
case message.FilterApproxMatch:
// Handle FilterApproxMatch case if needed
}
return ""
}

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"regexp"
"strings"
"time"
"unicode"
@@ -32,89 +33,89 @@ const (
DefaultFailedSigninFrozenTime = 15
)
func CheckUserSignup(application *Application, organization *Organization, form *form.AuthForm, lang string) string {
func CheckUserSignup(application *Application, organization *Organization, authForm *form.AuthForm, lang string) string {
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
}
if application.IsSignupItemVisible("Username") {
if len(form.Username) <= 1 {
if len(authForm.Username) <= 1 {
return i18n.Translate(lang, "check:Username must have at least 2 characters")
}
if unicode.IsDigit(rune(form.Username[0])) {
if unicode.IsDigit(rune(authForm.Username[0])) {
return i18n.Translate(lang, "check:Username cannot start with a digit")
}
if util.IsEmailValid(form.Username) {
if util.IsEmailValid(authForm.Username) {
return i18n.Translate(lang, "check:Username cannot be an email address")
}
if util.ReWhiteSpace.MatchString(form.Username) {
if util.ReWhiteSpace.MatchString(authForm.Username) {
return i18n.Translate(lang, "check:Username cannot contain white spaces")
}
if msg := CheckUsername(form.Username, lang); msg != "" {
if msg := CheckUsername(authForm.Username, lang); msg != "" {
return msg
}
if HasUserByField(organization.Name, "name", form.Username) {
if HasUserByField(organization.Name, "name", authForm.Username) {
return i18n.Translate(lang, "check:Username already exists")
}
if HasUserByField(organization.Name, "email", form.Email) {
if HasUserByField(organization.Name, "email", authForm.Email) {
return i18n.Translate(lang, "check:Email already exists")
}
if HasUserByField(organization.Name, "phone", form.Phone) {
if HasUserByField(organization.Name, "phone", authForm.Phone) {
return i18n.Translate(lang, "check:Phone already exists")
}
}
if application.IsSignupItemVisible("Password") {
msg := CheckPasswordComplexityByOrg(organization, form.Password)
msg := CheckPasswordComplexityByOrg(organization, authForm.Password)
if msg != "" {
return msg
}
}
if application.IsSignupItemVisible("Email") {
if form.Email == "" {
if authForm.Email == "" {
if application.IsSignupItemRequired("Email") {
return i18n.Translate(lang, "check:Email cannot be empty")
}
} else {
if HasUserByField(organization.Name, "email", form.Email) {
if HasUserByField(organization.Name, "email", authForm.Email) {
return i18n.Translate(lang, "check:Email already exists")
} else if !util.IsEmailValid(form.Email) {
} else if !util.IsEmailValid(authForm.Email) {
return i18n.Translate(lang, "check:Email is invalid")
}
}
}
if application.IsSignupItemVisible("Phone") {
if form.Phone == "" {
if authForm.Phone == "" {
if application.IsSignupItemRequired("Phone") {
return i18n.Translate(lang, "check:Phone cannot be empty")
}
} else {
if HasUserByField(organization.Name, "phone", form.Phone) {
if HasUserByField(organization.Name, "phone", authForm.Phone) {
return i18n.Translate(lang, "check:Phone already exists")
} else if !util.IsPhoneAllowInRegin(form.CountryCode, organization.CountryCodes) {
} else if !util.IsPhoneAllowInRegin(authForm.CountryCode, organization.CountryCodes) {
return i18n.Translate(lang, "check:Your region is not allow to signup by phone")
} else if !util.IsPhoneValid(form.Phone, form.CountryCode) {
} else if !util.IsPhoneValid(authForm.Phone, authForm.CountryCode) {
return i18n.Translate(lang, "check:Phone number is invalid")
}
}
}
if application.IsSignupItemVisible("Display name") {
if application.GetSignupItemRule("Display name") == "First, last" && (form.FirstName != "" || form.LastName != "") {
if form.FirstName == "" {
if application.GetSignupItemRule("Display name") == "First, last" && (authForm.FirstName != "" || authForm.LastName != "") {
if authForm.FirstName == "" {
return i18n.Translate(lang, "check:FirstName cannot be blank")
} else if form.LastName == "" {
} else if authForm.LastName == "" {
return i18n.Translate(lang, "check:LastName cannot be blank")
}
} else {
if form.Name == "" {
if authForm.Name == "" {
return i18n.Translate(lang, "check:DisplayName cannot be blank")
} else if application.GetSignupItemRule("Display name") == "Real name" {
if !isValidRealName(form.Name) {
if !isValidRealName(authForm.Name) {
return i18n.Translate(lang, "check:DisplayName is not valid real name")
}
}
@@ -122,23 +123,44 @@ func CheckUserSignup(application *Application, organization *Organization, form
}
if application.IsSignupItemVisible("Affiliation") {
if form.Affiliation == "" {
if authForm.Affiliation == "" {
return i18n.Translate(lang, "check:Affiliation cannot be blank")
}
}
if len(application.InvitationCodes) > 0 {
if form.InvitationCode == "" {
if authForm.InvitationCode == "" {
if application.IsSignupItemRequired("Invitation code") {
return i18n.Translate(lang, "check:Invitation code cannot be blank")
}
} else {
if !util.InSlice(application.InvitationCodes, form.InvitationCode) {
if !util.InSlice(application.InvitationCodes, authForm.InvitationCode) {
return i18n.Translate(lang, "check:Invitation code is invalid")
}
}
}
for _, signupItem := range application.SignupItems {
if signupItem.Regex == "" {
continue
}
isString, value := form.GetAuthFormFieldValue(authForm, signupItem.Name)
if !isString {
continue
}
regexSignupItem, err := regexp.Compile(signupItem.Regex)
if err != nil {
return err.Error()
}
matched := regexSignupItem.MatchString(value)
if !matched {
return fmt.Sprintf(i18n.Translate(lang, "check:The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\""), value, signupItem.Name, application.Name)
}
}
return ""
}

View File

@@ -24,7 +24,7 @@ var (
regexLowerCase = regexp.MustCompile(`[a-z]`)
regexUpperCase = regexp.MustCompile(`[A-Z]`)
regexDigit = regexp.MustCompile(`\d`)
regexSpecial = regexp.MustCompile(`[!@#$%^&*]`)
regexSpecial = regexp.MustCompile("[!-/:-@[-`{-~]")
)
func isValidOption_AtLeast6(password string) string {

View File

@@ -181,7 +181,7 @@ func initBuiltInApplication() {
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, SignupGroup: "", Rule: "None", Provider: nil},
},
SigninMethods: []*SigninMethod{
{Name: "Password", DisplayName: "Password", Rule: "None"},
{Name: "Password", DisplayName: "Password", Rule: "All"},
{Name: "Verification code", DisplayName: "Verification code", Rule: "All"},
{Name: "WebAuthn", DisplayName: "WebAuthn", Rule: "None"},
},

View File

@@ -116,22 +116,35 @@ func GetSyncer(id string) (*Syncer, error) {
return getSyncer(owner, name)
}
func GetMaskedSyncer(syncer *Syncer) *Syncer {
func GetMaskedSyncer(syncer *Syncer, errs ...error) (*Syncer, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
if syncer == nil {
return nil
return nil, nil
}
if syncer.Password != "" {
syncer.Password = "***"
}
return syncer
return syncer, nil
}
func GetMaskedSyncers(syncers []*Syncer) []*Syncer {
for _, syncer := range syncers {
syncer = GetMaskedSyncer(syncer)
func GetMaskedSyncers(syncers []*Syncer, errs ...error) ([]*Syncer, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
return syncers
var err error
for _, syncer := range syncers {
syncer, err = GetMaskedSyncer(syncer)
if err != nil {
return nil, err
}
}
return syncers, nil
}
func UpdateSyncer(id string, syncer *Syncer) (bool, error) {

View File

@@ -24,16 +24,18 @@ import (
)
const (
headerOrigin = "Origin"
headerAllowOrigin = "Access-Control-Allow-Origin"
headerAllowMethods = "Access-Control-Allow-Methods"
headerAllowHeaders = "Access-Control-Allow-Headers"
headerOrigin = "Origin"
headerAllowOrigin = "Access-Control-Allow-Origin"
headerAllowMethods = "Access-Control-Allow-Methods"
headerAllowHeaders = "Access-Control-Allow-Headers"
headerAllowCredentials = "Access-Control-Allow-Credentials"
)
func setCorsHeaders(ctx *context.Context, origin string) {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
ctx.Output.Header(headerAllowCredentials, "true")
if ctx.Input.Method() == "OPTIONS" {
ctx.ResponseWriter.WriteHeader(http.StatusOK)

View File

@@ -13,7 +13,7 @@
// limitations under the License.
// Package routers
// @APIVersion 1.376.1
// @APIVersion 1.503.0
// @Title Casdoor RESTful API
// @Description Swagger Docs of Casdoor Backend API
// @Contact casbin@googlegroups.com

View File

@@ -34,6 +34,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
return NewQiniuCloudKodoStorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "Google Cloud Storage":
return NewGoogleCloudStorageProvider(clientSecret, bucket, endpoint)
case "Synology":
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint)
}
return nil

31
storage/synology_nas.go Normal file
View File

@@ -0,0 +1,31 @@
// Copyright 2023 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 storage
import (
"github.com/casdoor/oss"
"github.com/casdoor/oss/synology"
)
func NewSynologyNasStorageProvider(clientId string, clientSecret string, endpoint string) oss.StorageInterface {
sp := synology.New(&synology.Config{
AccessID: clientId,
AccessKey: clientSecret,
Endpoint: endpoint,
SharedFolder: "/home",
})
return sp
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ swagger: "2.0"
info:
title: Casdoor RESTful API
description: Swagger Docs of Casdoor Backend API
version: 1.376.1
version: 1.503.0
contact:
email: casbin@googlegroups.com
basePath: /
@@ -31,6 +31,17 @@ paths:
description: ""
schema:
$ref: '#/definitions/object.OidcDiscovery'
/api/Callback:
post:
tags:
- Callback API
description: Get Login Error Counts
operationId: ApiController.Callback
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/add-adapter:
post:
tags:
@@ -121,6 +132,24 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-invitation:
post:
tags:
- Invitation API
description: add invitation
operationId: ApiController.AddInvitation
parameters:
- in: body
name: body
description: The details of the invitation
required: true
schema:
$ref: '#/definitions/object.Invitation'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-ldap:
post:
tags:
@@ -442,162 +471,10 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/Callback:
post:
tags:
- Callback API
description: Get Login Error Counts
operationId: ApiController.Callback
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/api/get-captcha:
get:
tags:
- Login API
operationId: ApiController.GetCaptcha
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/api/get-captcha-status:
get:
tags:
- Token API
description: Get Login Error Counts
operationId: ApiController.GetCaptchaStatus
parameters:
- in: query
name: id
description: The id ( owner/name ) of user
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/get-webhook-event:
get:
tags:
- GetWebhookEventType API
operationId: ApiController.GetWebhookEventType
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/api/reset-email-or-phone:
post:
tags:
- Account API
operationId: ApiController.ResetEmailOrPhone
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/api/send-email:
post:
tags:
- Service API
description: This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
operationId: ApiController.SendEmail
parameters:
- in: query
name: clientId
description: The clientId of the application
required: true
type: string
- in: query
name: clientSecret
description: The clientSecret of the application
required: true
type: string
- in: body
name: from
description: Details of the email request
required: true
schema:
$ref: '#/definitions/controllers.EmailForm'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/send-notification:
post:
tags:
- Service API
description: This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
operationId: ApiController.SendNotification
parameters:
- in: body
name: from
description: Details of the notification request
required: true
schema:
$ref: '#/definitions/controllers.NotificationForm'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/send-sms:
post:
tags:
- Service API
description: This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
operationId: ApiController.SendSms
parameters:
- in: query
name: clientId
description: The clientId of the application
required: true
type: string
- in: query
name: clientSecret
description: The clientSecret of the application
required: true
type: string
- in: body
name: from
description: Details of the sms request
required: true
schema:
$ref: '#/definitions/controllers.SmsForm'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/api/verify-code:
post:
tags:
- Verification API
operationId: ApiController.VerifyCode
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/api/webhook:
post:
tags:
- HandleOfficialAccountEvent API
operationId: ApiController.HandleOfficialAccountEvent
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/batch-enforce:
post:
tags:
- Enforce API
- Enforcer API
description: Call Casbin BatchEnforce API
operationId: ApiController.BatchEnforce
parameters:
@@ -617,6 +494,10 @@ paths:
name: modelId
description: model id
type: string
- in: query
name: owner
description: owner
type: string
responses:
"200":
description: The Response object
@@ -744,6 +625,24 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-invitation:
post:
tags:
- Invitation API
description: delete invitation
operationId: ApiController.DeleteInvitation
parameters:
- in: body
name: body
description: The details of the invitation
required: true
schema:
$ref: '#/definitions/object.Invitation'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-ldap:
post:
tags:
@@ -1064,7 +963,7 @@ paths:
/api/enforce:
post:
tags:
- Enforce API
- Enforcer API
description: Call Casbin Enforce API
operationId: ApiController.Enforce
parameters:
@@ -1088,6 +987,10 @@ paths:
name: resourceId
description: resource id
type: string
- in: query
name: owner
description: owner
type: string
responses:
"200":
description: The Response object
@@ -1213,6 +1116,33 @@ paths:
type: array
items:
$ref: '#/definitions/object.Application'
/api/get-captcha:
get:
tags:
- Login API
operationId: ApiController.GetCaptcha
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/get-captcha-status:
get:
tags:
- Token API
description: Get Login Error Counts
operationId: ApiController.GetCaptchaStatus
parameters:
- in: query
name: id
description: The id ( owner/name ) of user
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/get-cert:
get:
tags:
@@ -1252,7 +1182,7 @@ paths:
/api/get-dashboard:
get:
tags:
- GetDashboard API
- System API
description: get information of dashboard
operationId: ApiController.GetDashboard
responses:
@@ -1410,6 +1340,42 @@ paths:
type: array
items:
$ref: '#/definitions/object.Group'
/api/get-invitation:
get:
tags:
- Invitation API
description: get invitation
operationId: ApiController.GetInvitation
parameters:
- in: query
name: id
description: The id ( owner/name ) of the invitation
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Invitation'
/api/get-invitations:
get:
tags:
- Invitation API
description: get invitations
operationId: ApiController.GetInvitations
parameters:
- in: query
name: owner
description: The owner of invitations
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Invitation'
/api/get-ldap:
get:
tags:
@@ -1785,7 +1751,7 @@ paths:
/api/get-prometheus-info:
get:
tags:
- Prometheus API
- System API
description: get Prometheus Info
operationId: ApiController.GetPrometheusInfo
responses:
@@ -2269,6 +2235,16 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/object.Webhook'
/api/get-webhook-event:
get:
tags:
- System API
operationId: ApiController.GetWebhookEventType
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/get-webhooks:
get:
tags:
@@ -2396,8 +2372,50 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/login/oauth/access_token:
post:
tags:
- Token API
description: get OAuth access token
operationId: ApiController.GetOAuthToken
parameters:
- in: query
name: grant_type
description: OAuth grant type
required: true
type: string
- in: query
name: client_id
description: OAuth client id
required: true
type: string
- in: query
name: client_secret
description: OAuth client secret
required: true
type: string
- in: query
name: code
description: OAuth code
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.TokenWrapper'
"400":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
"401":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
/api/login/oauth/introspect:
post:
tags:
- Login API
description: The introspection endpoint is an OAuth 2.0 endpoint that takes a
operationId: ApiController.IntrospectToken
parameters:
@@ -2543,6 +2561,16 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/reset-email-or-phone:
post:
tags:
- Account API
operationId: ApiController.ResetEmailOrPhone
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/run-syncer:
get:
tags:
@@ -2561,6 +2589,80 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-email:
post:
tags:
- Service API
description: This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
operationId: ApiController.SendEmail
parameters:
- in: query
name: clientId
description: The clientId of the application
required: true
type: string
- in: query
name: clientSecret
description: The clientSecret of the application
required: true
type: string
- in: body
name: from
description: Details of the email request
required: true
schema:
$ref: '#/definitions/controllers.EmailForm'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-notification:
post:
tags:
- Service API
description: This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
operationId: ApiController.SendNotification
parameters:
- in: body
name: from
description: Details of the notification request
required: true
schema:
$ref: '#/definitions/controllers.NotificationForm'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-sms:
post:
tags:
- Service API
description: This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
operationId: ApiController.SendSms
parameters:
- in: query
name: clientId
description: The clientId of the application
required: true
type: string
- in: query
name: clientSecret
description: The clientSecret of the application
required: true
type: string
- in: body
name: from
description: Details of the sms request
required: true
schema:
$ref: '#/definitions/controllers.SmsForm'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-verification-code:
post:
tags:
@@ -2778,6 +2880,29 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-invitation:
post:
tags:
- Invitation API
description: update invitation
operationId: ApiController.UpdateInvitation
parameters:
- in: query
name: id
description: The id ( owner/name ) of the invitation
required: true
type: string
- in: body
name: body
description: The details of the invitation
required: true
schema:
$ref: '#/definitions/object.Invitation'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-ldap:
post:
tags:
@@ -3245,6 +3370,33 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/verify-code:
post:
tags:
- Verification API
operationId: ApiController.VerifyCode
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Userinfo'
/api/verify-invitation:
get:
tags:
- Invitation API
description: verify invitation
operationId: ApiController.VerifyInvitation
parameters:
- in: query
name: id
description: The id ( owner/name ) of the invitation
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/webauthn/signin/begin:
get:
tags:
@@ -3314,46 +3466,16 @@ paths:
description: '"The Response object"'
schema:
$ref: '#/definitions/controllers.Response'
/apiapi/login/oauth/access_token:
/api/webhook:
post:
tags:
- Token API
description: get OAuth access token
operationId: ApiController.GetOAuthToken
parameters:
- in: query
name: grant_type
description: OAuth grant type
required: true
type: string
- in: query
name: client_id
description: OAuth client id
required: true
type: string
- in: query
name: client_secret
description: OAuth client secret
required: true
type: string
- in: query
name: code
description: OAuth code
required: true
type: string
- System API
operationId: ApiController.HandleOfficialAccountEvent
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.TokenWrapper'
"400":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
"401":
description: The Response object
schema:
$ref: '#/definitions/object.TokenError'
$ref: '#/definitions/object.Userinfo'
definitions:
casbin.Enforcer:
title: Enforcer
@@ -3546,10 +3668,10 @@ definitions:
expireInHours:
type: integer
format: int64
failedSigninLimit:
failedSigninFrozenTime:
type: integer
format: int64
failedSigninFrozenTime:
failedSigninLimit:
type: integer
format: int64
forgetUrl:
@@ -3606,6 +3728,10 @@ definitions:
type: string
signinHtml:
type: string
signinMethods:
type: array
items:
$ref: '#/definitions/object.SigninMethod'
signinUrl:
type: string
signupHtml:
@@ -3624,6 +3750,10 @@ definitions:
type: string
themeData:
$ref: '#/definitions/object.ThemeData'
tokenFields:
type: array
items:
type: string
tokenFormat:
type: string
object.Cert:
@@ -3780,6 +3910,40 @@ definitions:
type: string
username:
type: string
object.Invitation:
title: Invitation
type: object
properties:
application:
type: string
code:
type: string
createdTime:
type: string
displayName:
type: string
email:
type: string
name:
type: string
owner:
type: string
phone:
type: string
quota:
type: integer
format: int64
signupGroup:
type: string
state:
type: string
updatedTime:
type: string
usedCount:
type: integer
format: int64
username:
type: string
object.Ldap:
title: Ldap
type: object
@@ -4455,6 +4619,16 @@ definitions:
type: string
value:
type: string
object.SigninMethod:
title: SigninMethod
type: object
properties:
displayName:
type: string
name:
type: string
rule:
type: string
object.SignupItem:
title: SignupItem
type: object
@@ -4467,6 +4641,8 @@ definitions:
type: string
prompted:
type: boolean
regex:
type: string
required:
type: boolean
rule:

View File

@@ -796,7 +796,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@@ -832,7 +832,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "MinIO", "Google Cloud Storage", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "MinIO", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :

View File

@@ -207,6 +207,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_google_cloud.png`,
url: "https://cloud.google.com/storage",
},
"Synology": {
logo: `${StaticBaseUrl}/img/social_synology.png`,
url: "https://www.synology.com/en-global/dsm/feature/file_sharing",
},
},
SAML: {
"Aliyun IDaaS": {
@@ -1024,6 +1028,7 @@ export function getProviderTypeOptions(category) {
{id: "Azure Blob", name: "Azure Blob"},
{id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"},
{id: "Google Cloud Storage", name: "Google Cloud Storage"},
{id: "Synology", name: "Synology"},
]
);
} else if (category === "SAML") {

View File

@@ -146,7 +146,7 @@ export function getWechatMessageEvent() {
}
export function getCaptchaStatus(values) {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&user_id=${values["username"]}`, {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}`, {
method: "GET",
credentials: "include",
headers: {

View File

@@ -37,7 +37,7 @@ function isValidOption_Aa123(password) {
}
function isValidOption_SpecialChar(password) {
const regex = /^(?=.*[!@#$%^&*]).+$/;
const regex = /^(?=.*[!-/:-@[-`{-~]).+$/;
if (!regex.test(password)) {
return i18next.t("user:The password must contain at least one special character");
}

View File

@@ -271,7 +271,7 @@
"Non-LDAP": "禁用LDAP",
"None": "无",
"OAuth providers": "OAuth提供方",
"OK": "OK",
"OK": "确定",
"Organization": "组织",
"Organization - Tooltip": "类似于租户、用户池等概念,每个用户和应用都从属于一个组织",
"Organizations": "组织",

View File

@@ -193,7 +193,7 @@ class ProviderTable extends React.Component {
title: i18next.t("application:Rule"),
dataIndex: "rule",
key: "rule",
width: "100px",
width: "120px",
render: (text, record, index) => {
if (record.provider?.type === "Google") {
if (text === "None") {