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
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,14 @@ func TestMyPkg(t *testing.T) {
- Use `ginkgo.Label("slow")` for tests taking more than 10 seconds.
- Use `ginkgo.Ordered` only when test steps must run sequentially.

### Plugins

Plugins are external binaries managed by Mission Control from `Plugin` CRDs and supervised through Hashicorp `go-plugin`.
They expose manifests which includes: UI tabs, and operations over gRPC, while Mission Control provides a host callback service for config lookup, connection resolution, logging, and artifacts.
Users invoke plugin operations through the Mission Control API, which enforces selectors and RBAC before minting a short-lived invocation token.

We maintain a registry of operations in a different repo - https://github.com/flanksource/mission-control-plugins

## Comments Guidelines

- Only add comments if really really necessary. Do not add comments that simply explain the code.
Expand Down
8 changes: 5 additions & 3 deletions auth/plugin_invocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ type PluginInvocationClaims struct {
Plugin uuid.UUID `json:"pluginID"`
Type string `json:"typ"`
Depth int `json:"depth,omitempty"`
Roles []string `json:"roles,omitempty"`
jwt.RegisteredClaims
}

func MintPluginInvocationToken(user models.Person, pluginID uuid.UUID) (string, error) {
return MintPluginInvocationTokenWithDepth(user, pluginID, 0)
func MintPluginInvocationToken(user models.Person, pluginID uuid.UUID, roles ...string) (string, error) {
return MintPluginInvocationTokenWithDepth(user, pluginID, 0, roles...)
}

func MintPluginInvocationTokenWithDepth(user models.Person, pluginID uuid.UUID, depth int) (string, error) {
func MintPluginInvocationTokenWithDepth(user models.Person, pluginID uuid.UUID, depth int, roles ...string) (string, error) {
now := time.Now()
claims := PluginInvocationClaims{
Plugin: pluginID,
Type: pluginInvocationTokenType,
Depth: depth,
Roles: append([]string(nil), roles...),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: pluginInvocationTokenIssuer,
Subject: user.ID.String(),
Expand Down
9 changes: 9 additions & 0 deletions plugin/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ func InvokeOperation(c echo.Context) error {
if err != nil {
return dutyAPI.WriteError(c, ctx.Oops().Code(dutyAPI.EINVALID).Errorf("config_id is invalid"))
}
entry, err := pluginruntime.ResolvePlugin(ctx, pluginRef)
if err != nil {
return dutyAPI.WriteError(c, err)
}
roles, err := pluginRolesForUser(ctx, entry, configID)
if err != nil {
return dutyAPI.WriteError(c, err)
}

body, err := io.ReadAll(c.Request().Body)
if err != nil {
Expand All @@ -111,6 +119,7 @@ func InvokeOperation(c echo.Context) error {
ConfigItemID: configID,
ParamsJSON: body,
User: ctx.User(),
Roles: roles,
Depth: 0,
Timeout: 60 * time.Second,
}, invokeViaSupervisor)
Expand Down
6 changes: 5 additions & 1 deletion plugin/controller/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ func operationHTTPProxy(c echo.Context) error {
if err := pluginruntime.EnforceInvokePermission(ctx, subject, entry, op, configID); err != nil {
return dutyAPI.WriteError(c, err)
}
roles, err := pluginRolesForUser(ctx, entry, configID)
if err != nil {
return dutyAPI.WriteError(c, err)
}

invocationToken, err := auth.MintPluginInvocationToken(*user, entry.ID)
invocationToken, err := auth.MintPluginInvocationToken(*user, entry.ID, roles...)
if err != nil {
return dutyAPI.WriteError(c, ctx.Oops().Wrapf(err, "mint plugin invocation token"))
}
Expand Down
55 changes: 55 additions & 0 deletions plugin/controller/roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package controller

import (
dutyAPI "github.com/flanksource/duty/api"
dutyContext "github.com/flanksource/duty/context"
"github.com/flanksource/duty/models"
"github.com/flanksource/duty/query"
dutyRBAC "github.com/flanksource/duty/rbac"
"github.com/flanksource/duty/rbac/policy"
"github.com/flanksource/incident-commander/plugin/registry"
)

func pluginRolesForUser(ctx dutyContext.Context, entry *registry.Entry, configID string) ([]string, error) {
user := ctx.User()
if user == nil {
return nil, ctx.Oops().Code(dutyAPI.EUNAUTHORIZED).Errorf("not logged in")
}
if entry == nil || entry.Manifest == nil {
return nil, nil
}

attr, err := pluginABACAttribute(ctx, configID)
if err != nil {
return nil, err
}

var roles []string
for _, role := range entry.Manifest.Roles {
if role == nil || role.Name == "" {
continue
}
if canAssumePluginRole(ctx, user.ID.String(), attr, entry.Name, role.Name) {
roles = append(roles, role.Name)
}
}
return roles, nil
}

func canAssumePluginRole(ctx dutyContext.Context, subject string, attr *models.ABACAttribute, pluginName, role string) bool {
return dutyRBAC.HasPermission(ctx, subject, attr, policy.NewPluginRoleAction(pluginName, role))
}

func pluginABACAttribute(ctx dutyContext.Context, configID string) (*models.ABACAttribute, error) {
attr := &models.ABACAttribute{}
if configID == "" {
return attr, nil
}

item, err := query.ConfigItemFromCache(ctx, configID)
if err != nil {
return nil, ctx.Oops().Wrapf(err, "get config item %s", configID)
}
attr.Config = item
return attr, nil
}
Loading