Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
112ab91
fix(auth): support ABAC-enabled registries by removing wildcard scopes
balcsida Aug 26, 2025
8506189
chore: remove unused refreshTokenForCatalog function
balcsida Aug 26, 2025
fec4ab2
test: add comprehensive ABAC registry test scripts
balcsida Aug 26, 2025
fbf2e53
fix: improve ABAC test script with proper authentication and Docker f…
balcsida Aug 26, 2025
8595d07
fix(test): correct ACR CLI binary path and add timeout for hanging co…
balcsida Aug 26, 2025
f6f2582
fix(test): add credentials to all ACR CLI commands in test scrips
balcsida Aug 26, 2025
989e303
fix(test): improve batch tag deletion test to check for specific tag …
balcsida Aug 26, 2025
70195ac
fix(test): comprehensive test fixes - exact pattern matching, debug o…
balcsida Aug 26, 2025
6426caa
fix(test): improve batch deletion with retry logic and fix regex esca…
balcsida Aug 26, 2025
3dff247
fix(test): resolve integer comparison errors and simplify tag deletio…
balcsida Aug 26, 2025
97eed9c
fix(test): final comprehensive fixes - exact pattern matching, proper…
balcsida Aug 26, 2025
49686b0
fix(test): add error handling for ACR CLI segfaults and integer compa…
balcsida Aug 26, 2025
3340d35
fix(test): improve integer handling in concurrent tests and fix v1 pa…
balcsida Aug 26, 2025
ddb5164
optimize(test): reduce test dataset sizes to prevent timeouts and imp…
balcsida Aug 26, 2025
e2ff253
fix(auth): address review comments - maintain backward compatibility …
balcsida Aug 26, 2025
f0e545a
feat(auth): implement scope tracking for ABAC registries
balcsida Aug 26, 2025
2abb822
chore: fix linting errors
balcsida Aug 27, 2025
b2379c2
feat: improve ABAC registry handling
balcsida Sep 16, 2025
b88473d
feat(test): fix tests
balcsida Sep 16, 2025
736a841
fix(auth): use TrimPrefix for safer scope parsing in hasRequiredScope
balcsida Jan 13, 2026
a095872
test(repo): add unit tests for isLikelyExactRepoName function
balcsida Jan 13, 2026
2d166ac
refactor(auth): use CutPrefix for cleaner scope parsing
balcsida Jan 13, 2026
ef4730a
test(auth): add unit tests for hasRequiredScope function
balcsida Jan 13, 2026
c11fa1a
chore(tagfilter): add comment to explain how we are doing tag filtering
balcsida Jan 13, 2026
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
52 changes: 48 additions & 4 deletions cmd/repository/image_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,21 @@ func GetRepositoryAndTagRegex(filter string) (string, string, error) {
// CollectTagFilters collects all matching repos and collects the associated tag filters
func CollectTagFilters(ctx context.Context, rawFilters []string, client acrapi.BaseClientAPI, regexMatchTimeout int64, repoPageSize int32) (map[string]string, error) {
allRepoNames, err := GetAllRepositoryNames(ctx, client, repoPageSize)
isABACRegistry := false

// If catalog listing fails (common in ABAC registries), we'll handle filters differently
if err != nil {
return nil, err
// Best-effort heuristic to detect ABAC registry permission issues.
// We check for common auth-related error strings since the underlying error type
// doesn't expose HTTP status codes directly. This may need adjustment if ACR
// changes its error message format.
if strings.Contains(err.Error(), "UNAUTHORIZED") || strings.Contains(err.Error(), "401") ||
strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "FORBIDDEN") {
isABACRegistry = true
allRepoNames = []string{} // Start with empty list for ABAC handling
} else {
return nil, err // Return other errors as-is
}
}

tagFilters := map[string]string{}
Expand All @@ -114,10 +127,26 @@ func CollectTagFilters(ctx context.Context, rawFilters []string, client acrapi.B
if err != nil {
return nil, err
}
repoNames, err := GetMatchingRepos(allRepoNames, "^"+repoRegex+"$", regexMatchTimeout)
if err != nil {
return nil, err

var repoNames []string

if isABACRegistry {
// For ABAC registries, treat repository patterns as exact names if they don't contain regex metacharacters
// This handles common cases where users specify exact repository names
if isLikelyExactRepoName(repoRegex) {
repoNames = []string{repoRegex}
} else {
// For complex repo patterns in ABAC registries, we can't list repositories,
// so return an error with helpful message
return nil, fmt.Errorf("ABAC registry detected: complex repository patterns (%s) require catalog listing permissions. Use exact repository names or add 'Container Registry Repository Catalog Lister' role", repoRegex)
}
} else {
repoNames, err = GetMatchingRepos(allRepoNames, "^"+repoRegex+"$", regexMatchTimeout)
if err != nil {
return nil, err
}
}

for _, repoName := range repoNames {
if _, ok := tagFilters[repoName]; ok {
// To only iterate through a repo once a big regex filter is made of all the filters of a particular repo.
Expand All @@ -131,6 +160,20 @@ func CollectTagFilters(ctx context.Context, rawFilters []string, client acrapi.B
return tagFilters, nil
}

// isLikelyExactRepoName checks if a repository pattern is likely an exact repository name
// rather than a regex pattern by looking for common regex metacharacters
func isLikelyExactRepoName(repoPattern string) bool {
// Common regex metacharacters that would indicate this is a pattern, not an exact name
regexChars := []string{".", "*", "+", "?", "^", "$", "[", "]", "(", ")", "|", "\\", "{", "}"}

for _, char := range regexChars {
if strings.Contains(repoPattern, char) {
return false
}
}
return true
}

// GetLastTagFromResponse extracts the last tag from pagination headers in the response.
func GetLastTagFromResponse(resultTags *acr.RepositoryTagsType) string {
// The lastTag is updated to keep the for loop going.
Expand Down Expand Up @@ -187,6 +230,7 @@ func GetUntaggedManifests(ctx context.Context, poolSize int, acrClient api.AcrCL
for resultManifests != nil && resultManifests.ManifestsAttributes != nil {
manifests := *resultManifests.ManifestsAttributes
for _, manifest := range manifests {
manifest := manifest // capture range variable for goroutines
// In the rare event that we run into an error with the errgroup while still doing the manifest acquisition loop,
// we need to check if the context is done to break out of the loop early.
if ctx.Err() != nil {
Expand Down
94 changes: 94 additions & 0 deletions cmd/repository/image_functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,97 @@ func secureRandomNum(minDepth, maxDepth int) int {
}
return int(n.Int64()) + minDepth
}

func TestIsLikelyExactRepoName(t *testing.T) {
testCases := []struct {
name string
pattern string
expected bool
}{
// Exact repository names (should return true)
{
name: "Simple repo name",
pattern: "myrepo",
expected: true,
},
{
name: "Repo name with slash",
pattern: "library/alpine",
expected: true,
},
{
name: "Repo name with multiple slashes",
pattern: "org/team/service",
expected: true,
},
{
name: "Repo name with hyphen and underscore",
pattern: "my-repo_name",
expected: true,
},

// Regex patterns (should return false)
{
name: "Wildcard pattern",
pattern: "repo.*",
expected: false,
},
{
name: "Character class",
pattern: "repo[0-9]",
expected: false,
},
{
name: "Alternation",
pattern: "repo1|repo2",
expected: false,
},
{
name: "Anchor start",
pattern: "^repo",
expected: false,
},
{
name: "Anchor end",
pattern: "repo$",
expected: false,
},
{
name: "Plus quantifier",
pattern: "repo+",
expected: false,
},
{
name: "Question mark quantifier",
pattern: "repo?",
expected: false,
},
{
name: "Grouping",
pattern: "(repo)",
expected: false,
},
{
name: "Escape sequence",
pattern: "repo\\.name",
expected: false,
},
{
name: "Curly braces quantifier",
pattern: "repo{2}",
expected: false,
},
{
name: "Dot metacharacter",
pattern: "repo.name",
expected: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := isLikelyExactRepoName(tc.pattern)
assert.Equal(t, tc.expected, result, "Pattern: %s", tc.pattern)
})
}
}
Loading