Skip to content

Commit 9b23298

Browse files
committed
e2e: app registration cleanup of orphaned app registrations that are expired
1 parent e3d2ca8 commit 9b23298

File tree

13 files changed

+455
-4
lines changed

13 files changed

+455
-4
lines changed

internal/graph/graphsdk/graph_base_service_client.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/graphsdk/kiota-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"descriptionHash": "DAC2FA87E0BD82AA2F0BDC3D7EACC6E2A50E16C33B5C67050C1CB898DBB7A8806C7221FF815AF7042586D869CB8C14A6ED3E07FB6F1D13FE2CEB1C70ADFD3B93",
2+
"descriptionHash": "81C12085B29621FDC2F8185AE6C59D08B34A9F9C612EBAF4E291DAE77C74D279E4544A848482CFF058AC2CF6B75E6457AD6120BD6DBB73CF9F4E1BDB4ED9A526",
33
"descriptionLocation": "../../../tooling/kiota/openapi.yaml",
44
"lockFileVersion": "1.0.0",
55
"kiotaVersion": "1.28.0",

internal/graph/graphsdk/me/me_request_builder.go

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/graphsdk/me/owned_objects_graph_application_request_builder.go

Lines changed: 105 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/graphsdk/me/owned_objects_request_builder.go

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/graphsdk/models/odataerrors/inner_error.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/util/applications.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@ import (
2020
"fmt"
2121
"time"
2222

23+
abstractions "github.com/microsoft/kiota-abstractions-go"
2324
"k8s.io/apimachinery/pkg/util/wait"
2425

2526
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
2627

2728
"github.com/Azure/ARO-HCP/internal/graph/graphsdk/applications"
29+
"github.com/Azure/ARO-HCP/internal/graph/graphsdk/me"
2830
"github.com/Azure/ARO-HCP/internal/graph/graphsdk/models"
2931
"github.com/Azure/ARO-HCP/internal/graph/graphsdk/models/odataerrors"
3032
)
3133

34+
const AppRegistrationPrefix = "aro-hcp-e2e-"
35+
3236
// Application represents a Microsoft Entra application
3337
type Application struct {
3438
ID string `json:"id"`
@@ -171,3 +175,46 @@ func (c *Client) GetApplication(ctx context.Context, appID string) (*Application
171175
DisplayName: *app.GetDisplayName(),
172176
}, nil
173177
}
178+
179+
// ListOwnedExpiredApplications retrieves applications owned by the current service principal
180+
// where all their credentials have expired and a display name starting with the e2e prefix.
181+
// This uses the /me/ownedObjects endpoint to ensure we only return applications we have
182+
// permission to delete.
183+
func (c *Client) ListOwnedExpiredApplications(ctx context.Context) ([]Application, error) {
184+
var apps []Application
185+
186+
headers := abstractions.NewRequestHeaders()
187+
headers.Add("ConsistencyLevel", "eventual")
188+
189+
resp, err := c.graphClient.Me().OwnedObjects().GraphApplication().Get(ctx,
190+
&me.OwnedObjectsGraphApplicationRequestBuilderGetRequestConfiguration{
191+
Headers: headers,
192+
QueryParameters: &me.OwnedObjectsGraphApplicationRequestBuilderGetQueryParameters{
193+
Filter: to.Ptr(fmt.Sprintf("startsWith(displayName,'%s')", AppRegistrationPrefix)),
194+
Count: to.Ptr(true),
195+
},
196+
})
197+
if err != nil {
198+
return nil, fmt.Errorf("list owned applications: %w", err)
199+
}
200+
201+
for _, app := range resp.GetValue() {
202+
// Skip app registrations that have credentials not yet expired
203+
skipApp := false
204+
for _, cred := range app.GetPasswordCredentials() {
205+
if cred.GetEndDateTime() != nil && cred.GetEndDateTime().After(time.Now()) {
206+
skipApp = true
207+
break
208+
}
209+
}
210+
if !skipApp {
211+
apps = append(apps, Application{
212+
ID: *app.GetId(),
213+
AppID: *app.GetAppId(),
214+
DisplayName: *app.GetDisplayName(),
215+
})
216+
}
217+
}
218+
219+
return apps, nil
220+
}

test/cmd/aro-hcp-tests/cleanup/cmd.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/Azure/ARO-HCP/test/pkg/logger"
2525
"github.com/Azure/ARO-HCP/test/util/cleanup"
26+
"github.com/Azure/ARO-HCP/test/util/cleanup/appregistrations"
2627
kustoroleassignments "github.com/Azure/ARO-HCP/test/util/cleanup/kusto-role-assignments"
2728
"github.com/Azure/ARO-HCP/test/util/cleanup/resourcegroups"
2829
)
@@ -40,6 +41,7 @@ func NewCommand() *cobra.Command {
4041
cmd.PersistentFlags().IntVarP(&opt.Verbosity, "verbosity", "v", opt.Verbosity, "Log verbosity level")
4142

4243
cmd.AddCommand(newCleanupResourceGroupsCommand())
44+
cmd.AddCommand(newCleanupAppRegistrationsCommand())
4345
cmd.AddCommand(newDeleteKustoRoleAssignmentsCommand())
4446

4547
cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
@@ -49,6 +51,35 @@ func NewCommand() *cobra.Command {
4951
return cmd
5052
}
5153

54+
func newCleanupAppRegistrationsCommand() *cobra.Command {
55+
cmd := &cobra.Command{
56+
Use: "app-registrations",
57+
Short: "Delete expired e2e app registrations",
58+
SilenceUsage: true,
59+
}
60+
rawOpt := appregistrations.DefaultOptions()
61+
cmd.Flags().BoolVar(&rawOpt.DryRun, "dry-run", rawOpt.DryRun, "Print which app registrations would be deleted without deleting")
62+
63+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
64+
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt)
65+
defer cancel()
66+
67+
validatedOpt, err := rawOpt.Validate()
68+
if err != nil {
69+
return err
70+
}
71+
72+
completedOpt, err := validatedOpt.Complete(ctx)
73+
if err != nil {
74+
return err
75+
}
76+
77+
return completedOpt.Run(ctx)
78+
}
79+
80+
return cmd
81+
}
82+
5283
func newDeleteKustoRoleAssignmentsCommand() *cobra.Command {
5384
cmd := &cobra.Command{
5485
Use: "kusto-role-assignments",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2026 Microsoft Corporation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package appregistrations
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
graphutil "github.com/Azure/ARO-HCP/internal/graph/util"
22+
"github.com/Azure/ARO-HCP/test/util/framework"
23+
)
24+
25+
type RawOptions struct {
26+
DryRun bool
27+
}
28+
29+
type ValidatedOptions struct {
30+
*RawOptions
31+
}
32+
33+
type Options struct {
34+
DryRun bool
35+
GraphClient *graphutil.Client
36+
}
37+
38+
func DefaultOptions() *RawOptions {
39+
return &RawOptions{}
40+
}
41+
42+
func (o *RawOptions) Validate() (*ValidatedOptions, error) {
43+
return &ValidatedOptions{RawOptions: o}, nil
44+
}
45+
46+
func (o *ValidatedOptions) Complete(ctx context.Context) (*Options, error) {
47+
tc := framework.NewTestContext()
48+
49+
graphClient, err := tc.GetGraphClient(ctx)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to get graph client: %w", err)
52+
}
53+
54+
return &Options{
55+
DryRun: o.DryRun,
56+
GraphClient: graphClient,
57+
}, nil
58+
}

0 commit comments

Comments
 (0)