Skip to content

Commit c6a5c1a

Browse files
authored
Merge pull request #84 from Infisical/feat/pam-approval-flow
feat: implemented approval flow for pam access
2 parents cf9a863 + d860628 commit c6a5c1a

File tree

4 files changed

+112
-0
lines changed

4 files changed

+112
-0
lines changed

packages/api/api.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const (
4848
operationCallGetOrgRelays = "CallGetOrgRelays"
4949
operationCallRegisterGateway = "CallRegisterGateway"
5050
operationCallPAMAccess = "CallPAMAccess"
51+
operationCallPAMAccessApprovalRequest = "CallPAMAccessApprovalRequest"
5152
operationCallPAMSessionCredentials = "CallPAMSessionCredentials"
5253
operationCallGetPamSessionKey = "CallGetPamSessionKey"
5354
operationCallUploadPamSessionLog = "CallUploadPamSessionLog"
@@ -865,6 +866,26 @@ func CallPAMAccess(httpClient *resty.Client, request PAMAccessRequest) (PAMAcces
865866
return pamAccessResponse, nil
866867
}
867868

869+
func CallPAMAccessApprovalRequest(httpClient *resty.Client, request PAMAccessApprovalRequest) (PAMAccessApprovalRequestResponse, error) {
870+
var pamAccessApprovalRequestResponse PAMAccessApprovalRequestResponse
871+
response, err := httpClient.
872+
R().
873+
SetResult(&pamAccessApprovalRequestResponse).
874+
SetHeader("User-Agent", USER_AGENT).
875+
SetBody(request).
876+
Post(fmt.Sprintf("%v/v1/approval-policies/pam-access/requests", config.INFISICAL_URL))
877+
878+
if err != nil {
879+
return PAMAccessApprovalRequestResponse{}, NewGenericRequestError(operationCallPAMAccessApprovalRequest, err)
880+
}
881+
882+
if response.IsError() {
883+
return PAMAccessApprovalRequestResponse{}, NewAPIErrorWithResponse(operationCallPAMAccessApprovalRequest, response, nil)
884+
}
885+
886+
return pamAccessApprovalRequestResponse, nil
887+
}
888+
868889
func CallPAMSessionCredentials(httpClient *resty.Client, sessionId string) (PAMSessionCredentialsResponse, error) {
869890
var pamSessionCredentialsResponse PAMSessionCredentialsResponse
870891
response, err := httpClient.

packages/api/model.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,24 @@ type PAMAccessResponse struct {
790790
Metadata map[string]string `json:"metadata,omitempty"`
791791
}
792792

793+
type PAMAccessApprovalRequestPayloadRequestData struct {
794+
AccountPath string `json:"accountPath"`
795+
AccessDuration string `json:"accessDuration"`
796+
}
797+
798+
type PAMAccessApprovalRequest struct {
799+
ProjectId string `json:"projectId"`
800+
RequestData PAMAccessApprovalRequestPayloadRequestData `json:"requestData"`
801+
}
802+
803+
type PAMAccessApprovalRequestResponse struct {
804+
Request struct {
805+
ID string `json:"id"`
806+
ProjectId string `json:"projectId"`
807+
OrgId string `json:"organizationId"`
808+
} `json:"request"`
809+
}
810+
793811
type PAMSessionCredentialsResponse struct {
794812
Credentials PAMSessionCredentials `json:"credentials"`
795813
}

packages/pam/local/database-proxy.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@ package pam
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78
"net"
89
"os"
910
"os/signal"
11+
"strings"
1012
"syscall"
1113
"time"
1214

1315
"github.com/Infisical/infisical-merge/packages/api"
16+
"github.com/Infisical/infisical-merge/packages/config"
1417
"github.com/Infisical/infisical-merge/packages/pam/session"
1518
"github.com/Infisical/infisical-merge/packages/util"
1619
"github.com/go-resty/resty/v2"
20+
"github.com/manifoldco/promptui"
1721
"github.com/rs/zerolog/log"
1822
)
1923

@@ -30,6 +34,18 @@ const (
3034
ALPNInfisicalPAMCancellation ALPN = "infisical-pam-session-cancellation"
3135
)
3236

37+
func askForApprovalRequestTrigger() (bool, error) {
38+
prompt := promptui.Prompt{
39+
Label: "This action requires approval. You may create an approval request now. Continue?",
40+
IsConfirm: true,
41+
}
42+
result, err := prompt.Run()
43+
if err != nil {
44+
return false, err
45+
}
46+
return strings.ToLower(result) == "y", nil
47+
}
48+
3349
func StartDatabaseLocalProxy(accessToken string, accountPath string, projectID string, durationStr string, port int) {
3450
log.Info().Msgf("Starting database proxy for account: %s", accountPath)
3551
log.Info().Msgf("Session duration: %s", durationStr)
@@ -46,6 +62,48 @@ func StartDatabaseLocalProxy(accessToken string, accountPath string, projectID s
4662

4763
pamResponse, err := api.CallPAMAccess(httpClient, pamRequest)
4864
if err != nil {
65+
var apiErr *api.APIError
66+
if errors.As(err, &apiErr) && apiErr.ErrorMessage == "A policy is in place for this resource" {
67+
if v, ok := apiErr.Details.(map[string]any); ok {
68+
log.Info().Msgf("Account is protected by approval policy: %s", v["policyName"])
69+
70+
shouldSendRequest, err := askForApprovalRequestTrigger()
71+
if err != nil {
72+
if errors.Is(err, promptui.ErrAbort) {
73+
log.Info().Msgf("Approval request was not created.")
74+
} else {
75+
util.HandleError(err, "Failed to send PAM account request")
76+
}
77+
return
78+
}
79+
80+
if !shouldSendRequest {
81+
log.Info().Msgf("Approval request was not created.")
82+
return
83+
}
84+
85+
approvalReq, err := api.CallPAMAccessApprovalRequest(httpClient, api.PAMAccessApprovalRequest{
86+
ProjectId: projectID,
87+
RequestData: api.PAMAccessApprovalRequestPayloadRequestData{
88+
AccountPath: accountPath,
89+
AccessDuration: durationStr,
90+
},
91+
})
92+
if err != nil {
93+
util.HandleError(err, "Failed to send PAM account request")
94+
return
95+
}
96+
97+
url := fmt.Sprintf("%s/organizations/%s/projects/pam/%s/approval-requests/%s", strings.TrimSuffix(config.INFISICAL_URL, "/api"), approvalReq.Request.OrgId, approvalReq.Request.ProjectId, approvalReq.Request.ID)
98+
if err := util.OpenBrowser(url); err != nil {
99+
log.Error().Msgf("Failed to do browser redirect: %v", err)
100+
}
101+
log.Info().Msgf("Approval request created.")
102+
log.Info().Msgf("View details at: %s", url)
103+
return
104+
}
105+
}
106+
49107
util.HandleError(err, "Failed to access PAM account")
50108
return
51109
}

packages/util/helper.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"os/exec"
1313
"path"
14+
"runtime"
1415
"sort"
1516
"strings"
1617
"sync"
@@ -558,3 +559,17 @@ func GenerateETagFromSecrets(secrets []models.SingleEnvironmentVariable) string
558559
func IsDevelopmentMode() bool {
559560
return CLI_VERSION == "devel"
560561
}
562+
563+
// OpenBrowser attempts to open a URL in the user's default browser
564+
func OpenBrowser(url string) error {
565+
var cmd *exec.Cmd
566+
switch runtime.GOOS {
567+
case "darwin":
568+
cmd = exec.Command("open", url)
569+
case "windows":
570+
cmd = exec.Command("cmd", "/c", "start", url)
571+
default: // linux and others
572+
cmd = exec.Command("xdg-open", url)
573+
}
574+
return cmd.Start()
575+
}

0 commit comments

Comments
 (0)