github.com/verrazzano/verrazzano@v1.7.0/cluster-operator/controllers/quickcreate/controller/oci/credentials.go (about)

     1  // Copyright (c) 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package oci
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"github.com/oracle/oci-go-sdk/v53/common"
    11  	"github.com/oracle/oci-go-sdk/v53/common/auth"
    12  	v1 "k8s.io/api/core/v1"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    15  	"k8s.io/apimachinery/pkg/runtime/schema"
    16  	"k8s.io/apimachinery/pkg/types"
    17  	"reflect"
    18  	clipkg "sigs.k8s.io/controller-runtime/pkg/client"
    19  )
    20  
    21  type (
    22  	CredentialsLoader interface {
    23  		GetCredentialsIfAllowed(ctx context.Context, cli clipkg.Client, identityRef types.NamespacedName, namespace string) (*Credentials, error)
    24  	}
    25  	CredentialsLoaderImpl struct{}
    26  	Credentials           struct {
    27  		Region               string
    28  		Tenancy              string
    29  		User                 string
    30  		PrivateKey           string
    31  		Fingerprint          string
    32  		Passphrase           string
    33  		UseInstancePrincipal string
    34  	}
    35  	CAPIIdentity struct {
    36  		Spec struct {
    37  			Namespaces      *AllowedNamespaces `json:"allowedNamespaces"`
    38  			PrincipalSecret struct {
    39  				Name      string `json:"name"`
    40  				Namespace string `json:"namespace"`
    41  			} `json:"principalSecret"`
    42  		} `json:"spec"`
    43  	}
    44  	AllowedNamespaces struct {
    45  		List     []string              `json:"list"`
    46  		Selector *metav1.LabelSelector `json:"selector"`
    47  	}
    48  )
    49  
    50  var (
    51  	_                     CredentialsLoader = CredentialsLoaderImpl{}
    52  	gvkOCIClusterIdentity                   = schema.GroupVersionKind{
    53  		Group:   "infrastructure.cluster.x-k8s.io",
    54  		Version: "v1beta2",
    55  		Kind:    "ociclusteridentity",
    56  	}
    57  )
    58  
    59  const (
    60  	ociTenancyField              = "tenancy"
    61  	ociUserField                 = "user"
    62  	ociFingerprintField          = "fingerprint"
    63  	ociRegionField               = "region"
    64  	ociPassphraseField           = "passphrase"
    65  	ociKeyField                  = "key"
    66  	ociUseInstancePrincipalField = "useInstancePrincipal"
    67  )
    68  
    69  // GetCredentialsIfAllowed fetches the OCI Credentials for an OCIClusterIdentity, if that OCIClusterIdentity exists, has a principal secret,
    70  // and allows access from a given namespace.
    71  func (c CredentialsLoaderImpl) GetCredentialsIfAllowed(ctx context.Context, cli clipkg.Client, identityRef types.NamespacedName, namespace string) (*Credentials, error) {
    72  	nsn := types.NamespacedName{
    73  		Name:      identityRef.Name,
    74  		Namespace: identityRef.Namespace,
    75  	}
    76  	identity, err := getIdentity(ctx, cli, nsn)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	if !IsAllowedNamespace(ctx, cli, identity, namespace) {
    81  		return nil, fmt.Errorf("cannot access OCI identity %s/%s", identityRef.Namespace, identityRef.Name)
    82  	}
    83  	return getCredentialsFromIdentity(ctx, cli, identity)
    84  }
    85  
    86  func (c Credentials) AsConfigurationProvider() (common.ConfigurationProvider, error) {
    87  	if c.UseInstancePrincipal == "true" {
    88  		return auth.InstancePrincipalConfigurationProvider()
    89  	}
    90  	var passphrase *string
    91  	if len(c.Passphrase) > 0 {
    92  		passphrase = &c.Passphrase
    93  	}
    94  	return common.NewRawConfigurationProvider(c.Tenancy, c.User, c.Region, c.Fingerprint, c.PrivateKey, passphrase), nil
    95  }
    96  
    97  // IsAllowedNamespace checks if a given identity allows access from a given namespace.
    98  func IsAllowedNamespace(ctx context.Context, cli clipkg.Client, identity *CAPIIdentity, namespace string) bool {
    99  	// No allowed namespaces means nothing allowed
   100  	if identity.Spec.Namespaces == nil {
   101  		return false
   102  	}
   103  	// Empty allowed namespaces means all namespaces allowed
   104  	if reflect.DeepEqual(*identity.Spec.Namespaces, AllowedNamespaces{}) {
   105  		return true
   106  	}
   107  	// If namespace is in the allowed namespaces list, access is permitted
   108  	for _, ns := range identity.Spec.Namespaces.List {
   109  		if ns == namespace {
   110  			return true
   111  		}
   112  	}
   113  	// Deny access for invalid or empty selectors
   114  	selector, err := metav1.LabelSelectorAsSelector(identity.Spec.Namespaces.Selector)
   115  	if err != nil {
   116  		return false
   117  	}
   118  	if selector.Empty() {
   119  		return false
   120  	}
   121  	// Allow access if the namespace matches any namespace from the selectors
   122  	namespaces := &v1.NamespaceList{}
   123  	if err := cli.List(ctx, namespaces, clipkg.MatchingLabelsSelector{Selector: selector}); err != nil {
   124  		return false
   125  	}
   126  	for _, ns := range namespaces.Items {
   127  		if ns.Name == namespace {
   128  			return true
   129  		}
   130  	}
   131  	return false
   132  }
   133  
   134  func getIdentity(ctx context.Context, cli clipkg.Client, nsn types.NamespacedName) (*CAPIIdentity, error) {
   135  	unstructuredIdentity := &unstructured.Unstructured{}
   136  	unstructuredIdentity.SetGroupVersionKind(gvkOCIClusterIdentity)
   137  	if err := cli.Get(ctx, nsn, unstructuredIdentity); err != nil {
   138  		return nil, err
   139  	}
   140  	identityBytes, err := json.Marshal(unstructuredIdentity.Object)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	identity := &CAPIIdentity{}
   145  	err = json.Unmarshal(identityBytes, identity)
   146  	return identity, err
   147  }
   148  
   149  func getCredentialsFromIdentity(ctx context.Context, cli clipkg.Client, identity *CAPIIdentity) (*Credentials, error) {
   150  	nsn := types.NamespacedName{
   151  		Namespace: identity.Spec.PrincipalSecret.Namespace,
   152  		Name:      identity.Spec.PrincipalSecret.Name,
   153  	}
   154  	s := &v1.Secret{}
   155  	if err := cli.Get(ctx, nsn, s); err != nil {
   156  		return nil, err
   157  	}
   158  	return &Credentials{
   159  		Region:               string(s.Data[ociRegionField]),
   160  		Tenancy:              string(s.Data[ociTenancyField]),
   161  		User:                 string(s.Data[ociUserField]),
   162  		PrivateKey:           string(s.Data[ociKeyField]),
   163  		Fingerprint:          string(s.Data[ociFingerprintField]),
   164  		Passphrase:           string(s.Data[ociPassphraseField]),
   165  		UseInstancePrincipal: string(s.Data[ociUseInstancePrincipalField]),
   166  	}, nil
   167  }