Files
kandev/apps/backend/internal/github/controller_app_registration.go
T

221 lines
7.1 KiB
Go

package github
import (
"crypto/sha256"
"errors"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
)
const (
maxDeploymentAppManifestConversionCodeLength = 1024
maxDeploymentAppProviderErrorLength = 256
)
func (c *Controller) httpListAppRegistrations(ctx *gin.Context) {
result, err := c.service.ListAppRegistrationCatalog(
ctx.Request.Context(), currentGitHubUserID(ctx), ctx.Query("workspace_id"),
)
if err != nil {
writeGitHubAuthError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusOK, result)
}
func (c *Controller) httpStartAppRegistrationManifest(ctx *gin.Context) {
var request AppRegistrationManifestStartRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
writeAppRegistrationInvalidRequest(ctx)
return
}
result, err := c.service.StartAppRegistrationManifest(
ctx.Request.Context(), currentGitHubUserID(ctx), request,
)
if err != nil {
writeGitHubAuthError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusOK, result)
}
func (c *Controller) httpCompleteAppRegistrationManifest(ctx *gin.Context) {
state, code, providerError := ctx.Query("state"), ctx.Query("code"), ctx.Query("error")
if !validGitHubCallbackState(state) ||
len(code) > maxDeploymentAppManifestConversionCodeLength ||
len(providerError) > maxDeploymentAppProviderErrorLength {
redirectAppRegistrationCallback(ctx, "", "github_app_invalid_callback")
return
}
result, err := c.service.CompleteAppRegistrationManifest(
ctx.Request.Context(), ctx.Param("registrationId"), AppRegistrationManifestCallback{
State: state, Code: code, Error: providerError,
},
)
if err != nil {
workspaceID := appRegistrationFlowWorkspace(ctx, c.service, state)
redirectAppRegistrationCallback(ctx, workspaceID, githubAuthErrorCode(err))
return
}
redirectAppRegistrationCallback(ctx, result.WorkspaceID, "app_registered")
}
func appRegistrationFlowWorkspace(ctx *gin.Context, service *Service, state string) string {
if service == nil || service.store == nil || state == "" {
return ""
}
digest := stateDigest(state)
flow, err := service.store.GetDeploymentAppRegistrationFlow(ctx.Request.Context(), digest)
if err != nil || flow == nil {
return ""
}
return flow.WorkspaceID
}
func stateDigest(state string) string {
digest := sha256Digest(strings.TrimSpace(state))
return stateDigestString(digest)
}
func sha256Digest(value string) [32]byte {
return sha256.Sum256([]byte(value))
}
func (c *Controller) httpImportAppRegistration(ctx *gin.Context) {
var request AppRegistrationImportRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
writeAppRegistrationInvalidRequest(ctx)
return
}
registration, err := c.service.ImportAppRegistration(
ctx.Request.Context(), currentGitHubUserID(ctx), request,
)
if err != nil {
writeGitHubAuthError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusCreated, registration)
}
func (c *Controller) httpPrepareAppRegistrationImport(ctx *gin.Context) {
var request AppRegistrationImportPrepareRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
writeAppRegistrationInvalidRequest(ctx)
return
}
preparation, err := c.service.PrepareAppRegistrationImport(
ctx.Request.Context(), currentGitHubUserID(ctx), request,
)
if err != nil {
writeGitHubAuthError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusCreated, preparation)
}
func (c *Controller) httpRenameAppRegistration(ctx *gin.Context) {
var request struct {
DisplayName string `json:"display_name"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
writeAppRegistrationInvalidRequest(ctx)
return
}
registration, err := c.service.RenameAppRegistration(
ctx.Request.Context(), currentGitHubUserID(ctx), ctx.Param("registrationId"), request.DisplayName,
)
if err != nil {
writeGitHubAuthError(ctx, err)
return
}
ctx.JSON(http.StatusOK, registration)
}
func (c *Controller) httpDeleteAppRegistration(ctx *gin.Context) {
err := c.service.DeleteAppRegistration(
ctx.Request.Context(), currentGitHubUserID(ctx), ctx.Param("registrationId"),
)
if err != nil {
var inUse *AppRegistrationInUseError
if errors.As(err, &inUse) {
ctx.JSON(http.StatusConflict, gin.H{
"code": "github_app_registration_in_use", "error": inUse.Error(),
"binding_count": inUse.BindingCount,
})
return
}
writeGitHubAuthError(ctx, err)
return
}
ctx.JSON(http.StatusOK, gin.H{"deleted": true})
}
func writeAppRegistrationInvalidRequest(ctx *gin.Context) {
ctx.JSON(http.StatusBadRequest, gin.H{
"code": "github_app_invalid_request", "error": "invalid GitHub App registration request",
})
}
func redirectAppRegistrationCallback(ctx *gin.Context, workspaceID, result string) {
query := url.Values{"github_result": []string{result}}
if workspaceID != "" {
query.Set("workspace_id", workspaceID)
}
ctx.Header("Cache-Control", "no-store")
ctx.Header("Referrer-Policy", "no-referrer")
ctx.Redirect(http.StatusSeeOther, "/settings/integrations/github?"+query.Encode())
}
func deploymentAppHTTPError(err error) (int, string, bool) {
switch {
case errors.Is(err, ErrDeploymentAppOperatorRequired):
return http.StatusForbidden, "github_app_operator_required", true
case errors.Is(err, ErrDeploymentAppRegistrationCancelled):
return http.StatusBadRequest, "github_app_registration_cancelled", true
case errors.Is(err, ErrAppRegistrationNotFound):
return http.StatusNotFound, "github_app_registration_not_found", true
case errors.Is(err, ErrDeploymentAppInUse):
return http.StatusConflict, "github_app_registration_in_use", true
case errors.Is(err, ErrDeploymentAppManifestOwnerInvalid),
errors.Is(err, ErrAppRegistrationIDInvalid),
errors.Is(err, ErrAppRegistrationVisibilityInvalid):
return http.StatusBadRequest, "github_app_invalid_request", true
case errors.Is(err, ErrPublicGitHubBaseURLInvalid),
errors.Is(err, ErrPublicGitHubBaseURLNotGlobal),
errors.Is(err, ErrPublicGitHubBaseURLUnresolvable):
return http.StatusBadRequest, "github_app_public_url_invalid", true
case errors.Is(err, ErrDeploymentAppManifestStateUnavailable),
errors.Is(err, ErrDeploymentAppIdentityMismatch),
errors.Is(err, ErrDeploymentAppPolicyMismatch):
return http.StatusBadRequest, "github_app_invalid_callback", true
case errors.Is(err, ErrAppRegistrationImportPreparationUnavailable):
return http.StatusBadRequest, "github_app_import_preparation_invalid", true
}
var importError *AppRegistrationImportError
if errors.As(err, &importError) {
code := "github_app_" + string(importError.Code)
if importError.Code == AppRegistrationImportAlreadyRegistered {
return http.StatusConflict, "github_app_already_registered", true
}
if importError.Code == AppRegistrationImportInvalidRequest {
return http.StatusBadRequest, "github_app_invalid_request", true
}
if importError.Code == AppRegistrationImportPersistenceFailed {
return http.StatusInternalServerError, code, true
}
return http.StatusUnprocessableEntity, code, true
}
var conversionError *ManifestConversionError
if errors.As(err, &conversionError) {
return http.StatusBadGateway, string(conversionError.Code), true
}
return 0, "", false
}