Merge "feat: support web SSO login"
This commit is contained in:
commit
f560965348
@ -0,0 +1,13 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Support SSO login via OpenID connect.
|
||||
|
||||
1. If Web SSO login is successfully configured on the back-end, you can
|
||||
select a login mode on the login page based on the configuration. The
|
||||
default login mode is Keystone authentication. If you select the Web SSO
|
||||
login mode, click Login button will switch to the Web SSO configuration page.
|
||||
|
||||
2. If SSO is not configured on the back-end, the front-end will directly
|
||||
display the user + password login mode (Keystone authentication). You do
|
||||
not need to select the login mode.
|
@ -126,6 +126,9 @@ export class SkylineClient extends Base {
|
||||
name: 'queryRange',
|
||||
key: 'query_range',
|
||||
},
|
||||
{
|
||||
key: 'sso',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -65,13 +65,23 @@ export default class index extends Component {
|
||||
const { formItems } = this.props;
|
||||
// eslint-disable-next-line no-shadow
|
||||
return formItems.map((it, index) => {
|
||||
const { name, hidden, dependencies = [], className, onChange } = it;
|
||||
const {
|
||||
name,
|
||||
hidden,
|
||||
dependencies = [],
|
||||
className,
|
||||
onChange,
|
||||
extra,
|
||||
label,
|
||||
} = it;
|
||||
const options = {
|
||||
name,
|
||||
rules: this.getFormItemRules(it),
|
||||
hidden,
|
||||
dependencies,
|
||||
className,
|
||||
extra,
|
||||
label,
|
||||
};
|
||||
if (onChange) {
|
||||
options.onChange = onChange;
|
||||
|
@ -1131,6 +1131,7 @@
|
||||
"If the capacity of the disk is large,the type modify operation may takes several hours. Please be cautious.": "If the capacity of the disk is large,the type modify operation may takes several hours. Please be cautious.",
|
||||
"If the volume associated with the snapshot has changed the volume type, please modify this option manually; if the volume associated with the snapshot keeps the volume type unchanged, please ignore this option. (no need to change).": "If the volume associated with the snapshot has changed the volume type, please modify this option manually; if the volume associated with the snapshot keeps the volume type unchanged, please ignore this option. (no need to change).",
|
||||
"If you are not authorized to access any project, or if the project you are involved in has been deleted or disabled, contact the platform administrator to reassign the project": "If you are not authorized to access any project, or if the project you are involved in has been deleted or disabled, contact the platform administrator to reassign the project",
|
||||
"If you are not sure which authentication method to use, please contact your administrator.": "If you are not sure which authentication method to use, please contact your administrator.",
|
||||
"If you choose a port which subnet is different from the subnet of LB, please ensure connectivity between the two.": "If you choose a port which subnet is different from the subnet of LB, please ensure connectivity between the two.",
|
||||
"If you do not fill in parameters such as cpus, memory_mb, local_gb, cpu_arch, etc., you can automatically inject the configuration and Mac address of the physical machine by performing the \"Auto Inspect\" operation.": "If you do not fill in parameters such as cpus, memory_mb, local_gb, cpu_arch, etc., you can automatically inject the configuration and Mac address of the physical machine by performing the \"Auto Inspect\" operation.",
|
||||
"If you still want to keep the disk data, it is recommended that you create a snapshot for the disk before deleting.": "If you still want to keep the disk data, it is recommended that you create a snapshot for the disk before deleting.",
|
||||
@ -1304,6 +1305,7 @@
|
||||
"Keypair": "Keypair",
|
||||
"Keypair Detail": "Keypair Detail",
|
||||
"Keypair Info": "Keypair Info",
|
||||
"Keystone Credentials": "Keystone Credentials",
|
||||
"Keystone token is expired.": "token has expired, please check whether the server time is correct and confirm whether the token is valid",
|
||||
"Kill": "Kill",
|
||||
"Kill Container": "Kill Container",
|
||||
@ -1613,6 +1615,7 @@
|
||||
"Only a MAC address or an OpenFlow based datapath_id of the switch are accepted in this field": "Only a MAC address or an OpenFlow based datapath_id of the switch are accepted in this field",
|
||||
"Only subnets that are already connected to the router can be selected.": "Only subnets that are already connected to the router can be selected.",
|
||||
"Open External Gateway": "Open External Gateway",
|
||||
"OpenID Connect": "OpenID Connect",
|
||||
"OpenStack Services": "OpenStack Services",
|
||||
"Operating Status": "Operating Status",
|
||||
"Operating System": "Operating System",
|
||||
@ -1756,6 +1759,7 @@
|
||||
"Please select availability zone": "Please select availability zone",
|
||||
"Please select item!": "Please select item!",
|
||||
"Please select key": "Please select key",
|
||||
"Please select login type!": "Please select login type!",
|
||||
"Please select policy": "Please select policy",
|
||||
"Please select source": "Please select source",
|
||||
"Please select type": "Please select type",
|
||||
@ -2037,6 +2041,7 @@
|
||||
"Select User Group": "Select User Group",
|
||||
"Select Volume Snapshot": "Select Volume Snapshot",
|
||||
"Select a domain": "Select a domain",
|
||||
"Select a login type": "Select a login type",
|
||||
"Select a region": "Select a region",
|
||||
"Selected": "Selected",
|
||||
"Selected Members": "Selected Members",
|
||||
|
@ -1131,6 +1131,7 @@
|
||||
"If the capacity of the disk is large,the type modify operation may takes several hours. Please be cautious.": "如果云硬盘容量较大,修改云硬盘类型可能需要花费几个小时,请您谨慎操作。",
|
||||
"If the volume associated with the snapshot has changed the volume type, please modify this option manually; if the volume associated with the snapshot keeps the volume type unchanged, please ignore this option. (no need to change).": "若快照关联的云硬盘修改过云硬盘类型,请手动修改此选项;若快照关联的云硬盘保持云硬盘类型不变,请忽略此选项(不需要做变更)。",
|
||||
"If you are not authorized to access any project, or if the project you are involved in has been deleted or disabled, contact the platform administrator to reassign the project": "您未被授权访问任何项目,或您参与中的项目已被删除或禁用,可联系平台管理员重新分配项目",
|
||||
"If you are not sure which authentication method to use, please contact your administrator.": "如果您不确定使用哪种认证方式,请联系管理员。",
|
||||
"If you choose a port which subnet is different from the subnet of LB, please ensure connectivity between the two.": "如果你选择了和LB子网不同的网卡,请确保两者的连通性。",
|
||||
"If you do not fill in parameters such as cpus, memory_mb, local_gb, cpu_arch, etc., you can automatically inject the configuration and Mac address of the physical machine by performing the \"Auto Inspect\" operation.": "如不填写cpus、memory_mb、local_gb、cpu_arch等参数,您可以通过执行“自动检测”操作来自动注入物理机的配置和 Mac 地址。",
|
||||
"If you still want to keep the disk data, it is recommended that you create a snapshot for the disk before deleting.": "如果您仍想保留云硬盘数据,建议您在删除之前为云硬盘创建快照。",
|
||||
@ -1304,6 +1305,7 @@
|
||||
"Keypair": "SSH密钥对",
|
||||
"Keypair Detail": "密钥详情",
|
||||
"Keypair Info": "密钥信息",
|
||||
"Keystone Credentials": "Keystone认证",
|
||||
"Keystone token is expired.": "token已过期,请检查服务器时间是否正确,确认token是否有效",
|
||||
"Kill": "终止",
|
||||
"Kill Container": "终止容器",
|
||||
@ -1613,6 +1615,7 @@
|
||||
"Only a MAC address or an OpenFlow based datapath_id of the switch are accepted in this field": "只可填写交换机的Mac地址或者交换机基于openflow的数据路径ID",
|
||||
"Only subnets that are already connected to the router can be selected.": "仅可选择已经连接过路由器的子网。",
|
||||
"Open External Gateway": "开启公网网关",
|
||||
"OpenID Connect": "OpenID连接",
|
||||
"OpenStack Services": "OpenStack服务",
|
||||
"Operating Status": "操作状态",
|
||||
"Operating System": "操作系统",
|
||||
@ -1756,6 +1759,7 @@
|
||||
"Please select availability zone": "请选择可用域",
|
||||
"Please select item!": "请选择一个条目!",
|
||||
"Please select key": "请选择一个键",
|
||||
"Please select login type!": "请选择登录方式!",
|
||||
"Please select policy": "请选择一个策略",
|
||||
"Please select source": "请选择源",
|
||||
"Please select type": "请选择类型",
|
||||
@ -2037,6 +2041,7 @@
|
||||
"Select User Group": "选择用户组",
|
||||
"Select Volume Snapshot": "选择云硬盘快照",
|
||||
"Select a domain": "请选择Domain",
|
||||
"Select a login type": "请选择登录方式",
|
||||
"Select a region": "请选择Region",
|
||||
"Selected": "已选",
|
||||
"Selected Members": "已选择成员",
|
||||
|
@ -31,12 +31,14 @@ export class Login extends Component {
|
||||
error: false,
|
||||
message: '',
|
||||
loading: false,
|
||||
loginTypeOption: this.passwordOption,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getDomains();
|
||||
this.getRegions();
|
||||
this.getSSO();
|
||||
}
|
||||
|
||||
async getDomains() {
|
||||
@ -49,6 +51,14 @@ export class Login extends Component {
|
||||
this.updateDefaultValue();
|
||||
}
|
||||
|
||||
async getSSO() {
|
||||
try {
|
||||
this.store.fetchSSO();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
get rootStore() {
|
||||
return this.props.rootStore;
|
||||
}
|
||||
@ -89,8 +99,67 @@ export class Login extends Component {
|
||||
return '/base/overview';
|
||||
}
|
||||
|
||||
get enableSSO() {
|
||||
const { sso: { enable_sso = false } = {} } = this.store;
|
||||
return enable_sso;
|
||||
}
|
||||
|
||||
get ssoProtocols() {
|
||||
return {
|
||||
openid: t('OpenID Connect'),
|
||||
};
|
||||
}
|
||||
|
||||
get SSOOptions() {
|
||||
if (!this.enableSSO) {
|
||||
return [];
|
||||
}
|
||||
const { sso: { protocols = [] } = {} } = this.store;
|
||||
return protocols.map((it) => {
|
||||
const { protocol, url } = it;
|
||||
return {
|
||||
label: this.ssoProtocols[protocol] || protocol,
|
||||
value: url,
|
||||
...it,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get passwordOption() {
|
||||
return {
|
||||
label: t('Keystone Credentials'),
|
||||
value: 'password',
|
||||
};
|
||||
}
|
||||
|
||||
get loginTypeOptions() {
|
||||
if (!this.enableSSO) {
|
||||
return [];
|
||||
}
|
||||
return [this.passwordOption, ...this.SSOOptions];
|
||||
}
|
||||
|
||||
onLoginTypeChange = (value, option) => {
|
||||
this.setState({ loginTypeOption: option });
|
||||
};
|
||||
|
||||
get currentLoginType() {
|
||||
const { loginTypeOption: { value } = {} } = this.state;
|
||||
if (value === 'password') {
|
||||
return 'password';
|
||||
}
|
||||
return 'sso';
|
||||
}
|
||||
|
||||
get currentSSOLink() {
|
||||
const { loginTypeOption: { value } = {} } = this.state;
|
||||
return value;
|
||||
}
|
||||
|
||||
get defaultValue() {
|
||||
const data = {};
|
||||
const data = {
|
||||
loginType: 'password',
|
||||
};
|
||||
if (this.regions.length === 1) {
|
||||
data.region = this.regions[0].value;
|
||||
}
|
||||
@ -107,84 +176,150 @@ export class Login extends Component {
|
||||
block: true,
|
||||
type: 'primary',
|
||||
};
|
||||
return [
|
||||
{
|
||||
name: 'error',
|
||||
hidden: !error,
|
||||
render: () => (
|
||||
<div className={styles['login-error']}>
|
||||
<InfoCircleFilled />
|
||||
{this.getErrorMessage()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'region',
|
||||
required: true,
|
||||
message: t('Please select your Region!'),
|
||||
render: () => (
|
||||
<Select placeholder={t('Select a region')} options={this.regions} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'domain',
|
||||
required: true,
|
||||
message: t('Please select your Domain!'),
|
||||
render: () => (
|
||||
<Select placeholder={t('Select a domain')} options={this.domains} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
required: true,
|
||||
message: t('Please input your Username!'),
|
||||
render: () => <Input placeholder={t('Username')} />,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: true,
|
||||
message: t('Please input your Password!'),
|
||||
render: () => <Input.Password placeholder={t('Password')} />,
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
hidden: true,
|
||||
render: () => (
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Link to="password">{t('Forgot your password?')}</Link>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Link to="register" className={styles.register}>
|
||||
{t('Sign up')}
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'submit',
|
||||
render: () => (
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className="login-form-button"
|
||||
>
|
||||
{t('Log in')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
const loginType = this.currentLoginType;
|
||||
const errorItem = {
|
||||
name: 'error',
|
||||
hidden: !error,
|
||||
render: () => (
|
||||
<div className={styles['login-error']}>
|
||||
<InfoCircleFilled />
|
||||
{this.getErrorMessage()}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
const regionItem = {
|
||||
name: 'region',
|
||||
required: true,
|
||||
message: t('Please select your Region!'),
|
||||
render: () => (
|
||||
<Select placeholder={t('Select a region')} options={this.regions} />
|
||||
),
|
||||
};
|
||||
const domainItem = {
|
||||
name: 'domain',
|
||||
required: true,
|
||||
message: t('Please select your Domain!'),
|
||||
render: () => (
|
||||
<Select placeholder={t('Select a domain')} options={this.domains} />
|
||||
),
|
||||
};
|
||||
const usernameItem = {
|
||||
name: 'username',
|
||||
required: true,
|
||||
message: t('Please input your Username!'),
|
||||
render: () => <Input placeholder={t('Username')} />,
|
||||
};
|
||||
const passwordItem = {
|
||||
name: 'password',
|
||||
required: true,
|
||||
message: t('Please input your Password!'),
|
||||
render: () => <Input.Password placeholder={t('Password')} />,
|
||||
};
|
||||
const extraItem = {
|
||||
name: 'extra',
|
||||
hidden: true,
|
||||
render: () => (
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Link to="password">{t('Forgot your password?')}</Link>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Link to="register" className={styles.register}>
|
||||
{t('Sign up')}
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
};
|
||||
const submitItem = {
|
||||
name: 'submit',
|
||||
render: () => (
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className="login-form-button"
|
||||
>
|
||||
{t('Log in')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
};
|
||||
const namePasswordItems = [
|
||||
errorItem,
|
||||
regionItem,
|
||||
domainItem,
|
||||
usernameItem,
|
||||
passwordItem,
|
||||
extraItem,
|
||||
];
|
||||
const typeItem = {
|
||||
name: 'loginType',
|
||||
required: true,
|
||||
message: t('Please select login type!'),
|
||||
extra: t(
|
||||
'If you are not sure which authentication method to use, please contact your administrator.'
|
||||
),
|
||||
render: () => (
|
||||
<Select
|
||||
placeholder={t('Select a login type')}
|
||||
options={this.loginTypeOptions}
|
||||
onChange={this.onLoginTypeChange}
|
||||
/>
|
||||
),
|
||||
};
|
||||
if (this.enableSSO) {
|
||||
if (loginType === 'password') {
|
||||
return [typeItem, ...namePasswordItems, submitItem];
|
||||
}
|
||||
|
||||
return [typeItem, submitItem];
|
||||
}
|
||||
return [...namePasswordItems, submitItem];
|
||||
}
|
||||
|
||||
getUserId = (str) => str.split(':')[1].trim().split('.')[0];
|
||||
|
||||
onLoginFailed = (error, values) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
const {
|
||||
data: { detail = '' },
|
||||
} = error.response;
|
||||
const message = detail || '';
|
||||
if (
|
||||
message.includes(
|
||||
'The password is expired and needs to be changed for user'
|
||||
)
|
||||
) {
|
||||
this.dealWithChangePassword(message, values);
|
||||
} else {
|
||||
this.setState({
|
||||
error: true,
|
||||
message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onLoginSuccess = () => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: false,
|
||||
});
|
||||
if (this.rootStore.user && !isEmpty(this.rootStore.user)) {
|
||||
this.rootStore.routing.push(this.nextPage);
|
||||
}
|
||||
};
|
||||
|
||||
onFinish = (values) => {
|
||||
if (this.currentLoginType === 'sso') {
|
||||
document.location.href = this.currentSSOLink;
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
loading: true,
|
||||
message: '',
|
||||
@ -194,40 +329,10 @@ export class Login extends Component {
|
||||
const body = { domain, password, region, username };
|
||||
this.rootStore.login(body).then(
|
||||
() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: false,
|
||||
});
|
||||
if (this.rootStore.user && !isEmpty(this.rootStore.user)) {
|
||||
this.rootStore.routing.push(this.nextPage);
|
||||
}
|
||||
this.onLoginSuccess();
|
||||
},
|
||||
(error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
const {
|
||||
data: { detail },
|
||||
} = error.response;
|
||||
if (
|
||||
detail.includes(
|
||||
'The password is expired and needs to be changed for user'
|
||||
)
|
||||
) {
|
||||
const userId = this.getUserId(detail);
|
||||
const data = {
|
||||
region: values.region,
|
||||
oldPassword: values.password,
|
||||
userId,
|
||||
};
|
||||
this.rootStore.setPasswordInfo(data);
|
||||
this.rootStore.routing.push('/auth/change-password');
|
||||
} else {
|
||||
this.setState({
|
||||
error: true,
|
||||
message: detail,
|
||||
});
|
||||
}
|
||||
this.onLoginFailed(error, values);
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -252,6 +357,17 @@ export class Login extends Component {
|
||||
return t('Username or password is incorrect');
|
||||
}
|
||||
|
||||
dealWithChangePassword = (detail, values) => {
|
||||
const userId = this.getUserId(detail);
|
||||
const data = {
|
||||
region: values.region,
|
||||
oldPassword: values.password,
|
||||
userId,
|
||||
};
|
||||
this.rootStore.setPasswordInfo(data);
|
||||
this.rootStore.routing.push('/auth/change-password');
|
||||
};
|
||||
|
||||
updateDefaultValue = () => {
|
||||
this.formRef.current.resetFields();
|
||||
if (this.formRef.current && this.formRef.current.resetFields) {
|
||||
|
@ -23,6 +23,9 @@ export class SkylineStore extends Base {
|
||||
@observable
|
||||
regions = [];
|
||||
|
||||
@observable
|
||||
sso = {};
|
||||
|
||||
get client() {
|
||||
return client.skyline.contrib;
|
||||
}
|
||||
@ -38,6 +41,12 @@ export class SkylineStore extends Base {
|
||||
const result = await this.client.regions();
|
||||
this.regions = result;
|
||||
}
|
||||
|
||||
@action
|
||||
async fetchSSO() {
|
||||
const result = await client.skyline.sso.list();
|
||||
this.sso = result;
|
||||
}
|
||||
}
|
||||
|
||||
const globalSkylineStore = new SkylineStore();
|
||||
|
Loading…
Reference in New Issue
Block a user