Skip to content

Commit 1112592

Browse files
authored
Merge pull request #22 from thand-io/store-state-for-rbac
Store state for rbac
2 parents 3b7f24d + 3692032 commit 1112592

39 files changed

Lines changed: 588 additions & 256 deletions

cmd/cli/access.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ var accessCmd = &cobra.Command{
4242
}
4343

4444
err = MakeElevationRequest(&models.ElevateRequest{
45-
Role: foundRole,
46-
Provider: resource,
45+
Role: foundRole,
46+
Providers: []string{resource},
47+
// Let the system pick the workflow based on role and provider
4748
Reason: reason,
4849
Duration: duration,
4950
})

cmd/cli/request.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/go-resty/resty/v2"
1717
"github.com/spf13/cobra"
18+
"github.com/thand-io/agent/internal/common"
1819
"github.com/thand-io/agent/internal/models"
1920
)
2021

@@ -118,11 +119,21 @@ var requestCmd = &cobra.Command{
118119
}
119120

120121
func MakeElevationRequest(request *models.ElevateRequest) error {
122+
121123
if err := validateElevationRequest(request); err != nil {
122124
return err
123125
}
124126

127+
if len(request.Workflow) == 0 {
128+
if len(request.Role.Workflows) == 0 {
129+
return fmt.Errorf("no workflow specified and role has no associated workflows")
130+
}
131+
132+
request.Workflow = request.Role.Workflows[0]
133+
}
134+
125135
requestingWorkflow, err := cfg.GetWorkflowFromElevationRequest(request)
136+
126137
if err != nil {
127138
return fmt.Errorf("failed to get workflow from elevation request: %w", err)
128139
}
@@ -146,6 +157,15 @@ func validateElevationRequest(request *models.ElevateRequest) error {
146157
if len(request.Reason) == 0 {
147158
return fmt.Errorf("invalid request: empty reason")
148159
}
160+
if request.Role == nil {
161+
return fmt.Errorf("invalid request: nil role")
162+
}
163+
if len(request.Providers) == 0 {
164+
return fmt.Errorf("invalid request: no providers")
165+
}
166+
if _, err := common.ValidateDuration(request.Duration); err != nil {
167+
return fmt.Errorf("invalid request: duration must be greater than zero")
168+
}
149169
return nil
150170
}
151171

cmd/cli/wizard.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func RunRequestWizard(config *config.Config) (*models.ElevateRequest, error) {
3030
if err != nil {
3131
return nil, err
3232
}
33-
data.Provider = provider
33+
data.Providers = []string{provider}
3434

3535
// Step 2: Select Role (filtered by provider)
3636
role, err := selectRole(config, provider)
@@ -341,7 +341,7 @@ func displaySummary(data *models.ElevateRequest) {
341341
fmt.Println(successStyle.Render("Request Configuration Complete!"))
342342
fmt.Println()
343343

344-
fmt.Printf("Provider: %s\n", data.Provider)
344+
fmt.Printf("Providers: %s\n", data.Providers)
345345
fmt.Printf("Role: %s\n", data.Role.Name)
346346
fmt.Printf("Duration: %s\n", data.Duration)
347347
fmt.Printf("Reason: %s\n", data.Reason)

internal/config/model.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -510,25 +510,33 @@ func (r *Config) GetWorkflowFromElevationRequest(elevationRequest *models.Elevat
510510
return nil, fmt.Errorf("role is nil")
511511
}
512512

513-
if len(elevationRequest.Provider) == 0 {
514-
return nil, fmt.Errorf("provider is nil")
513+
if len(elevationRequest.Providers) == 0 {
514+
return nil, fmt.Errorf("providers are empty")
515515
}
516516

517+
primaryProvider := strings.ToLower(elevationRequest.Providers[0])
518+
517519
roleName := strings.ToLower(elevationRequest.Role.Name)
518-
providerName := strings.ToLower(elevationRequest.Provider)
520+
providerName := strings.ToLower(primaryProvider)
521+
workflowName := strings.ToLower(elevationRequest.Workflow)
519522

520523
role := elevationRequest.Role
521524

525+
if len(workflowName) == 0 {
526+
// If no workflow is specified, use the first workflow associated with the role
527+
if len(role.Workflows) == 0 {
528+
return nil, fmt.Errorf("no workflow specified and role has no associated workflows")
529+
}
530+
531+
workflowName = role.Workflows[0]
532+
}
533+
522534
if !slices.Contains(role.Providers, providerName) {
523535
return nil, fmt.Errorf("provider '%s' not allowed for role '%s', roles: %v", providerName, roleName, role.Providers)
524536
}
525537

526-
// Get the workflow for elevation
527-
workflowName := strings.ToLower(role.Workflow)
528-
529-
// TODO: If this is empty then default to the default workflow
530-
if len(workflowName) == 0 {
531-
return nil, fmt.Errorf("workflow is nil for role '%s'", roleName)
538+
if !slices.Contains(role.Workflows, workflowName) {
539+
return nil, fmt.Errorf("workflow '%s' not allowed for role '%s', workflows: %v", workflowName, roleName, role.Workflows)
532540
}
533541

534542
workflow, foundWorkflow := r.Workflows.Definitions[workflowName]

internal/daemon/auth.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,26 +172,16 @@ func (s *Server) getAuthCallbackPage(c *gin.Context, auth models.AuthWrapper) {
172172
}
173173

174174
// Covert our sensitive session to one we can store on the users local system
175-
localSession := &models.LocalSession{
176-
Version: 1,
177-
Expiry: session.Expiry.UTC(),
178-
Session: session.GetEncodedSession(
179-
s.Config.GetServices().GetEncryption(),
180-
),
181-
}
175+
localSession := session.ToLocalSession(s.Config.GetServices().GetEncryption())
182176

183177
data := AuthCallbackPageData{
184178
TemplateData: s.GetTemplateData(c),
185179
Auth: auth,
186180
Session: localSession,
187181
}
188182

189-
cookie := sessions.Default(c)
190-
cookie.Set(auth.Provider, localSession.GetEncodedLocalSession())
191-
err = cookie.Save()
192-
193-
if err != nil {
194-
s.getErrorPage(c, http.StatusInternalServerError, "Failed to set cookie", err)
183+
if err := s.setAuthCookie(c, auth.Provider, localSession); err != nil {
184+
s.getErrorPage(c, http.StatusInternalServerError, "Failed to set auth cookie", err)
195185
return
196186
}
197187

internal/daemon/cookies.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package daemon
2+
3+
import (
4+
"github.com/gin-contrib/sessions"
5+
"github.com/gin-gonic/gin"
6+
"github.com/thand-io/agent/internal/models"
7+
)
8+
9+
func (s *Server) setAuthCookie(c *gin.Context, authProvider string, localSession *models.LocalSession) error {
10+
11+
cookie := sessions.Default(c)
12+
cookie.Set(authProvider, localSession.GetEncodedLocalSession())
13+
err := cookie.Save()
14+
15+
return err
16+
17+
}

internal/daemon/elevate.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,23 @@ func (s *Server) getElevate(c *gin.Context) {
3434
return
3535
}
3636

37+
primaryWorkflow := request.Workflow
38+
39+
if len(primaryWorkflow) == 0 {
40+
if len(role.Workflows) == 0 {
41+
s.getErrorPage(c, http.StatusBadRequest, "No workflow specified and role has no associated workflows", err)
42+
return
43+
}
44+
primaryWorkflow = role.Workflows[0]
45+
}
46+
3747
s.elevate(c, models.ElevateRequest{
38-
Role: role,
39-
Provider: request.Provider,
40-
Reason: request.Reason,
41-
Duration: request.Duration,
42-
Session: request.Session,
48+
Role: role,
49+
Providers: []string{request.Provider},
50+
Workflow: primaryWorkflow,
51+
Reason: request.Reason,
52+
Duration: request.Duration,
53+
Session: request.Session,
4354
})
4455
}
4556

@@ -129,7 +140,7 @@ func (s *Server) handleDynamicRequest(c *gin.Context, dynamicRequest models.Elev
129140
dynamicRole := &models.Role{
130141
Name: "dynamic-role-" + time.Now().Format("20060102-150405"),
131142
Description: "Dynamically created role: " + dynamicRequest.Reason,
132-
Workflow: dynamicRequest.Workflow,
143+
Workflows: []string{dynamicRequest.Workflow},
133144
Permissions: models.Permissions{
134145
Allow: dynamicRequest.Permissions,
135146
},
@@ -147,11 +158,12 @@ func (s *Server) handleDynamicRequest(c *gin.Context, dynamicRequest models.Elev
147158

148159
// Convert to standard ElevateRequest
149160
elevateRequest := models.ElevateRequest{
150-
Role: dynamicRole,
151-
Provider: dynamicRequest.Providers[0], // Use first provider for now
152-
Reason: dynamicRequest.Reason,
153-
Duration: dynamicRequest.Duration,
154-
Session: nil, // Session will be handled by the workflow if needed
161+
Role: dynamicRole,
162+
Providers: dynamicRequest.Providers, // Use first provider for now
163+
Workflow: dynamicRequest.Workflow,
164+
Reason: dynamicRequest.Reason,
165+
Duration: dynamicRequest.Duration,
166+
Session: nil, // Session will be handled by the workflow if needed
155167
}
156168

157169
s.elevate(c, elevateRequest)
@@ -164,7 +176,36 @@ func (s *Server) elevate(c *gin.Context, request models.ElevateRequest) {
164176

165177
ctx := context.Background()
166178

167-
WorkflowTask, err := s.Workflows.CreateWorkflow(ctx, request)
179+
// If we have a web session and one hasn't been set then
180+
// lets attach a user session to the request.
181+
if s.Config.IsServer() {
182+
183+
// Get the auth provider from the workflow if set
184+
authProvider := []string{}
185+
186+
if len(request.Workflow) > 0 {
187+
workflowDef, err := s.Config.GetWorkflowByName(request.Workflow)
188+
if err != nil {
189+
s.getErrorPage(c, http.StatusBadRequest, "Invalid workflow specified", err)
190+
return
191+
}
192+
authProvider = []string{workflowDef.GetAuthentication()}
193+
}
194+
195+
foundUser, err := s.getUser(c, authProvider...)
196+
197+
if err != nil {
198+
s.getErrorPage(c, http.StatusUnauthorized, "Unauthorized: unable to get user for list of available roles", err)
199+
return
200+
}
201+
202+
if foundUser != nil {
203+
request.Session = foundUser.ToLocalSession(s.Config.GetServices().GetEncryption())
204+
}
205+
206+
}
207+
208+
workflowTask, err := s.Workflows.CreateWorkflow(ctx, request)
168209

169210
if err != nil {
170211
s.getErrorPage(c, http.StatusBadRequest, "Failed to execute workflow", err)
@@ -173,7 +214,7 @@ func (s *Server) elevate(c *gin.Context, request models.ElevateRequest) {
173214

174215
// We now redirect the user to the next workflow step.
175216
c.Redirect(http.StatusTemporaryRedirect,
176-
WorkflowTask.GetRedirectURL(),
217+
workflowTask.GetRedirectURL(),
177218
)
178219
}
179220

@@ -289,6 +330,13 @@ func (s *Server) getElevateAuthOAuth2(c *gin.Context) {
289330

290331
workflowTask.SetUser(session.User)
291332

333+
localSession := session.ToLocalSession(s.Config.GetServices().GetEncryption())
334+
335+
if err := s.setAuthCookie(c, authProvider, localSession); err != nil {
336+
s.getErrorPage(c, http.StatusInternalServerError, "Failed to set auth cookie", err)
337+
return
338+
}
339+
292340
s.resumeWorkflow(c, workflowTask)
293341

294342
}

internal/daemon/elevate/llm/main.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,13 @@ func buildElevateRequest(role *models.Role, evaluationResponse *ElevationRequest
144144
}
145145

146146
role.Providers = []string{evaluationResponse.Provider}
147-
role.Workflow = evaluationResponse.Workflow
147+
role.Workflows = []string{evaluationResponse.Workflow}
148148

149149
return &models.ElevateRequest{
150-
Role: role,
151-
Provider: evaluationResponse.Provider,
152-
Reason: reason,
153-
Duration: evaluationResponse.Duration.String(),
150+
Role: role,
151+
Providers: []string{evaluationResponse.Provider},
152+
Workflow: evaluationResponse.Workflow,
153+
Reason: reason,
154+
Duration: evaluationResponse.Duration.String(),
154155
}
155156
}

internal/daemon/executions.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func (s *Server) getRunningWorkflow(c *gin.Context) {
168168
elevationReq, err := workflowInfo.GetContextAsElevationRequest()
169169

170170
if err == nil && elevationReq != nil {
171-
workflowName = elevationReq.Role.Workflow
171+
workflowName = elevationReq.Workflow
172172
}
173173

174174
}
@@ -197,7 +197,8 @@ func (s *Server) getRunningWorkflow(c *gin.Context) {
197197
workflowInfo.Output = err.Error()
198198
}
199199

200-
workflowInfo.Status = swctx.StatusPhase(strings.ToLower(workflowExecInfo.Status))
200+
workflowInfo.Status = swctx.StatusPhase(
201+
strings.ToLower(workflowExecInfo.Status))
201202

202203
}
203204

internal/daemon/static/elevate_llm.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ <h2 class="section-title">AI Generated Role</h2>
155155
const role = generatedRole;
156156

157157
// Set workflow (use first workflow from generated role if available, otherwise default)
158-
document.getElementById('hiddenWorkflow').value = role.workflow || 'default-workflow';
158+
document.getElementById('hiddenWorkflow').value = role.workflow || '';
159159

160160
// Set providers from the generated role
161161
const providersContainer = document.getElementById('hiddenProviders');

0 commit comments

Comments
 (0)