diff --git a/terraformer/cmd/harp-terraformer/internal/cmd/agent.go b/terraformer/cmd/harp-terraformer/internal/cmd/agent.go index 115c0ee..12f0301 100644 --- a/terraformer/cmd/harp-terraformer/internal/cmd/agent.go +++ b/terraformer/cmd/harp-terraformer/internal/cmd/agent.go @@ -29,10 +29,11 @@ import ( ) var ( - terraformerAgentInputSpec string - terraformerAgentOutputPath string - terraformerAgentDisableTokenWrap bool - terraformerAgentEnvironment string + terraformerAgentInputSpec string + terraformerAgentOutputPath string + terraformerAgentDisableTokenWrap bool + terraformerAgentDisableEnvironmentSuffix bool + terraformerAgentEnvironment string ) // ----------------------------------------------------------------------------- @@ -49,6 +50,7 @@ var terraformerAgentCmd = func() *cobra.Command { cmd.Flags().StringVar(&terraformerAgentOutputPath, "out", "-", "Output file ('-' for stdout or a filename)") cmd.Flags().StringVar(&terraformerAgentEnvironment, "env", "production", "Target environment") cmd.Flags().BoolVar(&terraformerAgentDisableTokenWrap, "no-token-wrap", false, "Disable token wrapping") + cmd.Flags().BoolVar(&terraformerAgentDisableEnvironmentSuffix, "no-env-suffix", false, "Disable environment suffix in role and policy names") return cmd } @@ -75,7 +77,7 @@ func runTerraformerAgent(cmd *cobra.Command, _ []string) { } // Run terraformer - if err := terraformer.Run(ctx, reader, terraformerAgentEnvironment, terraformerAgentDisableTokenWrap, terraformer.AgentTemplate, writer); err != nil { + if err := terraformer.Run(ctx, reader, terraformerAgentEnvironment, terraformerAgentDisableTokenWrap, terraformerAgentDisableEnvironmentSuffix, terraformer.AgentTemplate, writer); err != nil { log.For(ctx).Fatal("unable to process specification", zap.Error(err), zap.String("path", terraformerAgentInputSpec)) } } diff --git a/terraformer/cmd/harp-terraformer/internal/cmd/approle.go b/terraformer/cmd/harp-terraformer/internal/cmd/approle.go new file mode 100644 index 0000000..a8655cd --- /dev/null +++ b/terraformer/cmd/harp-terraformer/internal/cmd/approle.go @@ -0,0 +1,83 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package cmd + +import ( + "io" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/elastic/harp-plugins/terraformer/pkg/terraformer" + "github.com/elastic/harp/pkg/sdk/cmdutil" + "github.com/elastic/harp/pkg/sdk/log" +) + +var ( + terraformerApproleInputSpec string + terraformerApproleOutputPath string + terraformerApproleDisableTokenWrap bool + terraformerApproleDisableEnvironmentSuffix bool + terraformerApproleEnvironment string +) + +// ----------------------------------------------------------------------------- + +var terraformerApproleCmd = func() *cobra.Command { + cmd := &cobra.Command{ + Use: "approle", + Short: "policy and approle with approle engine", + Run: runTerraformerApprole, + } + + // Parameters + cmd.Flags().StringVar(&terraformerApproleInputSpec, "spec", "-", "AppRole specification path ('-' for stdin or filename)") + cmd.Flags().StringVar(&terraformerApproleOutputPath, "out", "-", "Output file ('-' for stdout or a filename)") + cmd.Flags().StringVar(&terraformerApproleEnvironment, "env", "production", "Target environment") + cmd.Flags().BoolVar(&terraformerApproleDisableTokenWrap, "no-token-wrap", false, "Disable token wrapping") + cmd.Flags().BoolVar(&terraformerApproleDisableEnvironmentSuffix, "no-env-suffix", false, "Disable environment suffix in role and policy names") + + return cmd +} + +func runTerraformerApprole(cmd *cobra.Command, _ []string) { + ctx, cancel := cmdutil.Context(cmd.Context(), "harp-terraformer-approle", conf.Debug.Enable, conf.Instrumentation.Logs.Level) + defer cancel() + + var ( + reader io.Reader + err error + ) + + // Create input reader + reader, err = cmdutil.Reader(terraformerApproleInputSpec) + if err != nil { + log.For(ctx).Fatal("unable to open input specification", zap.Error(err), zap.String("path", terraformerApproleInputSpec)) + } + + // Create output writer + writer, err := cmdutil.Writer(terraformerApproleOutputPath) + if err != nil { + log.For(ctx).Fatal("unable to create output writer", zap.Error(err), zap.String("path", terraformerApproleOutputPath)) + } + + // Run terraformer + if err := terraformer.Run(ctx, reader, terraformerApproleEnvironment, terraformerApproleDisableTokenWrap, terraformerApproleDisableEnvironmentSuffix, terraformer.ApproleTemplate, writer); err != nil { + log.For(ctx).Fatal("unable to process specification", zap.Error(err), zap.String("path", terraformerApproleInputSpec)) + } +} diff --git a/terraformer/cmd/harp-terraformer/internal/cmd/policy.go b/terraformer/cmd/harp-terraformer/internal/cmd/policy.go index db57076..8419e2a 100644 --- a/terraformer/cmd/harp-terraformer/internal/cmd/policy.go +++ b/terraformer/cmd/harp-terraformer/internal/cmd/policy.go @@ -29,9 +29,10 @@ import ( ) var ( - terraformerPolicyInputSpec string - terraformerPolicyOutputPath string - terraformerPolicyEnvironment string + terraformerPolicyInputSpec string + terraformerPolicyOutputPath string + terraformerPolicyEnvironment string + terraformerPolicyDisableEnvironmentSuffix bool ) // ----------------------------------------------------------------------------- @@ -47,6 +48,7 @@ var terraformerPolicyCmd = func() *cobra.Command { cmd.Flags().StringVar(&terraformerPolicyInputSpec, "spec", "-", "AppRole specification path ('-' for stdin or filename)") cmd.Flags().StringVar(&terraformerPolicyOutputPath, "out", "-", "Output file ('-' for stdout or a filename)") cmd.Flags().StringVar(&terraformerPolicyEnvironment, "env", "production", "Target environment") + cmd.Flags().BoolVar(&terraformerPolicyDisableEnvironmentSuffix, "no-env-suffix", false, "Disable environment suffix in policy names") return cmd } @@ -73,7 +75,7 @@ func runTerraformerPolicy(cmd *cobra.Command, _ []string) { } // Run terraformer - if err := terraformer.Run(ctx, reader, terraformerPolicyEnvironment, true, terraformer.PolicyTemplate, writer); err != nil { + if err := terraformer.Run(ctx, reader, terraformerPolicyEnvironment, true, terraformerPolicyDisableEnvironmentSuffix, terraformer.PolicyTemplate, writer); err != nil { log.For(ctx).Fatal("unable to process specification", zap.Error(err), zap.String("path", terraformerPolicyInputSpec)) } } diff --git a/terraformer/cmd/harp-terraformer/internal/cmd/root.go b/terraformer/cmd/harp-terraformer/internal/cmd/root.go index fa2a840..f80a6b0 100644 --- a/terraformer/cmd/harp-terraformer/internal/cmd/root.go +++ b/terraformer/cmd/harp-terraformer/internal/cmd/root.go @@ -57,6 +57,7 @@ var mainCmd = func() *cobra.Command { // Add subcommands cmd.AddCommand(terraformerAgentCmd()) + cmd.AddCommand(terraformerApproleCmd()) cmd.AddCommand(terraformerPolicyCmd()) cmd.AddCommand(terraformerServiceCmd()) diff --git a/terraformer/cmd/harp-terraformer/internal/cmd/service.go b/terraformer/cmd/harp-terraformer/internal/cmd/service.go index bb91ccb..e18d2ea 100644 --- a/terraformer/cmd/harp-terraformer/internal/cmd/service.go +++ b/terraformer/cmd/harp-terraformer/internal/cmd/service.go @@ -29,9 +29,10 @@ import ( ) var ( - terraformerServiceInputSpec string - terraformerServiceOutputPath string - terraformerServiceEnvironment string + terraformerServiceInputSpec string + terraformerServiceOutputPath string + terraformerServiceEnvironment string + terraformerServiceDisableEnvironmentSuffix bool ) // ----------------------------------------------------------------------------- @@ -47,6 +48,7 @@ var terraformerServiceCmd = func() *cobra.Command { cmd.Flags().StringVar(&terraformerServiceInputSpec, "spec", "-", "AppRole specification path ('-' for stdin or filename)") cmd.Flags().StringVar(&terraformerServiceOutputPath, "out", "-", "Output file ('-' for stdout or a filename)") cmd.Flags().StringVar(&terraformerServiceEnvironment, "env", "production", "Target environment") + cmd.Flags().BoolVar(&terraformerServiceDisableEnvironmentSuffix, "no-env-suffix", false, "Disable environment suffix in role and policy names") return cmd } @@ -73,7 +75,7 @@ func runTerraformerService(cmd *cobra.Command, _ []string) { } // Run terraformer - if err := terraformer.Run(ctx, reader, terraformerServiceEnvironment, true, terraformer.ServiceTemplate, writer); err != nil { + if err := terraformer.Run(ctx, reader, terraformerServiceEnvironment, true, terraformerServiceDisableEnvironmentSuffix, terraformer.ServiceTemplate, writer); err != nil { log.For(ctx).Fatal("unable to process specification", zap.Error(err), zap.String("path", terraformerServiceInputSpec)) } } diff --git a/terraformer/pkg/terraformer/compiler.go b/terraformer/pkg/terraformer/compiler.go index 5c4db20..1eebdfe 100644 --- a/terraformer/pkg/terraformer/compiler.go +++ b/terraformer/pkg/terraformer/compiler.go @@ -129,21 +129,28 @@ func pathCompiler(ring csov1.Ring, prefix []string, suffixFunc func() []*terrafo return nil } -func compile(env string, def *terraformerv1.AppRoleDefinition, specHash string, noTokenWrap bool) (*tmplModel, error) { +func compile(env string, def *terraformerv1.AppRoleDefinition, specHash string, noTokenWrap, noEnvironmentSuffix bool) (*tmplModel, error) { // Check arguments if err := validate(def); err != nil { return nil, err } + // Check environment and suffix removal + objectName := slug.Make(fmt.Sprintf("%s %s", def.Meta.Name, env)) + if noEnvironmentSuffix { + objectName = slug.Make(def.Meta.Name) + } + res := &tmplModel{ - Date: time.Now().UTC().Format(time.RFC3339), - SpecHash: specHash, - Meta: def.Meta, - Environment: slug.Make(env), - RoleName: slug.Make(def.Meta.Name), - ObjectName: slug.Make(fmt.Sprintf("%s %s", def.Meta.Name, env)), - Namespaces: map[string][]tmpSecretModel{}, - DisableTokenWrap: noTokenWrap, + Date: time.Now().UTC().Format(time.RFC3339), + SpecHash: specHash, + Meta: def.Meta, + Environment: slug.Make(env), + RoleName: slug.Make(def.Meta.Name), + ObjectName: objectName, + Namespaces: map[string][]tmpSecretModel{}, + DisableTokenWrap: noTokenWrap, + DisableEnvironmentSuffix: noEnvironmentSuffix, } if def.Spec.Namespaces != nil { diff --git a/terraformer/pkg/terraformer/compiler_test.go b/terraformer/pkg/terraformer/compiler_test.go index d2496e1..43501c7 100644 --- a/terraformer/pkg/terraformer/compiler_test.go +++ b/terraformer/pkg/terraformer/compiler_test.go @@ -18,19 +18,23 @@ package terraformer import ( + "fmt" "reflect" "testing" + "github.com/gosimple/slug" + terraformerv1 "github.com/elastic/harp-plugins/terraformer/api/gen/go/harp/terraformer/v1" fuzz "github.com/google/gofuzz" ) func Test_compile(t *testing.T) { type args struct { - env string - def *terraformerv1.AppRoleDefinition - specHash string - noTokenWrap bool + env string + def *terraformerv1.AppRoleDefinition + specHash string + noTokenWrap bool + noEnvironmentSuffix bool } tests := []struct { name string @@ -50,8 +54,9 @@ func Test_compile(t *testing.T) { ApiVersion: "harp.elastic.co/terraformer/v1", Kind: "AppRoleDefinition", }, - noTokenWrap: false, - specHash: "123456", + noTokenWrap: false, + noEnvironmentSuffix: false, + specHash: "123456", }, wantErr: true, @@ -65,6 +70,7 @@ func Test_compile(t *testing.T) { Kind: "AppRoleDefinition", Meta: &terraformerv1.AppRoleDefinitionMeta{}, }, + noEnvironmentSuffix: false, }, wantErr: true, }, @@ -79,6 +85,7 @@ func Test_compile(t *testing.T) { Name: "foo", }, }, + noEnvironmentSuffix: false, }, wantErr: true, }, @@ -94,6 +101,7 @@ func Test_compile(t *testing.T) { Owner: "security@elastic.co", }, }, + noEnvironmentSuffix: false, }, wantErr: true, }, @@ -110,8 +118,9 @@ func Test_compile(t *testing.T) { Description: "test", }, }, - noTokenWrap: false, - specHash: "123456", + noTokenWrap: false, + noEnvironmentSuffix: false, + specHash: "123456", }, wantErr: true, }, @@ -129,8 +138,9 @@ func Test_compile(t *testing.T) { }, Spec: &terraformerv1.AppRoleDefinitionSpec{}, }, - noTokenWrap: false, - specHash: "123456", + noTokenWrap: false, + noEnvironmentSuffix: false, + specHash: "123456", }, wantErr: true, }, @@ -150,25 +160,138 @@ func Test_compile(t *testing.T) { Selector: &terraformerv1.AppRoleDefinitionSelector{}, }, }, - noTokenWrap: false, - specHash: "123456", + noTokenWrap: false, + noEnvironmentSuffix: false, + specHash: "123456", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := compile(tt.args.env, tt.args.def, tt.args.specHash, tt.args.noTokenWrap, tt.args.noEnvironmentSuffix) + if (err != nil) != tt.wantErr { + t.Errorf("compile() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func Test_compile_template_object(t *testing.T) { + type args struct { + env string + def *terraformerv1.AppRoleDefinition + specHash string + noTokenWrap bool + noEnvironmentSuffix bool + } + tests := []struct { + name string + args args + want *tmplModel + wantErr bool + }{ + { + name: "empty spec selector with noEnvironmentSuffix=true", + args: args{ + env: "production", + def: &terraformerv1.AppRoleDefinition{ + ApiVersion: "harp.elastic.co/terraformer/v1", + Kind: "AppRoleDefinition", + Meta: &terraformerv1.AppRoleDefinitionMeta{ + Name: "foo", + Owner: "security@elastic.co", + Description: "test", + }, + Spec: &terraformerv1.AppRoleDefinitionSpec{ + Selector: &terraformerv1.AppRoleDefinitionSelector{}, + }, + }, + noTokenWrap: false, + noEnvironmentSuffix: false, + specHash: "123456", + }, + want: &tmplModel{ + Meta: &terraformerv1.AppRoleDefinitionMeta{ + Name: "foo", + Owner: "security@elastic.co", + Description: "test", + }, + Environment: "production", + RoleName: "foo", + ObjectName: "foo-production", + DisableEnvironmentSuffix: true, + }, + wantErr: false, + }, + { + name: "empty spec selector with noEnvironmentSuffix=false", + args: args{ + env: "production", + def: &terraformerv1.AppRoleDefinition{ + ApiVersion: "harp.elastic.co/terraformer/v1", + Kind: "AppRoleDefinition", + Meta: &terraformerv1.AppRoleDefinitionMeta{ + Name: "foo", + Owner: "security@elastic.co", + Description: "test", + }, + Spec: &terraformerv1.AppRoleDefinitionSpec{ + Selector: &terraformerv1.AppRoleDefinitionSelector{}, + }, + }, + noTokenWrap: false, + noEnvironmentSuffix: true, + specHash: "123456", + }, + want: &tmplModel{ + Meta: &terraformerv1.AppRoleDefinitionMeta{ + Name: "foo", + Owner: "security@elastic.co", + Description: "test", + }, + Environment: "production", + RoleName: "foo", + ObjectName: "foo", + DisableEnvironmentSuffix: true, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := compile(tt.args.env, tt.args.def, tt.args.specHash, tt.args.noTokenWrap) + res, err := compile(tt.args.env, tt.args.def, tt.args.specHash, tt.args.noTokenWrap, tt.args.noEnvironmentSuffix) if (err != nil) != tt.wantErr { t.Errorf("compile() error = %v, wantErr %v", err, tt.wantErr) return } + + if res.DisableEnvironmentSuffix != tt.args.noEnvironmentSuffix { + t.Errorf("compile() error = %v, wantErr %v", err, tt.wantErr) + return + } + + switch tt.args.noEnvironmentSuffix { + case true: + expectedObjectName := slug.Make(tt.args.def.Meta.Name) + if res.ObjectName != slug.Make(expectedObjectName) { + t.Errorf("compile() error = %v, wantErr %v", res.ObjectName, expectedObjectName) + return + } + case false: + expectedObjectName := slug.Make(fmt.Sprintf("%s-%s", tt.args.def.Meta.Name, tt.args.env)) + if res.ObjectName != expectedObjectName { + t.Errorf("compile() error = %v, wantErr %v", res.ObjectName, expectedObjectName) + return + } + } }) } } func Test_compile_Fuzz(t *testing.T) { - // Making sure the description never panics + // Making sure the descrption never panics for i := 0; i < 50; i++ { f := fuzz.New() @@ -188,6 +311,7 @@ func Test_compile_Fuzz(t *testing.T) { } var specHash string var tokenWrap bool + var noEnvSuffix bool // Fuzz input f.Fuzz(&env) @@ -196,9 +320,10 @@ func Test_compile_Fuzz(t *testing.T) { f.Fuzz(&spec.Spec.Custom) f.Fuzz(&specHash) f.Fuzz(&tokenWrap) + f.Fuzz(&noEnvSuffix) // Execute - compile(env, spec, specHash, tokenWrap) + compile(env, spec, specHash, tokenWrap, noEnvSuffix) } } diff --git a/terraformer/pkg/terraformer/spec.go b/terraformer/pkg/terraformer/spec.go index 23b40ae..d4a0303 100644 --- a/terraformer/pkg/terraformer/spec.go +++ b/terraformer/pkg/terraformer/spec.go @@ -37,7 +37,7 @@ import ( // ----------------------------------------------------------------------------- // Run the template generation -func Run(_ context.Context, reader io.Reader, environmentParam string, noTokenWrap bool, templateRaw string, w io.Writer) error { +func Run(_ context.Context, reader io.Reader, environmentParam string, noTokenWrap, noEnvironmentSuffix bool, templateRaw string, w io.Writer) error { // Drain input reader specificationRaw, err := io.ReadAll(reader) if err != nil { @@ -65,7 +65,7 @@ func Run(_ context.Context, reader io.Reader, environmentParam string, noTokenWr specHash := sha256.Sum256(specProto) // Compile the definition - m, err := compile(environmentParam, def, base64.StdEncoding.EncodeToString(specHash[:]), noTokenWrap) + m, err := compile(environmentParam, def, base64.StdEncoding.EncodeToString(specHash[:]), noTokenWrap, noEnvironmentSuffix) if err != nil { return fmt.Errorf("unable to compile specification: %w", err) } diff --git a/terraformer/pkg/terraformer/templates.go b/terraformer/pkg/terraformer/templates.go index 9033396..8bb9cd5 100644 --- a/terraformer/pkg/terraformer/templates.go +++ b/terraformer/pkg/terraformer/templates.go @@ -127,6 +127,61 @@ resource "vault_approle_auth_backend_role" "agent-{{.ObjectName}}" { } ` +// ApproleTemplate is the TF >=0.12 Agent template. +const ApproleTemplate = `# Generated with Harp Terraformer, Don't modify. +# https://github.com/elastic/harp-plugins/tree/main/cmd/harp-terraformer +# --- +# SpecificationHash: "{{.SpecHash}}" +# Owner: "{{.Meta.Owner}}" +# Date: "{{.Date}}" +# Description: "{{.Meta.Description}}" +# Issues:{{range .Meta.Issues}} +# - {{.}}{{ end }} +# --- +# +# ------------------------------------------------------------------------------ + +# Create the policy +data "vault_policy_document" "approle-{{.ObjectName}}" { +{{- range $ns, $secrets := .Namespaces }} + # {{ $ns }} secrets{{ range $k, $item := $secrets }} + rule { + description = "{{$item.Description}}" + path = "{{$item.Path}}" + capabilities = [{{range $i, $v := $item.Capabilities}}{{if $i}} ,{{end}}{{printf "%q" $v}}{{end}}] + } + {{end -}} +{{end}}{{if .CustomRules }} + # Custom secret paths{{ range $k, $item := .CustomRules }} + rule { + description = "{{$item.Description}}" + path = "{{$item.Path}}" + capabilities = [{{range $i, $v := $item.Capabilities}}{{if $i}} ,{{end}}{{printf "%q" $v}}{{end}}] + } + {{end}}{{end -}} +} + +# Register the policy +resource "vault_policy" "approle-{{.ObjectName}}" { + name = "approle-{{.ObjectName}}" + policy = data.vault_policy_document.approle-{{.ObjectName}}.hcl +} + +# ------------------------------------------------------------------------------ +# +# Register the backend role +resource "vault_approle_auth_backend_role" "{{.ObjectName}}" { + backend = "approle" + role_name = "{{.ObjectName}}" + + token_policies = [ + "cso-default", + "service-default", + "approle-{{.ObjectName}}", + ] +} +` + // PolicyTemplate is the TF >=0.12 Agent template. const PolicyTemplate = `# Generated with Harp Terraformer, Don't modify. # https://github.com/elastic/harp-plugins/tree/main/cmd/harp-terraformer @@ -189,6 +244,8 @@ type tmplModel struct { CustomRules []tmpSecretModel // DisableTokenWrap disable token wrap enforcement DisableTokenWrap bool + // DisableEnvironmentSuffix disable environment suffix in role and policy names + DisableEnvironmentSuffix bool } type tmpSecretModel struct {