Skip to content

Commit 6f2867c

Browse files
Nathandrake229Naman TyagiCopilot
authored
feat(azure.ai.agents): add full OAuth2 fields and connector-name support (#8358)
* feat(azure.ai.agents): move OAuth2 to raw REST, add full OAuth2 fields Fixes #8355 Move OAuth2 from typed ARM SDK path to raw REST to support fields not modeled in the ARM Go SDK: authorizationUrl, tokenUrl, refreshUrl, scopes, and connectorName. New flags for connection create: --authorization-url OAuth2 authorization endpoint --token-url OAuth2 token endpoint --refresh-url OAuth2 refresh endpoint --scopes OAuth2 scopes (space-separated) --connector-name Managed connector name OAuth2 credentials (--client-id/--client-secret) now sent as nested credentials object in the raw REST body. Update path also routes oauth2 through raw REST for consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add either/or validation for OAuth2 connector-name vs BYO fields OAuth2 auth now supports two mutually exclusive modes: - --connector-name alone (managed connector flow) - All of --authorization-url, --token-url, --refresh-url, --scopes, --client-id, --client-secret together (BYO OAuth2) Partial combinations are rejected with a clear error listing missing flags. Added test for connector-name-only body serialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: allow --audience for project-managed-identity, scopes are comma-separated Address Linda's review comments: - --audience now also valid with --auth-type project-managed-identity - --scopes help text updated to comma-separated (was space-separated) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review — scopes as []string, optional refresh-url, validation tests - Change Scopes from string to []string to match schema wire format - Switch --scopes flag to StringSliceVar (repeatable/comma-separated) - Make --refresh-url and --scopes optional in BYO OAuth2 validation - Fix test: remove ConnectorName from BYO test (mutually exclusive) - Add TestOAuth2Validation with 7 subtests covering all branches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: resolve lint failures — gofmt alignment and gosec nolint annotations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Naman Tyagi <namantyagi@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aea43b8 commit 6f2867c

3 files changed

Lines changed: 328 additions & 84 deletions

File tree

cli/azd/extensions/azure.ai.agents/internal/connections/cmd/connection.go

Lines changed: 128 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"log"
1111
"maps"
1212
"os"
13+
"strings"
1314
"text/tabwriter"
1415

1516
"azureaiagent/internal/connections/exterrors"
@@ -190,18 +191,23 @@ func newConnectionShowCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
190191

191192
// connectionCreateFlags holds validated input for ConnectionCreateAction.
192193
type connectionCreateFlags struct {
193-
name string
194-
kind string
195-
target string
196-
authType string
197-
key string
198-
customKeys []string
199-
metadata []string
200-
force bool
201-
projectEndpoint string
202-
clientID string // OAuth2 client ID
203-
clientSecret string // OAuth2 client secret
204-
audience string // Token audience for user-entra-token / agentic-identity
194+
name string
195+
kind string
196+
target string
197+
authType string
198+
key string
199+
customKeys []string
200+
metadata []string
201+
force bool
202+
projectEndpoint string
203+
clientID string // OAuth2 client ID
204+
clientSecret string // OAuth2 client secret
205+
audience string // Token audience for user-entra-token / agentic-identity / project-managed-identity
206+
authorizationURL string // OAuth2 authorization endpoint
207+
tokenURL string // OAuth2 token endpoint
208+
refreshURL string // OAuth2 refresh endpoint
209+
scopes []string // OAuth2 scopes
210+
connectorName string // Managed connector name
205211
}
206212

207213
// ConnectionCreateAction implements connection creation.
@@ -239,25 +245,82 @@ func (a *ConnectionCreateAction) Run(ctx context.Context) error {
239245
"Specify at least one custom key (e.g., --custom-key x-api-key=value).",
240246
)
241247
}
242-
if a.flags.authType == "oauth2" && (a.flags.clientID == "" || a.flags.clientSecret == "") {
243-
return exterrors.Validation(
244-
exterrors.CodeMissingConnectionField,
245-
"Missing required flags --client-id and --client-secret for oauth2 auth.",
246-
"Specify both OAuth2 client credentials.",
247-
)
248+
// OAuth2-only flags must not be used with other auth types.
249+
if a.flags.authType != "oauth2" {
250+
if a.flags.clientID != "" || a.flags.clientSecret != "" {
251+
return exterrors.Validation(
252+
exterrors.CodeConflictingArguments,
253+
"--client-id and --client-secret are only valid with --auth-type oauth2.",
254+
"",
255+
)
256+
}
257+
if a.flags.authorizationURL != "" || a.flags.tokenURL != "" ||
258+
a.flags.refreshURL != "" || len(a.flags.scopes) > 0 || a.flags.connectorName != "" {
259+
return exterrors.Validation(
260+
exterrors.CodeConflictingArguments,
261+
"--authorization-url, --token-url, --refresh-url, --scopes, and --connector-name "+
262+
"are only valid with --auth-type oauth2.",
263+
"",
264+
)
265+
}
248266
}
249-
if a.flags.authType != "oauth2" && (a.flags.clientID != "" || a.flags.clientSecret != "") {
250-
return exterrors.Validation(
251-
exterrors.CodeConflictingArguments,
252-
"--client-id and --client-secret are only valid with --auth-type oauth2.",
253-
"",
254-
)
267+
// OAuth2 validation: either --connector-name alone (managed connector) or all of
268+
// --authorization-url, --token-url, --refresh-url, --scopes, --client-id, --client-secret.
269+
if a.flags.authType == "oauth2" {
270+
hasConnector := a.flags.connectorName != ""
271+
hasBYO := a.flags.authorizationURL != "" || a.flags.tokenURL != "" ||
272+
a.flags.refreshURL != "" || len(a.flags.scopes) > 0 ||
273+
a.flags.clientID != "" || a.flags.clientSecret != ""
274+
275+
if hasConnector && hasBYO {
276+
return exterrors.Validation(
277+
exterrors.CodeConflictingArguments,
278+
"--connector-name cannot be combined with --authorization-url, --token-url, "+
279+
"--refresh-url, --scopes, --client-id, or --client-secret. "+
280+
"Use --connector-name alone for managed connectors, or provide the other flags for BYO OAuth2.",
281+
"",
282+
)
283+
}
284+
if !hasConnector && !hasBYO {
285+
return exterrors.Validation(
286+
exterrors.CodeMissingConnectionField,
287+
"OAuth2 auth requires either --connector-name (managed connector) or "+
288+
"--authorization-url, --token-url, --client-id, --client-secret "+
289+
"(and optionally --refresh-url, --scopes).",
290+
"",
291+
)
292+
}
293+
if !hasConnector {
294+
// BYO mode — required: authorization-url, token-url, client-id, client-secret.
295+
// Optional: refresh-url, scopes.
296+
missing := []string{}
297+
if a.flags.authorizationURL == "" {
298+
missing = append(missing, "--authorization-url")
299+
}
300+
if a.flags.tokenURL == "" {
301+
missing = append(missing, "--token-url")
302+
}
303+
if a.flags.clientID == "" {
304+
missing = append(missing, "--client-id")
305+
}
306+
if a.flags.clientSecret == "" {
307+
missing = append(missing, "--client-secret")
308+
}
309+
if len(missing) > 0 {
310+
return exterrors.Validation(
311+
exterrors.CodeMissingConnectionField,
312+
"BYO OAuth2 requires: --authorization-url, --token-url, --client-id, "+
313+
"--client-secret. Missing: "+strings.Join(missing, ", "),
314+
"",
315+
)
316+
}
317+
}
255318
}
256319
if a.flags.audience != "" && a.flags.authType != "user-entra-token" &&
257-
a.flags.authType != "agentic-identity" {
320+
a.flags.authType != "agentic-identity" && a.flags.authType != "project-managed-identity" {
258321
return exterrors.Validation(
259322
exterrors.CodeConflictingArguments,
260-
"--audience is only valid with --auth-type user-entra-token or agentic-identity.",
323+
"--audience is only valid with --auth-type user-entra-token, agentic-identity, or project-managed-identity.",
261324
"",
262325
)
263326
}
@@ -283,18 +346,26 @@ func (a *ConnectionCreateAction) Run(ctx context.Context) error {
283346

284347
// Route to raw REST or typed SDK based on auth type
285348
switch a.flags.authType {
286-
case "user-entra-token", "project-managed-identity", "agentic-identity":
287-
err = rawCreateConnection(
288-
ctx, connCtx,
289-
a.flags.name,
290-
rawConnectionProperties{
291-
AuthType: normalizeAuthTypeToARM(a.flags.authType),
292-
Category: normalizeKind(a.flags.kind),
293-
Target: a.flags.target,
294-
Audience: a.flags.audience,
295-
Metadata: parseKVMap(a.flags.metadata),
296-
},
297-
)
349+
case "oauth2", "user-entra-token", "project-managed-identity", "agentic-identity":
350+
props := rawConnectionProperties{
351+
AuthType: normalizeAuthTypeToARM(a.flags.authType),
352+
Category: normalizeKind(a.flags.kind),
353+
Target: a.flags.target,
354+
Audience: a.flags.audience,
355+
Metadata: parseKVMap(a.flags.metadata),
356+
AuthorizationURL: a.flags.authorizationURL,
357+
TokenURL: a.flags.tokenURL,
358+
RefreshURL: a.flags.refreshURL,
359+
Scopes: a.flags.scopes,
360+
ConnectorName: a.flags.connectorName,
361+
}
362+
if a.flags.clientID != "" || a.flags.clientSecret != "" {
363+
props.Credentials = &rawCredentials{
364+
ClientID: a.flags.clientID,
365+
ClientSecret: a.flags.clientSecret,
366+
}
367+
}
368+
err = rawCreateConnection(ctx, connCtx, a.flags.name, props)
298369
default:
299370
body, buildErr := buildConnectionBody(
300371
a.flags.kind, a.flags.target, a.flags.authType,
@@ -361,11 +432,21 @@ func newConnectionCreateCommand(extCtx *azdext.ExtensionContext) *cobra.Command
361432
cmd.Flags().BoolVar(&flags.force, "force", false,
362433
"Replace existing connection (upsert)")
363434
cmd.Flags().StringVar(&flags.clientID, "client-id", "",
364-
"OAuth2 client ID (required for oauth2 auth)")
435+
"OAuth2 client ID (required for BYO OAuth2)")
365436
cmd.Flags().StringVar(&flags.clientSecret, "client-secret", "",
366-
"OAuth2 client secret (required for oauth2 auth)")
437+
"OAuth2 client secret (required for BYO OAuth2)")
367438
cmd.Flags().StringVar(&flags.audience, "audience", "",
368-
"Token audience for user-entra-token/agentic-identity auth")
439+
"Token audience for user-entra-token/agentic-identity/project-managed-identity auth")
440+
cmd.Flags().StringVar(&flags.authorizationURL, "authorization-url", "",
441+
"OAuth2 authorization endpoint URL")
442+
cmd.Flags().StringVar(&flags.tokenURL, "token-url", "",
443+
"OAuth2 token endpoint URL")
444+
cmd.Flags().StringVar(&flags.refreshURL, "refresh-url", "",
445+
"OAuth2 token refresh URL")
446+
cmd.Flags().StringSliceVar(&flags.scopes, "scopes", nil,
447+
"OAuth2 scopes (repeatable or comma-separated, e.g. --scopes read:user,user:email)")
448+
cmd.Flags().StringVar(&flags.connectorName, "connector-name", "",
449+
"Managed connector name (for OAuth2 connectors)")
369450
return cmd
370451
}
371452

@@ -471,8 +552,8 @@ func (a *ConnectionUpdateAction) Run(ctx context.Context) error {
471552

472553
// Route to raw REST or typed SDK based on auth type
473554
switch normalizedAuth {
474-
case "user-entra-token", "project-managed-identity", "agentic-identity":
475-
// Identity auth types lack ARM SDK structs — update via raw REST
555+
case "oauth2", "user-entra-token", "project-managed-identity", "agentic-identity":
556+
// Auth types that lack full ARM SDK support — update via raw REST
476557
err = rawCreateConnection(
477558
ctx, connCtx,
478559
a.flags.name,
@@ -736,31 +817,12 @@ func buildConnectionBody(
736817
},
737818
}, nil
738819

739-
case "oauth2":
740-
at := armcognitiveservices.ConnectionAuthTypeOAuth2
741-
creds := &armcognitiveservices.ConnectionOAuth2{}
742-
if clientID != "" {
743-
creds.ClientID = &clientID
744-
}
745-
if clientSecret != "" {
746-
creds.ClientSecret = &clientSecret
747-
}
748-
return &armcognitiveservices.ConnectionPropertiesV2BasicResource{
749-
Properties: &armcognitiveservices.OAuth2AuthTypeConnectionProperties{
750-
AuthType: &at,
751-
Category: &cat,
752-
Target: &target,
753-
Credentials: creds,
754-
Metadata: metaMap,
755-
},
756-
}, nil
757-
758820
default:
759821
return nil, exterrors.Validation(
760822
exterrors.CodeInvalidAuthType,
761823
fmt.Sprintf("Unsupported auth type %q.", authType),
762-
"Supported: api-key, custom-keys, none, oauth2. "+
763-
"For identity-based auth types (user-entra-token, project-managed-identity, "+
824+
"Supported: api-key, custom-keys, none. "+
825+
"For oauth2 and identity-based auth types (user-entra-token, project-managed-identity, "+
764826
"agentic-identity), use 'connection create' directly.",
765827
)
766828
}
@@ -899,6 +961,8 @@ func normalizeAuthType(armAuthType string) string {
899961
// Used for auth types that lack ARM SDK structs and require raw REST.
900962
func normalizeAuthTypeToARM(cliAuthType string) string {
901963
switch cliAuthType {
964+
case "oauth2":
965+
return "OAuth2"
902966
case "user-entra-token":
903967
return "UserEntraToken"
904968
case "project-managed-identity":

0 commit comments

Comments
 (0)