forked from casdoor/casdoor
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aba471b4e8 | ||
|
|
72b70c3b03 | ||
|
|
a1c56894c7 | ||
|
|
a9ae9394c7 | ||
|
|
5f0fa5f23e | ||
|
|
f99aa047a9 |
12
Dockerfile
12
Dockerfile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
39
ldap/util.go
39
ldap/util.go
@@ -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"
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))} :
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user