Prevent running plan or apply without permissions

This commit is contained in:
Sander van Harmelen 2018-10-04 22:53:56 +02:00
parent 53d322ec69
commit ffc67a8e90
5 changed files with 121 additions and 14 deletions

View File

@ -22,6 +22,11 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
return nil, generalError("error retrieving workspace", err)
}
if !w.Permissions.CanUpdate {
return nil, fmt.Errorf(strings.TrimSpace(
fmt.Sprintf(applyErrNoUpdateRights, b.hostname, b.organization, op.Workspace)))
}
if w.VCSRepo != nil {
return nil, fmt.Errorf(strings.TrimSpace(applyErrVCSNotSupported))
}
@ -76,6 +81,9 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
return r, nil
}
// Since we already checked the permissions before creating the run
// this should never happen. But it doesn't hurt to keep this in as
// a safeguard for any unexpected situations.
if !r.Permissions.CanApply {
// Make sure we discard the run if possible.
if r.Actions.IsDiscardable {
@ -261,10 +269,20 @@ func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *t
return nil
}
const applyErrVCSNotSupported = `
Apply not allowed for workspaces with a VCS connection!
const applyErrNoUpdateRights = `
Insufficient rights to apply changes!
A workspace that is connected to a VCS requires the VCS based workflow
[reset][yellow]The provided credentials have insufficient rights to apply changes. In order
to apply changes at least write permissions on the workspace are required. To
queue a run that can be approved by someone else, please use the 'Queue Plan'
button in the web UI:
https://%s/app/%s/%s/runs[reset]
`
const applyErrVCSNotSupported = `
Apply not allowed for workspaces with a VCS connection.
A workspace that is connected to a VCS requires the VCS-driven workflow
to ensure that the VCS remains the single source of truth.
`
@ -293,10 +311,10 @@ does not require any configuration files.
const applyErrNoApplyRights = `
Insufficient rights to approve the pending changes!
[reset][yellow]There are pending changes, but the used credentials have insufficient rights
to approve them. The run will be discarded to prevent it from blocking the
queue waiting for external approval. To trigger a run that can be approved by
someone else, please use the 'Queue Plan' button in the web UI:
[reset][yellow]There are pending changes, but the provided credentials have insufficient rights
to approve them. The run will be discarded to prevent it from blocking the queue
waiting for external approval. To queue a run that can be approved by someone
else, please use the 'Queue Plan' button in the web UI:
https://%s/app/%s/%s/runs[reset]
`

View File

@ -61,10 +61,47 @@ func TestRemote_applyBasic(t *testing.T) {
}
}
func TestRemote_applyWithoutPermissions(t *testing.T) {
b := testBackendNoDefault(t)
// Create a named workspace without permissions.
w, err := b.client.Workspaces.Create(
context.Background(),
b.organization,
tfe.WorkspaceCreateOptions{
Name: tfe.String(b.prefix + "prod"),
},
)
if err != nil {
t.Fatalf("error creating named workspace: %v", err)
}
w.Permissions.CanUpdate = false
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
defer modCleanup()
op := testOperationApply()
op.Module = mod
op.Workspace = "prod"
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "insufficient rights to apply changes") {
t.Fatalf("expected a permissions error, got: %v", run.Err)
}
}
func TestRemote_applyWithVCS(t *testing.T) {
b := testBackendNoDefault(t)
// Create the named workspace with a VCS.
// Create a named workspace with a VCS.
_, err := b.client.Workspaces.Create(
context.Background(),
b.organization,

View File

@ -853,6 +853,10 @@ func (m *mockWorkspaces) Create(ctx context.Context, organization string, option
w := &tfe.Workspace{
ID: generateID("ws-"),
Name: *options.Name,
Permissions: &tfe.WorkspacePermissions{
CanQueueRun: true,
CanUpdate: true,
},
}
if options.VCSRepo != nil {
w.VCSRepo = &tfe.VCSRepo{}

View File

@ -20,6 +20,16 @@ import (
func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation) (*tfe.Run, error) {
log.Printf("[INFO] backend/remote: starting Plan operation")
// Retrieve the workspace used to run this operation in.
w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace)
if err != nil {
return nil, generalError("error retrieving workspace", err)
}
if !w.Permissions.CanQueueRun {
return nil, fmt.Errorf(strings.TrimSpace(fmt.Sprintf(planErrNoQueueRunRights)))
}
if op.Plan != nil {
return nil, fmt.Errorf(strings.TrimSpace(planErrPlanNotSupported))
}
@ -36,12 +46,6 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio
return nil, fmt.Errorf(strings.TrimSpace(planErrNoConfig))
}
// Retrieve the workspace used to run this operation in.
w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace)
if err != nil {
return nil, generalError("error retrieving workspace", err)
}
return b.plan(stopCtx, cancelCtx, op, w)
}
@ -192,6 +196,13 @@ func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation,
return r, nil
}
const planErrNoQueueRunRights = `
Insufficient rights to generate a plan!
[reset][yellow]The provided credentials have insufficient rights to generate a plan. In order
to generate plans, at least plan permissions on the workspace are required.[reset]
`
const planErrPlanNotSupported = `
Displaying a saved plan is currently not supported!

View File

@ -48,6 +48,43 @@ func TestRemote_planBasic(t *testing.T) {
}
}
func TestRemote_planWithoutPermissions(t *testing.T) {
b := testBackendNoDefault(t)
// Create a named workspace without permissions.
w, err := b.client.Workspaces.Create(
context.Background(),
b.organization,
tfe.WorkspaceCreateOptions{
Name: tfe.String(b.prefix + "prod"),
},
)
if err != nil {
t.Fatalf("error creating named workspace: %v", err)
}
w.Permissions.CanQueueRun = false
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op.Workspace = "prod"
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "insufficient rights to generate a plan") {
t.Fatalf("expected a permissions error, got: %v", run.Err)
}
}
func TestRemote_planWithPlan(t *testing.T) {
b := testBackendDefault(t)