Skip to content
Merged
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: 1 addition & 0 deletions internal/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestEmbeddedAdminShell(t *testing.T) {
`workflow.admin.auth.request`,
`workflow.admin.auth.response`,
`grantedScopes: adminToolGrants`,
`payload.granted_permissions`,
`adminToolFrames`,
`event.origin !== window.location.origin`,
`startsWith('/admin')`,
Expand Down
51 changes: 46 additions & 5 deletions internal/module_dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,21 @@ func (m *dashboardModule) InvokeMethod(method string, args map[string]any) (map[
}, nil
case "ListContributions":
appContext, _ := args["app_context"].(string)
contributions := m.registry.listForPermissions(appContext, stringSliceValue(args, "granted_permissions"))
out := make([]map[string]any, 0, len(contributions))
grantedPermissions := stringSliceValue(args, "granted_permissions")
contributions := contributionListValue(args["contributions"])
if len(contributions) == 0 {
contributions = contributionListValue([]any{args["auth_contribution"], args["authz_contribution"]})
}
if len(contributions) == 0 {
contributions = m.registry.listForPermissions(appContext, grantedPermissions)
} else {
grants := make(map[string]struct{}, len(grantedPermissions))
for _, permission := range grantedPermissions {
grants[permission] = struct{}{}
}
contributions = filterContributionList(contributions, appContext, grants)
}
out := make([]any, 0, len(contributions))
for _, contribution := range contributions {
out = append(out, contributionToMap(contribution))
}
Expand Down Expand Up @@ -139,6 +152,26 @@ func contributionFromMap(args map[string]any) *contracts.AdminContribution {
return contribution
}

func contributionListValue(value any) []*contracts.AdminContribution {
switch items := value.(type) {
case []*contracts.AdminContribution:
return items
case []any:
out := make([]*contracts.AdminContribution, 0, len(items))
for _, item := range items {
switch contribution := item.(type) {
case *contracts.AdminContribution:
out = append(out, contribution)
case map[string]any:
out = append(out, contributionFromMap(contribution))
}
}
return out
default:
return nil
}
}

func contributionToMap(contribution *contracts.AdminContribution) map[string]any {
if contribution == nil {
return nil
Expand All @@ -150,12 +183,20 @@ func contributionToMap(contribution *contracts.AdminContribution) map[string]any
"path": contribution.Path,
"render_mode": contribution.RenderMode,
"app_context": contribution.AppContext,
"actions": append([]string(nil), contribution.Actions...),
"actions": stringAnySlice(contribution.Actions),
"permissions": permissionMaps(contribution.Permissions),
"metadata": contributionMetadataMap(contribution),
}
}

func stringAnySlice(values []string) []any {
out := make([]any, 0, len(values))
for _, value := range values {
out = append(out, value)
}
return out
}

func contributionMetadataMap(contribution *contracts.AdminContribution) map[string]any {
if contribution == nil || contribution.Metadata == nil {
return map[string]any{}
Expand Down Expand Up @@ -191,8 +232,8 @@ func permissionValues(value any) []*contracts.AdminPermission {
}
}

func permissionMaps(permissions []*contracts.AdminPermission) []map[string]any {
out := make([]map[string]any, 0, len(permissions))
func permissionMaps(permissions []*contracts.AdminPermission) []any {
out := make([]any, 0, len(permissions))
for _, permission := range permissions {
out = append(out, map[string]any{
"permission": permission.GetPermission(),
Expand Down
14 changes: 9 additions & 5 deletions internal/module_dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,20 @@ func TestDashboardModuleServiceInvoker(t *testing.T) {
if err != nil {
t.Fatalf("ListContributions: %v", err)
}
contributions, ok := listed["contributions"].([]map[string]any)
contributions, ok := listed["contributions"].([]any)
if !ok {
t.Fatalf("contributions type = %T, want []map[string]any", listed["contributions"])
t.Fatalf("contributions type = %T, want []any", listed["contributions"])
}
if len(contributions) != 1 || contributions[0]["id"] != "orders" {
first, ok := contributions[0].(map[string]any)
if !ok {
t.Fatalf("first contribution type = %T, want map[string]any", contributions[0])
}
if len(contributions) != 1 || first["id"] != "orders" {
t.Fatalf("unexpected contributions: %#v", contributions)
}
metadata, ok := contributions[0]["metadata"].(map[string]any)
metadata, ok := first["metadata"].(map[string]any)
if !ok {
t.Fatalf("metadata type = %T, want map[string]any", contributions[0]["metadata"])
t.Fatalf("metadata type = %T, want map[string]any", first["metadata"])
}
if metadata["validate_path"] != "/api/admin/orders/config/validate" {
t.Fatalf("metadata validate_path = %v", metadata["validate_path"])
Expand Down
19 changes: 13 additions & 6 deletions internal/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ func (p *adminPlugin) StepTypes() []string {
}

func (p *adminPlugin) TypedStepTypes() []string {
return p.StepTypes()
return []string{
"step.admin_authorize_action",
"step.admin_resource_action",
}
}

func (p *adminPlugin) CreateStep(typeName, _ string, config map[string]any) (sdk.StepInstance, error) {
Expand All @@ -142,9 +145,9 @@ func (p *adminPlugin) CreateStep(typeName, _ string, config map[string]any) (sdk
func (p *adminPlugin) CreateTypedStep(typeName, name string, config *anypb.Any) (sdk.StepInstance, error) {
switch typeName {
case "step.admin_register_contribution":
return sdk.NewTypedStepFactory(typeName, &contracts.AdminStepConfig{}, &contracts.RegisterContributionInput{}, typedRegisterContribution).CreateTypedStep(typeName, name, config)
return nil, fmt.Errorf("%w: step type %q", sdk.ErrTypedContractNotHandled, typeName)
case "step.admin_list_contributions":
return sdk.NewTypedStepFactory(typeName, &contracts.AdminStepConfig{}, &contracts.ListContributionsInput{}, typedListContributions).CreateTypedStep(typeName, name, config)
return nil, fmt.Errorf("%w: step type %q", sdk.ErrTypedContractNotHandled, typeName)
case "step.admin_authorize_action":
return sdk.NewTypedStepFactory(typeName, &contracts.AdminStepConfig{}, &contracts.AuthorizeAdminActionInput{}, typedAuthorizeAction).CreateTypedStep(typeName, name, config)
case "step.admin_resource_action":
Expand All @@ -163,8 +166,8 @@ func (p *adminPlugin) ContractRegistry() *pb.ContractRegistry {
}},
Contracts: []*pb.ContractDescriptor{
adminModuleContract("admin.dashboard", "AdminDashboardConfig"),
adminStepContract("step.admin_register_contribution", "AdminStepConfig", "RegisterContributionInput", "RegisterContributionOutput"),
adminStepContract("step.admin_list_contributions", "AdminStepConfig", "ListContributionsInput", "ListContributionsOutput"),
adminStepContractWithMode("step.admin_register_contribution", "AdminStepConfig", "RegisterContributionInput", "RegisterContributionOutput", pb.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT),
adminStepContractWithMode("step.admin_list_contributions", "AdminStepConfig", "ListContributionsInput", "ListContributionsOutput", pb.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT),
adminStepContract("step.admin_authorize_action", "AdminStepConfig", "AuthorizeAdminActionInput", "AuthorizeAdminActionOutput"),
adminStepContract("step.admin_resource_action", "AdminStepConfig", "AdminResourceActionInput", "AdminResourceActionOutput"),
adminServiceContract("admin.dashboard", "AdminDashboard", "RegisterContribution", "RegisterContributionInput", "RegisterContributionOutput"),
Expand All @@ -186,14 +189,18 @@ func adminModuleContract(moduleType, configMessage string) *pb.ContractDescripto
}

func adminStepContract(stepType, configMessage, inputMessage, outputMessage string) *pb.ContractDescriptor {
return adminStepContractWithMode(stepType, configMessage, inputMessage, outputMessage, pb.ContractMode_CONTRACT_MODE_STRICT_PROTO)
}

func adminStepContractWithMode(stepType, configMessage, inputMessage, outputMessage string, mode pb.ContractMode) *pb.ContractDescriptor {
const pkg = "workflow.plugins.admin.v1."
return &pb.ContractDescriptor{
Kind: pb.ContractKind_CONTRACT_KIND_STEP,
StepType: stepType,
ConfigMessage: pkg + configMessage,
InputMessage: pkg + inputMessage,
OutputMessage: pkg + outputMessage,
Mode: pb.ContractMode_CONTRACT_MODE_STRICT_PROTO,
Mode: mode,
}
}

Expand Down
28 changes: 21 additions & 7 deletions internal/plugin_contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ func TestAdminPlugin_ContractRegistry(t *testing.T) {

contracts := make(map[string]*proto.ContractDescriptor, len(registry.Contracts))
for _, contract := range registry.Contracts {
if contract.Mode != proto.ContractMode_CONTRACT_MODE_STRICT_PROTO {
if contractKey(contract) == "step:step.admin_register_contribution" || contractKey(contract) == "step:step.admin_list_contributions" {
if contract.Mode != proto.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT {
t.Fatalf("%s mode = %s, want proto with legacy struct", contractKey(contract), contract.Mode)
}
} else if contract.Mode != proto.ContractMode_CONTRACT_MODE_STRICT_PROTO {
t.Fatalf("%s mode = %s, want strict proto", contractKey(contract), contract.Mode)
}
key := contractKey(contract)
Expand Down Expand Up @@ -60,8 +64,8 @@ func TestAdminPlugin_PluginContractsJSON(t *testing.T) {
if !ok {
t.Fatalf("%s missing from runtime contracts", key)
}
if manifest.Config != runtimeContract.ConfigMessage || manifest.Input != runtimeContract.InputMessage || manifest.Output != runtimeContract.OutputMessage {
t.Fatalf("%s manifest = %#v, runtime config/input/output = %q/%q/%q", key, manifest, runtimeContract.ConfigMessage, runtimeContract.InputMessage, runtimeContract.OutputMessage)
if manifest.Config != runtimeContract.ConfigMessage || manifest.Input != runtimeContract.InputMessage || manifest.Output != runtimeContract.OutputMessage || manifest.Mode != contractModeString(runtimeContract.Mode) {
t.Fatalf("%s manifest = %#v, runtime mode/config/input/output = %q/%q/%q/%q", key, manifest, contractModeString(runtimeContract.Mode), runtimeContract.ConfigMessage, runtimeContract.InputMessage, runtimeContract.OutputMessage)
}
}
}
Expand Down Expand Up @@ -102,6 +106,20 @@ type pluginContract struct {
Config string `json:"config"`
Input string `json:"input"`
Output string `json:"output"`
Mode string `json:"mode"`
}

func contractModeString(mode proto.ContractMode) string {
switch mode {
case proto.ContractMode_CONTRACT_MODE_STRICT_PROTO:
return "strict"
case proto.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT:
return "proto_with_legacy_struct"
case proto.ContractMode_CONTRACT_MODE_LEGACY_STRUCT:
return "legacy_struct"
default:
return ""
}
}

func readPluginContracts(t *testing.T) map[string]pluginContract {
Expand All @@ -121,7 +139,6 @@ func readPluginContracts(t *testing.T) map[string]pluginContract {
Type string `json:"type"`
ServiceName string `json:"serviceName"`
Method string `json:"method"`
Mode string `json:"mode"`
pluginContract
} `json:"contracts"`
}
Expand All @@ -133,9 +150,6 @@ func readPluginContracts(t *testing.T) map[string]pluginContract {
}
out := make(map[string]pluginContract, len(manifest.Contracts))
for _, contract := range manifest.Contracts {
if contract.Mode != "strict" {
t.Fatalf("%s mode = %q, want strict", contract.Type, contract.Mode)
}
var key string
switch contract.Kind {
case "module":
Expand Down
18 changes: 14 additions & 4 deletions internal/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,26 @@ func (r *contributionRegistry) list(appContext string) []*contracts.AdminContrib
}

func (r *contributionRegistry) listForPermissions(appContext string, grantedPermissions []string) []*contracts.AdminContribution {
r.mu.RLock()
defer r.mu.RUnlock()

grants := make(map[string]struct{}, len(grantedPermissions))
for _, permission := range grantedPermissions {
grants[permission] = struct{}{}
}

out := make([]*contracts.AdminContribution, 0, len(r.contributions))
r.mu.RLock()
snapshot := make([]*contracts.AdminContribution, 0, len(r.contributions))
for _, contribution := range r.contributions {
snapshot = append(snapshot, contribution)
}
r.mu.RUnlock()
return filterContributionList(snapshot, appContext, grants)
}

func filterContributionList(contributions []*contracts.AdminContribution, appContext string, grants map[string]struct{}) []*contracts.AdminContribution {
out := make([]*contracts.AdminContribution, 0, len(contributions))
for _, contribution := range contributions {
if contribution == nil {
continue
}
if appContext != "" && contribution.AppContext != "" && contribution.AppContext != appContext {
continue
}
Expand Down
31 changes: 27 additions & 4 deletions internal/step_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,22 @@ func newAdminStep(method string, config map[string]any) *adminStep {
return &adminStep{method: method, module: module}
}

func (s *adminStep) Execute(_ context.Context, _ map[string]any, _ map[string]map[string]any, current map[string]any, _ map[string]any, config map[string]any) (*sdk.StepResult, error) {
func (s *adminStep) Execute(_ context.Context, _ map[string]any, stepOutputs map[string]map[string]any, current map[string]any, _ map[string]any, config map[string]any) (*sdk.StepResult, error) {
args := mergeMaps(config, current)
if args["module"] == nil {
args["module"] = s.module
}
if s.method == "ListContributions" && args["contributions"] == nil {
contributions := make([]any, 0)
for _, stepName := range stringSliceValue(args, "contribution_steps") {
if contribution := stepOutputs[stepName]["contribution"]; contribution != nil {
contributions = append(contributions, contribution)
}
}
if len(contributions) > 0 {
args["contributions"] = contributions
}
}
moduleName := stringValue(args, "module")
module, err := lookupDashboardModule(moduleName)
if err != nil {
Expand Down Expand Up @@ -58,11 +69,15 @@ func typedRegisterContribution(ctx context.Context, req sdk.TypedStepRequest[*co
if err != nil {
return nil, err
}
if err := module.registry.register(req.Input.GetContribution()); err != nil {
contribution := req.Input.GetContribution()
if contribution == nil {
contribution = contributionFromMap(req.Current)
}
if err := module.registry.register(contribution); err != nil {
return &sdk.TypedStepResult[*contracts.RegisterContributionOutput]{Output: &contracts.RegisterContributionOutput{Error: err.Error()}}, nil
}
return &sdk.TypedStepResult[*contracts.RegisterContributionOutput]{
Output: &contracts.RegisterContributionOutput{Registered: true, Contribution: req.Input.GetContribution()},
Output: &contracts.RegisterContributionOutput{Registered: true, Contribution: contribution},
}, nil
}

Expand All @@ -75,8 +90,16 @@ func typedListContributions(_ context.Context, req sdk.TypedStepRequest[*contrac
if err != nil {
return nil, err
}
appContext := req.Input.GetAppContext()
if appContext == "" {
appContext = stringValue(req.Current, "app_context")
}
grantedPermissions := req.Input.GetGrantedPermissions()
if len(grantedPermissions) == 0 {
grantedPermissions = stringSliceValue(req.Current, "granted_permissions")
}
return &sdk.TypedStepResult[*contracts.ListContributionsOutput]{
Output: &contracts.ListContributionsOutput{Contributions: module.registry.listForPermissions(req.Input.GetAppContext(), req.Input.GetGrantedPermissions())},
Output: &contracts.ListContributionsOutput{Contributions: module.registry.listForPermissions(appContext, grantedPermissions)},
}, nil
}

Expand Down
Loading
Loading