Compare commits

...

6 Commits

9 changed files with 242 additions and 53 deletions

View File

@@ -51,22 +51,14 @@ COPY --from=FRONT --chown=$USER:$USER /web/build ./web/build
ENTRYPOINT ["/server"]
FROM debian:latest AS db
RUN apt update \
&& apt install -y \
mariadb-server \
mariadb-client \
&& rm -rf /var/lib/apt/lists/*
FROM db AS ALLINONE
FROM debian:latest AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/"
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates
RUN apt install -y ca-certificates lsof && update-ca-certificates
WORKDIR /
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server

View File

@@ -1,8 +1,10 @@
#!/bin/bash
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ;fi
service mariadb start
if [ -z "${driverName:-}" ]; then
export driverName=sqlite
fi
if [ -z "${dataSourceName:-}" ]; then
export dataSourceName="file:casdoor.db?cache=shared"
fi
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
exec /server --createDatabase=true
exec /server

View File

@@ -216,6 +216,16 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
e.AddAttribute("sn", message.AttributeValue(user.LastName))
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
// Add POSIX attributes for Linux machine login support
e.AddAttribute("loginShell", getAttribute("loginShell", user))
e.AddAttribute("gecos", getAttribute("gecos", user))
// Add SSH public key if available
sshKey := getAttribute("sshPublicKey", user)
if sshKey != "" {
e.AddAttribute("sshPublicKey", sshKey)
}
// Add objectClass for posixAccount
e.AddAttribute("objectClass", "posixAccount")
for _, group := range user.Groups {
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
}

View File

@@ -83,6 +83,45 @@ var ldapAttributesMapping = map[string]FieldRelation{
return message.AttributeValue(getUserPasswordWithType(user))
},
},
"loginShell": {
userField: "loginShell",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Check user properties first, otherwise return default shell
if user.Properties != nil {
if shell, ok := user.Properties["loginShell"]; ok && shell != "" {
return message.AttributeValue(shell)
}
}
return message.AttributeValue("/bin/bash")
},
},
"gecos": {
userField: "gecos",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// GECOS field typically contains full name and other user info
// Format: Full Name,Room Number,Work Phone,Home Phone,Other
gecos := user.DisplayName
if gecos == "" {
gecos = user.Name
}
return message.AttributeValue(gecos)
},
},
"sshPublicKey": {
userField: "sshPublicKey",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Return SSH public key from user properties
if user.Properties != nil {
if sshKey, ok := user.Properties["sshPublicKey"]; ok && sshKey != "" {
return message.AttributeValue(sshKey)
}
}
return message.AttributeValue("")
},
},
}
const ldapMemberOfAttr = "memberOf"

View File

@@ -32,6 +32,7 @@ type AccountItem struct {
ViewRule string `json:"viewRule"`
ModifyRule string `json:"modifyRule"`
Regex string `json:"regex"`
Tab string `json:"tab"`
}
type ThemeData struct {
@@ -88,6 +89,7 @@ type Organization struct {
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberInHours int `json:"mfaRememberInHours"`
AccountMenu string `xorm:"varchar(20)" json:"accountMenu"`
AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
OrgBalance float64 `json:"orgBalance"`

View File

@@ -918,6 +918,8 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
err = setReflectAttr[[]MfaAccount](&fv, v)
case reflect.TypeOf([]webauthn.Credential{}):
err = setReflectAttr[[]webauthn.Credential](&fv, v)
case reflect.TypeOf(map[string]string{}):
err = setReflectAttr[map[string]string](&fv, v)
}
if err != nil {

View File

@@ -678,6 +678,16 @@ class OrganizationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account menu"), i18next.t("organization:Account menu - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.accountMenu || "Horizontal"} onChange={(value => {this.updateOrganizationField("accountMenu", value);})}
options={[{value: "Horizontal", label: i18next.t("general:Horizontal")}, {value: "Vertical", label: i18next.t("general:Vertical")}].map(item => Setting.getOption(item.label, item.value))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

View File

@@ -13,7 +13,10 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag, Tooltip} from "antd";
import {
Button, Card, Col, Form, Input, InputNumber, Layout, List,
Menu, Result, Row, Select, Space, Spin, Switch, Tabs, Tag, Tooltip
} from "antd";
import {withRouter} from "react-router-dom";
import {TotpMfaType} from "./auth/MfaSetupPage";
import * as GroupBackend from "./backend/GroupBackend";
@@ -46,6 +49,8 @@ import MfaAccountTable from "./table/MfaAccountTable";
import MfaTable from "./table/MfaTable";
import TransactionTable from "./table/TransactionTable";
import * as TransactionBackend from "./backend/TransactionBackend";
import {Content, Header} from "antd/es/layout/layout";
import Sider from "antd/es/layout/Sider";
const {Option} = Select;
@@ -67,6 +72,8 @@ class UserEditPage extends React.Component {
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
openFaceRecognitionModal: false,
transactions: [],
activeMenuKey: window.location.hash?.slice(1) || "",
menuMode: "Horizontal",
};
}
@@ -175,6 +182,7 @@ class UserEditPage extends React.Component {
}
this.setState({
menuMode: res.data?.organizationObj?.accountMenu ?? "Horizontal",
application: res.data,
});
});
@@ -1333,6 +1341,152 @@ class UserEditPage extends React.Component {
);
}
isAccountItemVisible(item) {
if (!item.visible) {
return false;
}
const isAdmin = Setting.isLocalAdminUser(this.props.account);
if (item.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {
return false;
}
} else if (item.viewRule === "Admin") {
if (!isAdmin) {
return false;
}
}
return true;
}
getAccountItemsByTab(tab) {
const accountItems = this.getUserOrganization()?.accountItems || [];
return accountItems.filter(item => {
if (!this.isAccountItemVisible(item)) {
return false;
}
const itemTab = item.tab || "";
return itemTab === tab;
});
}
getUniqueTabs() {
const accountItems = this.getUserOrganization()?.accountItems || [];
const tabs = new Set();
accountItems.forEach(item => {
if (this.isAccountItemVisible(item)) {
tabs.add(item.tab || "");
}
});
return Array.from(tabs).sort((a, b) => {
// Empty string (default tab) comes first
if (a === "") {
return -1;
}
if (b === "") {
return 1;
}
return a.localeCompare(b);
});
}
renderUserForm() {
const tabs = this.getUniqueTabs();
// If there are no tabs or only one tab (default), render without tab navigation
if (tabs.length === 0 || (tabs.length === 1 && tabs[0] === "")) {
const accountItems = this.getAccountItemsByTab("");
return (
<Form>
{accountItems.map(accountItem => (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
))}
</Form>
);
}
// Render with tabs
const activeKey = this.state.activeMenuKey || tabs[0] || "";
return (
<Layout style={{background: "inherit"}}>
{
this.state.menuMode === "Vertical" ? null : (
<Header style={{background: "inherit", padding: "0px"}}>
<Tabs
onChange={(key) => {
this.setState({activeMenuKey: key});
window.location.hash = key;
}}
type="card"
activeKey={activeKey}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
key: tab,
}))}
/>
</Header>
)
}
<Layout style={{background: "inherit", maxHeight: "70vh", overflow: "auto"}}>
{
this.state.menuMode === "Vertical" ? (
<Sider width={200} style={{background: "inherit", position: "sticky", top: 0}}>
<Menu
mode="vertical"
selectedKeys={[activeKey]}
onClick={({key}) => {
this.setState({activeMenuKey: key});
window.location.hash = key;
}}
style={{marginBottom: "20px", height: "100%"}}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
key: tab,
}))}
/>
</Sider>) : null
}
<Content style={{padding: "15px"}}>
<Form>
{this.getAccountItemsByTab(activeKey).map(accountItem => (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
))}
</Form>
</Content>
</Layout>
</Layout>
);
}
renderUser() {
return (
<div>
@@ -1346,42 +1500,7 @@ class UserEditPage extends React.Component {
</div>
)
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Form>
{
this.getUserOrganization()?.accountItems?.map(accountItem => {
if (!accountItem.visible) {
return null;
}
const isAdmin = Setting.isLocalAdminUser(this.props.account);
if (accountItem.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {
return null;
}
} else if (accountItem.viewRule === "Admin") {
if (!isAdmin) {
return null;
}
}
return (
<React.Fragment key={accountItem.name}>
<Form.Item name={accountItem.name}
validateTrigger="onChange"
rules={[
{
pattern: accountItem.regex ? new RegExp(accountItem.regex, "g") : null,
message: i18next.t("user:This field value doesn't match the pattern rule"),
},
]}
style={{margin: 0}}>
{this.renderAccountItem(accountItem)}
</Form.Item>
</React.Fragment>
);
})
}
</Form>
{this.renderUserForm()}
</Card>
</div>
);

View File

@@ -38,7 +38,7 @@ class AccountTable extends React.Component {
}
addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true, viewRule: "Public", modifyRule: "Self"};
const row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true, viewRule: "Public", modifyRule: "Self", tab: ""};
if (table === undefined) {
table = [];
}
@@ -93,6 +93,19 @@ class AccountTable extends React.Component {
);
},
},
{
title: i18next.t("general:Tab"),
dataIndex: "tab",
key: "tab",
width: "150px",
render: (text, record, index) => {
return (
<Input value={text} placeholder={i18next.t("user:Default")} onChange={e => {
this.updateField(table, index, "tab", e.target.value);
}} />
);
},
},
{
title: i18next.t("signup:Regex"),
dataIndex: "regex",