Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ tooling/bin
**/__pycache__
**/charts/*.tgz
/vendor/
**/*.sha256
**/*.sha256.env
83 changes: 83 additions & 0 deletions demo/bicep/external_auth.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
param name string
param location string = resourceGroup().location
param currentTime string = utcNow()
param consoleCallbackUrl string // e.g., https://console-openshift-console.apps.example.com/auth/callback

resource script 'Microsoft.Resources/deploymentScripts@2019-10-01-preview' = {
name: name
location: location
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${resourceId("app-reg-automation", "Microsoft.ManagedIdentity/userAssignedIdentities", "AppRegCreator")}': {}
}
}
properties: {
azPowerShellVersion: '5.0'
arguments: '-resourceName "${name}" -consoleCallback "${consoleCallbackUrl}"'
scriptContent: '''
param([string] $resourceName, [string] $consoleCallback)

$token = (Get-AzAccessToken -ResourceUrl https://graph.microsoft.com).Token
$headers = @{'Content-Type' = 'application/json'; 'Authorization' = 'Bearer ' + $token}

$template = @{
displayName = $resourceName
signInAudience = "AzureADMyOrg"
web = @{
redirectUris = @($consoleCallback)
}
requiredResourceAccess = @(
@{
resourceAppId = "00000003-0000-0000-c000-000000000000"
resourceAccess = @(
@{
id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"
type = "Scope"
}
)
}
)
}

# Upsert App registration
$app = (Invoke-RestMethod -Method Get -Headers $headers -Uri "https://graph.microsoft.com/beta/applications?filter=displayName eq '$($resourceName)'").value
$principal = @{}
if ($app) {
$ignore = Invoke-RestMethod -Method Patch -Headers $headers -Uri "https://graph.microsoft.com/beta/applications/$($app.id)" -Body ($template | ConvertTo-Json -Depth 10)
$principal = (Invoke-RestMethod -Method Get -Headers $headers -Uri "https://graph.microsoft.com/beta/servicePrincipals?filter=appId eq '$($app.appId)'").value
} else {
$app = (Invoke-RestMethod -Method Post -Headers $headers -Uri "https://graph.microsoft.com/beta/applications" -Body ($template | ConvertTo-Json -Depth 10))
$principal = Invoke-RestMethod -Method POST -Headers $headers -Uri "https://graph.microsoft.com/beta/servicePrincipals" -Body (@{ "appId" = $app.appId } | ConvertTo-Json)
}

# Regenerate client secret
$app = (Invoke-RestMethod -Method Get -Headers $headers -Uri "https://graph.microsoft.com/beta/applications/$($app.id)")
foreach ($password in $app.passwordCredentials) {
$body = @{ "keyId" = $password.keyId }
$ignore = Invoke-RestMethod -Method POST -Headers $headers -Uri "https://graph.microsoft.com/beta/applications/$($app.id)/removePassword" -Body ($body | ConvertTo-Json)
}

$body = @{ "passwordCredential" = @{ "displayName"= "Client Secret" } }
$secret = (Invoke-RestMethod -Method POST -Headers $headers -Uri "https://graph.microsoft.com/beta/applications/$($app.id)/addPassword" -Body ($body | ConvertTo-Json)).secretText

$DeploymentScriptOutputs = @{
objectId = $app.id
clientId = $app.appId
clientSecret = $secret
principalId = $principal.id
redirectUri = $consoleCallback
}
'''
cleanupPreference: 'OnSuccess'
retentionInterval: 'P1D'
forceUpdateTag: currentTime
}
}

output objectId string = script.properties.outputs.objectId
output clientId string = script.properties.outputs.clientId
output clientSecret string = script.properties.outputs.clientSecret
output principalId string = script.properties.outputs.principalId
output redirectUri string = script.properties.outputs.redirectUri
7 changes: 7 additions & 0 deletions test/e2e/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export LOCAL_DEVELOPMENT=true
export AROHCP_ENV=development
export CUSTOMER_SUBSCRIPTION=1d3378d3-5a3f-4712-85a1-2485495dfc4b
export RESOURCE_GROUP=mfreer-rg-03
export CLUSTER_NAME=mfreer
export OIDC_ISSUER_URL=https://login.microsoftonline.com/fa5d3dd8-b8ec-4407-a55c-ced639f1c8c5/v2.0
export OIDC_CLIENT_ID=3bc2c42a-4198-42c1-a052-1a505faf8a90
118 changes: 118 additions & 0 deletions test/e2e/external_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2025 Microsoft Corporation
//
// Licensed 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 e2e

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("ExternalAuth via RP Frontend", Label("external-auth"), func() {
var (
baseURL = "http://localhost:8443"
subscriptionID string
resourceGroup string
clusterName string
providerID = "e2e-entra"
apiVersion = "2024-06-10-preview"
issuerURL = "https://login.microsoftonline.com/<tenant-id>/v2.0"
clientID = "<client-id>"
)

BeforeEach(func() {
subscriptionID = os.Getenv("CUSTOMER_SUBSCRIPTION")
Expect(subscriptionID).ToNot(BeEmpty(), "CUSTOMER_SUBSCRIPTION must be set")

resourceGroup = os.Getenv("RESOURCE_GROUP")
Expect(resourceGroup).ToNot(BeEmpty(), "RESOURCE_GROUP must be set")

clusterName = os.Getenv("CLUSTER_NAME")
Expect(clusterName).ToNot(BeEmpty(), "CLUSTER_NAME must be set")

if val := os.Getenv("OIDC_ISSUER_URL"); val != "" {
issuerURL = val
}
if val := os.Getenv("OIDC_CLIENT_ID"); val != "" {
clientID = val
}
})

It("should create, get, list and delete an ExternalAuth config via RP API", func() {
resourceURL := fmt.Sprintf("%s/subscriptions/%s/resourceGroups/%s/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/%s/externalAuths/%s?api-version=%s",
baseURL, subscriptionID, resourceGroup, clusterName, providerID, apiVersion)

payload := map[string]interface{}{
"properties": map[string]interface{}{
"issuer": map[string]interface{}{
"url": issuerURL,
"audiences": []string{clientID},
},
"claimMappings": map[string]interface{}{
"username": map[string]interface{}{"claim": "email"},
"groups": map[string]interface{}{"claim": "groups"},
},
},
}

By("Creating ExternalAuth config")
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", resourceURL, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Ms-Identity-Url", "https://dummy.identity.azure.net")
req.Header.Set("X-Ms-Arm-Resource-System-Data",
fmt.Sprintf(`{"createdBy": "e2e","createdByType":"User","createdAt":"%s"}`, time.Now().UTC().Format(time.RFC3339)))

resp, err := http.DefaultClient.Do(req)
Expect(err).ToNot(HaveOccurred())
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Fprintf(GinkgoWriter, "PUT response body: %s\n", respBody)
Expect(resp.StatusCode).To(Equal(http.StatusOK))

By("Fetching the created ExternalAuth config")
Eventually(func() int {
respGet, err := http.Get(resourceURL)
Expect(err).ToNot(HaveOccurred())
defer respGet.Body.Close()
return respGet.StatusCode
}, 10*time.Second, 1*time.Second).Should(Equal(http.StatusOK))

By("Listing ExternalAuth configs")
listURL := fmt.Sprintf("%s/subscriptions/%s/resourceGroups/%s/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/%s/externalAuths?api-version=%s",
baseURL, subscriptionID, resourceGroup, clusterName, apiVersion)
respList, err := http.Get(listURL)
Expect(err).ToNot(HaveOccurred())
Expect(respList.StatusCode).To(Equal(http.StatusOK))
defer respList.Body.Close()

By("Deleting the ExternalAuth config")
reqDel, _ := http.NewRequest("DELETE", resourceURL, nil)
reqDel.Header.Set("X-Ms-Identity-Url", "https://dummy.identity.azure.net")
reqDel.Header.Set("X-Ms-Arm-Resource-System-Data",
fmt.Sprintf(`{"createdBy": "e2e","createdByType":"User","createdAt":"%s"}`, time.Now().UTC().Format(time.RFC3339)))
respDel, err := http.DefaultClient.Do(reqDel)
Expect(err).ToNot(HaveOccurred())
Expect(respDel.StatusCode).To(Equal(http.StatusNoContent))
defer respDel.Body.Close()
})
})
Loading