feat(agentic): add auth IP restrictions
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
0779a7dbe5
commit
4e82ec8da6
5 changed files with 89 additions and 38 deletions
|
|
@ -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"]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}`))
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue