mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-03 20:50:19 +08:00
feat: support LDAP (#160)
Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
This commit is contained in:
@ -27,6 +27,8 @@ import ProviderListPage from "./ProviderListPage";
|
||||
import ProviderEditPage from "./ProviderEditPage";
|
||||
import ApplicationListPage from "./ApplicationListPage";
|
||||
import ApplicationEditPage from "./ApplicationEditPage";
|
||||
import LdapEditPage from "./LdapEditPage";
|
||||
import LdapSyncPage from "./LdapSyncPage";
|
||||
import TokenListPage from "./TokenListPage";
|
||||
import TokenEditPage from "./TokenEditPage";
|
||||
import RecordListPage from "./RecordListPage";
|
||||
@ -327,7 +329,7 @@ class App extends Component {
|
||||
);
|
||||
}
|
||||
res.push(
|
||||
<Menu.Item key="6" onClick={() => window.location.href = "/swagger"}>
|
||||
<Menu.Item key="7" onClick={() => window.location.href = "/swagger"}>
|
||||
{i18next.t("general:Swagger")}
|
||||
</Menu.Item>
|
||||
);
|
||||
@ -399,6 +401,8 @@ class App extends Component {
|
||||
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/applications/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>
|
||||
|
237
web/src/LdapEditPage.js
Normal file
237
web/src/LdapEditPage.js
Normal file
@ -0,0 +1,237 @@
|
||||
// Copyright 2021 The casbin 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.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
|
||||
import {EyeInvisibleOutlined, EyeTwoTone} from "@ant-design/icons";
|
||||
import * as LddpBackend from "./backend/LdapBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import i18n from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class LdapEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ldapId: props.match.params.ldapId,
|
||||
ldap: null,
|
||||
organizations: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getLdap();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getLdap() {
|
||||
LddpBackend.getLdap(this.state.ldapId)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
ldap: res.data
|
||||
})
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateLdapField(key, value) {
|
||||
this.setState((prevState) => {
|
||||
prevState.ldap[key] = value;
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
renderAutoSyncWarn() {
|
||||
if (this.state.ldap.autoSync > 0) {
|
||||
return (
|
||||
<span style={{
|
||||
color: "#faad14",
|
||||
marginLeft: "20px"
|
||||
}}>{i18next.t("ldap:The Auto Sync option will sync all users to specify organization")}</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderLdap() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{i18next.t("ldap:Edit LDAP")}
|
||||
<Button type="primary" onClick={() => this.submitLdapEdit()}>{i18next.t("general:Save")}</Button>
|
||||
</div>
|
||||
} style={{marginLeft: "5px"}} type="inner">
|
||||
<Row style={{marginTop: "10px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)}
|
||||
value={this.state.ldap.owner} onChange={(value => {
|
||||
this.updateLdapField("owner", value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index}
|
||||
value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:ID"), i18next.t("general:ID - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Input value={this.state.ldap.id} disabled={true}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Server Name"), i18next.t("ldap:Server Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Input value={this.state.ldap.serverName} onChange={e => {
|
||||
this.updateLdapField("serverName", e.target.value);
|
||||
}}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Server Host"), i18next.t("ldap:Server Host - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Input value={this.state.ldap.host} onChange={e => {
|
||||
this.updateLdapField("host", e.target.value);
|
||||
}}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Server Port"), i18next.t("ldap:Server Port - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<InputNumber min={0} max={65535} formatter={value => value.replace(/\$\s?|(,*)/g, "")}
|
||||
value={this.state.ldap.port} onChange={value => {
|
||||
this.updateLdapField("port", value);
|
||||
}}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Input value={this.state.ldap.baseDn} onChange={e => {
|
||||
this.updateLdapField("baseDn", e.target.value);
|
||||
}}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Input value={this.state.ldap.admin} onChange={e => {
|
||||
this.updateLdapField("admin", e.target.value);
|
||||
}}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Admin Password"), i18next.t("ldap:Admin Password - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Input.Password
|
||||
iconRender={visible => (visible ? <EyeTwoTone/> : <EyeInvisibleOutlined/>)} value={this.state.ldap.passwd}
|
||||
onChange={e => {
|
||||
this.updateLdapField("passwd", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<InputNumber min={0} formatter={value => value.replace(/\$\s?|(,*)/g, "")} disabled={true}
|
||||
value={this.state.ldap.autoSync} onChange={value => {
|
||||
this.updateLdapField("autoSync", value);
|
||||
}}/><span> mins</span>
|
||||
{this.renderAutoSyncWarn()}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
submitLdapEdit() {
|
||||
LddpBackend.updateLdap(this.state.ldap)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", `Update LDAP server success`);
|
||||
this.setState((prevState) => {
|
||||
prevState.ldap = res.data2;
|
||||
})
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Update LDAP server failed: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.state.ldap !== null ? this.renderLdap() : null
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{margin: 10}}>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Button type="primary" size="large"
|
||||
onClick={() => this.submitLdapEdit()}>{i18next.t("general:Save")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LdapEditPage;
|
193
web/src/LdapListPage.js
Normal file
193
web/src/LdapListPage.js
Normal file
@ -0,0 +1,193 @@
|
||||
// Copyright 2021 The casbin 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.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Col, Popconfirm, Row, Table} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import * as LdapBackend from "./backend/LdapBackend";
|
||||
import i18next from "i18next";
|
||||
|
||||
class LdapListPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ldaps: null
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getLdaps()
|
||||
}
|
||||
|
||||
getLdaps() {
|
||||
LdapBackend.getLdaps("")
|
||||
.then((res) => {
|
||||
let ldapsData = [];
|
||||
if (res.status === "ok") {
|
||||
ldapsData = res.data;
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
this.setState((prevState) => {
|
||||
prevState.ldaps = ldapsData;
|
||||
return prevState;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
deleteLdap(index) {
|
||||
|
||||
}
|
||||
|
||||
renderTable(ldaps) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("ldap:Server Name"),
|
||||
dataIndex: "serverName",
|
||||
key: "serverName",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.serverName.localeCompare(b.serverName),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/ldaps/${record.id}`}>
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "140px",
|
||||
sorter: (a, b) => a.owner.localeCompare(b.owner),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Server"),
|
||||
dataIndex: "host",
|
||||
key: "host",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.host.localeCompare(b.host),
|
||||
render: (text, record, index) => {
|
||||
return `${text}:${record.port}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Base DN"),
|
||||
dataIndex: "baseDn",
|
||||
key: "baseDn",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.baseDn.localeCompare(b.baseDn),
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Admin"),
|
||||
dataIndex: "admin",
|
||||
key: "admin",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.admin.localeCompare(b.admin),
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Auto Sync"),
|
||||
dataIndex: "autoSync",
|
||||
key: "autoSync",
|
||||
width: "100px",
|
||||
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
|
||||
render: (text, record, index) => {
|
||||
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
|
||||
<span style={{color: "#52c41a"}}>{text + " mins"}</span>)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Last Sync"),
|
||||
dataIndex: "lastSync",
|
||||
key: "lastSync",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
|
||||
render: (text, record, index) => {
|
||||
return text
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "240px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
|
||||
type="primary"
|
||||
onClick={() => Setting.goToLink(`/ldap/sync/${record.id}`)}>{i18next.t("ldap:Sync")}</Button>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
|
||||
onClick={() => Setting.goToLink(`/ldap/${record.id}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete LDAP Config: ${record.serverName} ?`}
|
||||
onConfirm={() => this.deleteLdap(index)}
|
||||
>
|
||||
<Button style={{marginBottom: "10px"}}
|
||||
type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table columns={columns} dataSource={ldaps} rowKey="id" size="middle" bordered
|
||||
pagination={{pageSize: 100}}
|
||||
title={() => (
|
||||
<div>
|
||||
<span>{i18next.t("general:LDAPs")}</span>
|
||||
<Button type="primary" size="small" style={{marginLeft: "10px"}}
|
||||
onClick={() => {
|
||||
this.addLdap()
|
||||
}}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={ldaps === null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.renderTable(this.state.ldaps)
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LdapListPage;
|
253
web/src/LdapSyncPage.js
Normal file
253
web/src/LdapSyncPage.js
Normal file
@ -0,0 +1,253 @@
|
||||
// Copyright 2021 The casbin 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.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Col, Row, Table, Popconfirm} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import * as LdapBackend from "./backend/LdapBackend";
|
||||
import i18next from "i18next";
|
||||
|
||||
class LdapSyncPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ldapId: props.match.params.ldapId,
|
||||
ldap: null,
|
||||
users: [],
|
||||
existUuids: [],
|
||||
selectedUsers: []
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getLdap()
|
||||
}
|
||||
|
||||
syncUsers() {
|
||||
let selectedUsers = this.state.selectedUsers;
|
||||
if (selectedUsers === null || selectedUsers.length === 0) {
|
||||
Setting.showMessage("error", "Please select al least 1 user first");
|
||||
return
|
||||
}
|
||||
|
||||
LdapBackend.syncUsers(this.state.ldap.owner, this.state.ldap.id, selectedUsers)
|
||||
.then((res => {
|
||||
if (res.status === "ok") {
|
||||
let exist = res.data.exist;
|
||||
let failed = res.data.failed;
|
||||
let existUser = [];
|
||||
let failedUser = [];
|
||||
|
||||
if ((!exist || exist.length === 0) && (!failed || failed.length === 0)) {
|
||||
Setting.goToLink(`/organizations/${this.state.ldap.owner}/users`);
|
||||
} else {
|
||||
if (exist && exist.length > 0) {
|
||||
exist.forEach(elem => {
|
||||
existUser.push(elem.cn);
|
||||
});
|
||||
Setting.showMessage("error", `User [${existUser}] is already exist`);
|
||||
}
|
||||
|
||||
if (failed && failed.length > 0) {
|
||||
failed.forEach(elem => {
|
||||
failedUser.push(elem.cn);
|
||||
})
|
||||
Setting.showMessage("error", `Sync [${failedUser}] failed`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
getLdap() {
|
||||
LdapBackend.getLdap(this.state.ldapId)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState((prevState) => {
|
||||
prevState.ldap = res.data;
|
||||
return prevState;
|
||||
})
|
||||
this.getLdapUser(res.data);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getLdapUser(ldap) {
|
||||
LdapBackend.getLdapUser(ldap)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState((prevState) => {
|
||||
prevState.users = res.data.users;
|
||||
return prevState;
|
||||
})
|
||||
this.getExistUsers(ldap.owner, res.data.users);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getExistUsers(owner, users) {
|
||||
let uuidArray = [];
|
||||
users.forEach(elem => {
|
||||
uuidArray.push(elem.uuid);
|
||||
})
|
||||
LdapBackend.checkLdapUsersExist(owner, uuidArray)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState(prevState => {
|
||||
prevState.existUuids = res.data?.length > 0 ? res.data : [];
|
||||
return prevState;
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buildValArray(data, key) {
|
||||
let valTypesArray = [];
|
||||
|
||||
if (data !== null && data.length > 0) {
|
||||
data.forEach(elem => {
|
||||
let val = elem[key];
|
||||
if (!valTypesArray.includes(val)) {
|
||||
valTypesArray.push(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
return valTypesArray;
|
||||
}
|
||||
|
||||
buildFilter(data, key) {
|
||||
let filterArray = [];
|
||||
|
||||
if (data !== null && data.length > 0) {
|
||||
let valArray = this.buildValArray(data, key)
|
||||
valArray.forEach(elem => {
|
||||
filterArray.push({
|
||||
text: elem,
|
||||
value: elem,
|
||||
});
|
||||
});
|
||||
}
|
||||
return filterArray;
|
||||
}
|
||||
|
||||
renderTable(users) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("ldap:CN"),
|
||||
dataIndex: "cn",
|
||||
key: "cn",
|
||||
sorter: (a, b) => a.cn.localeCompare(b.cn),
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:UidNumber / Uid"),
|
||||
dataIndex: "uidNumber",
|
||||
key: "uidNumber",
|
||||
width: "200px",
|
||||
sorter: (a, b) => a.uidNumber.localeCompare(b.uidNumber),
|
||||
render: (text, record, index) => {
|
||||
return `${text} / ${record.uid}`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Group Id"),
|
||||
dataIndex: "groupId",
|
||||
key: "groupId",
|
||||
width: "140px",
|
||||
sorter: (a, b) => a.groupId.localeCompare(b.groupId),
|
||||
filters: this.buildFilter(this.state.users, "groupId"),
|
||||
onFilter: (value, record) => record.groupId.indexOf(value) === 0,
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Email"),
|
||||
dataIndex: "email",
|
||||
key: "email",
|
||||
width: "240px",
|
||||
sorter: (a, b) => a.email.localeCompare(b.email),
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Phone"),
|
||||
dataIndex: "phone",
|
||||
key: "phone",
|
||||
width: "160px",
|
||||
sorter: (a, b) => a.phone.localeCompare(b.phone),
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Address"),
|
||||
dataIndex: "address",
|
||||
key: "address",
|
||||
sorter: (a, b) => a.address.localeCompare(b.address),
|
||||
},
|
||||
];
|
||||
|
||||
const rowSelection = {
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
this.setState(prevState => {
|
||||
prevState.selectedUsers = selectedRows;
|
||||
return prevState;
|
||||
})
|
||||
},
|
||||
getCheckboxProps: record => ({
|
||||
disabled: this.state.existUuids.indexOf(record.uuid) !== -1,
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table rowSelection={rowSelection} columns={columns} dataSource={users} rowKey="uuid" bordered
|
||||
pagination={{pageSize: 100}}
|
||||
title={() => (
|
||||
<div>
|
||||
<span>{this.state.ldap?.serverName}</span>
|
||||
<Popconfirm placement={"right"}
|
||||
title={`Please confirm to sync selected users`}
|
||||
onConfirm={() => this.syncUsers()}
|
||||
>
|
||||
<Button type="primary" size="small"
|
||||
style={{marginLeft: "10px"}}>{i18next.t("ldap:Sync")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
)}
|
||||
loading={users === null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{width: "100%"}}>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{
|
||||
this.renderTable(this.state.users)
|
||||
}
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LdapSyncPage;
|
201
web/src/LdapTable.js
Normal file
201
web/src/LdapTable.js
Normal file
@ -0,0 +1,201 @@
|
||||
// Copyright 2021 The casbin 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.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Col, Popconfirm, Row, Table} from 'antd';
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as LdapBackend from "./backend/LdapBackend";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
class LdapTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
};
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
newLdap() {
|
||||
return {
|
||||
id: "",
|
||||
owner: this.props.organizationName,
|
||||
createdTime: "",
|
||||
serverName: "Example LDAP Server",
|
||||
host: "example.com",
|
||||
port: 389,
|
||||
admin: "cn=admin,dc=example,dc=com",
|
||||
passwd: "123",
|
||||
baseDn: "ou=People,dc=example,dc=com",
|
||||
autosync: 0,
|
||||
lastSync: ""
|
||||
}
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const newLdap = this.newLdap();
|
||||
LdapBackend.addLdap(newLdap)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", `Add LDAP server success`);
|
||||
if (table === undefined) {
|
||||
table = [];
|
||||
}
|
||||
table = Setting.addRow(table, res.data2);
|
||||
this.updateTable(table);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Add LDAP server failed: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteRow(table, i) {
|
||||
LdapBackend.deleteLdap(table[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", `Delete LDAP server success`);
|
||||
table = Setting.deleteRow(table, i);
|
||||
this.updateTable(table);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Delete LDAP server failed: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("ldap:Server Name"),
|
||||
dataIndex: "serverName",
|
||||
key: "serverName",
|
||||
width: "160px",
|
||||
sorter: (a, b) => a.serverName.localeCompare(b.serverName),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/ldaps/${record.id}`}>
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Server"),
|
||||
dataIndex: "host",
|
||||
key: "host",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.host.localeCompare(b.host),
|
||||
render: (text, record, index) => {
|
||||
return `${text}:${record.port}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Base DN"),
|
||||
dataIndex: "baseDn",
|
||||
key: "baseDn",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.baseDn.localeCompare(b.baseDn),
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Auto Sync"),
|
||||
dataIndex: "autoSync",
|
||||
key: "autoSync",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
|
||||
render: (text, record, index) => {
|
||||
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
|
||||
<span style={{color: "#52c41a"}}>{text + " mins"}</span>)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("ldap:Last Sync"),
|
||||
dataIndex: "lastSync",
|
||||
key: "lastSync",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
|
||||
render: (text, record, index) => {
|
||||
return text
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "240px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
|
||||
type="primary"
|
||||
onClick={() => Setting.goToLink(`/ldap/sync/${record.id}`)}>{i18next.t("ldap:Sync")}</Button>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
|
||||
onClick={() => Setting.goToLink(`/ldap/${record.id}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete LDAP Config: ${record.serverName} ?`}
|
||||
onConfirm={() => this.deleteRow(table, index)}
|
||||
>
|
||||
<Button style={{marginBottom: "10px"}}
|
||||
type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table rowKey="id" columns={columns} dataSource={table} size="middle" bordered pagination={false}
|
||||
title={() => (
|
||||
<div>
|
||||
{this.props.title}
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small"
|
||||
onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{marginTop: '20px'}}>
|
||||
<Col span={24}>
|
||||
{
|
||||
this.renderTable(this.props.table)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LdapTable;
|
@ -15,9 +15,11 @@
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Row, Select} from 'antd';
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as LdapBackend from "./backend/LdapBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
import LdapTable from "./LdapTable";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@ -28,11 +30,13 @@ class OrganizationEditPage extends React.Component {
|
||||
classes: props,
|
||||
organizationName: props.match.params.organizationName,
|
||||
organization: null,
|
||||
ldaps: null,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrganization();
|
||||
this.getLdaps();
|
||||
}
|
||||
|
||||
getOrganization() {
|
||||
@ -44,6 +48,21 @@ class OrganizationEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getLdaps() {
|
||||
LdapBackend.getLdaps(this.state.organizationName)
|
||||
.then(res => {
|
||||
let resdata = []
|
||||
if (res.status === "ok") {
|
||||
if (res.data !== null) {
|
||||
resdata = res.data;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
ldaps: resdata
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
parseOrganizationField(key, value) {
|
||||
// if ([].includes(key)) {
|
||||
// value = Setting.myParseInt(value);
|
||||
@ -186,6 +205,20 @@ class OrganizationEditPage extends React.Component {
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}}>
|
||||
<Col style={{marginTop: '5px'}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<LdapTable
|
||||
title={i18next.t("general:LDAPs")}
|
||||
table={this.state.ldaps}
|
||||
organizationName={this.state.organizationName}
|
||||
onUpdateTable={(value) => {
|
||||
this.setState({ldaps: value}) }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@ -228,7 +261,8 @@ class OrganizationEditPage extends React.Component {
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Button type="primary" size="large" onClick={this.submitOrganizationEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
<Button type="primary" size="large"
|
||||
onClick={this.submitOrganizationEdit.bind(this)}>{i18next.t("general:Save")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
77
web/src/backend/LdapBackend.js
Normal file
77
web/src/backend/LdapBackend.js
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2021 The casbin 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.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getLdaps(owner) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-ldaps?owner=${owner}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getLdap(id) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-ldap?id=${id}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addLdap(body) {
|
||||
return fetch(`${Setting.ServerUrl}/api/add-ldap`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteLdap(body) {
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-ldap`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateLdap(body) {
|
||||
return fetch(`${Setting.ServerUrl}/api/update-ldap`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getLdapUser(body) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-ldap-user`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function syncUsers(owner, ldapId, body) {
|
||||
return fetch(`${Setting.ServerUrl}/api/sync-ldap-users?owner=${owner}&ldapId=${ldapId}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function checkLdapUsersExist(owner, body) {
|
||||
return fetch(`${Setting.ServerUrl}/api/check-ldap-users-exist?owner=${owner}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body),
|
||||
}).then(res => res.json());
|
||||
}
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2021 The casbin 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.
|
||||
|
||||
import React from "react";
|
||||
import {Cascader, Col, Input, Row, Select} from 'antd';
|
||||
import i18next from "i18next";
|
||||
|
@ -18,6 +18,7 @@
|
||||
"Timestamp": "Timestamp",
|
||||
"Username": "Username",
|
||||
"Request uri": "Request uri",
|
||||
"LDAPs": "LDAPs",
|
||||
"Save": "Save",
|
||||
"Add": "Add",
|
||||
"Action": "Action",
|
||||
@ -282,5 +283,29 @@
|
||||
"Email/Phone": "Email/Phone",
|
||||
"Change Password": "Change Password",
|
||||
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
|
||||
},
|
||||
"ldap":
|
||||
{
|
||||
"Server Name": "Server Name",
|
||||
"Host": "Host",
|
||||
"Server": "Server",
|
||||
"Base DN": "Base DN",
|
||||
"Admin": "Admin",
|
||||
"Admin Password": "Admin Password",
|
||||
"Auto Sync": "Auto Sync",
|
||||
"Last Sync": "Last Sync",
|
||||
"Sync": "Sync",
|
||||
"ID": "ID",
|
||||
"Server Host": "Server Host",
|
||||
"Server Port": "Server Port",
|
||||
"Edit LDAP": "Edit LDAP",
|
||||
"Sync users": "Sync users",
|
||||
"Server Name - Tooltip": "LDAP server config display name",
|
||||
"Server Host - Tooltip": "LDAP server host",
|
||||
"Server Port - Tooltip": "LDAP server port",
|
||||
"Base DN - Tooltip": "LDAP search base DN",
|
||||
"Admin - Tooltip": "LDAP server admin CN or ID",
|
||||
"Admin Password - Tooltip": "LDAP server admin password",
|
||||
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
"Timestamp": "Timestamp",
|
||||
"Username": "Username",
|
||||
"Request uri": "Request uri",
|
||||
"LDAPs": "LDAPs",
|
||||
"Save": "保存",
|
||||
"Add": "添加",
|
||||
"Action": "操作",
|
||||
@ -272,5 +273,28 @@
|
||||
"Email/Phone": "邮箱/手机号",
|
||||
"Change Password": "修改密码",
|
||||
"Choose email verification or mobile verification": "选择邮箱验证或手机验证"
|
||||
},
|
||||
"ldap": {
|
||||
"Server Name": "LDAP 服务器",
|
||||
"Host": "域名",
|
||||
"Server": "服务器",
|
||||
"Base DN": "基本 DN",
|
||||
"Admin": "管理员",
|
||||
"Admin Password": "密码",
|
||||
"Auto Sync": "自动同步",
|
||||
"Last Sync": "最近同步",
|
||||
"Sync": "同步",
|
||||
"ID": "ID",
|
||||
"Server Host": "域名",
|
||||
"Server Port": "端口",
|
||||
"Edit LDAP": "编辑 LDAP",
|
||||
"Sync users": "同步用户",
|
||||
"Server Name - Tooltip": "LDAP 服务器配置显示名称",
|
||||
"Server Host - Tooltip": "LDAP 服务器地址",
|
||||
"Server Port - Tooltip": "LDAP 服务器端口号",
|
||||
"Base DN - Tooltip": "LDAP 搜索时的基本 DN",
|
||||
"Admin - Tooltip": "LDAP 服务器管理员的 CN 或 ID",
|
||||
"Admin Password - Tooltip": "LDAP 服务器管理员密码",
|
||||
"Auto Sync - Tooltip": "自动同步配置,为 0 时禁用"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user