Repository: inspektor-dev/inspektor (language: rust)

Issue:
add way to get short lived credentials
all the credentials that Inspektor creates are long lived. 

There should be a way where admins can create short lived credentials for data sources. 

This can help teams to give temporary credentials to the user


Discussion / hints:

Fix patch:
diff --git a/controlplane/dashboard/src/api/api.js b/controlplane/dashboard/src/api/api.js
index 87ba1b0..fc29c41 100644
--- a/controlplane/dashboard/src/api/api.js
+++ b/controlplane/dashboard/src/api/api.js
@@ -52,6 +52,13 @@ export default {
     updateRoles: async(data) => {
         await axios.post("/api/roles", data)
     },
+    createTempCredentials: async(data) => {
+        await axios.post("/api/session/temp", data)
+    },
+    getTempCredentials: async(data) => {
+       let res =  await axios.get("/api/session/temp", data)
+       return res.data
+    },
     intializeToken: () => {
         let token = localStorage.getItem("access-token");
         if (token != null) {
diff --git a/controlplane/dashboard/src/components/AddTempCredentials.vue b/controlplane/dashboard/src/components/AddTempCredentials.vue
new file mode 100644
index 0000000..4709281
--- /dev/null
+++ b/controlplane/dashboard/src/components/AddTempCredentials.vue
@@ -0,0 +1,150 @@
+<!--
+ Copyright 2022 Balaji (rbalajis25@gmail.com)
+ 
+ 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.
+-->
+
+<template>
+  <div>
+    <n-form :model="formValue" :rules="rules" ref="formRef">
+      <n-form-item label="datasources" path="datasourceId">
+        <n-select
+          placeholder="Select datasource"
+          :options="generalOptions"
+          v-model:value="formValue.datasourceId"
+        />
+      </n-form-item>
+      <n-form-item label="user" path="userId">
+        <n-select
+          placeholder="Select User"
+          :options="userOptions"
+          v-model:value="formValue.userId"
+        />
+      </n-form-item>
+      <n-form-item label="valid expiry minutes" path="expiryMinute">
+        <n-input-number
+          v-model:value="formValue.expiryMinute"
+          placeholder="Enter valid expiry minutes"
+        />
+      </n-form-item>
+
+      <n-form-item path="roles" label="roles">
+        <n-dynamic-tags v-model:value="formValue.roles" />
+      </n-form-item>
+      <div style="display: flex; justify-content: flex-end">
+        <n-button type="success" @click="createTempCredentials"
+          >Create Datasource</n-button
+        >
+      </div>
+    </n-form>
+  </div>
+</template>
+
+<script>
+import { ref, computed } from "vue";
+import { useMessage } from "naive-ui";
+import api from "@/api/api";
+import { useStore } from "vuex";
+
+export default {
+  async setup(_, { emit }) {
+    let store = useStore();
+    await store.dispatch("updateUsers");
+    //let roles = ref(['admin']);
+    let formRef = ref(null);
+    const message = useMessage();
+
+    let formValue = ref({
+      datasourceId: null,
+      userId: null,
+      expiryMinute: null,
+      roles: [],
+    });
+    let options = store.state.datasources.map((v) => ({
+      label: v.name,
+      value: v.id,
+    }));
+    let userOptions = store.state.users.map((v) => ({
+      label: v.username,
+      value: v.id,
+    }));
+    return {
+      formRef,
+      formValue,
+      rules: {
+        datasourceId: {
+          required: true,
+          message: "please select datasource",
+          trigger: "blur",
+          validator: (_rule, value) => {
+            if (value > 0) {
+              return true;
+            }
+            return false;
+          },
+        },
+        userId: {
+          required: true,
+          message: "please select user",
+          trigger: "blur",
+          validator: (_rule, value) => {
+            console.log("validator user", value);
+            if (value > 0) {
+              return true;
+            }
+            return false;
+          },
+        },
+        expiryMinute: {
+          required: true,
+          message: "Please select expiry minute",
+          trigger: "blur",
+          validator: (_rule, value) => {
+            if (value <= 0) {
+              return false;
+            }
+            return true;
+          },
+        },
+        roles: {
+          required: true,
+          message: "Please enter role",
+          trigger: "blur",
+          validator: (_rule, value) => {
+            if (value.length == 0) {
+              return false;
+            }
+            return true;
+          },
+        },
+      },
+      generalOptions: options,
+      userOptions: userOptions,
+      createTempCredentials(e) {
+        e.preventDefault();
+        formRef.value.validate(async (errors) => {
+          if (errors) {
+            console.log(errors);
+            message.error("Invalid data");
+            return;
+          }
+          await api.createTempCredentials(formValue.value);
+          
+          emit('onAdd')
+        });
+      },
+    };
+  },
+  name: "AddTempCredentials",
+};
+</script>
\ No newline at end of file
diff --git a/controlplane/dashboard/src/components/TempSessions.vue b/controlplane/dashboard/src/components/TempSessions.vue
new file mode 100644
index 0000000..d6e67b7
--- /dev/null
+++ b/controlplane/dashboard/src/components/TempSessions.vue
@@ -0,0 +1,133 @@
+<!--
+ Copyright 2022 Balaji (rbalajis25@gmail.com)
+ 
+ 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.
+-->
+
+<template >
+  <n-modal v-model:show="showSessionModal">
+    <n-card
+      style="width: 600px"
+      title="Postgres Login Credentials"
+      :bordered="false"
+      size="huge"
+    >
+      <session-modal :session="currentSessionMeta" />
+    </n-card>
+  </n-modal>
+
+  <div style="padding-top: 2%">
+    <n-data-table :columns="columns" :data="data" :row-key="rowKey" />
+  </div>
+</template>
+<script>
+import { ref, h, computed } from "vue";
+import { NButton, NTag, NDynamicTags } from "naive-ui";
+import { useStore } from "vuex";
+import { useMessage } from "naive-ui";
+
+import api from "@/api/api";
+import SessionModal from "./SessionModal.vue";
+
+const createColumn = (message, showSessionModal, currentSessionMeta, store) => {
+  return [
+    {
+      title: "Datasource Name",
+      key: "name",
+    },
+    {
+      title: "Type",
+      key: "type",
+    },
+    {
+      title: "sidecar hostname",
+      key: "sidecarHostname",
+    },
+    {
+      title: "Session",
+      render(row) {
+        let buttonProperty = {
+          type: "success",
+          onClick: () => {
+            showSessionModal.value = true;
+          },
+        };
+        currentSessionMeta.value = row.sessionMeta.meta;
+        let buttonText = "Show Credentials";
+        return h(NButton, buttonProperty, buttonText);
+      },
+    },
+    {
+      title: "Roles",
+      render: (row) => {
+        // let tags = [];
+        // for (let i = 0; i < row.roles.length; i++) {
+        //   tags.push(
+        //     h(
+        //       NTag,
+        //       { style: { marginLeft: "1px" }, round: true, type: "info" },
+        //       row.roles[i]
+        //     )
+        //   );
+        // }
+        // tags.push(
+        //   h(
+        //     NTag,
+        //     { style: { marginLeft: "1px" }, round: true, type: "success" },
+        //     "Add Role"
+        //   )
+        // );
+        return h(
+          NDynamicTags,
+          {
+            disabled: true,
+            closable: false,
+            defaultValue: row.sessionMeta.meta.tempRoles,
+          },
+          ""
+        );
+      },
+    },
+  ];
+};
+export default {
+  components: {  SessionModal },
+  async setup() {
+    let store = useStore();
+    let showModal = ref(false);
+    let showSessionModal = ref(false);
+    let currentSessionMeta = ref({});
+    let message = useMessage();
+    return {
+      currentSessionMeta: currentSessionMeta,
+      showModal: showModal,
+      showSessionModal: showSessionModal,
+      data: ref(computed(() => store.state.tempDatasource)),
+      columns: createColumn(
+        message,
+        showSessionModal,
+        currentSessionMeta,
+        store
+      ),
+      count: computed(() => {
+        return store.state.count;
+      }),
+      isAdmin: computed(() => {
+        return store.state.isAdmin;
+      }),
+      rowKey: (data) => data.id,
+    };
+  },
+  name: "TempSessions",
+};
+</script>
\ No newline at end of file
diff --git a/controlplane/dashboard/src/store/store.js b/controlplane/dashboard/src/store/store.js
index a79e26a..00356a3 100644
--- a/controlplane/dashboard/src/store/store.js
+++ b/controlplane/dashboard/src/store/store.js
@@ -6,6 +6,7 @@ import api from "@/api/api";
 function getDefaultStore() {
     return {
         datasources: [],
+        tempDatasource: [],
         users: [],
         isAdmin: false,
         sup: true,
@@ -28,7 +29,6 @@ const store = createStore({
                 state.datasources.pop()
             }
             datasource.forEach(data => {
-                console.log("pushing", data)
                 state.datasources.push(data)
             })
         },
@@ -46,6 +46,9 @@ const store = createStore({
         },
         config(state, data) {
             state.config = data
+        },
+        setTempDatasource(state, datasource) {
+            state.tempDatasource = datasource
         }
     },
     actions: {
@@ -67,12 +70,20 @@ const store = createStore({
             // merge session and meta in same object.
             for (let i = 0; i < datasources.length; i++) {
                 for (let j = 0; j < sessions.length; j++) {
-                    if (sessions[j].objectID == datasources[i].id) {
+                    if (sessions[j].objectID == datasources[i].id && sessions[j].meta.expiresAt == 0) {
                         datasources[i].sessionMeta = sessions[j].meta
                     }
                 }
             }
             commit("setDatasource", datasources)
+            let tempSesions = await api.getTempCredentials()
+            let tempDatasources = []
+            for (let k= 0; k< tempSesions.length; k++){
+                let datasource = tempSesions[k].datasource;
+                datasource.sessionMeta = tempSesions[k]
+                tempDatasources.push(datasource)
+            }
+            commit("setTempDatasource", tempDatasources)
         },
         async updateUsers({ commit }) {
             let users = await api.getUsers()
@@ -82,7 +93,7 @@ const store = createStore({
         async reset({ commit }) {
             commit("reset")
         },
-        async refreshConfig({commit}) {
+        async refreshConfig({ commit }) {
             let config = await api.config()
             commit("config", config)
         }
diff --git a/controlplane/dashboard/src/views/dashboard.vue b/controlplane/dashboard/src/views/dashboard.vue
index 544f702..71d807c 100644
--- a/controlplane/dashboard/src/views/dashboard.vue
+++ b/controlplane/dashboard/src/views/dashboard.vue
@@ -5,9 +5,27 @@
         <n-gi>
           <h1>Inspektor dashboard</h1>
         </n-gi>
+        <n-gi>
+          <n-modal v-model:show="showTempCredModal">
+            <n-card
+              style="width: 600px"
+              title="Add Temp Credentials"
+              :bordered="false"
+              size="huge"
+            >
+              <add-temp-credentials @onAdd="tempCredAdded">
+              </add-temp-credentials>
+            </n-card>
+          </n-modal>
+          <div v-if="isAdmin" class="temp-button-pos">
+            <n-button type="success" @click="showTempCredModal = true"
+              >Create Temp Credentials</n-button
+            >
+          </div>
+        </n-gi>
         <n-gi>
           <div class="refresh-button-pos" v-if="policyExist">
-            <p>Policy hash: {{policyHash}}</p>
+            <p>Policy hash: {{ policyHash }}</p>
             <n-button type="success" @click="refreshPolicy"
               >Refresh Policy</n-button
             >
@@ -24,12 +42,21 @@
         ></n-tab-pane>
         <n-tab-pane name="the beatles" tab="Admin" v-if="isAdmin"
           ><admin
-        /></n-tab-pane> </n-tabs
+        /></n-tab-pane>
+          <n-tab-pane name="temp sessions" tab="Temp Sessions" 
+          ><temp-sessions
+        /></n-tab-pane>
+      </n-tabs
     ></n-card>
   </div>
 </template>
 
 <style scoped>
+.temp-button-pos {
+  position: absolute;
+  top: 17%;
+  right: 15%;
+}
 .button-pos {
   position: absolute;
   top: 12%;
@@ -44,12 +71,15 @@
 <script>
 import Datasources from "@/components/Datasources.vue";
 import Admin from "@/components/Admin.vue";
+import AddTempCredentials from "@/components/AddTempCredentials.vue";
+import TempSessions from "@/components/TempSessions.vue";
 import { useRouter } from "vue-router";
 import { useStore } from "vuex";
 import { computed } from "vue";
 import { useLoadingBar } from "naive-ui";
 import { useMessage } from "naive-ui";
 import api from "@/api/api";
+import { ref } from "vue";
 
 export default {
   async setup() {
@@ -57,6 +87,7 @@ export default {
     let store = useStore();
     const loadingBar = useLoadingBar();
     const message = useMessage();
+    let showTempCredModal = ref(false);
     await store.dispatch("init");
     await store.dispatch("updateDatasource");
     return {
@@ -68,7 +99,7 @@ export default {
       refreshPolicy: async () => {
         loadingBar.start();
         await api.refreshPolicy();
-        await store.dispatch("refreshConfig")
+        await store.dispatch("refreshConfig");
         loadingBar.finish();
         message.success("Policy updated");
       },
@@ -79,14 +110,20 @@ export default {
         }
         return store.state.config.policyRepoUrl != "";
       }),
-      policyHash: computed(() =>{
-        return store.state.config.policyHash
-      })
+      policyHash: computed(() => {
+        return store.state.config.policyHash;
+      }),
+      showTempCredModal: showTempCredModal,
+      tempCredAdded: () => {
+        showTempCredModal.value = false;
+      },
     };
   },
   components: {
     Datasources,
     Admin,
+    AddTempCredentials,
+    TempSessions
   },
 };
 </script>
\ No newline at end of file
diff --git a/controlplane/handlers/session.go b/controlplane/handlers/session.go
index 0e11f58..15f2cf9 100644
--- a/controlplane/handlers/session.go
+++ b/controlplane/handlers/session.go
@@ -18,7 +18,44 @@ func (h *Handlers) GetSesssion() InspectorHandler {
 			utils.WriteErrorMsg("server down", http.StatusInternalServerError, ctx.Rw)
 			return
 		}
-		utils.WriteSuccesMsgWithData("ok", http.StatusOK, sessions, ctx.Rw)
+		// remove temp session
+		filteredSession := []*models.Session{}
+		for _, session := range sessions {
+			session.UnmarshalMeta()
+			if len(session.SessionMeta.TempRoles) != 0 {
+				continue
+			}
+			filteredSession = append(filteredSession, session)
+		}
+		utils.WriteSuccesMsgWithData("ok", http.StatusOK, filteredSession, ctx.Rw)
+	}
+}
+
+func (h *Handlers) GetTempSessions() InspectorHandler {
+	return func(ctx *types.Ctx) {
+		sessions, err := h.Store.GetSessionForUser(ctx.Claim.ObjectID)
+		if err != nil {
+			utils.Logger.Error("error while retriving user session", zap.String("err_msg", err.Error()))
+			utils.WriteErrorMsg("server down", http.StatusInternalServerError, ctx.Rw)
+			return
+		}
+		filteredSession := []*models.Session{}
+		for _, session := range sessions {
+			session.UnmarshalMeta()
+			if len(session.SessionMeta.TempRoles) == 0 {
+				continue
+			}
+			datasource, err := h.Store.GetDatasource(session.ObjectID)
+			if err != nil {
+				utils.Logger.Error("error while retriving temp sessions", zap.String("err_msg", err.Error()))
+				utils.WriteErrorMsg("server down", http.StatusInternalServerError, ctx.Rw)
+				return
+			}
+			datasource.SideCarToken = ""
+			session.Datasource = datasource
+			filteredSession = append(filteredSession, session)
+		}
+		utils.WriteSuccesMsgWithData("ok", http.StatusOK, filteredSession, ctx.Rw)
 	}
 }
 
diff --git a/controlplane/handlers/user.go b/controlplane/handlers/user.go
index a975845..fffc836 100644
--- a/controlplane/handlers/user.go
+++ b/controlplane/handlers/user.go
@@ -18,6 +18,7 @@ import (
 	"time"
 
 	"github.com/golang-jwt/jwt"
+	"github.com/goombaio/namegenerator"
 	"github.com/gorilla/handlers"
 	"github.com/gorilla/mux"
 	"go.uber.org/zap"
@@ -209,6 +210,62 @@ func (h *Handlers) AddRoles() InspectorHandler {
 	}
 }
 
+func (h *Handlers) CreateTempSession() InspectorHandler {
+	return func(ctx *types.Ctx) {
+		if utils.IndexOf(ctx.Claim.Roles, "admin") == -1 {
+			utils.WriteErrorMsgWithErrCode("only admin can create data source", types.ErrInvalidAccess, http.StatusUnauthorized, ctx.Rw)
+			return
+		}
+		req := &types.CreateTempSession{}
+		if err := json.NewDecoder(ctx.R.Body).Decode(req); err != nil {
+			utils.Logger.Error("error while decoding user request", zap.String("err_msg", err.Error()))
+			utils.WriteErrorMsg("invalid request", http.StatusBadRequest, ctx.Rw)
+			return
+		}
+		// validate user and datasource exist.
+		_, err := h.Store.GetDatasource(req.DatasourceID)
+		if err != nil {
+			utils.Logger.Error("error while retriving datasoruce", zap.String("err_msg", err.Error()))
+			handleErr(err, ctx)
+			return
+		}
+		_, err = h.Store.GetUserByID(req.UserID)
+		if err != nil {
+			utils.Logger.Error("error while retriving datasoruce", zap.String("err_msg", err.Error()))
+			handleErr(err, ctx)
+			return
+		}
+		if len(req.Roles) == 0 {
+			utils.WriteErrorMsg("expected atleast one role to create session", http.StatusBadRequest, ctx.Rw)
+			return
+		}
+
+		if req.ExpiryMinute == 0 {
+			utils.WriteErrorMsg("expiry minute should be greater than zero", http.StatusBadRequest, ctx.Rw)
+			return
+		}
+
+		session := &models.Session{
+			ObjectID: req.DatasourceID,
+			UserID:   req.UserID,
+			SessionMeta: &models.SessionMeta{
+				Type:             "postgres",
+				PostgresPassword: utils.GenerateSecureToken(7),
+				PostgresUsername: namegenerator.NewNameGenerator(time.Now().UnixNano()).Generate(),
+				TempRoles:        req.Roles,
+				ExpiresAt:        time.Now().Add(time.Minute * time.Duration(req.ExpiryMinute)).UnixNano(),
+			},
+		}
+		session.MarshalMeta()
+		if err := h.Store.CreateSession(session); err != nil {
+			utils.Logger.Error("error while creating temp session", zap.String("err_msg", err.Error()))
+			handleErr(err, ctx)
+			return
+		}
+		utils.WriteSuccesMsg("ok", http.StatusOK, ctx.Rw)
+	}
+}
+
 func (h *Handlers) OAuthUrl() http.HandlerFunc {
 	return func(rw http.ResponseWriter, r *http.Request) {
 		res := &types.OauthResponse{}
@@ -322,6 +379,8 @@ func (h *Handlers) Init(router *mux.Router) {
 	router.HandleFunc("/api/datasource", h.AuthMiddleWare(h.GetDataSources())).Methods("GET", "OPTIONS")
 	router.HandleFunc("/api/session", h.AuthMiddleWare(h.CreateSession())).Methods("POST", "OPTIONS")
 	router.HandleFunc("/api/session", h.AuthMiddleWare(h.GetSesssion())).Methods("GET", "OPTIONS")
+	router.HandleFunc("/api/session/temp", h.AuthMiddleWare(h.CreateTempSession())).Methods("POST", "OPTIONS")
+	router.HandleFunc("/api/session/temp", h.AuthMiddleWare(h.GetTempSessions())).Methods("GET", "OPTIONS")
 	router.HandleFunc("/api/policy/nofification", h.PolicyNotification()).Methods("POST", "OPTIONS")
 	router.HandleFunc("/api/user", h.AuthMiddleWare(h.AddUser())).Methods("POST", "OPTIONS")
 	router.HandleFunc("/api/users", h.AuthMiddleWare(h.GetUsers())).Methods("GET", "OPTIONS")
diff --git a/controlplane/models/session.go b/controlplane/models/session.go
index 800b66c..c77e79f 100644
--- a/controlplane/models/session.go
+++ b/controlplane/models/session.go
@@ -14,12 +14,15 @@ type Session struct {
 	UserID      uint           `json:"-"`
 	Meta        datatypes.JSON `json:"meta"`
 	SessionMeta *SessionMeta   `gorm:"-" json:"-"`
+	Datasource  *DataSource    `gorm:"-" json:"datasource"`
 }
 
 type SessionMeta struct {
-	Type             string `json:"type"`
-	PostgresPassword string `json:"postgresPassword"`
-	PostgresUsername string `json:"postgresUsername"`
+	Type             string   `json:"type"`
+	PostgresPassword string   `json:"postgresPassword"`
+	PostgresUsername string   `json:"postgresUsername"`
+	TempRoles        []string `json:"tempRoles"`
+	ExpiresAt        int64    `json:"expiresAt"`
 }
 
 func (s *Session) UnmarshalMeta() {
diff --git a/controlplane/models/user.go b/controlplane/models/user.go
index 59a69dd..7fa9c9c 100644
--- a/controlplane/models/user.go
+++ b/controlplane/models/user.go
@@ -27,7 +27,7 @@ func (u *User) UnmarshalMeta() {
 
 func (u *User) MarshalJSON() ([]byte, error) {
 	u.UnmarshalMeta()
-	return json.Marshal(map[string]interface{}{"username": u.Name, "roles": u.Roles, "meta": u.UserMeta})
+	return json.Marshal(map[string]interface{}{"username": u.Name, "roles": u.Roles, "meta": u.UserMeta, "id": u.ID})
 }
 
 func (u *User) MarshalMeta() {
diff --git a/controlplane/store/store.go b/controlplane/store/store.go
index f51a9bb..33145bf 100644
--- a/controlplane/store/store.go
+++ b/controlplane/store/store.go
@@ -118,6 +118,16 @@ func (s *Store) GetDataSources(ids ...uint) ([]*models.DataSource, error) {
 	return dataSources, nil
 }
 
+func (s *Store) GetDatasource(id uint) (*models.DataSource, error) {
+	datasource := &models.DataSource{}
+	err := s.db.Model(&models.DataSource{}).Where("id = ?", id).First(datasource).Error
+	if err != nil {
+		utils.Logger.Error("error while retriving data source", zap.String("err_msg", err.Error()))
+		return datasource, err
+	}
+	return datasource, nil
+}
+
 func (s *Store) GetDatasourceByWhere(query interface{}, args ...interface{}) (*models.DataSource, error) {
 	dataSource := &models.DataSource{}
 	if err := s.db.Model(&models.DataSource{}).Where(query, args...).First(dataSource).Error; err != nil {
@@ -232,6 +242,12 @@ func (s *Store) CreateSessionForUser(userID uint, datasourceID uint) error {
 	})
 }
 
+func (s *Store) CreateSession(sess *models.Session) error {
+	return s.db.Transaction(func(tx *gorm.DB) error {
+		return tx.Model(&models.Session{}).Create(sess).Error
+	})
+}
+
 func (s *Store) CreateUser(username, password string) (*models.User, error) {
 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 	if err != nil {
@@ -302,3 +318,14 @@ func (s *Store) SyncRoles(objectID uint, objectType string, roles []string) erro
 	}
 	return s.WriteRoleForObjectID(objectID, roles, objectType)
 }
+
+func handleGormErr(err error) error {
+	if err == nil {
+		return nil
+	}
+	switch err {
+	case gorm.ErrRecordNotFound:
+		return types.ErrNotExist
+	}
+	return err
+}
diff --git a/controlplane/types/types.go b/controlplane/types/types.go
index d3f9261..e305fcf 100644
--- a/controlplane/types/types.go
+++ b/controlplane/types/types.go
@@ -91,3 +91,10 @@ type OauthResponse struct {
 	Provider string `json:"provider"`
 	Url      string `json:"url"`
 }
+
+type CreateTempSession struct {
+	UserID       uint     `json:"userId"`
+	DatasourceID uint     `json:"datasourceId"`
+	ExpiryMinute int64    `json:"expiryMinute"`
+	Roles        []string `json:"roles"`
+}


Test patch: