Compare commits

..

8 Commits

11 changed files with 162 additions and 40 deletions

View File

@@ -17,7 +17,6 @@ package controllers
import (
"encoding/json"
"fmt"
"strings"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
@@ -200,37 +199,28 @@ func (c *ApiController) GetPolicies() {
// GetFilteredPolicies
// @Title GetFilteredPolicies
// @Tag Enforcer API
// @Description get filtered policies
// @Description get filtered policies with support for multiple filters via POST body
// @Param id query string true "The id ( owner/name ) of enforcer"
// @Param ptype query string false "Policy type, default is 'p'"
// @Param fieldIndex query int false "Field index for filtering"
// @Param fieldValues query string false "Field values for filtering, comma-separated"
// @Param body body []object.Filter true "Array of filter objects for multiple filters"
// @Success 200 {array} xormadapter.CasbinRule
// @router /get-filtered-policies [get]
// @router /get-filtered-policies [post]
func (c *ApiController) GetFilteredPolicies() {
id := c.Input().Get("id")
ptype := c.Input().Get("ptype")
fieldIndexStr := c.Input().Get("fieldIndex")
fieldValuesStr := c.Input().Get("fieldValues")
if ptype == "" {
ptype = "p"
}
fieldIndex := util.ParseInt(fieldIndexStr)
var fieldValues []string
if fieldValuesStr != "" {
fieldValues = strings.Split(fieldValuesStr, ",")
}
policies, err := object.GetFilteredPolicies(id, ptype, fieldIndex, fieldValues...)
var filters []object.Filter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &filters)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(policies)
filteredPolicies, err := object.GetFilteredPoliciesMulti(id, filters)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(filteredPolicies)
}
// UpdatePolicy

View File

@@ -20,6 +20,7 @@ import (
"fmt"
"io"
"mime"
"path"
"path/filepath"
"strings"
@@ -187,6 +188,11 @@ func (c *ApiController) DeleteResource() {
}
_, resource.Name = refineFullFilePath(resource.Name)
tag := c.Input().Get("tag")
if tag == "Direct" {
resource.Name = path.Join(provider.PathPrefix, resource.Name)
}
err = object.DeleteFile(provider, resource.Name, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())

View File

@@ -20,6 +20,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"github.com/casdoor/casdoor/util"
"github.com/mitchellh/mapstructure"
@@ -64,6 +65,25 @@ func (idp *CustomIdProvider) GetToken(code string) (*oauth2.Token, error) {
return idp.Config.Exchange(ctx, code)
}
func getNestedValue(data map[string]interface{}, path string) (interface{}, error) {
keys := strings.Split(path, ".")
var val interface{} = data
for _, key := range keys {
m, ok := val.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("path '%s' is not valid: %s is not a map", path, key)
}
val, ok = m[key]
if !ok {
return nil, fmt.Errorf("key '%s' not found in path '%s'", key, path)
}
}
return val, nil
}
type CustomUserInfo struct {
Id string `mapstructure:"id"`
Username string `mapstructure:"username"`
@@ -108,11 +128,11 @@ func (idp *CustomIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
// map user info
for k, v := range idp.UserMapping {
_, ok := dataMap[v]
if !ok {
return nil, fmt.Errorf("cannot find %s in user from custom provider", v)
val, err := getNestedValue(dataMap, v)
if err != nil {
return nil, fmt.Errorf("cannot find %s in user from custom provider: %v", v, err)
}
dataMap[k] = dataMap[v]
dataMap[k] = val
}
// try to parse id to string

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"slices"
"github.com/casbin/casbin/v2"
"github.com/casdoor/casdoor/util"
@@ -206,6 +207,13 @@ func GetPolicies(id string) ([]*xormadapter.CasbinRule, error) {
return res, nil
}
// Filter represents filter criteria with optional policy type
type Filter struct {
Ptype string `json:"ptype,omitempty"`
FieldIndex *int `json:"fieldIndex,omitempty"`
FieldValues []string `json:"fieldValues"`
}
func GetFilteredPolicies(id string, ptype string, fieldIndex int, fieldValues ...string) ([]*xormadapter.CasbinRule, error) {
enforcer, err := GetInitializedEnforcer(id)
if err != nil {
@@ -236,6 +244,78 @@ func GetFilteredPolicies(id string, ptype string, fieldIndex int, fieldValues ..
return res, nil
}
// GetFilteredPoliciesMulti applies multiple filters to policies
// Doing this in our loop is more efficient than using GetFilteredGroupingPolicy / GetFilteredPolicy which
// iterates over all policies again and again
func GetFilteredPoliciesMulti(id string, filters []Filter) ([]*xormadapter.CasbinRule, error) {
// Get all policies first
allPolicies, err := GetPolicies(id)
if err != nil {
return nil, err
}
// Filter policies based on multiple criteria
var filteredPolicies []*xormadapter.CasbinRule
if len(filters) == 0 {
// No filters, return all policies
return allPolicies, nil
} else {
for _, policy := range allPolicies {
matchesAllFilters := true
for _, filter := range filters {
// Default policy type if unspecified
if filter.Ptype == "" {
filter.Ptype = "p"
}
// Always check policy type
if policy.Ptype != filter.Ptype {
matchesAllFilters = false
break
}
// If FieldIndex is nil, only filter via ptype (skip field-value checks)
if filter.FieldIndex == nil {
continue
}
fieldIndex := *filter.FieldIndex
// If FieldIndex is out of range, also only filter via ptype
if fieldIndex < 0 || fieldIndex > 5 {
continue
}
var fieldValue string
switch fieldIndex {
case 0:
fieldValue = policy.V0
case 1:
fieldValue = policy.V1
case 2:
fieldValue = policy.V2
case 3:
fieldValue = policy.V3
case 4:
fieldValue = policy.V4
case 5:
fieldValue = policy.V5
}
// When FieldIndex is provided and valid, enforce FieldValues (if any)
if len(filter.FieldValues) > 0 && !slices.Contains(filter.FieldValues, fieldValue) {
matchesAllFilters = false
break
}
}
if matchesAllFilters {
filteredPolicies = append(filteredPolicies, policy)
}
}
}
return filteredPolicies, nil
}
func UpdatePolicy(id string, ptype string, oldPolicy []string, newPolicy []string) (bool, error) {
enforcer, err := GetInitializedEnforcer(id)
if err != nil {

View File

@@ -31,9 +31,11 @@ func GetDirectResources(owner string, user string, provider *Provider, prefix st
fullPathPrefix := util.UrlJoin(provider.PathPrefix, prefix)
objects, err := storageProvider.List(fullPathPrefix)
for _, obj := range objects {
name := strings.TrimPrefix(obj.Path, "/")
name = strings.TrimPrefix(name, provider.PathPrefix+"/")
resource := &Resource{
Owner: owner,
Name: strings.TrimPrefix(obj.Path, "/"),
Name: name,
CreatedTime: obj.LastModified.Local().Format(time.RFC3339),
User: user,
Provider: "",

View File

@@ -160,7 +160,7 @@ func initAPI() {
beego.Router("/api/add-adapter", &controllers.ApiController{}, "POST:AddAdapter")
beego.Router("/api/delete-adapter", &controllers.ApiController{}, "POST:DeleteAdapter")
beego.Router("/api/get-policies", &controllers.ApiController{}, "GET:GetPolicies")
beego.Router("/api/get-filtered-policies", &controllers.ApiController{}, "GET:GetFilteredPolicies")
beego.Router("/api/get-filtered-policies", &controllers.ApiController{}, "POST:GetFilteredPolicies")
beego.Router("/api/update-policy", &controllers.ApiController{}, "POST:UpdatePolicy")
beego.Router("/api/add-policy", &controllers.ApiController{}, "POST:AddPolicy")
beego.Router("/api/remove-policy", &controllers.ApiController{}, "POST:RemovePolicy")

View File

@@ -174,7 +174,11 @@ class ProviderEditPage extends React.Component {
}
}
provider.userMapping[key] = value;
if (value === "") {
delete provider.userMapping[key];
} else {
provider.userMapping[key] = value;
}
this.setState({
provider: provider,

View File

@@ -1296,6 +1296,9 @@ export function renderSignupLink(application, text) {
} else {
if (application.signupUrl === "") {
url = `/signup/${application.name}`;
if (application.isShared) {
url = `/signup/${application.name}-org-${application.organization}`;
}
} else {
url = application.signupUrl;
}

View File

@@ -648,6 +648,9 @@ class LoginPage extends React.Component {
)
;
} else if (signinItem.name === "Username") {
if (this.state.loginMethod === "wechat") {
return (<WeChatLoginPanel application={application} loginMethod={this.state.loginMethod} />);
}
return (
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
@@ -750,6 +753,9 @@ class LoginPage extends React.Component {
} else if (signinItem.name === "Agreement") {
return AgreementModal.isAgreementRequired(application) ? AgreementModal.renderAgreementFormItem(application, true, {}, this) : null;
} else if (signinItem.name === "Login button") {
if (this.state.loginMethod === "wechat") {
return null;
}
return (
<Form.Item key={resultItemKey} className="login-button-box">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
@@ -896,10 +902,6 @@ class LoginPage extends React.Component {
loginWidth += 10;
}
if (this.state.loginMethod === "wechat") {
return (<WeChatLoginPanel application={application} renderFormItem={this.renderFormItem.bind(this)} loginMethod={this.state.loginMethod} loginWidth={loginWidth} renderMethodChoiceBox={this.renderMethodChoiceBox.bind(this)} />);
}
return (
<Form
name="normal_login"
@@ -1109,8 +1111,7 @@ class LoginPage extends React.Component {
.then(res => res.json())
.then((credentialRequestOptions) => {
if ("status" in credentialRequestOptions) {
Setting.showMessage("error", credentialRequestOptions.msg);
throw credentialRequestOptions.status.msg;
return Promise.reject(new Error(credentialRequestOptions.msg));
}
credentialRequestOptions.publicKey.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.publicKey.challenge);
@@ -1169,7 +1170,7 @@ class LoginPage extends React.Component {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}${error}`);
});
}).catch(error => {
Setting.showMessage("error", `${error}`);
Setting.showMessage("error", `${error.message}`);
}).finally(() => {
this.setState({
loginLoading: false,
@@ -1240,6 +1241,7 @@ class LoginPage extends React.Component {
[generateItemKey("WebAuthn", "None"), {label: i18next.t("login:WebAuthn"), key: "webAuthn"}],
[generateItemKey("LDAP", "None"), {label: i18next.t("login:LDAP"), key: "ldap"}],
[generateItemKey("Face ID", "None"), {label: i18next.t("login:Face ID"), key: "faceId"}],
[generateItemKey("WeChat", "Tab"), {label: i18next.t("login:WeChat"), key: "wechat"}],
[generateItemKey("WeChat", "None"), {label: i18next.t("login:WeChat"), key: "wechat"}],
]);
@@ -1404,6 +1406,8 @@ class LoginPage extends React.Component {
);
}
const wechatSigninMethods = application.signinMethods?.filter(method => method.name === "WeChat" && method.rule === "Login page");
return (
<React.Fragment>
<CustomGithubCorner />
@@ -1421,6 +1425,15 @@ class LoginPage extends React.Component {
}
</div>
</div>
{
wechatSigninMethods?.length > 0 ? (<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<div>
<h3 style={{textAlign: "center", width: 320}}>{i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in")}</h3>
<WeChatLoginPanel application={application} loginMethod={this.state.loginMethod} />
</div>
</div>
) : null
}
</div>
</div>
</React.Fragment>

View File

@@ -78,13 +78,10 @@ class WeChatLoginPanel extends React.Component {
}
render() {
const {application, loginWidth = 320} = this.props;
const {loginWidth = 320} = this.props;
const {status, qrCode} = this.state;
return (
<div style={{width: loginWidth, margin: "0 auto", textAlign: "center", marginTop: 16}}>
{application.signinItems?.filter(item => item.name === "Logo").map(signinItem => this.props.renderFormItem(application, signinItem))}
{this.props.renderMethodChoiceBox()}
{application.signinItems?.filter(item => item.name === "Languages").map(signinItem => this.props.renderFormItem(application, signinItem))}
<div style={{marginTop: 2}}>
<QRCode style={{margin: "auto", marginTop: "20px", marginBottom: "20px"}} bordered={false} status={status} value={qrCode ?? " "} size={230} />
<div style={{marginTop: 8}}>

View File

@@ -96,6 +96,8 @@ class SigninMethodTable extends React.Component {
this.updateField(table, index, "displayName", value);
if (value === "Verification code" || value === "Password") {
this.updateField(table, index, "rule", "All");
} else if (value === "WeChat") {
this.updateField(table, index, "rule", "Tab");
} else {
this.updateField(table, index, "rule", "None");
}
@@ -139,6 +141,11 @@ class SigninMethodTable extends React.Component {
{id: "Non-LDAP", name: i18next.t("general:Non-LDAP")},
{id: "Hide password", name: i18next.t("general:Hide password")},
];
} else if (record.name === "WeChat") {
options = [
{id: "Tab", name: i18next.t("general:Tab")},
{id: "Login page", name: i18next.t("general:Login page")},
];
}
if (options.length === 0) {