Secret validation

This commit is contained in:
rxdn 2023-06-08 16:16:58 +01:00
parent d64062eab1
commit 48cb88c0ec
10 changed files with 102 additions and 13 deletions

View File

@ -1,9 +1,11 @@
package api
import (
"encoding/json"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)
@ -38,6 +40,17 @@ func ActivateIntegrationHandler(ctx *gin.Context) {
return
}
integration, ok, err := dbclient.Client.CustomIntegrations.Get(integrationId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Integration not found"))
return
}
// Check the integration is public or the user created it
canActivate, err := dbclient.Client.CustomIntegrationGuilds.CanActivate(integrationId, userId)
if err != nil {
@ -87,6 +100,48 @@ func ActivateIntegrationHandler(ctx *gin.Context) {
}
}
// Validate secrets
if integration.Public && integration.Approved && integration.ValidationUrl != nil {
res, statusCode, err := utils.SecureProxyClient.DoRequest(http.MethodPost, *integration.ValidationUrl, nil, data.Secrets)
if err != nil {
if statusCode == http.StatusRequestTimeout {
ctx.JSON(400, utils.ErrorStr("Secret validation server did not respond in time (contact the integration author)"))
return
} else {
ctx.JSON(500, utils.ErrorJson(err))
return
}
}
type validationResponse struct {
Error string `json:"error"`
}
var useClientError bool
var parsed validationResponse
if err := json.Unmarshal(res, &parsed); err == nil {
useClientError = len(parsed.Error) > 0
if len(parsed.Error) > 255 {
parsed.Error = parsed.Error[:255]
}
}
if statusCode > 299 {
if useClientError {
ctx.JSON(400, gin.H{
"success": false,
"error": "Integration rejected the secret values (contact the integration author for help)",
"client_error": parsed.Error,
})
} else {
ctx.JSON(400, utils.ErrorStr("Integration rejected the secret values (contact the integration author for help)"))
}
return
}
}
if err := dbclient.Client.CustomIntegrationGuilds.AddToGuildWithSecrets(integrationId, guildId, secretMap); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return

View File

@ -15,7 +15,8 @@ type integrationCreateBody struct {
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
Method string `json:"http_method" validate:"required,oneof=GET POST"`
WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255"`
WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"`
ValidationUrl *string `json:"validation_url" validate:"omitempty,url,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"`
Secrets []struct {
Name string `json:"name" validate:"required,min=1,max=32,excludesall=% "`
@ -67,7 +68,7 @@ func CreateIntegrationHandler(ctx *gin.Context) {
return
}
integration, err := dbclient.Client.CustomIntegrations.Create(userId, data.WebhookUrl, data.Method, data.Name, data.Description, data.ImageUrl, data.PrivacyPolicyUrl)
integration, err := dbclient.Client.CustomIntegrations.Create(userId, data.WebhookUrl, data.ValidationUrl, data.Method, data.Name, data.Description, data.ImageUrl, data.PrivacyPolicyUrl)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return

View File

@ -18,7 +18,8 @@ type integrationUpdateBody struct {
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
Method string `json:"http_method" validate:"required,oneof=GET POST"`
WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255"`
WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"`
ValidationUrl *string `json:"validation_url" validate:"omitempty,url,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"`
Secrets []struct {
Id int `json:"id" validate:"omitempty,min=1"`
@ -93,6 +94,7 @@ func UpdateIntegrationHandler(ctx *gin.Context) {
OwnerId: integration.OwnerId,
HttpMethod: data.Method,
WebhookUrl: data.WebhookUrl,
ValidationUrl: data.ValidationUrl,
Name: data.Name,
Description: data.Description,
ImageUrl: data.ImageUrl,

View File

@ -160,7 +160,13 @@ func StartServer() {
guildAuthApiAdmin.GET("/integrations/available", api_integrations.ListIntegrationsHandler)
guildAuthApiAdmin.GET("/integrations/:integrationid", api_integrations.IsIntegrationActiveHandler)
guildAuthApiAdmin.POST("/integrations/:integrationid", api_integrations.ActivateIntegrationHandler)
guildAuthApiAdmin.POST("/integrations/:integrationid",
rl(middleware.RateLimitTypeUser, 10, time.Minute),
rl(middleware.RateLimitTypeGuild, 10, time.Minute),
rl(middleware.RateLimitTypeUser, 30, time.Minute*30),
rl(middleware.RateLimitTypeGuild, 30, time.Minute*30),
api_integrations.ActivateIntegrationHandler,
)
guildAuthApiAdmin.PATCH("/integrations/:integrationid", api_integrations.UpdateIntegrationSecretsHandler)
guildAuthApiAdmin.DELETE("/integrations/:integrationid", api_integrations.RemoveIntegrationHandler)
}

View File

@ -15,6 +15,7 @@ import (
"github.com/TicketsBot/archiverclient"
"github.com/TicketsBot/common/chatrelay"
"github.com/TicketsBot/common/premium"
"github.com/TicketsBot/common/secureproxy"
"github.com/TicketsBot/worker/i18n"
"github.com/apex/log"
"github.com/getsentry/sentry-go"
@ -54,6 +55,7 @@ func main() {
cache.Instance = cache.NewCache()
utils.ArchiverClient = archiverclient.NewArchiverClientWithTimeout(config.Conf.Bot.ObjectStore, time.Second*15, []byte(config.Conf.Bot.AesKey))
utils.SecureProxyClient = secureproxy.NewSecureProxy(config.Conf.SecureProxyUrl)
utils.LoadEmoji()

View File

@ -20,6 +20,7 @@ type (
Bot Bot
Redis Redis
Cache Cache
SecureProxyUrl string
}
Server struct {
@ -177,5 +178,6 @@ func fromEnvvar() {
Cache: Cache{
Uri: os.Getenv("CACHE_URI"),
},
SecureProxyUrl: os.Getenv("SECURE_PROXY_URL"),
}
}

View File

@ -136,6 +136,17 @@
</div>
</div>
<div>
<h3>Secret Validation (Optional)</h3>
<div class="section">
<p>You can specify a URL to send a POST request to when a user adds your integration to their server /
updates the secrets. Respond with 2XX if the secrets are valid, or any other status code to reject
them. You can read more about secret validation in our <a class="link-blue" href="https://docs.ticketsbot.net/integrations/building-integrations#secret-validation">documentation</a>.</p>
<Input col1 label="Validation URL (Optional)" bind:value={data.validation_url}
on:change={ensureNullIfBlank} placeholder="https://api.example.com/validate"/>
</div>
</div>
<div>
<h3>Request Headers</h3>
<div class="section">
@ -366,6 +377,10 @@
if (data.privacy_policy_url !== undefined && data.privacy_policy_url !== null && data.privacy_policy_url.length === 0) {
data.privacy_policy_url = null;
}
if (data.validation_url !== undefined && data.validation_url !== null && data.validation_url.length === 0) {
data.validation_url = null;
}
}
function updateExampleJson() {

4
go.mod
View File

@ -5,8 +5,8 @@ go 1.18
require (
github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b
github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26
github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
github.com/TicketsBot/worker v0.0.0-20220830131837-12d85aca5c71
github.com/apex/log v1.1.2

9
go.sum
View File

@ -37,10 +37,10 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/ReneKroon/ttlcache v1.6.0/go.mod h1:DG6nbhXKUQhrExfwwLuZUdH7UnRDDRA1IW+nBuCssvs=
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc h1:n15W8Eg+ik3/0yqPzZVRP2oZJcIZCIgQ071cZleedKo=
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc=
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqSbjJ4o7WKkdoPngmhjAGEXFwteEjpqs=
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b h1:ZlPTCuJVEjvt6Rdz1mUgkJUVMj8XrLOwrbh05vcl1KI=
github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26 h1:jFQj7JTrDULhSpVqn1L3RUs5YOghOvsGMCjndTrv6HM=
github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408 h1:MkcYud/oOmmplBIK7zWhgnnE5vU9XSnS4e30vH2+Rv0=
github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
@ -481,7 +481,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=

7
utils/secureproxy.go Normal file
View File

@ -0,0 +1,7 @@
package utils
import (
"github.com/TicketsBot/common/secureproxy"
)
var SecureProxyClient *secureproxy.Client