293 lines
10 KiB
Go
293 lines
10 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/kandev/kandev/internal/auth/authn"
|
|
canvasservice "github.com/kandev/kandev/internal/canvas"
|
|
"github.com/kandev/kandev/internal/plugins/instances"
|
|
"github.com/kandev/kandev/internal/plugins/manifest"
|
|
"github.com/kandev/kandev/internal/plugins/webapp"
|
|
userstore "github.com/kandev/kandev/internal/user/store"
|
|
)
|
|
|
|
var errCanvasWebAppNotDeclared = errors.New("canvas web application is not declared")
|
|
|
|
func (h *canvasHTTPHandler) runtimeBinding(c *gin.Context, canvas canvasservice.Canvas, request canvasRuntimeRequest) (string, instances.Release, manifest.WebApp, webapp.CapabilityBinding, error) {
|
|
instance, release, appManifest, err := h.loadRuntimeRelease(c, canvas)
|
|
if err != nil {
|
|
return "", instances.Release{}, manifest.WebApp{}, webapp.CapabilityBinding{}, err
|
|
}
|
|
app, placement, err := selectRuntimeWebApp(appManifest, request, canvas.ScopeKind)
|
|
if err != nil {
|
|
return "", instances.Release{}, manifest.WebApp{}, webapp.CapabilityBinding{}, err
|
|
}
|
|
grants, err := h.plugins.Instances().ListGrants(c.Request.Context(), instance.ID)
|
|
if err != nil {
|
|
return "", instances.Release{}, manifest.WebApp{}, webapp.CapabilityBinding{}, err
|
|
}
|
|
binding := webapp.CapabilityBinding{
|
|
PluginID: instance.PluginID,
|
|
UserID: canvasRuntimeUser(c),
|
|
InstanceID: instance.ID,
|
|
ReleaseID: release.ID,
|
|
WebAppKey: app.Key,
|
|
Placement: placement,
|
|
ScopeKind: instance.ScopeKind,
|
|
DataScopeKind: instance.EffectiveDataScopeKind(),
|
|
WorkspaceID: instance.WorkspaceID,
|
|
TaskID: instance.TaskID,
|
|
SessionID: instance.SessionID,
|
|
RepositoryID: instance.RepositoryID,
|
|
GrantGeneration: instance.GrantGeneration,
|
|
Permissions: runtimeGrantedPermissions(appManifest, instance.EffectiveDataScopeKind(), grants),
|
|
Artifact: webapp.Artifact{
|
|
Digest: release.PackageDigest,
|
|
RelativePath: release.ArtifactPath,
|
|
Bytes: release.ArtifactBytes,
|
|
Available: true,
|
|
},
|
|
Entry: app.Entry,
|
|
NetworkOrigins: runtimeNetworkOrigins(app, instance.EffectiveDataScopeKind(), grants),
|
|
}
|
|
runtime := h.plugins.WebRuntime()
|
|
if runtime == nil {
|
|
return "", instances.Release{}, manifest.WebApp{}, webapp.CapabilityBinding{}, errors.New("canvas runtime is not configured")
|
|
}
|
|
path, err := runtime.IssueCapabilityPath(binding, webapp.RuntimeTokenTTL)
|
|
if err != nil {
|
|
return "", instances.Release{}, manifest.WebApp{}, webapp.CapabilityBinding{}, err
|
|
}
|
|
return path, release, app, binding, nil
|
|
}
|
|
|
|
func (h *canvasHTTPHandler) loadRuntimeRelease(c *gin.Context, canvas canvasservice.Canvas) (instances.Instance, instances.Release, *manifest.Manifest, error) {
|
|
store := h.plugins.Instances()
|
|
if store == nil {
|
|
return instances.Instance{}, instances.Release{}, nil, errors.New("canvas runtime is not configured")
|
|
}
|
|
instance, err := store.Get(c.Request.Context(), canvas.PluginInstanceID)
|
|
if err != nil {
|
|
return instances.Instance{}, instances.Release{}, nil, err
|
|
}
|
|
release, err := store.GetRelease(c.Request.Context(), instance.ActiveReleaseID)
|
|
if err != nil {
|
|
return instances.Instance{}, instances.Release{}, nil, err
|
|
}
|
|
appManifest, err := manifest.Parse(release.ManifestJSON)
|
|
if err != nil {
|
|
return instances.Instance{}, instances.Release{}, nil, err
|
|
}
|
|
return instance, release, appManifest, nil
|
|
}
|
|
|
|
func selectRuntimeWebApp(appManifest *manifest.Manifest, request canvasRuntimeRequest, scope string) (manifest.WebApp, string, error) {
|
|
placement := request.Placement
|
|
if placement == "" {
|
|
placement = appPlacement(manifest.WebApp{}, "", scope)
|
|
}
|
|
var app manifest.WebApp
|
|
for _, candidate := range appManifest.UI.WebApps {
|
|
if request.WebAppKey != "" && candidate.Key != request.WebAppKey {
|
|
continue
|
|
}
|
|
if placement != "" && !containsString(candidate.Placements, placement) {
|
|
continue
|
|
}
|
|
app = candidate
|
|
break
|
|
}
|
|
if app.Key == "" {
|
|
return manifest.WebApp{}, "", errCanvasWebAppNotDeclared
|
|
}
|
|
return app, appPlacement(app, request.Placement, scope), nil
|
|
}
|
|
|
|
func runtimeNetworkOrigins(app manifest.WebApp, scope string, grants []instances.Grant) []string {
|
|
declared, err := webapp.NormalizeNetworkOrigins(app.NetworkOrigins)
|
|
if err != nil || len(declared) == 0 {
|
|
return nil
|
|
}
|
|
origins := make([]string, 0, len(grants))
|
|
for _, origin := range declared {
|
|
for _, grant := range grants {
|
|
if grant.NetworkOrigin == origin && runtimeGrantScopeCovers(grant.ScopeCeiling, scope) && !containsString(origins, grant.NetworkOrigin) {
|
|
origins = append(origins, grant.NetworkOrigin)
|
|
}
|
|
}
|
|
}
|
|
return origins
|
|
}
|
|
|
|
func (h *canvasHTTPHandler) writeError(c *gin.Context, err error) {
|
|
status := http.StatusInternalServerError
|
|
code := "canvas_error"
|
|
switch {
|
|
case errors.Is(err, canvasservice.ErrCanvasNotFound), errors.Is(err, instances.ErrNotFound):
|
|
status, code = http.StatusNotFound, "canvas_not_found"
|
|
case errors.Is(err, canvasservice.ErrTaskCanvasLimit), errors.Is(err, canvasservice.ErrWorkspaceCanvasLimit), errors.Is(err, instances.ErrTaskCanvasLimit), errors.Is(err, instances.ErrWorkspaceCanvasLimit):
|
|
status, code = http.StatusConflict, "canvas_limit_exceeded"
|
|
case errors.Is(err, instances.ErrWorkspaceStorageLimit), errors.Is(err, instances.ErrInstallationStorageLimit):
|
|
status, code = http.StatusConflict, "canvas_storage_limit_exceeded"
|
|
case errors.Is(err, instances.ErrInvalidRelease):
|
|
status, code = http.StatusConflict, "invalid_release"
|
|
case errors.Is(err, instances.ErrStaleCanvasPublish), errors.Is(err, canvasservice.ErrStaleCanvasPublish):
|
|
status, code = http.StatusConflict, "canvas_publish_stale"
|
|
case errors.Is(err, instances.ErrInvalidLifecycleState), errors.Is(err, canvasservice.ErrInvalidLifecycleState):
|
|
status, code = http.StatusBadRequest, canvasErrorCodeInvalid
|
|
case errors.Is(err, canvasservice.ErrStalePromotionReview):
|
|
status, code = http.StatusConflict, "promotion_review_stale"
|
|
case errors.Is(err, canvasservice.ErrStaleWorkspaceDataReview):
|
|
status, code = http.StatusConflict, "workspace_data_review_stale"
|
|
case errors.Is(err, instances.ErrWorkspaceOwnerRequired):
|
|
status, code = http.StatusForbidden, "workspace_owner_required"
|
|
case errors.Is(err, canvasservice.ErrStaleCanvasEdit):
|
|
status, code = http.StatusConflict, "canvas_edit_stale"
|
|
case errors.Is(err, canvasservice.ErrInvalidCanvas), errors.Is(err, canvasservice.ErrInvalidCanvasState):
|
|
status, code = http.StatusBadRequest, canvasErrorCodeInvalid
|
|
case errors.Is(err, errCanvasWebAppNotDeclared):
|
|
status, code = http.StatusBadRequest, "web_app_not_declared"
|
|
default:
|
|
}
|
|
writeCanvasError(c, status, code, nil)
|
|
}
|
|
|
|
func mapCanvasResponses(values []canvasservice.Canvas) []canvasHTTPResponse {
|
|
result := make([]canvasHTTPResponse, 0, len(values))
|
|
for _, value := range values {
|
|
result = append(result, canvasResponse(value))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func canvasIncludeArchived(c *gin.Context) (bool, bool) {
|
|
value := c.Query("include_archived")
|
|
if value == "" {
|
|
return false, true
|
|
}
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
writeCanvasError(c, http.StatusBadRequest, "invalid_include_archived", nil)
|
|
return false, false
|
|
}
|
|
return parsed, true
|
|
}
|
|
|
|
func canvasRuntimeUser(c *gin.Context) string {
|
|
if identity, ok := authn.FromGin(c); ok && identity.UserID != "" {
|
|
return identity.UserID
|
|
}
|
|
return userstore.DefaultUserID
|
|
}
|
|
|
|
func appPlacement(app manifest.WebApp, requested, scope string) string {
|
|
if requested != "" {
|
|
return requested
|
|
}
|
|
wanted := manifest.WebAppPlacementWorkspace
|
|
if scope == instances.ScopeTask {
|
|
wanted = manifest.WebAppPlacementTask
|
|
}
|
|
if containsString(app.Placements, wanted) {
|
|
return wanted
|
|
}
|
|
if len(app.Placements) > 0 {
|
|
return app.Placements[0]
|
|
}
|
|
return wanted
|
|
}
|
|
|
|
func declaredPermissions(appManifest *manifest.Manifest) []string {
|
|
if appManifest == nil {
|
|
return nil
|
|
}
|
|
permissions := make([]string, 0, len(appManifest.Capabilities.APIRead)+len(appManifest.Capabilities.APIWrite)+len(appManifest.Capabilities.Events)+1)
|
|
for _, resource := range appManifest.Capabilities.APIRead {
|
|
permissions = appendUnique(permissions, "api_read:"+resource)
|
|
}
|
|
for _, resource := range appManifest.Capabilities.APIWrite {
|
|
permissions = appendUnique(permissions, "api_write:"+resource)
|
|
}
|
|
for _, event := range appManifest.Capabilities.Events {
|
|
permissions = appendUnique(permissions, "events:"+event)
|
|
}
|
|
if appManifest.Capabilities.State {
|
|
permissions = appendUnique(permissions, "state")
|
|
}
|
|
return permissions
|
|
}
|
|
|
|
func runtimeGrantedPermissions(appManifest *manifest.Manifest, scope string, grants []instances.Grant) []string {
|
|
declared := declaredPermissions(appManifest)
|
|
permissions := make([]string, 0, len(declared))
|
|
for _, permission := range declared {
|
|
parts := strings.SplitN(permission, ":", 2)
|
|
kind, resource := parts[0], ""
|
|
if len(parts) == 2 {
|
|
resource = parts[1]
|
|
}
|
|
for _, grant := range grants {
|
|
if grant.PermissionKind == kind && grant.Resource == resource && runtimeGrantScopeCovers(grant.ScopeCeiling, scope) {
|
|
permissions = append(permissions, permission)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return permissions
|
|
}
|
|
|
|
func runtimeGrantScopeCovers(ceiling, scope string) bool {
|
|
if ceiling == instances.ScopeInstance {
|
|
return true
|
|
}
|
|
return ceiling == scope
|
|
}
|
|
|
|
func appendUnique(values []string, value string) []string {
|
|
if value == "" || containsString(values, value) {
|
|
return values
|
|
}
|
|
return append(values, value)
|
|
}
|
|
|
|
func containsString(values []string, target string) bool {
|
|
for _, value := range values {
|
|
if value == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func canvasHostError(status string) string {
|
|
switch status {
|
|
case instances.ValidationPendingPermission:
|
|
return "permission_review_required"
|
|
case instances.ValidationInvalid:
|
|
return "invalid_release"
|
|
case instances.ValidationUnavailable:
|
|
return "runtime_unavailable"
|
|
default:
|
|
return "canvas_release_unavailable"
|
|
}
|
|
}
|
|
|
|
func writeCanvasJSON(c *gin.Context, status int, value interface{}) {
|
|
c.Header("Cache-Control", "no-store")
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
c.JSON(status, value)
|
|
}
|
|
|
|
func writeCanvasError(c *gin.Context, status int, code string, details map[string]interface{}) {
|
|
body := gin.H{"error": code, "error_code": code}
|
|
for key, value := range details {
|
|
body[key] = value
|
|
}
|
|
writeCanvasJSON(c, status, body)
|
|
}
|