Secret validation
This commit is contained in:
parent
d64062eab1
commit
48cb88c0ec
@ -1,9 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,6 +40,17 @@ func ActivateIntegrationHandler(ctx *gin.Context) {
|
|||||||
return
|
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
|
// Check the integration is public or the user created it
|
||||||
canActivate, err := dbclient.Client.CustomIntegrationGuilds.CanActivate(integrationId, userId)
|
canActivate, err := dbclient.Client.CustomIntegrationGuilds.CanActivate(integrationId, userId)
|
||||||
if err != nil {
|
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 {
|
if err := dbclient.Client.CustomIntegrationGuilds.AddToGuildWithSecrets(integrationId, guildId, secretMap); err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
|
@ -14,8 +14,9 @@ type integrationCreateBody struct {
|
|||||||
ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"`
|
ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
|
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
|
|
||||||
Method string `json:"http_method" validate:"required,oneof=GET POST"`
|
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 {
|
Secrets []struct {
|
||||||
Name string `json:"name" validate:"required,min=1,max=32,excludesall=% "`
|
Name string `json:"name" validate:"required,min=1,max=32,excludesall=% "`
|
||||||
@ -67,7 +68,7 @@ func CreateIntegrationHandler(ctx *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
|
@ -17,8 +17,9 @@ type integrationUpdateBody struct {
|
|||||||
ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"`
|
ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
|
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
|
|
||||||
Method string `json:"http_method" validate:"required,oneof=GET POST"`
|
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 {
|
Secrets []struct {
|
||||||
Id int `json:"id" validate:"omitempty,min=1"`
|
Id int `json:"id" validate:"omitempty,min=1"`
|
||||||
@ -93,6 +94,7 @@ func UpdateIntegrationHandler(ctx *gin.Context) {
|
|||||||
OwnerId: integration.OwnerId,
|
OwnerId: integration.OwnerId,
|
||||||
HttpMethod: data.Method,
|
HttpMethod: data.Method,
|
||||||
WebhookUrl: data.WebhookUrl,
|
WebhookUrl: data.WebhookUrl,
|
||||||
|
ValidationUrl: data.ValidationUrl,
|
||||||
Name: data.Name,
|
Name: data.Name,
|
||||||
Description: data.Description,
|
Description: data.Description,
|
||||||
ImageUrl: data.ImageUrl,
|
ImageUrl: data.ImageUrl,
|
||||||
|
@ -160,7 +160,13 @@ func StartServer() {
|
|||||||
|
|
||||||
guildAuthApiAdmin.GET("/integrations/available", api_integrations.ListIntegrationsHandler)
|
guildAuthApiAdmin.GET("/integrations/available", api_integrations.ListIntegrationsHandler)
|
||||||
guildAuthApiAdmin.GET("/integrations/:integrationid", api_integrations.IsIntegrationActiveHandler)
|
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.PATCH("/integrations/:integrationid", api_integrations.UpdateIntegrationSecretsHandler)
|
||||||
guildAuthApiAdmin.DELETE("/integrations/:integrationid", api_integrations.RemoveIntegrationHandler)
|
guildAuthApiAdmin.DELETE("/integrations/:integrationid", api_integrations.RemoveIntegrationHandler)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/TicketsBot/archiverclient"
|
"github.com/TicketsBot/archiverclient"
|
||||||
"github.com/TicketsBot/common/chatrelay"
|
"github.com/TicketsBot/common/chatrelay"
|
||||||
"github.com/TicketsBot/common/premium"
|
"github.com/TicketsBot/common/premium"
|
||||||
|
"github.com/TicketsBot/common/secureproxy"
|
||||||
"github.com/TicketsBot/worker/i18n"
|
"github.com/TicketsBot/worker/i18n"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
@ -54,6 +55,7 @@ func main() {
|
|||||||
cache.Instance = cache.NewCache()
|
cache.Instance = cache.NewCache()
|
||||||
|
|
||||||
utils.ArchiverClient = archiverclient.NewArchiverClientWithTimeout(config.Conf.Bot.ObjectStore, time.Second*15, []byte(config.Conf.Bot.AesKey))
|
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()
|
utils.LoadEmoji()
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ type (
|
|||||||
Bot Bot
|
Bot Bot
|
||||||
Redis Redis
|
Redis Redis
|
||||||
Cache Cache
|
Cache Cache
|
||||||
|
SecureProxyUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
Server struct {
|
Server struct {
|
||||||
@ -177,5 +178,6 @@ func fromEnvvar() {
|
|||||||
Cache: Cache{
|
Cache: Cache{
|
||||||
Uri: os.Getenv("CACHE_URI"),
|
Uri: os.Getenv("CACHE_URI"),
|
||||||
},
|
},
|
||||||
|
SecureProxyUrl: os.Getenv("SECURE_PROXY_URL"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,6 +136,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
<h3>Request Headers</h3>
|
<h3>Request Headers</h3>
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@ -366,6 +377,10 @@
|
|||||||
if (data.privacy_policy_url !== undefined && data.privacy_policy_url !== null && data.privacy_policy_url.length === 0) {
|
if (data.privacy_policy_url !== undefined && data.privacy_policy_url !== null && data.privacy_policy_url.length === 0) {
|
||||||
data.privacy_policy_url = null;
|
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() {
|
function updateExampleJson() {
|
||||||
|
4
go.mod
4
go.mod
@ -5,8 +5,8 @@ go 1.18
|
|||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.3.1
|
github.com/BurntSushi/toml v0.3.1
|
||||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
||||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
|
github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26
|
||||||
github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b
|
github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
||||||
github.com/TicketsBot/worker v0.0.0-20220830131837-12d85aca5c71
|
github.com/TicketsBot/worker v0.0.0-20220830131837-12d85aca5c71
|
||||||
github.com/apex/log v1.1.2
|
github.com/apex/log v1.1.2
|
||||||
|
9
go.sum
9
go.sum
@ -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/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 h1:n15W8Eg+ik3/0yqPzZVRP2oZJcIZCIgQ071cZleedKo=
|
||||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc=
|
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-20230608150251-8d29dcf6ae26 h1:jFQj7JTrDULhSpVqn1L3RUs5YOghOvsGMCjndTrv6HM=
|
||||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
|
github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
|
||||||
github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b h1:ZlPTCuJVEjvt6Rdz1mUgkJUVMj8XrLOwrbh05vcl1KI=
|
github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408 h1:MkcYud/oOmmplBIK7zWhgnnE5vU9XSnS4e30vH2+Rv0=
|
||||||
github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
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 h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
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=
|
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-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-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-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.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 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
|
7
utils/secureproxy.go
Normal file
7
utils/secureproxy.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TicketsBot/common/secureproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SecureProxyClient *secureproxy.Client
|
Loading…
x
Reference in New Issue
Block a user