Skip to content
Open
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
1 change: 0 additions & 1 deletion cmd/ui_plugins/create.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved.
package ui_plugins

import (
Expand Down
1 change: 0 additions & 1 deletion cmd/ui_plugins/delete.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved.
package ui_plugins

import (
Expand Down
1 change: 0 additions & 1 deletion cmd/ui_plugins/init.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved.
package ui_plugins

import (
Expand Down
1 change: 0 additions & 1 deletion cmd/ui_plugins/link.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved.
package ui_plugins

import (
Expand Down
1 change: 0 additions & 1 deletion cmd/ui_plugins/list.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved.
package ui_plugins

import (
Expand Down
34 changes: 34 additions & 0 deletions cmd/ui_plugins/manifest_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ui_plugins

// uiPluginWorkspaceConfig represents the sp-ui-plugin.json workspace contract.
type uiPluginWorkspaceConfig struct {
Version int `json:"version"`
Manifest uiPluginManifest `json:"manifest"`
Build *uiPluginBuildConfig `json:"build,omitempty"`
}

// uiPluginSlot declares a UI extension point the plugin occupies.
type uiPluginSlot struct {
SlotID string `json:"slotId"`
RequiredCapabilities []string `json:"requiredCapabilities,omitempty"`
RestrictToUsers []string `json:"restrictToUsers,omitempty"`
}

// uiPluginManifest is the backend-facing payload section.
type uiPluginManifest struct {
Alias string `json:"alias"`
Name map[string]string `json:"name"`
Description map[string]string `json:"description"`
APIScopes []string `json:"apiScopes,omitempty"`
ContentSecurityPolicies map[string][]string `json:"contentSecurityPolicies,omitempty"`
PermissionPolicy map[string][]string `json:"permissionPolicy,omitempty"`
IframeAllow map[string][]string `json:"iframeAllow,omitempty"`
Slots []uiPluginSlot `json:"slots"`
}

// uiPluginBuildConfig is local CLI-only config and never sent to backend.
type uiPluginBuildConfig struct {
OutDir string `json:"outDir,omitempty"`
Port *int `json:"port,omitempty"`
}

92 changes: 92 additions & 0 deletions cmd/ui_plugins/manifest_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package ui_plugins

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
)

const (
manifestFileName = "sp-ui-plugin.json"
supportedVersion1 = 1
)

// loadAndValidateWorkspaceManifest is the shared full-validation entrypoint for
// ui-plugins commands. It performs strict parse + semantic validation.
func loadAndValidateWorkspaceManifest(path string) (*uiPluginWorkspaceConfig, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("unable to read %s: %w", path, err)
}

cfg, err := parseWorkspaceManifestStrict(raw)
if err != nil {
return nil, fmt.Errorf("invalid %s: %w", path, err)
}

if err := validateWorkspaceManifest(cfg); err != nil {
return nil, fmt.Errorf("invalid %s: %w", path, err)
}

return cfg, nil
}

func parseWorkspaceManifestStrict(raw []byte) (*uiPluginWorkspaceConfig, error) {
var cfg uiPluginWorkspaceConfig

dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&cfg); err != nil {
return nil, err
}

// Enforce a single JSON object in file.
if err := dec.Decode(&struct{}{}); err != io.EOF {
return nil, fmt.Errorf("manifest must contain a single JSON object")
}

return &cfg, nil
}

func validateWorkspaceManifest(cfg *uiPluginWorkspaceConfig) error {
if cfg.Version == 0 {
return fmt.Errorf("version is required")
}
if cfg.Version != supportedVersion1 {
return fmt.Errorf("unsupported version %d (supported: %d)", cfg.Version, supportedVersion1)
}

manifest := cfg.Manifest
if strings.TrimSpace(manifest.Alias) == "" {
return fmt.Errorf("manifest.alias is required")
}
if len(manifest.Name) == 0 {
return fmt.Errorf("manifest.name is required and must contain at least one locale entry")
}
if len(manifest.Description) == 0 {
return fmt.Errorf("manifest.description is required and must contain at least one locale entry")
}
if len(manifest.Slots) == 0 {
return fmt.Errorf("manifest.slots is required and must contain at least one slot")
}
for i, slot := range manifest.Slots {
if strings.TrimSpace(slot.SlotID) == "" {
return fmt.Errorf("manifest.slots[%d].slotId is required", i)
}
}

if cfg.Build != nil {
if cfg.Build.Port != nil && *cfg.Build.Port <= 0 {
return fmt.Errorf("build.port must be greater than 0")
}
if strings.TrimSpace(cfg.Build.OutDir) == "" && cfg.Build.OutDir != "" {
return fmt.Errorf("build.outDir must not be empty when provided")
}
}

return nil
}

Loading