Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vault_config_ui_custom_message resource #2154

Merged
merged 11 commits into from
Mar 19, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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)).

## 3.25.0 (Feb 14, 2024)

Expand Down
6 changes: 6 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,12 @@ const (
FieldWrappingToken = "wrapping_token"
FieldWithWrappedAccessor = "with_wrapped_accessor"
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 @@ -761,6 +761,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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this endpoint return a 404 error if the resource isn't found or is the next if block how we know the resource does not exist in Vault? If Vault does return a 404, we should remove it from TF state (d.setId("")) here as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an example of doing this check:

if err != nil {
if util.Is404(err) {
log.Printf("[DEBUG] AppRole auth backend role %q not found, removing from state", path)
d.SetId("")
return nil
} else {
return diag.Errorf("error deleting AppRole auth backend role %q, err=%s", path, err)
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vault does indeed return an HTTP 404 error if no message with the ID provided to the ReadUICustomMessage function exists.

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
Loading