pkg/bundle/codec.go (164 lines of code) (raw):
// 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 bundle
import (
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
bundlev1 "github.com/elastic/harp/api/gen/go/harp/bundle/v1"
"github.com/elastic/harp/pkg/bundle/secret"
"github.com/elastic/harp/pkg/sdk/security"
"github.com/elastic/harp/pkg/sdk/types"
)
// Load a file bundle from the buffer.
func Load(r io.Reader) (*bundlev1.Bundle, error) {
// Check parameters
if types.IsNil(r) {
return nil, fmt.Errorf("unable to process nil reader")
}
decoded, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("unable to decompress bundle content")
}
// Deserialize protobuf payload
bundle := &bundlev1.Bundle{}
if err = proto.Unmarshal(decoded, bundle); err != nil {
return nil, fmt.Errorf("unable to decode bundle content")
}
// Compute merkle tree root
tree, _, err := Tree(bundle)
if err != nil {
return nil, fmt.Errorf("unable to compute merkle tree of bundle content: %w", err)
}
// Check if root match
if !security.SecureCompare(bundle.MerkleTreeRoot, tree.Root()) {
return nil, fmt.Errorf("invalid merkle tree root, bundle is corrupted")
}
// No error
return bundle, nil
}
// Dump a file bundle to the writer.
func Dump(w io.Writer, b *bundlev1.Bundle) error {
// Check parameters
if types.IsNil(w) {
return fmt.Errorf("unable to process nil writer")
}
if b == nil {
return fmt.Errorf("unable to process nil bundle")
}
// Sort packages
sort.SliceStable(b.Packages, func(i, j int) bool {
return b.Packages[i].Name < b.Packages[j].Name
})
// Compute merkle tree
tree, _, err := Tree(b)
if err != nil {
return fmt.Errorf("unable to compute merkle tree of bundle content: %w", err)
}
// Assign to bundle
b.MerkleTreeRoot = tree.Root()
// Serialize protobuf payload
payload, err := proto.Marshal(b)
if err != nil {
return fmt.Errorf("unable to encode bundle content: %w", err)
}
// WWrite to writer
if _, err = w.Write(payload); err != nil {
return fmt.Errorf("unable to write serialized Bundle: %w", err)
}
// No error
return nil
}
// Read a secret located at secretPath from the given bundle.
func Read(b *bundlev1.Bundle, secretPath string) (map[string]interface{}, error) {
// Check bundle
if b == nil {
return nil, fmt.Errorf("unable to process nil bundle")
}
if secretPath == "" {
return nil, fmt.Errorf("unable to process with empty path")
}
// Lookup secret package
var found *bundlev1.Package
for _, item := range b.Packages {
if strings.EqualFold(item.Name, secretPath) {
found = item
break
}
}
if found == nil {
return nil, fmt.Errorf("unable to lookup secret with path '%s'", secretPath)
}
// Transform secret value
result := map[string]interface{}{}
for _, s := range found.Secrets.Data {
// Unpack secret value
var obj interface{}
if err := secret.Unpack(s.Value, &obj); err != nil {
return nil, fmt.Errorf("unable to unpack secret value for path '%s': %w", secretPath, err)
}
// Add to result
result[s.Key] = obj
}
// No error
return result, nil
}
// AsProtoJSON export given bundle as a JSON representation.
//
//nolint:interfacer // Tighly coupled with type
func AsProtoJSON(w io.Writer, b *bundlev1.Bundle) error {
// Check parameters
if types.IsNil(w) {
return fmt.Errorf("unable to process nil writer")
}
if b == nil {
return fmt.Errorf("unable to process nil bundle")
}
// Clone bundle (we don't want to modify input bundle)
cloned, ok := proto.Clone(b).(*bundlev1.Bundle)
if !ok {
return fmt.Errorf("the cloned bundle does not have a correct type: %T", cloned)
}
// Initialize marshaller
m := &protojson.MarshalOptions{}
// Decode packed values
for _, p := range cloned.Packages {
for _, s := range p.Secrets.Data {
// Unpack secret value
var data interface{}
if err := secret.Unpack(s.Value, &data); err != nil {
return fmt.Errorf("unable to unpack '%s' - '%s' secret value: %w", p.Name, s.Key, err)
}
// Re-encode as json
payload, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("unable to encode '%s' - '%s' secret value as json: %w", p.Name, s.Key, err)
}
// Replace current packed secret value by json encoded one.
s.Value = payload
}
}
// Marshal bundle
out, err := m.Marshal(cloned)
if err != nil {
return fmt.Errorf("unable to produce JSON from bundle object: %w", err)
}
// Write to writer
if _, err := fmt.Fprintf(w, "%s", string(out)); err != nil {
return fmt.Errorf("unable to write JSON bundle: %w", err)
}
// No error
return nil
}
// AsMap returns a bundle as map
func AsMap(b *bundlev1.Bundle) (KV, error) {
// Check input
if b == nil {
return nil, fmt.Errorf("unable to process nil bundle")
}
res := KV{}
for _, p := range b.Packages {
// Check if secret is locked
if p.Secrets.Locked != nil {
// Encode value
res[p.Name] = KV{
"@type": packageEncryptedValueType,
"value": p.Secrets.Locked.Value,
}
continue
}
// Map package secrets
secrets, err := AsSecretMap(p)
if err != nil {
return nil, fmt.Errorf("unable to pack secrets as a map: %w", err)
}
// Assign result
res[p.Name] = secrets
}
// No error
return res, nil
}
// AsMetadataMap exports given bundle metadata as a map.
func AsMetadataMap(b *bundlev1.Bundle) (KV, error) {
// Check input
if b == nil {
return nil, fmt.Errorf("unable to process nil bundle")
}
metaMap := KV{}
// Check if bundle as metadata
if len(b.Annotations) > 0 {
metaMap[bundleAnnotationsKey] = b.Annotations
}
if len(b.Labels) > 0 {
metaMap[bundleLabelsKey] = b.Labels
}
// Export bundle metadata
for _, p := range b.Packages {
metadata := KV{}
// Has annotations
if len(p.Annotations) > 0 {
// Assign json
metadata[packageAnnotations] = p.Annotations
}
// Has labels
if len(p.Labels) > 0 {
// Assign json
metadata[packageLabels] = p.Labels
}
// Assign to package
metaMap[p.Name] = metadata
}
// No error
return metaMap, nil
}