From 4e82ec8da69c74ca39a6a649c4742373e8d8ebfe Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 08:18:43 +0000 Subject: [PATCH] feat(agentic): add auth IP restrictions Co-Authored-By: Virgil --- pkg/agentic/auth.go | 79 +++++++++++++++------------ pkg/agentic/auth_test.go | 8 ++- pkg/agentic/commands_platform.go | 5 +- pkg/agentic/commands_platform_test.go | 34 ++++++++++++ pkg/agentic/platform_tools.go | 1 + 5 files changed, 89 insertions(+), 38 deletions(-) diff --git a/pkg/agentic/auth.go b/pkg/agentic/auth.go index 52adbf5..fc44ad1 100644 --- a/pkg/agentic/auth.go +++ b/pkg/agentic/auth.go @@ -8,29 +8,31 @@ import ( core "dappco.re/go/core" ) -// key := agentic.AgentApiKey{ID: 7, Name: "codex local", Prefix: "ak_abcd", RateLimit: 60} +// key := agentic.AgentApiKey{ID: 7, Name: "codex local", Prefix: "ak_abcd", IPRestrictions: []string{"10.0.0.0/8"}, RateLimit: 60} type AgentApiKey struct { - ID int `json:"id"` - WorkspaceID int `json:"workspace_id,omitempty"` - Name string `json:"name,omitempty"` - Key string `json:"key,omitempty"` - Prefix string `json:"prefix,omitempty"` - Permissions []string `json:"permissions,omitempty"` - RateLimit int `json:"rate_limit,omitempty"` - CallCount int `json:"call_count,omitempty"` - LastUsedAt string `json:"last_used_at,omitempty"` - ExpiresAt string `json:"expires_at,omitempty"` - RevokedAt string `json:"revoked_at,omitempty"` - CreatedAt string `json:"created_at,omitempty"` + ID int `json:"id"` + WorkspaceID int `json:"workspace_id,omitempty"` + Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` + Prefix string `json:"prefix,omitempty"` + Permissions []string `json:"permissions,omitempty"` + IPRestrictions []string `json:"ip_restrictions,omitempty"` + RateLimit int `json:"rate_limit,omitempty"` + CallCount int `json:"call_count,omitempty"` + LastUsedAt string `json:"last_used_at,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` + RevokedAt string `json:"revoked_at,omitempty"` + CreatedAt string `json:"created_at,omitempty"` } -// input := agentic.AuthProvisionInput{OAuthUserID: "user-42", Permissions: []string{"plans:read"}} +// input := agentic.AuthProvisionInput{OAuthUserID: "user-42", Permissions: []string{"plans:read"}, IPRestrictions: []string{"10.0.0.0/8"}} type AuthProvisionInput struct { - OAuthUserID string `json:"oauth_user_id"` - Name string `json:"name,omitempty"` - Permissions []string `json:"permissions,omitempty"` - RateLimit int `json:"rate_limit,omitempty"` - ExpiresAt string `json:"expires_at,omitempty"` + OAuthUserID string `json:"oauth_user_id"` + Name string `json:"name,omitempty"` + Permissions []string `json:"permissions,omitempty"` + IPRestrictions []string `json:"ip_restrictions,omitempty"` + RateLimit int `json:"rate_limit,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` } // out := agentic.AuthProvisionOutput{Success: true, Key: agentic.AgentApiKey{Prefix: "ak_abcd"}} @@ -59,11 +61,12 @@ type AuthRevokeOutput struct { // )) func (s *PrepSubsystem) handleAuthProvision(ctx context.Context, options core.Options) core.Result { input := AuthProvisionInput{ - OAuthUserID: optionStringValue(options, "oauth_user_id", "oauth-user-id", "user_id", "user-id", "_arg"), - Name: optionStringValue(options, "name"), - Permissions: optionStringSliceValue(options, "permissions"), - RateLimit: optionIntValue(options, "rate_limit", "rate-limit"), - ExpiresAt: optionStringValue(options, "expires_at", "expires-at"), + OAuthUserID: optionStringValue(options, "oauth_user_id", "oauth-user-id", "user_id", "user-id", "_arg"), + Name: optionStringValue(options, "name"), + Permissions: optionStringSliceValue(options, "permissions"), + IPRestrictions: optionStringSliceValue(options, "ip_restrictions", "ip-restrictions", "allowed_ips", "allowed-ips"), + RateLimit: optionIntValue(options, "rate_limit", "rate-limit"), + ExpiresAt: optionStringValue(options, "expires_at", "expires-at"), } if input.OAuthUserID == "" { return core.Result{Value: core.E("agentic.auth.provision", "oauth_user_id is required", nil), OK: false} @@ -78,6 +81,9 @@ func (s *PrepSubsystem) handleAuthProvision(ctx context.Context, options core.Op if len(input.Permissions) > 0 { body["permissions"] = input.Permissions } + if len(input.IPRestrictions) > 0 { + body["ip_restrictions"] = input.IPRestrictions + } if input.RateLimit > 0 { body["rate_limit"] = input.RateLimit } @@ -145,18 +151,19 @@ func (s *PrepSubsystem) handleAuthRevoke(ctx context.Context, options core.Optio func parseAgentApiKey(values map[string]any) AgentApiKey { return AgentApiKey{ - ID: intValue(values["id"]), - WorkspaceID: intValue(values["workspace_id"]), - Name: stringValue(values["name"]), - Key: stringValue(values["key"]), - Prefix: stringValue(values["prefix"]), - Permissions: listValue(values["permissions"]), - RateLimit: intValue(values["rate_limit"]), - CallCount: intValue(values["call_count"]), - LastUsedAt: stringValue(values["last_used_at"]), - ExpiresAt: stringValue(values["expires_at"]), - RevokedAt: stringValue(values["revoked_at"]), - CreatedAt: stringValue(values["created_at"]), + ID: intValue(values["id"]), + WorkspaceID: intValue(values["workspace_id"]), + Name: stringValue(values["name"]), + Key: stringValue(values["key"]), + Prefix: stringValue(values["prefix"]), + Permissions: listValue(values["permissions"]), + IPRestrictions: listValue(values["ip_restrictions"]), + RateLimit: intValue(values["rate_limit"]), + CallCount: intValue(values["call_count"]), + LastUsedAt: stringValue(values["last_used_at"]), + ExpiresAt: stringValue(values["expires_at"]), + RevokedAt: stringValue(values["revoked_at"]), + CreatedAt: stringValue(values["created_at"]), } } diff --git a/pkg/agentic/auth_test.go b/pkg/agentic/auth_test.go index a7a0704..435c880 100644 --- a/pkg/agentic/auth_test.go +++ b/pkg/agentic/auth_test.go @@ -34,7 +34,11 @@ func TestAuth_HandleAuthProvision_Good(t *testing.T) { require.True(t, ok) require.Equal(t, []any{"plans:read", "plans:write"}, permissions) - _, _ = w.Write([]byte(`{"data":{"id":7,"workspace_id":3,"name":"codex local","key":"ak_live_secret","prefix":"ak_live","permissions":["plans:read","plans:write"],"rate_limit":60,"call_count":2,"expires_at":"2026-04-01T00:00:00Z"}}`)) + ipRestrictions, ok := payload["ip_restrictions"].([]any) + require.True(t, ok) + require.Equal(t, []any{"10.0.0.0/8", "192.168.0.0/16"}, ipRestrictions) + + _, _ = w.Write([]byte(`{"data":{"id":7,"workspace_id":3,"name":"codex local","key":"ak_live_secret","prefix":"ak_live","permissions":["plans:read","plans:write"],"ip_restrictions":["10.0.0.0/8","192.168.0.0/16"],"rate_limit":60,"call_count":2,"expires_at":"2026-04-01T00:00:00Z"}}`)) })) defer server.Close() @@ -43,6 +47,7 @@ func TestAuth_HandleAuthProvision_Good(t *testing.T) { core.Option{Key: "oauth_user_id", Value: "user-42"}, core.Option{Key: "name", Value: "codex local"}, core.Option{Key: "permissions", Value: "plans:read,plans:write"}, + core.Option{Key: "ip_restrictions", Value: "10.0.0.0/8,192.168.0.0/16"}, core.Option{Key: "rate_limit", Value: 60}, core.Option{Key: "expires_at", Value: "2026-04-01T00:00:00Z"}, )) @@ -57,6 +62,7 @@ func TestAuth_HandleAuthProvision_Good(t *testing.T) { assert.Equal(t, "ak_live_secret", output.Key.Key) assert.Equal(t, "ak_live", output.Key.Prefix) assert.Equal(t, []string{"plans:read", "plans:write"}, output.Key.Permissions) + assert.Equal(t, []string{"10.0.0.0/8", "192.168.0.0/16"}, output.Key.IPRestrictions) assert.Equal(t, 60, output.Key.RateLimit) assert.Equal(t, 2, output.Key.CallCount) } diff --git a/pkg/agentic/commands_platform.go b/pkg/agentic/commands_platform.go index d6f4ddf..fe747c1 100644 --- a/pkg/agentic/commands_platform.go +++ b/pkg/agentic/commands_platform.go @@ -69,7 +69,7 @@ func (s *PrepSubsystem) registerPlatformCommands() { func (s *PrepSubsystem) cmdAuthProvision(options core.Options) core.Result { if optionStringValue(options, "oauth_user_id", "oauth-user-id", "user_id", "user-id", "_arg") == "" { - core.Print(nil, "usage: core-agent auth provision [--name=codex] [--permissions=plans:read,plans:write] [--rate-limit=60] [--expires-at=2026-04-01T00:00:00Z]") + core.Print(nil, "usage: core-agent auth provision [--name=codex] [--permissions=plans:read,plans:write] [--ip-restrictions=10.0.0.0/8,192.168.0.0/16] [--rate-limit=60] [--expires-at=2026-04-01T00:00:00Z]") return core.Result{Value: core.E("agentic.cmdAuthProvision", "oauth_user_id is required", nil), OK: false} } @@ -96,6 +96,9 @@ func (s *PrepSubsystem) cmdAuthProvision(options core.Options) core.Result { if len(output.Key.Permissions) > 0 { core.Print(nil, "permissions: %s", core.Join(",", output.Key.Permissions...)) } + if len(output.Key.IPRestrictions) > 0 { + core.Print(nil, "ip restrictions: %s", core.Join(",", output.Key.IPRestrictions...)) + } if output.Key.ExpiresAt != "" { core.Print(nil, "expires: %s", output.Key.ExpiresAt) } diff --git a/pkg/agentic/commands_platform_test.go b/pkg/agentic/commands_platform_test.go index 517c08e..e5c54ad 100644 --- a/pkg/agentic/commands_platform_test.go +++ b/pkg/agentic/commands_platform_test.go @@ -23,6 +23,40 @@ func TestCommandsplatform_CmdAuthProvision_Bad(t *testing.T) { assert.False(t, result.OK) } +func TestCommandsplatform_CmdAuthProvision_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/v1/agent/auth/provision", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + bodyResult := core.ReadAll(r.Body) + assert.True(t, bodyResult.OK) + + var payload map[string]any + parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload) + assert.True(t, parseResult.OK) + assert.Equal(t, []any{"10.0.0.0/8", "192.168.0.0/16"}, payload["ip_restrictions"]) + + _, _ = w.Write([]byte(`{"data":{"id":7,"workspace_id":3,"name":"codex local","key":"ak_live_secret","prefix":"ak_live","permissions":["plans:read","plans:write"],"ip_restrictions":["10.0.0.0/8","192.168.0.0/16"],"rate_limit":60,"call_count":2,"expires_at":"2026-04-01T00:00:00Z"}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + output := captureStdout(t, func() { + result := subsystem.cmdAuthProvision(core.NewOptions( + core.Option{Key: "_arg", Value: "user-42"}, + core.Option{Key: "name", Value: "codex local"}, + core.Option{Key: "permissions", Value: "plans:read,plans:write"}, + core.Option{Key: "ip_restrictions", Value: "10.0.0.0/8,192.168.0.0/16"}, + core.Option{Key: "rate_limit", Value: 60}, + core.Option{Key: "expires_at", Value: "2026-04-01T00:00:00Z"}, + )) + assert.True(t, result.OK) + }) + + assert.Contains(t, output, "ip restrictions: 10.0.0.0/8,192.168.0.0/16") + assert.Contains(t, output, "prefix: ak_live") +} + func TestCommandsplatform_CmdAuthRevoke_Good(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"data":{"key_id":"7","revoked":true}}`)) diff --git a/pkg/agentic/platform_tools.go b/pkg/agentic/platform_tools.go index 855097e..83c3eb0 100644 --- a/pkg/agentic/platform_tools.go +++ b/pkg/agentic/platform_tools.go @@ -228,6 +228,7 @@ func (s *PrepSubsystem) authProvisionTool(ctx context.Context, _ *mcp.CallToolRe core.Option{Key: "oauth_user_id", Value: input.OAuthUserID}, core.Option{Key: "name", Value: input.Name}, core.Option{Key: "permissions", Value: input.Permissions}, + core.Option{Key: "ip_restrictions", Value: input.IPRestrictions}, core.Option{Key: "rate_limit", Value: input.RateLimit}, core.Option{Key: "expires_at", Value: input.ExpiresAt}, )