You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2020/02/19 12:22:52 UTC

[cloudstack-primate] branch master updated: login: add SAML single-sign-on support (#169)

This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git


The following commit(s) were added to refs/heads/master by this push:
     new 71b7412  login: add SAML single-sign-on support (#169)
71b7412 is described below

commit 71b7412d69744ffd5d9ac2a018c6f321c162826a
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Wed Feb 19 17:52:46 2020 +0530

    login: add SAML single-sign-on support (#169)
    
    This adds SAML single-sign-on support.
    
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
    Co-authored-by: Rohit Yadav <ro...@apache.org>
---
 src/permission.js        |  2 ++
 src/utils/request.js     | 17 +++++++-----
 src/views/auth/Login.vue | 72 ++++++++++++++++++++++++++++++++++--------------
 3 files changed, 64 insertions(+), 27 deletions(-)

diff --git a/src/permission.js b/src/permission.js
index bf43720..2bbc789 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -22,6 +22,7 @@ import store from './store'
 
 import NProgress from 'nprogress' // progress bar
 import 'nprogress/nprogress.css' // progress bar style
+import message from 'ant-design-vue/es/message'
 import notification from 'ant-design-vue/es/notification'
 import { setDocumentTitle, domTitle } from '@/utils/domUtil'
 import { ACCESS_TOKEN } from '@/store/mutation-types'
@@ -41,6 +42,7 @@ router.beforeEach((to, from, next) => {
       NProgress.done()
     } else {
       if (Object.keys(store.getters.apis).length === 0) {
+        message.loading('Discovering features...', 5)
         store
           .dispatch('GetInfo')
           .then(apis => {
diff --git a/src/utils/request.js b/src/utils/request.js
index 235793a..f28958f 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -29,15 +29,18 @@ const service = axios.create({
 })
 
 const err = (error) => {
-  if (error.response) {
-    console.log('error has occurred')
-    console.log(error)
+  const response = error.response
+  if (response) {
+    console.log(response)
     const token = Vue.ls.get(ACCESS_TOKEN)
-    if (error.response.status === 403) {
-      const data = error.response.data
+    if (response.status === 403) {
+      const data = response.data
       notification.error({ message: 'Forbidden', description: data.message })
     }
-    if (error.response.status === 401) {
+    if (response.status === 401) {
+      if (response.config && response.config.params && ['listIdps'].includes(response.config.params.command)) {
+        return
+      }
       notification.error({ message: 'Unauthorized', description: 'Authorization verification failed' })
       if (token) {
         store.dispatch('Logout').then(() => {
@@ -47,7 +50,7 @@ const err = (error) => {
         })
       }
     }
-    if (error.response.status === 404) {
+    if (response.status === 404) {
       notification.error({ message: 'Not Found', description: 'Resource not found' })
       this.$router.push({ path: '/exception/404' })
     }
diff --git a/src/views/auth/Login.vue b/src/views/auth/Login.vue
index 1bc6ec0..2e6f019 100644
--- a/src/views/auth/Login.vue
+++ b/src/views/auth/Login.vue
@@ -28,11 +28,12 @@
       size="large"
       :tabBarStyle="{ textAlign: 'center', borderBottom: 'unset' }"
       @change="handleTabClick"
+      :animated="false"
     >
-      <a-tab-pane key="tab1">
+      <a-tab-pane key="cs">
         <span slot="tab">
           <a-icon type="safety" />
-          <b>CloudStack Login</b>
+          Portal Login
         </span>
         <a-form-item>
           <a-input
@@ -78,11 +79,18 @@
         </a-form-item>
 
       </a-tab-pane>
-      <a-tab-pane key="tab2" disabled>
+      <a-tab-pane key="saml" :disabled="idps.length === 0">
         <span slot="tab">
           <a-icon type="audit" />
-          <b>SAML</b>
+          Single-Sign-On
         </span>
+        <a-form-item>
+          <a-select v-decorator="['idp', { initialValue: selectedIdp } ]">
+            <a-select-option v-for="(idp, idx) in idps" :key="idx" :value="idp.id">
+              {{ idp.orgName }}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
       </a-tab-pane>
     </a-tabs>
 
@@ -100,14 +108,18 @@
 </template>
 
 <script>
+import { api } from '@/api'
 import { mapActions } from 'vuex'
+import config from '@/config/settings'
 
 export default {
   components: {
   },
   data () {
     return {
-      customActiveKey: 'tab1',
+      idps: [],
+      selectedIdp: '',
+      customActiveKey: 'cs',
       loginBtn: false,
       loginType: 0,
       form: this.$form.createForm(this),
@@ -120,8 +132,19 @@ export default {
   },
   created () {
   },
+  mounted () {
+    this.fetchData()
+  },
   methods: {
     ...mapActions(['Login', 'Logout']),
+    fetchData () {
+      api('listIdps').then(response => {
+        if (response) {
+          this.idps = response.listidpsresponse.idp || []
+          this.selectedIdp = this.idps[0].id || ''
+        }
+      })
+    },
     // handler
     handleUsernameOrEmail (rule, value, callback) {
       const { state } = this
@@ -148,24 +171,33 @@ export default {
 
       state.loginBtn = true
 
-      const validateFieldsKey = customActiveKey === 'tab1' ? ['username', 'password', 'domain'] : ['mobile', 'captcha']
+      const validateFieldsKey = customActiveKey === 'cs' ? ['username', 'password', 'domain'] : ['idp']
 
       validateFields(validateFieldsKey, { force: true }, (err, values) => {
         if (!err) {
-          const loginParams = { ...values }
-          delete loginParams.username
-          loginParams[!state.loginType ? 'email' : 'username'] = values.username
-          loginParams.password = values.password
-          loginParams.domain = values.domain
-          if (!loginParams.domain) {
-            loginParams.domain = '/'
+          if (customActiveKey === 'cs') {
+            const loginParams = { ...values }
+            delete loginParams.username
+            loginParams[!state.loginType ? 'email' : 'username'] = values.username
+            loginParams.password = values.password
+            loginParams.domain = values.domain
+            if (!loginParams.domain) {
+              loginParams.domain = '/'
+            }
+            Login(loginParams)
+              .then((res) => this.loginSuccess(res))
+              .catch(err => this.requestFailed(err))
+              .finally(() => {
+                state.loginBtn = false
+              })
+          } else if (customActiveKey === 'saml') {
+            state.loginBtn = false
+            var samlUrl = config.apiBase + '?command=samlSso'
+            if (values.idp) {
+              samlUrl += ('&idpid=' + values.idp)
+            }
+            window.location.href = samlUrl
           }
-          Login(loginParams)
-            .then((res) => this.loginSuccess(res))
-            .catch(err => this.requestFailed(err))
-            .finally(() => {
-              state.loginBtn = false
-            })
         } else {
           setTimeout(() => {
             state.loginBtn = false
@@ -174,7 +206,6 @@ export default {
       })
     },
     loginSuccess (res) {
-      this.$message.loading('Login Successful. Discovering Features...', 5)
       this.$router.push({ path: '/dashboard' }).catch(() => {})
     },
     requestFailed (err) {
@@ -204,6 +235,7 @@ export default {
   }
 
   button.login-button {
+    margin-top: 8px;
     padding: 0 15px;
     font-size: 16px;
     height: 40px;