Skip to content
Merged
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
4 changes: 2 additions & 2 deletions internal/agent/auth_hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestEnrollmentAuthRegistersAgentThenWhoamiUsesTheEnrolledIdentity(t *testin

whoami := authHookEnvelope{
API: "realmroot", Profile: "default",
Requirements: []authRequirement{{ID: oauth2SchemeID, Kind: "oauth2-dpop", Needs: []string{"agent:read"}}},
Requirements: []authRequirement{{ID: oauth2SchemeID, Kind: "oauth2", Needs: []string{"agent:read"}}},
Request: plugin.HookRequest{Method: http.MethodGet, URI: "https://auth.example.com/api/agent"},
}
output, err = authenticateHookRequest(whoami, states, client, prompt)
Expand Down Expand Up @@ -166,7 +166,7 @@ func testWhoamiWithoutLocalRegistrationRequiresExplicitEnrollment(t *testing.T)
prompt := &promptRecorder{}
_, err := authenticateHookRequest(authHookEnvelope{
API: "realmroot", Profile: "default",
Requirements: []authRequirement{{ID: oauth2SchemeID, Kind: "oauth2-dpop", Needs: []string{"agent:read"}}},
Requirements: []authRequirement{{ID: oauth2SchemeID, Kind: "oauth2", Needs: []string{"agent:read"}}},
Request: plugin.HookRequest{Method: http.MethodGet, URI: "https://auth.example.com/api/agent"},
}, &memoryStateStore{}, client, prompt)
if err == nil || !strings.Contains(err.Error(), "realmroot agent enroll") {
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/protocol_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func supportedProtocolAlternative(requirements []authRequirement) bool {
switch requirements[0].ID {
case oauth2SchemeID:
switch requirements[0].Kind {
case "oauth2-dpop", "oauth2", "openid":
case "oauth2", "openid":
return true
}
case agentAssertionSchemeID:
Expand Down
7 changes: 6 additions & 1 deletion internal/agent/protocol_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ func TestSupportedProtocolAlternativeSeparatesEnrollmentFromResourceOAuth(t *tes
},
{
name: "Resource OAuth scheme with an Agent bootstrap scope",
requirements: []authRequirement{{ID: "oauth2", Kind: "oauth2-dpop", Needs: []string{"resource-servers:read"}}},
requirements: []authRequirement{{ID: "oauth2", Kind: "oauth2", Needs: []string{"resource-servers:read"}}},
want: true,
},
{
name: "obsolete OAuth DPoP extension kind",
requirements: []authRequirement{{ID: "oauth2", Kind: "oauth2-dpop", Needs: []string{"resource-servers:read"}}},
want: false,
},
{
name: "Agent enrollment assertion",
requirements: []authRequirement{{ID: "agentAssertion", Kind: "http-bearer"}},
Expand Down
25 changes: 19 additions & 6 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,13 +507,14 @@ func TestSelectedGenericOperationUsesConfiguredOperationBase(t *testing.T) {
}

func TestBindProfileCredentialsSupportsGenericHTTPRequests(t *testing.T) {
// [spec: cli/direct-resource-operation]
config := &restish.Config{APIs: map[string]*restish.APIConfig{"platform": {}}}
binding := agent.CredentialBinding{Reference: "selected-reference", Scopes: []string{"resource-servers:read"}}
if err := bindProfileCredentials(config, catalog.ResourceServer{CommandName: "platform"}, githubOperationInspection(), "default", binding); err != nil {
t.Fatal(err)
}
credential := config.APIs["platform"].Profiles["default"].Credentials["realmrootOidc"]
if credential == nil || credential.Auth == nil || credential.Auth.Params["reference"] != binding.Reference || strings.Join(credential.Satisfies, " ") != "resource-servers:read" {
if credential == nil || credential.Auth == nil || credential.Auth.Type != "dpop" || credential.Auth.Params["reference"] != binding.Reference || strings.Join(credential.Satisfies, " ") != "resource-servers:read" {
t.Fatalf("credential = %#v", credential)
}
}
Expand All @@ -525,6 +526,18 @@ func TestOperationScopeAlternativesPreserveOAuthAlternatives(t *testing.T) {
}
}

func TestOperationAuthorityRejectsObsoleteOAuthDPoPKind(t *testing.T) {
operation := restish.OperationInspection{CredentialAlternatives: [][]restish.CredentialRequirementInspection{{{
ID: "oauth2", Kind: "oauth2-dpop", Needs: []string{"applications:read"},
}}}}
if got := operationCredentialScopeAlternatives(operation); len(got) != 0 {
t.Fatalf("obsolete scope alternatives = %#v", got)
}
if operationCoveredByScopes(operation, []string{"applications:read"}) {
t.Fatal("obsolete OAuth DPoP extension kind accepted Agent authority")
}
}

type recordingOperationCredentialResolver struct {
binding agent.CredentialBinding
err error
Expand Down Expand Up @@ -650,8 +663,8 @@ func githubOperationInspection() restish.APIInspection {
return restish.APIInspection{Operations: []restish.OperationInspection{{
ID: "issuesGet", Command: []string{"issues", "issues-get"}, Method: "GET", Path: "/repos/{owner}/{repo}/issues/{number}",
CredentialAlternatives: [][]restish.CredentialRequirementInspection{
{{ID: "realmrootOidc", Kind: "oauth2-dpop", Needs: []string{"issues:read"}}},
{{ID: "realmrootOidc", Kind: "oauth2-dpop", Needs: []string{"metadata:read"}}},
{{ID: "realmrootOidc", Kind: "oauth2", Needs: []string{"issues:read"}}},
{{ID: "realmrootOidc", Kind: "oauth2", Needs: []string{"metadata:read"}}},
},
}}}
}
Expand Down Expand Up @@ -770,8 +783,8 @@ func TestScopeFilterKeepsOnlyMatchingScopeAlternativesAndHidesCredentialSchemes(
operations := []restish.OperationInspection{{
ID: "getContent", Command: []string{"repos", "get-content"}, Method: "GET", Path: "/repos/{owner}/{repo}/contents/{path}",
CredentialAlternatives: [][]restish.CredentialRequirementInspection{
{{ID: "realmrootOidc", Kind: "oauth2-dpop", Needs: []string{"contents:read"}}},
{{ID: "realmrootOidc", Kind: "oauth2-dpop", Needs: []string{"metadata:read"}}},
{{ID: "realmrootOidc", Kind: "oauth2", Needs: []string{"contents:read"}}},
{{ID: "realmrootOidc", Kind: "oauth2", Needs: []string{"metadata:read"}}},
},
}}
overview := buildResourceServerOverview(catalog.ResourceServer{CommandName: "github"}, nil, operations, discoveryOptions{Scope: "contents:read"})
Expand All @@ -783,7 +796,7 @@ func TestScopeFilterKeepsOnlyMatchingScopeAlternativesAndHidesCredentialSchemes(
if err != nil {
t.Fatal(err)
}
for _, internal := range []string{"realmrootOidc", "oauth2-dpop", "metadata:read", "credentialAlternatives"} {
for _, internal := range []string{"realmrootOidc", "metadata:read", "credentialAlternatives"} {
if strings.Contains(string(encoded), internal) {
t.Fatalf("overview JSON exposed %q: %s", internal, encoded)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/operation_authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func bindProfileCredentials(
for _, candidate := range inspection.Operations {
for _, alternative := range candidate.CredentialAlternatives {
for _, requirement := range alternative {
if requirement.Kind != "oauth2-dpop" {
if requirement.Kind != "oauth2" {
continue
}
profile.Credentials[requirement.ID] = &restishconfig.CredentialConfig{
Expand All @@ -312,7 +312,7 @@ func operationCredentialScopeAlternatives(operation restish.OperationInspection)
scopes := make([]string, 0)
supported := len(alternative) > 0
for _, requirement := range alternative {
if requirement.Kind != "oauth2-dpop" {
if requirement.Kind != "oauth2" {
supported = false
break
}
Expand Down Expand Up @@ -379,7 +379,7 @@ func operationCoveredByScopes(operation restish.OperationInspection, scopes []st
for _, alternative := range operation.CredentialAlternatives {
covered := len(alternative) > 0
for _, requirement := range alternative {
if requirement.Kind != "oauth2-dpop" {
if requirement.Kind != "oauth2" {
covered = false
break
}
Expand Down
3 changes: 2 additions & 1 deletion specs/cli.feature
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ Feature: Realmroot Toolbox command line
@journey:direct-resource-operation @entrypoint:toolbox-operation
Scenario: Invoke an OpenAPI-generated Resource operation
Given one current cumulative credential is stored for each approved Resource Context
And the Resource Server publishes standard OAuth 2.0 security requirements
When the Agent invokes the generated Toolbox operation
Then Toolbox uses the selected Context's current cumulative authority
And it never selects among historical access-request credentials
And Restish sends the request directly to the Resource Server with the selected proof-bound credential
And Restish sends the request directly to the Resource Server with the selected DPoP-bound credential
And missing authority is reported using Realmroot Resource Server and scope vocabulary
But embedded engine profiles, credential bindings, and setup commands are never exposed

Expand Down