зеркало из https://github.com/Azure/ARO-RP.git
219 строки
6.4 KiB
Go
219 строки
6.4 KiB
Go
package cluster
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the Apache License 2.0.
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
|
"github.com/Azure/msi-dataplane/pkg/dataplane"
|
|
"github.com/Azure/msi-dataplane/pkg/dataplane/swagger"
|
|
"github.com/Azure/msi-dataplane/pkg/store"
|
|
|
|
"github.com/Azure/ARO-RP/pkg/api"
|
|
"github.com/Azure/ARO-RP/pkg/env"
|
|
"github.com/Azure/ARO-RP/pkg/util/azureclient/azuresdk/armmsi"
|
|
)
|
|
|
|
const (
|
|
mockMsiCertValidityDays = 90
|
|
)
|
|
|
|
var (
|
|
errClusterMsiNotPresentInResponse = errors.New("cluster msi not present in msi credentials response")
|
|
)
|
|
|
|
// ensureClusterMsiCertificate leverages the MSI dataplane module to fetch the MSI's
|
|
// backing certificate (if needed) and store the certificate in the cluster MSI key
|
|
// vault. It does not concern itself with whether an existing certificate is valid
|
|
// or not; that can be left to the certificate refresher component.
|
|
func (m *manager) ensureClusterMsiCertificate(ctx context.Context) error {
|
|
secretName, err := m.clusterMsiSecretName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = m.clusterMsiKeyVaultStore.GetCredentialsObject(ctx, secretName)
|
|
if err == nil {
|
|
return nil
|
|
} else if azcoreErr, ok := err.(*azcore.ResponseError); !ok || azcoreErr.StatusCode != http.StatusNotFound {
|
|
return err
|
|
}
|
|
|
|
clusterMsiResourceId, err := m.doc.OpenShiftCluster.ClusterMsiResourceId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uaMsiRequest := dataplane.UserAssignedMSIRequest{
|
|
IdentityURL: m.doc.OpenShiftCluster.Identity.IdentityURL,
|
|
ResourceIDs: []string{clusterMsiResourceId.String()},
|
|
TenantID: m.doc.OpenShiftCluster.Identity.TenantID,
|
|
}
|
|
|
|
msiCredObj, err := m.msiDataplane.GetUserAssignedIdentities(ctx, uaMsiRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
var expirationDate time.Time
|
|
if m.env.FeatureIsSet(env.FeatureUseMockMsiRp) {
|
|
expirationDate = now.AddDate(0, 0, mockMsiCertValidityDays)
|
|
} else {
|
|
identity, err := getSingleExplicitIdentity(msiCredObj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if identity.NotAfter == nil {
|
|
return errors.New("unable to pull NotAfter from the MSI CredentialsObject")
|
|
}
|
|
|
|
// The swagger API spec for the MI RP specifies that NotAfter will be "in the format 2017-03-01T14:11:00Z".
|
|
expirationDate, err = time.Parse(time.RFC3339, *identity.NotAfter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
secretProperties := store.SecretProperties{
|
|
Enabled: true,
|
|
Expires: expirationDate,
|
|
Name: secretName,
|
|
NotBefore: now,
|
|
}
|
|
|
|
return m.clusterMsiKeyVaultStore.SetCredentialsObject(ctx, secretProperties, msiCredObj.CredentialsObject)
|
|
}
|
|
|
|
// initializeClusterMsiClients intializes any Azure clients that use the cluster
|
|
// MSI certificate.
|
|
func (m *manager) initializeClusterMsiClients(ctx context.Context) error {
|
|
secretName, err := m.clusterMsiSecretName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
kvSecret, err := m.clusterMsiKeyVaultStore.GetCredentialsObject(ctx, secretName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cloud, err := m.env.Environment().CloudNameForMsiDataplane()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uaIdentities, err := dataplane.NewUserAssignedIdentities(kvSecret.CredentialsObject, cloud)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
msiResourceId, err := m.doc.OpenShiftCluster.ClusterMsiResourceId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
azureCred, err := uaIdentities.GetCredential(msiResourceId.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Note that we are assuming that all of the platform MIs are in the same subscription as the ARO resource.
|
|
subId := m.subscriptionDoc.ID
|
|
clientOptions := m.env.Environment().ArmClientOptions()
|
|
clusterMsiFederatedIdentityCredentials, err := armmsi.NewFederatedIdentityCredentialsClient(subId, azureCred, clientOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userAssignedIdentities, err := armmsi.NewUserAssignedIdentitiesClient(subId, azureCred, clientOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.clusterMsiFederatedIdentityCredentials = clusterMsiFederatedIdentityCredentials
|
|
m.userAssignedIdentities = userAssignedIdentities
|
|
return nil
|
|
}
|
|
|
|
// clusterMsiSecretName returns the name to store the cluster MSI certificate under in
|
|
// the cluster MSI key vault.
|
|
func (m *manager) clusterMsiSecretName() (string, error) {
|
|
clusterMsi, err := m.doc.OpenShiftCluster.ClusterMsiResourceId()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fmt.Sprintf("%s-%s", m.doc.ID, clusterMsi.Name), nil
|
|
}
|
|
|
|
func (m *manager) clusterIdentityIDs(ctx context.Context) error {
|
|
if !m.doc.OpenShiftCluster.UsesWorkloadIdentity() {
|
|
return fmt.Errorf("clusterIdentityIDs called for CSP cluster")
|
|
}
|
|
|
|
clusterMsiResourceId, err := m.doc.OpenShiftCluster.ClusterMsiResourceId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uaMsiRequest := dataplane.UserAssignedMSIRequest{
|
|
IdentityURL: m.doc.OpenShiftCluster.Identity.IdentityURL,
|
|
ResourceIDs: []string{clusterMsiResourceId.String()},
|
|
TenantID: m.doc.OpenShiftCluster.Identity.TenantID,
|
|
}
|
|
|
|
msiCredObj, err := m.msiDataplane.GetUserAssignedIdentities(ctx, uaMsiRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
identity, err := getSingleExplicitIdentity(msiCredObj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if identity.ClientID == nil || identity.ObjectID == nil {
|
|
return fmt.Errorf("unable to pull clientID and objectID from the MSI CredentialsObject")
|
|
}
|
|
|
|
m.doc, err = m.db.PatchWithLease(ctx, m.doc.Key, func(doc *api.OpenShiftClusterDocument) error {
|
|
// we iterate through the existing identities to find the identity matching
|
|
// the expected resourceID with casefolding, to ensure we preserve the
|
|
// passed-in casing on IDs even if it may be incorrect
|
|
for k, v := range doc.OpenShiftCluster.Identity.UserAssignedIdentities {
|
|
if strings.EqualFold(k, clusterMsiResourceId.String()) {
|
|
v.ClientID = *identity.ClientID
|
|
v.PrincipalID = *identity.ObjectID
|
|
|
|
doc.OpenShiftCluster.Identity.UserAssignedIdentities[k] = v
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("no entries found matching clusterMsiResourceId")
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// We expect the GetUserAssignedIdentities request to only ever be made for one identity
|
|
// at a time (the cluster MSI) and thus we expect the response to only contain a single
|
|
// identity's details.
|
|
func getSingleExplicitIdentity(msiCredObj *dataplane.UserAssignedIdentities) (*swagger.NestedCredentialsObject, error) {
|
|
if msiCredObj.ExplicitIdentities == nil ||
|
|
len(msiCredObj.ExplicitIdentities) == 0 ||
|
|
msiCredObj.ExplicitIdentities[0] == nil {
|
|
return nil, errClusterMsiNotPresentInResponse
|
|
}
|
|
|
|
return msiCredObj.ExplicitIdentities[0], nil
|
|
}
|