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 }