Skip to content

Commit

Permalink
Add vault_config_ui_custom_message resource (#2154)
Browse files Browse the repository at this point in the history
* add custom message resource

* fix filename

* corrected field type for link

* adjusted link field value based on ssh_backend_role example

* fixed error with reading link value from API response

* add CHANGELOG entry

* fix failing unit test
clean up unnecessary code

* rename files to non-plural form

* improve change description and moved it to existing FEATURES section

* add documentation for new resource

* remove custom-message resource from state if it doesn't exist in Vault
fix debug log message format
  • Loading branch information
Marc Boudreau authored Mar 19, 2024
1 parent 9b4557c commit 7f4b29f
Show file tree
Hide file tree
Showing 7 changed files with 459 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ FEATURES:
* Add support to `enable_templating` in `vault_pki_secret_backend_config_urls` ([#2147](https://github.com/hashicorp/terraform-provider-vault/pull/2147)).
* Add support for `skip_import_rotation` and `skip_static_role_import_rotation` in `ldap_secret_backend_static_role` and `ldap_secret_backend` respectively. Requires Vault 1.16+ ([#2128](https://github.com/hashicorp/terraform-provider-vault/pull/2128)).
* Improve logging to track full API exchanges between the provider and Vault ([#2139](https://github.com/hashicorp/terraform-provider-vault/pull/2139))
* Add new resource `vault_config_ui_custom_message`. Requires Vault 1.16+ Enterprise: ([#2154](https://github.com/hashicorp/terraform-provider-vault/pull/2154)).

IMPROVEMENTS:
* Improve performance of READ operations across many resources: ([#2145](https://github.com/hashicorp/terraform-provider-vault/pull/2145)), ([#2152](https://github.com/hashicorp/terraform-provider-vault/pull/2152))
Expand Down
6 changes: 6 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@ const (
FieldInstallationID = "installation_id"
FieldAppID = "app_id"
FieldAIAPath = "aia_path"
FieldTitle = "title"
FieldMessageBase64 = "message_base64"
FieldAuthenticated = "authenticated"
FieldStartTime = "start_time"
FieldEndTime = "end_time"
FieldLink = "link"

/*
common environment variables
Expand Down
4 changes: 4 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,10 @@ var (
Resource: UpdateSchemaResource(secretsSyncAssociationResource()),
PathInventory: []string{"/sys/sync/destinations/{type}/{name}/associations/set"},
},
"vault_config_ui_custom_message": {
Resource: UpdateSchemaResource(configUICustomMessageResource()),
PathInventory: []string{"/sys/config/ui/custom-messages"},
},
}
)

Expand Down
283 changes: 283 additions & 0 deletions vault/resource_config_ui_custom_message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vault

import (
"context"
"log"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-vault/internal/consts"
"github.com/hashicorp/terraform-provider-vault/internal/provider"
"github.com/hashicorp/terraform-provider-vault/util"
"github.com/hashicorp/vault/api"
)

func configUICustomMessageResource() *schema.Resource {
return &schema.Resource{
CreateContext: provider.MountCreateContextWrapper(configUICustomMessageCreate, provider.VaultVersion116),
ReadContext: configUICustomMessageRead,
UpdateContext: configUICustomMessageUpdate,
DeleteContext: configUICustomMessageDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
consts.FieldID: {
Type: schema.TypeString,
Computed: true,
Description: "The unique ID for the custom message",
},
consts.FieldTitle: {
Type: schema.TypeString,
Required: true,
Description: "The title of the custom message",
},
consts.FieldMessageBase64: {
Type: schema.TypeString,
Required: true,
Description: "The base64-encoded content of the custom message",
},
consts.FieldAuthenticated: {
Type: schema.TypeBool,
Optional: true,

Default: true,
Description: "A flag indicating whether the custom message is displayed pre-login (false) or post-login (true)",
},
consts.FieldType: {
Type: schema.TypeString,
Optional: true,
Default: "banner",
ValidateDiagFunc: func(value interface{}, _ cty.Path) diag.Diagnostics {
stringValue := value.(string)
switch {
case stringValue != "banner" && stringValue != "modal":
return diag.Diagnostics{diag.Diagnostic{
Severity: diag.Error,
Summary: "invalid value for \"type\" argument",
Detail: "The \"type\" argument can only be set to \"banner\" or \"modal\".",
}}
}

return nil
},
Description: "The display type of custom message. Allowed values are banner and modal",
},
consts.FieldStartTime: {
Type: schema.TypeString,
Required: true,
Description: "The starting time of the active period of the custom message",
},
consts.FieldEndTime: {
Type: schema.TypeString,
Optional: true,
Description: "The ending time of the active period of the custom message. Can be omitted for non-expiring message",
},
consts.FieldLink: {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"title": {
Type: schema.TypeString,
Required: true,
Description: "The title of the hyperlink",
},
"href": {
Type: schema.TypeString,
Required: true,
Description: "The URL of the hyperlink",
},
},
},
Description: "A block containing a hyperlink associated with the custom message",
},
consts.FieldOptions: {
Type: schema.TypeMap,
Optional: true,
Description: "A map containing additional options for the custom message",
},
},
}
}

func configUICustomMessageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if !provider.IsEnterpriseSupported(meta) {
return diag.Errorf("config_ui_custom_message is not supported by this version of vault")
}

client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

secret, e := client.Sys().CreateUICustomMessageWithContext(ctx, buildUICustomMessageRequest(d))
if e != nil {
return diag.FromErr(e)
}

if secret == nil || secret.Data == nil {
return diag.Errorf(`response from Vault server is empty`)
}

id, ok := secret.Data[consts.FieldID]
if !ok {
return diag.Errorf("error creating custom message: %s", secret.Data["error"])
}

d.SetId(id.(string))

return configUICustomMessageRead(ctx, d, meta)
}

func configUICustomMessageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

id := d.Id()

log.Printf("[DEBUG] Reading custom message %q", id)
secret, e := client.Sys().ReadUICustomMessage(id)
if e != nil {
if util.Is404(e) {
log.Printf("[DEBUG] custom message %q not found, removing from state", id)
d.SetId("")
return nil
}
return diag.FromErr(e)
}

if secret == nil || secret.Data == nil {
log.Printf("[DEBUG] response from Vault server is empty for %q, removing from state", id)
d.SetId("")
return nil
}

secretData := secret.Data

if _, ok := secretData["error"]; ok {
errorList := secretData["error"].([]string)
return diag.Errorf("errors received from Vault server: %s", errorList)
}

var endTimeValue string
if v, ok := secretData[consts.FieldEndTime]; ok {
if v != nil {
endTimeValue = v.(string)
}
}

var linkValue []map[string]interface{}

if v, ok := secretData[consts.FieldLink]; ok {
if v != nil {
linkMap := v.(map[string]any)

if len(linkMap) > 1 {
return diag.Errorf(`invalid link specification: only a single link can be specified`)
}

for k, v := range linkMap {
stringV, ok := v.(string)
if !ok {
return diag.Errorf("invalid href value in link specification: %v", v)
}
if len(k) > 0 && len(stringV) > 0 {
linkValue = append(linkValue, map[string]interface{}{
"title": k,
"href": stringV,
},
)
}
break
}
}
}

d.Set(consts.FieldTitle, secretData[consts.FieldTitle])
d.Set(consts.FieldMessageBase64, secretData["message"])
d.Set(consts.FieldAuthenticated, secretData[consts.FieldAuthenticated])
d.Set(consts.FieldType, secretData[consts.FieldType])
d.Set(consts.FieldStartTime, secretData[consts.FieldStartTime])
d.Set(consts.FieldEndTime, endTimeValue)

if linkValue != nil {
d.Set(consts.FieldLink, linkValue)
}

d.Set(consts.FieldOptions, secretData[consts.FieldOptions])

log.Printf("[DEBUG] Read custom message %q", id)
return nil
}

func configUICustomMessageUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

id := d.Id()

if d.HasChanges(consts.FieldTitle, consts.FieldMessageBase64, consts.FieldAuthenticated, consts.FieldType, consts.FieldStartTime, consts.FieldEndTime, consts.FieldOptions, consts.FieldLink) {
log.Printf("[DEBUG] Updating custom message %q", id)
e = client.Sys().UpdateUICustomMessageWithContext(ctx, id, buildUICustomMessageRequest(d))
if e != nil {
return diag.FromErr(e)
}
}

log.Printf("[DEBUG] Updated custom message %q", id)
return configUICustomMessageRead(ctx, d, meta)
}

func configUICustomMessageDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

id := d.Id()

log.Printf("[DEBUG] Deleting custom message %q", id)
e = client.Sys().DeleteUICustomMessageWithContext(ctx, id)
if e != nil {
return diag.Errorf("error deleting custom message %q: %s", id, e)
}

log.Printf("[DEBUG] Deleted custom message %q", id)
return nil
}

func buildUICustomMessageRequest(d *schema.ResourceData) api.UICustomMessageRequest {
request := api.UICustomMessageRequest{
Title: d.Get(consts.FieldTitle).(string),
Message: d.Get(consts.FieldMessageBase64).(string),
Authenticated: d.Get(consts.FieldAuthenticated).(bool),
Type: d.Get(consts.FieldType).(string),
StartTime: d.Get(consts.FieldStartTime).(string),
EndTime: d.Get(consts.FieldEndTime).(string),
Options: d.Get(consts.FieldOptions).(map[string]interface{}),
}

linkValue := d.Get(consts.FieldLink).(*schema.Set)
if linkValue.Len() == 1 {
slice := linkValue.List()

m := slice[0].(map[string]interface{})
linkTitle := m["title"].(string)
linkHref := m["href"].(string)

request.WithLink(linkTitle, linkHref)
}

return request
}
Loading

0 comments on commit 7f4b29f

Please sign in to comment.