fix: restrict agnet simulation controls
This commit is contained in:
@@ -1230,6 +1230,10 @@ func normalizeAgnetSimulationEvents(values []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AgnetSimulateUserDeploymentEvents(c *gin.Context) {
|
func AgnetSimulateUserDeploymentEvents(c *gin.Context) {
|
||||||
|
if c.GetInt("role") < common.RoleAdminUser && !common.GetEnvOrDefaultBool("AGNET_SIMULATION_ENABLED", false) {
|
||||||
|
agnetError(c, "SIMULATION_DISABLED", "simulation endpoint is admin-only unless AGNET_SIMULATION_ENABLED=true")
|
||||||
|
return
|
||||||
|
}
|
||||||
record, ok := requireAuthenticatedUserAgnetDeployment(c)
|
record, ok := requireAuthenticatedUserAgnetDeployment(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -504,6 +504,7 @@ func TestAgnetUserDeploymentSimulatedEventsArePersistedAndPrefixed(t *testing.T)
|
|||||||
simRecorder := httptest.NewRecorder()
|
simRecorder := httptest.NewRecorder()
|
||||||
simCtx, _ := gin.CreateTestContext(simRecorder)
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
||||||
simCtx.Set("id", 7)
|
simCtx.Set("id", 7)
|
||||||
|
simCtx.Set("role", common.RoleAdminUser)
|
||||||
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{"events":["deployment.started","simulation.agent.done"]}`))
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{"events":["deployment.started","simulation.agent.done"]}`))
|
||||||
simCtx.Request.Header.Set("Content-Type", "application/json")
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
||||||
@@ -546,6 +547,7 @@ func TestAgnetUserDeploymentSimulationRejectsOtherUsersDeployment(t *testing.T)
|
|||||||
simRecorder := httptest.NewRecorder()
|
simRecorder := httptest.NewRecorder()
|
||||||
simCtx, _ := gin.CreateTestContext(simRecorder)
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
||||||
simCtx.Set("id", 8)
|
simCtx.Set("id", 8)
|
||||||
|
simCtx.Set("role", common.RoleAdminUser)
|
||||||
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`))
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`))
|
||||||
simCtx.Request.Header.Set("Content-Type", "application/json")
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
||||||
@@ -557,6 +559,37 @@ func TestAgnetUserDeploymentSimulationRejectsOtherUsersDeployment(t *testing.T)
|
|||||||
require.Equal(t, "DEPLOYMENT_FORBIDDEN", envelope.Error.Code)
|
require.Equal(t, "DEPLOYMENT_FORBIDDEN", envelope.Error.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgnetUserDeploymentSimulationIsDisabledForRegularUserByDefault(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.UserContext.UserID = "7"
|
||||||
|
for idx := range plan.Agents[0].ResourceGrants {
|
||||||
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
||||||
|
}
|
||||||
|
createRecorder, createEnvelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||||
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
||||||
|
require.True(t, createEnvelope.Success)
|
||||||
|
var createBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
||||||
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||||
|
|
||||||
|
simRecorder := httptest.NewRecorder()
|
||||||
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
||||||
|
simCtx.Set("id", 7)
|
||||||
|
simCtx.Set("role", common.RoleCommonUser)
|
||||||
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`))
|
||||||
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
AgnetSimulateUserDeploymentEvents(simCtx)
|
||||||
|
|
||||||
|
var envelope agnetCreateTestEnvelope
|
||||||
|
require.NoError(t, common.Unmarshal(simRecorder.Body.Bytes(), &envelope))
|
||||||
|
require.False(t, envelope.Success)
|
||||||
|
require.Equal(t, "SIMULATION_DISABLED", envelope.Error.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgnetUserSwarmsAdapterCreatesScopedDeployment(t *testing.T) {
|
func TestAgnetUserSwarmsAdapterCreatesScopedDeployment(t *testing.T) {
|
||||||
setupAgnetControlPlaneTestDB(t)
|
setupAgnetControlPlaneTestDB(t)
|
||||||
resetAgnetControlPlaneState(t)
|
resetAgnetControlPlaneState(t)
|
||||||
|
|||||||
+16
-11
@@ -23,6 +23,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
|
// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
|
||||||
// controller — the contract doc names that endpoint as the canonical
|
// controller — the contract doc names that endpoint as the canonical
|
||||||
@@ -349,6 +350,8 @@ function grantStatusToneClass(status: string | undefined): string {
|
|||||||
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const currentUser = useAuthStore((state) => state.auth.user)
|
||||||
|
const canSimulate = Boolean(currentUser?.role && currentUser.role >= 10)
|
||||||
const phase = dep.phase || dep.status
|
const phase = dep.phase || dep.status
|
||||||
const risk = describeRiskLevel(dep)
|
const risk = describeRiskLevel(dep)
|
||||||
const grants = collectResourceGrants(dep)
|
const grants = collectResourceGrants(dep)
|
||||||
@@ -389,17 +392,19 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex shrink-0 items-center gap-2'>
|
<div className='flex shrink-0 items-center gap-2'>
|
||||||
<Button
|
{canSimulate && (
|
||||||
type='button'
|
<Button
|
||||||
variant='outline'
|
type='button'
|
||||||
size='sm'
|
variant='outline'
|
||||||
className='h-8 gap-1 rounded-xl text-xs'
|
size='sm'
|
||||||
disabled={simulateMutation.isPending}
|
className='h-8 gap-1 rounded-xl text-xs'
|
||||||
onClick={() => simulateMutation.mutate()}
|
disabled={simulateMutation.isPending}
|
||||||
>
|
onClick={() => simulateMutation.mutate()}
|
||||||
<Rocket className='h-3.5 w-3.5' />
|
>
|
||||||
{t('Simulate')}
|
<Rocket className='h-3.5 w-3.5' />
|
||||||
</Button>
|
{t('Simulate')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<StatusBadge phase={phase} />
|
<StatusBadge phase={phase} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user