feat: support web SSO login

After the 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 OK to switch to the Web SSO configuration page.

Implements: blueprint skyline-sso-oid
Change-Id: I438adb31c758287525ba896b4d2b21d00842c3e7
This commit is contained in:
Jingwei.Zhang 2022-08-16 11:02:19 +08:00
parent a4283c7bc0
commit 84f06c8288
7 changed files with 267 additions and 106 deletions

View File

@ -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.

View File

@ -126,6 +126,9 @@ export class SkylineClient extends Base {
name: 'queryRange',
key: 'query_range',
},
{
key: 'sso',
},
];
}
}

View File

@ -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;

View File

@ -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",

View File

@ -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": "已选择成员",

View File

@ -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,8 +176,8 @@ export class Login extends Component {
block: true,
type: 'primary',
};
return [
{
const loginType = this.currentLoginType;
const errorItem = {
name: 'error',
hidden: !error,
render: () => (
@ -117,36 +186,36 @@ export class Login extends Component {
{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: () => (
@ -161,8 +230,8 @@ export class Login extends Component {
</Col>
</Row>
),
},
{
};
const submitItem = {
name: 'submit',
render: () => (
<Row gutter={8}>
@ -178,13 +247,79 @@ export class Login extends Component {
</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) {

View File

@ -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();