diff --git a/internal/cmds/cmds.go b/internal/cmds/cmds.go index 43a1008..0e0a427 100755 --- a/internal/cmds/cmds.go +++ b/internal/cmds/cmds.go @@ -266,7 +266,8 @@ func enable(ctx *log.Context, h types.HandlerEnvironment, report *types.RunComma blobCreateOrReplaceError := "Error creating AppendBlob '%s' using SAS token or Managed identity. Please use a valid blob SAS URI with [read, append, create, write] permissions OR managed identity. If managed identity is used, make sure Azure blob and identity exist, and identity has been given access to storage blob's container with 'Storage Blob Data Contributor' role assignment. In case of user-assigned identity, make sure you add it under VM's identity and provide outputBlobUri / errorBlobUri and corresponding clientId in outputBlobManagedIdentity / errorBlobManagedIdentity parameter(s). In case of system-assigned identity, do not use outputBlobManagedIdentity / errorBlobManagedIdentity parameter(s). For more info, refer https://aka.ms/RunCommandManagedLinux" - // TO-DO: disable output blob if the policy settings has disableOutputBlobs set to true. + // DisableOutputBlobs (policy settings) should have already been validated in ValidateHandlerSettingsAgainstPolicy, + // so we don't need to check it again. If a blob URI was passed in, we assume disableOutputBlobs == false. var outputBlobSASRef *storage.Blob var outputBlobAppendClient *appendblob.Client var outputBlobAppendCreateOrReplaceError error diff --git a/internal/cmds/cmds_test.go b/internal/cmds/cmds_test.go index e7e5b00..a9530d8 100755 --- a/internal/cmds/cmds_test.go +++ b/internal/cmds/cmds_test.go @@ -1516,7 +1516,7 @@ func Test_downloadScript_AllowedByAllowlist(t *testing.T) { require.NoError(t, err) } -func setupPolicyE2E(t *testing.T, dataDir, extName string, seqNum int, scriptURI string, treatFailureAsDeploymentFailure bool, policy *extensionpolicysettingsrc.RCv2ExtensionPolicySettings, +func setupPolicyE2E(t *testing.T, dataDir, extName string, seqNum int, scriptURI string, treatFailureAsDeploymentFailure bool, outputBlobURI string, errorBlobURI string, policy *extensionpolicysettingsrc.RCv2ExtensionPolicySettings, ) types.HandlerEnvironment { t.Helper() configFolder := create_folder(t, dataDir, "config") @@ -1537,6 +1537,8 @@ func setupPolicyE2E(t *testing.T, dataDir, extName string, seqNum int, scriptURI "scriptUri": scriptURI, "scriptType": string(handlersettings.DownloadedScript), }, + "OutputBlobURI": outputBlobURI, + "ErrorBlobURI": errorBlobURI, "treatFailureAsDeploymentFailure": treatFailureAsDeploymentFailure, }, } @@ -1606,7 +1608,7 @@ func Test_enable_e2e_extension_policy_settings_pass(t *testing.T) { DownloadedScriptsAllowlist: []string{correctHash}, } // Policy will be marshaled and written to a file in the config folder. - fakeEnv := setupPolicyE2E(t, dataDir, extName, seqNum, srv.URL+"/script.sh", false, policy) + fakeEnv := setupPolicyE2E(t, dataDir, extName, seqNum, srv.URL+"/script.sh", false, "", "", policy) scriptWasExecuted := false RunCmd = func(ctx *log.Context, dir, scriptFilePath string, cfg *handlersettings.HandlerSettings, metadata types.RCMetadata) (error, int) { @@ -1642,7 +1644,7 @@ func Test_enable_e2e_extension_policy_settings_block_statussuccess(t *testing.T) DownloadedScriptsAllowlist: []string{"000000000000"}, } // Policy will be marshaled and written to a file in the config folder. - fakeEnv := setupPolicyE2E(t, dataDir, extName, seqNum, srv.URL+"/script.sh", false, policy) + fakeEnv := setupPolicyE2E(t, dataDir, extName, seqNum, srv.URL+"/script.sh", false, "", "", policy) scriptWasExecuted := false RunCmd = func(ctx *log.Context, dir, scriptFilePath string, cfg *handlersettings.HandlerSettings, metadata types.RCMetadata) (error, int) { @@ -1678,7 +1680,7 @@ func Test_enable_e2e_extension_policy_settings_block_statusfail(t *testing.T) { DownloadedScriptsAllowlist: []string{"000000000000"}, } // treatFailureAsDeploymentFailure set to true - fakeEnv := setupPolicyE2E(t, dataDir, extName, seqNum, srv.URL+"/script.sh", true, policy) + fakeEnv := setupPolicyE2E(t, dataDir, extName, seqNum, srv.URL+"/script.sh", true, "", "", policy) scriptWasExecuted := false RunCmd = func(ctx *log.Context, dir, scriptFilePath string, cfg *handlersettings.HandlerSettings, metadata types.RCMetadata) (error, int) { @@ -1694,3 +1696,40 @@ func Test_enable_e2e_extension_policy_settings_block_statusfail(t *testing.T) { require.Equal(t, types.StatusError, report[0].Status.Status, "status report should indicate failure") require.True(t, strings.Contains(report[0].Status.FormattedMessage.Message, "executionState\":\"Failed\",\"executionMessage\":\"Execution failed"), "execution message should indicate failure") } + +func Test_enable_e2e_extension_policy_settings_block_statussuccess_disableOutputBlobs(t *testing.T) { + ctx := log.NewContext(log.NewNopLogger()) + extName, seqNum := "badPolicyRun", 0 + scriptContent := []byte("#!/bin/bash\necho hello\n") + + srv := make_server_with_content(scriptContent) + defer srv.Close() + + dataDir, err := os.MkdirTemp("", "policy-pass") + require.Nil(t, err) + defer os.RemoveAll(dataDir) + + policy := &extensionpolicysettingsrc.RCv2ExtensionPolicySettings{ + LimitScripts: "alloweddownloaded", + DownloadedScriptsAllowlist: []string{"000000000000"}, + DisableOutputBlobs: true, + } + // Policy will be marshaled and written to a file in the config folder. + fakeEnv := setupPolicyE2E(t, dataDir, extName, seqNum, srv.URL+"/script.sh", false, "https://example.com/outputBlob", "", policy) + + scriptWasExecuted := false + RunCmd = func(ctx *log.Context, dir, scriptFilePath string, cfg *handlersettings.HandlerSettings, metadata types.RCMetadata) (error, int) { + scriptWasExecuted = true + return nil, 0 + } + + err = commandProcessor.ProcessHandlerCommandWithDetails(ctx, CmdEnable, fakeEnv, extName, seqNum, constants.DownloadFolder, dataDir) + require.Nil(t, err, "enable command should succeed") + require.False(t, scriptWasExecuted, "script should not be executed because output blobs are disabled") + + report := readStatusReport(t, fakeEnv, extName, seqNum) // verify status report exists and is valid + require.Equal(t, types.StatusSuccess, report[0].Status.Status, "status report should indicate success") + require.True(t, strings.Contains(report[0].Status.FormattedMessage.Message, "executionState\":\"Failed\",\"executionMessage\":\"Execution failed"), "execution message should indicate failure") + require.True(t, strings.Contains(report[0].Status.FormattedMessage.Message, "output blobs are disabled in policy, but settings specify"), "execution message should indicate failure") + +} diff --git a/internal/constants/exitcodes.go b/internal/constants/exitcodes.go index 6ce07ca..5eb12fc 100755 --- a/internal/constants/exitcodes.go +++ b/internal/constants/exitcodes.go @@ -5,13 +5,14 @@ const ( ExitCode_Okay = 0 // User errors (-100s): - ExitCode_ScriptBlobDownloadFailed = -100 - ExitCode_BlobCreateOrReplaceFailed = -101 - ExitCode_RunAsLookupUserFailed = -102 - ExitCode_ScriptTypeNotAllowedByExtensionPolicy = -103 - ExitCode_CommandIdNotAllowedByExtensionPolicy = -104 - ExitCode_RunAsUserNotAllowedByExtensionPolicy = -105 - ExitCode_DownloadedScriptBlockedByExtensionPolicy = -106 + ExitCode_ScriptBlobDownloadFailed = -100 + ExitCode_BlobCreateOrReplaceFailed = -101 + ExitCode_RunAsLookupUserFailed = -102 + ExitCode_ScriptTypeNotAllowedByExtensionPolicy = -103 + ExitCode_CommandIdNotAllowedByExtensionPolicy = -104 + ExitCode_RunAsUserNotAllowedByExtensionPolicy = -105 + ExitCode_DownloadedScriptBlockedByExtensionPolicy = -106 + ExitCode_OutputBlobSpecifiedButNotAllowedByExtensionPolicy = -107 // Service Errors (-200s): ExitCode_CreateDataDirectoryFailed = -200 diff --git a/internal/extensionpolicysettingsrc/extensionpolicysettingsrc.go b/internal/extensionpolicysettingsrc/extensionpolicysettingsrc.go index 0ff0a6f..df2f900 100644 --- a/internal/extensionpolicysettingsrc/extensionpolicysettingsrc.go +++ b/internal/extensionpolicysettingsrc/extensionpolicysettingsrc.go @@ -7,6 +7,7 @@ import ( "github.com/Azure/azure-extension-platform/pkg/extensionpolicysettings" "github.com/Azure/run-command-handler-linux/internal/constants" "github.com/Azure/run-command-handler-linux/internal/handlersettings" + "github.com/Azure/run-command-handler-linux/pkg/download" "github.com/go-kit/kit/log" "github.com/pkg/errors" ) @@ -59,8 +60,13 @@ func ValidateHandlerSettingsAgainstPolicy(ctx *log.Context, settings *handlerset return err, constants.ExitCode_RunAsUserNotAllowedByExtensionPolicy } } + if policy.DisableOutputBlobs { + if err := ValidateDisableOutputBlobs(ctx, settings, policy); err != nil { + return err, constants.ExitCode_OutputBlobSpecifiedButNotAllowedByExtensionPolicy + } + } - // TO-DO: Validate Disable Outputblob and RequireSigning once those features are implemented for RCv2. + // TO-DO: Validate RequireSigning once that feature is implemented for RCv2. return nil, 0 } @@ -104,3 +110,17 @@ func ValidateRunAsUser(ctx *log.Context, settings *handlersettings.HandlerSettin } return nil } + +func ValidateDisableOutputBlobs(ctx *log.Context, settings *handlersettings.HandlerSettings, policy *RCv2ExtensionPolicySettings) error { + if policy.DisableOutputBlobs { + trimmedOutputBlobURI := strings.TrimSpace(settings.OutputBlobURI) + trimmedErrorBlobURI := strings.TrimSpace(settings.ErrorBlobURI) + + if trimmedOutputBlobURI != "" || trimmedErrorBlobURI != "" { + err := fmt.Errorf("output blobs are disabled in policy, but settings specify outputBlobURI '%s' or errorBlobURI '%s'", download.GetUriForLogging(settings.OutputBlobURI), download.GetUriForLogging(settings.ErrorBlobURI)) + ctx.Log("message", "output blobs are disabled in policy, but settings specify output or error blob URIs", "error", err, "outputBlobURI", download.GetUriForLogging(settings.OutputBlobURI), "errorBlobURI", download.GetUriForLogging(settings.ErrorBlobURI)) + return err + } + } + return nil +} diff --git a/internal/extensionpolicysettingsrc/extensionpolicysettingsrc_test.go b/internal/extensionpolicysettingsrc/extensionpolicysettingsrc_test.go index 54f938b..3a7fd00 100644 --- a/internal/extensionpolicysettingsrc/extensionpolicysettingsrc_test.go +++ b/internal/extensionpolicysettingsrc/extensionpolicysettingsrc_test.go @@ -5,13 +5,15 @@ import ( "path/filepath" "testing" + "fmt" + "github.com/Azure/run-command-handler-linux/internal/constants" "github.com/Azure/run-command-handler-linux/internal/handlersettings" "github.com/go-kit/kit/log" "github.com/stretchr/testify/require" ) -func makeSettings(scriptType handlersettings.ScriptType, commandID string, runAsUser string, outputBlobURI string) *handlersettings.HandlerSettings { +func makeSettings(scriptType handlersettings.ScriptType, commandID string, runAsUser string, outputBlobURI string, errorBlobURI string) *handlersettings.HandlerSettings { return &handlersettings.HandlerSettings{ PublicSettings: handlersettings.PublicSettings{ Source: &handlersettings.ScriptSource{ @@ -20,6 +22,7 @@ func makeSettings(scriptType handlersettings.ScriptType, commandID string, runAs }, RunAsUser: runAsUser, OutputBlobURI: outputBlobURI, + ErrorBlobURI: errorBlobURI, }, } } @@ -85,7 +88,7 @@ func TestInitializeExtensionPolicySettings_PopulatesOutputStruct(t *testing.T) { // Test that validation passes and fails as expected. func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { t.Run("nil policy", func(t *testing.T) { - settings := makeSettings(handlersettings.InlineScript, "", "", "") + settings := makeSettings(handlersettings.InlineScript, "", "", "", "") err, exitCode := ValidateHandlerSettingsAgainstPolicy(nopCtx(), settings, nil) require.Error(t, err) require.Contains(t, err.Error(), "no policy provided to validate handler settings") @@ -95,7 +98,7 @@ func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { // This test mimicks running an inline script, but policy only allows gallery scripts. // Validation fails. t.Run("script type blocked by policy", func(t *testing.T) { - settings := makeSettings(handlersettings.InlineScript, "", "", "") + settings := makeSettings(handlersettings.InlineScript, "", "", "", "") policy := &RCv2ExtensionPolicySettings{ LimitScripts: "gallery", } @@ -109,7 +112,7 @@ func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { // This test mimicks running a commandId that is not in the allowlist. // Additionally, only commandId types are allowed. t.Run("command ID not in allowlist", func(t *testing.T) { - settings := makeSettings(handlersettings.CommandIdScript, "restartVM", "", "") + settings := makeSettings(handlersettings.CommandIdScript, "restartVM", "", "", "") policy := &RCv2ExtensionPolicySettings{ LimitScripts: "allowedcommandid", CommandIdAllowlist: []string{"safeCommand"}, @@ -121,7 +124,7 @@ func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { }) t.Run("runAs mismatch", func(t *testing.T) { - settings := makeSettings(handlersettings.InlineScript, "", "bob", "") + settings := makeSettings(handlersettings.InlineScript, "", "bob", "", "") policy := &RCv2ExtensionPolicySettings{ LimitScripts: "inline", RunAsUser: "alice", @@ -134,7 +137,7 @@ func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { }) t.Run("enforce limitScripts must be set. If not set, all commands fail", func(t *testing.T) { - settings := makeSettings(handlersettings.CommandIdScript, "safeCommand", " Alice ", "https://example/blob") + settings := makeSettings(handlersettings.CommandIdScript, "safeCommand", " Alice ", "https://example/blob", "https://example/errorBlob") policy := &RCv2ExtensionPolicySettings{ LimitScripts: "", CommandIdAllowlist: []string{"safeCommand"}, @@ -147,8 +150,8 @@ func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { require.Equal(t, constants.ExitCode_ScriptTypeNotAllowedByExtensionPolicy, exitCode) }) - t.Run("all checks pass commandId", func(t *testing.T) { - settings := makeSettings(handlersettings.CommandIdScript, "safeCommand", " Alice ", "https://example/blob") + t.Run("output blobs disabled, output blob provided", func(t *testing.T) { + settings := makeSettings(handlersettings.InlineScript, "", " Alice ", "https://example/blob", "") policy := &RCv2ExtensionPolicySettings{ LimitScripts: "allowall", CommandIdAllowlist: []string{"safeCommand"}, @@ -156,13 +159,42 @@ func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { DisableOutputBlobs: true, } + err, exitCode := ValidateHandlerSettingsAgainstPolicy(nopCtx(), settings, policy) + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("output blobs are disabled in policy, but settings specify outputBlobURI '%s' or errorBlobURI '%s'", settings.OutputBlobURI, settings.ErrorBlobURI)) + require.Equal(t, constants.ExitCode_OutputBlobSpecifiedButNotAllowedByExtensionPolicy, exitCode) + }) + + t.Run("all checks pass commandId", func(t *testing.T) { + settings := makeSettings(handlersettings.CommandIdScript, "safeCommand", " Alice ", "https://example/blob", "https://example/errorBlob") + policy := &RCv2ExtensionPolicySettings{ + LimitScripts: "allowall", + CommandIdAllowlist: []string{"safeCommand"}, + RunAsUser: "alice", + DisableOutputBlobs: false, + } + err, exitCode := ValidateHandlerSettingsAgainstPolicy(nopCtx(), settings, policy) require.NoError(t, err) require.Equal(t, 0, exitCode) }) t.Run("all checks pass downloadedScript", func(t *testing.T) { - settings := makeSettings(handlersettings.DownloadedScript, "safeCommand", " Alice ", "https://example/blob") + settings := makeSettings(handlersettings.DownloadedScript, "safeCommand", " Alice ", "https://example/blob", "https://example/errorBlob") + policy := &RCv2ExtensionPolicySettings{ + LimitScripts: "alloweddownloaded", + CommandIdAllowlist: []string{"safeCommand"}, + RunAsUser: "alice", + DisableOutputBlobs: false, + } + + err, exitCode := ValidateHandlerSettingsAgainstPolicy(nopCtx(), settings, policy) + require.NoError(t, err) + require.Equal(t, 0, exitCode) + }) + + t.Run("all checks pass disable output blobs", func(t *testing.T) { + settings := makeSettings(handlersettings.DownloadedScript, "safeCommand", " Alice ", "", "") policy := &RCv2ExtensionPolicySettings{ LimitScripts: "alloweddownloaded", CommandIdAllowlist: []string{"safeCommand"}, @@ -174,6 +206,7 @@ func TestValidateHandlerSettingsAgainstPolicy(t *testing.T) { require.NoError(t, err) require.Equal(t, 0, exitCode) }) + } func TestValidateScriptTypeAgainstPolicy(t *testing.T) { @@ -198,7 +231,7 @@ func TestValidateScriptTypeAgainstPolicy(t *testing.T) { func TestValidateCommandId(t *testing.T) { t.Run("empty allowlist allows all", func(t *testing.T) { - settings := makeSettings(handlersettings.CommandIdScript, "anything", "", "") + settings := makeSettings(handlersettings.CommandIdScript, "anything", "", "", "") policy := &RCv2ExtensionPolicySettings{ CommandIdAllowlist: nil, } @@ -207,7 +240,7 @@ func TestValidateCommandId(t *testing.T) { }) t.Run("value present in allowlist", func(t *testing.T) { - settings := makeSettings(handlersettings.CommandIdScript, "safeCommand", "", "") + settings := makeSettings(handlersettings.CommandIdScript, "safeCommand", "", "", "") policy := &RCv2ExtensionPolicySettings{ CommandIdAllowlist: []string{"safeCommand", "other"}, } @@ -216,7 +249,7 @@ func TestValidateCommandId(t *testing.T) { }) t.Run("value missing from allowlist", func(t *testing.T) { - settings := makeSettings(handlersettings.CommandIdScript, "restartVM", "", "") + settings := makeSettings(handlersettings.CommandIdScript, "restartVM", "", "", "") policy := &RCv2ExtensionPolicySettings{ CommandIdAllowlist: []string{"safeCommand", "other"}, } @@ -228,7 +261,7 @@ func TestValidateCommandId(t *testing.T) { func TestValidateRunAsUser(t *testing.T) { t.Run("match with whitespace and case differences", func(t *testing.T) { - settings := makeSettings(handlersettings.InlineScript, "", " Alice ", "") + settings := makeSettings(handlersettings.InlineScript, "", " Alice ", "", "") policy := &RCv2ExtensionPolicySettings{ RunAsUser: "alice", } @@ -237,7 +270,7 @@ func TestValidateRunAsUser(t *testing.T) { }) t.Run("mismatch", func(t *testing.T) { - settings := makeSettings(handlersettings.InlineScript, "", "bob", "") + settings := makeSettings(handlersettings.InlineScript, "", "bob", "", "") policy := &RCv2ExtensionPolicySettings{ RunAsUser: "alice", } @@ -247,6 +280,56 @@ func TestValidateRunAsUser(t *testing.T) { }) } +func TestValidateDisableOutputBlobs(t *testing.T) { + t.Run("output blobs disabled in policy, outputblob provided", func(t *testing.T) { + settings := makeSettings(handlersettings.InlineScript, "", " Alice ", "https://example/blob", "") + policy := &RCv2ExtensionPolicySettings{ + DisableOutputBlobs: true, + } + err := ValidateDisableOutputBlobs(nopCtx(), settings, policy) + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("output blobs are disabled in policy, but settings specify outputBlobURI '%s' or errorBlobURI '%s'", settings.OutputBlobURI, settings.ErrorBlobURI)) + }) + + t.Run("output blobs disabled in policy, error blob provided", func(t *testing.T) { + settings := makeSettings(handlersettings.InlineScript, "", " Alice ", "", "https://example/errorBlob") + policy := &RCv2ExtensionPolicySettings{ + DisableOutputBlobs: true, + } + err := ValidateDisableOutputBlobs(nopCtx(), settings, policy) + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("output blobs are disabled in policy, but settings specify outputBlobURI '%s' or errorBlobURI '%s'", settings.OutputBlobURI, settings.ErrorBlobURI)) + }) + + t.Run("output blobs disabled in policy, both output and error blobs provided", func(t *testing.T) { + settings := makeSettings(handlersettings.InlineScript, "", " Alice ", "https://example/blob", "https://example/errorBlob") + policy := &RCv2ExtensionPolicySettings{ + DisableOutputBlobs: true, + } + err := ValidateDisableOutputBlobs(nopCtx(), settings, policy) + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("output blobs are disabled in policy, but settings specify outputBlobURI '%s' or errorBlobURI '%s'", settings.OutputBlobURI, settings.ErrorBlobURI)) + }) + + t.Run("output blobs disabled in policy, pass", func(t *testing.T) { + settings := makeSettings(handlersettings.InlineScript, "", " Alice ", "", "") + policy := &RCv2ExtensionPolicySettings{ + DisableOutputBlobs: true, + } + err := ValidateDisableOutputBlobs(nopCtx(), settings, policy) + require.NoError(t, err) + }) + + t.Run("output blobs disabled in policy, whitespace pass", func(t *testing.T) { + settings := makeSettings(handlersettings.InlineScript, "", " Alice ", " ", " ") + policy := &RCv2ExtensionPolicySettings{ + DisableOutputBlobs: true, + } + err := ValidateDisableOutputBlobs(nopCtx(), settings, policy) + require.NoError(t, err) + }) +} + func nopCtx() *log.Context { return log.NewContext(log.NewNopLogger()) }