Compare commits

..

9 Commits

11 changed files with 169 additions and 21 deletions

View File

@@ -98,6 +98,7 @@ p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, *
p, *, *, GET, /api/get-all-actions, *, *
p, *, *, GET, /api/get-all-roles, *, *
p, *, *, GET, /api/run-casbin-command, *, *
p, *, *, GET, /api/get-invitation-info, *, *
p, *, *, GET, /api/faceid-signin-begin, *, *
`

View File

@@ -0,0 +1,114 @@
// Copyright 2024 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 (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
func processArgsToTempFiles(args []string) ([]string, []string, error) {
tempFiles := []string{}
newArgs := []string{}
for i := 0; i < len(args); i++ {
if (args[i] == "-m" || args[i] == "-p") && i+1 < len(args) {
pattern := fmt.Sprintf("casbin_temp_%s_*.conf", args[i])
tempFile, err := os.CreateTemp("", pattern)
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp file: %v", err)
}
_, err = tempFile.WriteString(args[i+1])
if err != nil {
tempFile.Close()
return nil, nil, fmt.Errorf("failed to write to temp file: %v", err)
}
tempFile.Close()
tempFiles = append(tempFiles, tempFile.Name())
newArgs = append(newArgs, args[i], tempFile.Name())
i++
} else {
newArgs = append(newArgs, args[i])
}
}
return tempFiles, newArgs, nil
}
// RunCasbinCommand
// @Title RunCasbinCommand
// @Tag Enforcer API
// @Description Call Casbin CLI commands
// @Success 200 {object} controllers.Response The Response object
// @router /run-casbin-command [get]
func (c *ApiController) RunCasbinCommand() {
language := c.Input().Get("language")
argString := c.Input().Get("args")
if language == "" {
language = "go"
}
// use "casbin-go-cli" by default, can be also "casbin-java-cli", "casbin-node-cli", etc.
// the pre-built binary of "casbin-go-cli" can be found at: https://github.com/casbin/casbin-go-cli/releases
binaryName := fmt.Sprintf("casbin-%s-cli", language)
_, err := exec.LookPath(binaryName)
if err != nil {
c.ResponseError(fmt.Sprintf("executable file: %s not found in PATH", binaryName))
return
}
// RBAC model & policy example:
// https://door.casdoor.com/api/run-casbin-command?language=go&args=["enforce", "-m", "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) %26%26 r.obj == p.obj %26%26 r.act == p.act", "-p", "p, alice, data1, read\np, bob, data2, write\np, data2_admin, data2, read\np, data2_admin, data2, write\ng, alice, data2_admin", "alice", "data1", "read"]
// Casbin CLI usage:
// https://github.com/jcasbin/casbin-java-cli?tab=readme-ov-file#get-started
var args []string
err = json.Unmarshal([]byte(argString), &args)
if err != nil {
c.ResponseError(err.Error())
return
}
tempFiles, processedArgs, err := processArgsToTempFiles(args)
defer func() {
for _, file := range tempFiles {
os.Remove(file)
}
}()
if err != nil {
c.ResponseError(err.Error())
return
}
command := exec.Command(binaryName, processedArgs...)
outputBytes, err := command.CombinedOutput()
if err != nil {
errorString := err.Error()
if outputBytes != nil {
output := string(outputBytes)
errorString = fmt.Sprintf("%s, error: %s", output, err.Error())
}
c.ResponseError(errorString)
return
}
output := string(outputBytes)
output = strings.TrimSuffix(output, "\n")
c.ResponseOk(output)
}

View File

@@ -62,6 +62,7 @@ type Organization struct {
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
PasswordExpireDays int `json:"passwordExpireDays"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`

View File

@@ -26,6 +26,7 @@ import (
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/beevik/etree"
@@ -222,10 +223,13 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
originFrontend, originBackend := getOriginFromHost(host)
idpLocation := ""
idpBinding := ""
if enablePostBinding {
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
} else {
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
}
d := IdpEntityDescriptor{
@@ -258,7 +262,7 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"},
},
SingleSignOnService: SingleSignOnService{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Binding: idpBinding,
Location: idpLocation,
},
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
@@ -273,29 +277,38 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) {
// request type
method := "GET"
samlRequest = strings.ReplaceAll(samlRequest, " ", "+")
// base64 decode
defated, err := base64.StdEncoding.DecodeString(samlRequest)
if err != nil {
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
}
// decompress
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
var requestByte []byte
for {
_, err = io.CopyN(&buffer, rdr, 1024)
if err != nil {
if err == io.EOF {
break
if strings.Contains(string(defated), "xmlns:") {
requestByte = defated
} else {
// decompress
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
for {
_, err = io.CopyN(&buffer, rdr, 1024)
if err != nil {
if err == io.EOF {
break
}
return "", "", "", err
}
return "", "", "", err
}
requestByte = buffer.Bytes()
}
var authnRequest saml.AuthNRequest
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
err = xml.Unmarshal(requestByte, &authnRequest)
if err != nil {
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
}

View File

@@ -174,6 +174,8 @@ func initAPI() {
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
beego.Router("/api/update-session", &controllers.ApiController{}, "POST:UpdateSession")

View File

@@ -765,7 +765,7 @@ class ApplicationEditPage extends React.Component {
/>
<br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`);
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>

View File

@@ -198,11 +198,11 @@ function ManagementPage(props) {
</div>
</Tooltip>
<OpenTour />
{Setting.isAdminUser(props.account) && !Setting.isMobile() && (props.uri.indexOf("/trees") === -1) &&
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect
initValue={Setting.getOrganization()}
withAll={true}
style={{marginRight: "20px", width: "180px", display: "flex"}}
style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
onChange={(value) => {
Setting.setOrganization(value);
}}

View File

@@ -339,6 +339,16 @@ class OrganizationEditPage extends React.Component {
</Col>
</Row>)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Password expire days"), i18next.t("organization:Password expire days - Tooltip"))} :
</Col>
<Col span={4} >
<InputNumber value={this.state.organization.passwordExpireDays} onChange={value => {
this.updateOrganizationField("passwordExpireDays", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :

View File

@@ -37,6 +37,7 @@ class OrganizationListPage extends BaseListPage {
passwordOptions: [],
passwordObfuscatorType: "Plain",
passwordObfuscatorKey: "",
passwordExpireDays: 0,
countryCodes: ["US"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",

View File

@@ -243,7 +243,10 @@ class LoginPage extends React.Component {
}
}
getPlaceholder() {
getPlaceholder(defaultPlaceholder = null) {
if (defaultPlaceholder) {
return defaultPlaceholder;
}
switch (this.state.loginMethod) {
case "verificationCode": return i18next.t("login:Email or phone");
case "verificationCodeEmail": return i18next.t("login:Email");
@@ -679,7 +682,7 @@ class LoginPage extends React.Component {
id="input"
className="login-username-input"
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.getPlaceholder()}
placeholder={this.getPlaceholder(signinItem.placeholder)}
onChange={e => {
this.setState({
username: e.target.value,
@@ -1093,7 +1096,7 @@ class LoginPage extends React.Component {
className="login-password-input"
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("general:Password")}
placeholder={signinItem.placeholder ? signinItem.placeholder : i18next.t("general:Password")}
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
/>
</Form.Item>

View File

@@ -113,6 +113,9 @@ export function getCasLoginParameters(owner, name) {
export function getOAuthGetParameters(params) {
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
const lowercaseQueries = {};
queries.forEach((val, key) => {lowercaseQueries[key.toLowerCase()] = val;});
const clientId = getRefinedValue(queries.get("client_id"));
const responseType = getRefinedValue(queries.get("response_type"));
@@ -138,9 +141,9 @@ export function getOAuthGetParameters(params) {
const nonce = getRefinedValue(queries.get("nonce"));
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
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"));
const samlRequest = getRefinedValue(lowercaseQueries["samlRequest".toLowerCase()]);
const relayState = getRefinedValue(lowercaseQueries["RelayState".toLowerCase()]);
const noRedirect = getRefinedValue(lowercaseQueries["noRedirect".toLowerCase()]);
if (clientId === "" && samlRequest === "") {
// login