feat(agentic): add auth IP restrictions

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 08:18:43 +00:00
parent 0779a7dbe5
commit 4e82ec8da6
5 changed files with 89 additions and 38 deletions

View file

@ -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"]),
}
}

View file

@ -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)
}

View file

@ -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 <oauth-user-id> [--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 <oauth-user-id> [--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)
}

View file

@ -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}}`))

View file

@ -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},
)