mirror of
https://github.com/go-sigma/sigma.git
synced 2026-10-28 14:34:34 +08:00
feat(coderepos): configure a per-provider SCM credential
Users had no way to supply their own access token: the only credential in
play was the OAuth token captured at login, which expires and which the
builders then copied into their own table. The clone credential table
existed but had no write path, so every lookup fell back to that token.
Give each provider an explicit credential:
- GET/PUT/DELETE /api/v1/coderepos/{provider}/credential plus
POST .../credential/validate; the read endpoint returns a masked token and
"has" flags, never the secret
- UpsertCodeRepositoryCredential validates a token against the provider,
resolves the account id from it, ensures the third-party binding exists
(409 when the account already belongs to another user) and seals
token/ssh_key/password before storing them
- only token credentials can list repositories: ssh and username/password
are clone credentials, so validate reports validated=false for them and
no sync is queued
- ListCodeRepositoryProviders returns the configured providers joined with
the caller's binding, credential and last sync state, so a provider that
is configured but not linked yet is still offered by the UI
- fix the AccountID and UpdatedAt mistakes in the user3rdparty response,
which dereferenced a nil pointer and reported the creation time twice
- regenerate the coderepos and user repository mocks and the operator
swagger spec
Verification:
- make gormgen
- make swagen
- CI_DATABASE_TYPE=turso go test -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./pkg/service/coderepos/... ./pkg/server/handlers/coderepos/...
This commit is contained in:
1 parent
3cb76aae80
commit
a80519d86f
11 files changed
+1330
-26
No files matched your search
+58
-2
@@ -141,9 +141,65 @@ type PutUserSelfRequest struct {
|
||||
Email *string `json:"email,omitempty" validate:"omitempty,is_valid_email" example:"test@mail.com"`
|
||||
}
|
||||
|
||||
// ListCodeRepositoryProvidersResponse represents an enabled source code provider.
|
||||
// ListCodeRepositoryProvidersResponse describes one source code provider together with the
|
||||
// caller's state: whether the provider is usable for OAuth login, whether the account is
|
||||
// linked and whether an SCM credential is configured, plus the last sync result.
|
||||
type ListCodeRepositoryProvidersResponse struct {
|
||||
Provider enums.Provider `json:"provider" example:"github"`
|
||||
Provider enums.Provider `json:"provider" example:"github"`
|
||||
DisplayName string `json:"display_name" example:"GitHub"`
|
||||
BaseURL string `json:"base_url" example:"https://github.com"`
|
||||
OauthEnabled bool `json:"oauth_enabled"`
|
||||
Linked bool `json:"linked"`
|
||||
AccountID *string `json:"account_id"`
|
||||
CredentialType enums.ScmCredentialType `json:"credential_type" example:"none"`
|
||||
HasCredential bool `json:"has_credential"`
|
||||
CrLastUpdateTimestamp string `json:"cr_last_update_timestamp"`
|
||||
CrLastUpdateStatus enums.TaskCommonStatus `json:"cr_last_update_status"`
|
||||
CrLastUpdateMessage *string `json:"cr_last_update_message"`
|
||||
}
|
||||
|
||||
// GetCodeRepositoryCredentialRequest is the request for getting the SCM credential of a provider.
|
||||
type GetCodeRepositoryCredentialRequest struct {
|
||||
Provider enums.Provider `json:"provider" param:"provider" validate:"required,is_valid_provider"`
|
||||
}
|
||||
|
||||
// GetCodeRepositoryCredentialResponse returns credential metadata. Secrets are never echoed
|
||||
// back: only a masked token and "has" flags are exposed.
|
||||
type GetCodeRepositoryCredentialResponse struct {
|
||||
Type enums.ScmCredentialType `json:"type" example:"token"`
|
||||
Token *string `json:"token" example:"ghp_****abcd"`
|
||||
Username *string `json:"username" example:"sigma"`
|
||||
HasSshKey bool `json:"has_ssh_key"`
|
||||
HasPassword bool `json:"has_password"`
|
||||
CreatedAt string `json:"created_at" example:"2006-01-02 15:04:05"`
|
||||
UpdatedAt string `json:"updated_at" example:"2006-01-02 15:04:05"`
|
||||
}
|
||||
|
||||
// UpdateCodeRepositoryCredentialRequest creates or replaces the SCM credential of a provider.
|
||||
type UpdateCodeRepositoryCredentialRequest struct {
|
||||
Provider enums.Provider `json:"provider" param:"provider" validate:"required,is_valid_provider"`
|
||||
Type enums.ScmCredentialType `json:"type" validate:"required,is_valid_scm_credential_type" example:"token"`
|
||||
Token *string `json:"token" validate:"required_if=Type token,max=1024" example:"ghp_xxx"`
|
||||
SshKey *string `json:"ssh_key" validate:"required_if=Type ssh,max=8192" example:"-----BEGIN OPENSSH PRIVATE KEY-----"`
|
||||
Username *string `json:"username" validate:"required_if=Type username,max=256" example:"sigma"`
|
||||
Password *string `json:"password" validate:"required_if=Type username,max=512" example:"sigma"`
|
||||
}
|
||||
|
||||
// DeleteCodeRepositoryCredentialRequest clears the SCM credential of a provider.
|
||||
type DeleteCodeRepositoryCredentialRequest struct {
|
||||
Provider enums.Provider `json:"provider" param:"provider" validate:"required,is_valid_provider"`
|
||||
}
|
||||
|
||||
// ValidateCodeRepositoryCredentialResponse reports the account a credential resolves to.
|
||||
type ValidateCodeRepositoryCredentialResponse struct {
|
||||
Provider enums.Provider `json:"provider" example:"github"`
|
||||
Type enums.ScmCredentialType `json:"type" example:"token"`
|
||||
AccountID string `json:"account_id" example:"1234567"`
|
||||
Username string `json:"username" example:"sigma"`
|
||||
// Validated reports whether the credential was verified against the provider API.
|
||||
// SSH keys and username/password credentials are only usable for cloning, so they
|
||||
// cannot be verified.
|
||||
Validated bool `json:"validated"`
|
||||
}
|
||||
|
||||
// GetCodeRepositoryResyncRequest is the request for resynchronizing source code repositories.
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package coderepos
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/consts"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
"github.com/go-sigma/sigma/pkg/utils"
|
||||
)
|
||||
|
||||
// maskedSecretLength is the number of leading and trailing characters kept when masking a secret.
|
||||
const maskedSecretLength = 4
|
||||
|
||||
// GetCredential gets the SCM credential of a provider
|
||||
//
|
||||
// @Summary Get code repository credential
|
||||
// @security BasicAuth
|
||||
// @Tags CodeRepository
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Router /coderepos/{provider}/credential [get]
|
||||
// @Param provider path string true "Get the SCM credential of the provider"
|
||||
// @Success 200 {object} api.GetCodeRepositoryCredentialResponse
|
||||
// @Failure 404 {object} errcode.ErrCode
|
||||
// @Failure 500 {object} errcode.ErrCode
|
||||
func (h *handler) GetCredential(c *gin.Context, req *api.GetCodeRepositoryCredentialRequest) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
user, ok := utils.GetFromCtx[*models.User](c, consts.ContextUser)
|
||||
if !ok {
|
||||
slog.Error("get user from context failed")
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
credentialObj, err := h.CodeRepoSvc.GetCodeRepositoryCredential(ctx, user.ID, req.Provider)
|
||||
if err != nil {
|
||||
if e, ok := errcode.AsType[errcode.ErrCode](err); ok {
|
||||
errcode.NewHTTPError(c, e)
|
||||
return
|
||||
}
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, api.GetCodeRepositoryCredentialResponse{
|
||||
Type: credentialObj.Type,
|
||||
Token: maskSecret(credentialObj.Token),
|
||||
Username: credentialObj.Username,
|
||||
HasSshKey: credentialObj.SshKey != nil,
|
||||
HasPassword: credentialObj.Password != nil,
|
||||
CreatedAt: time.UnixMilli(credentialObj.CreatedAt).UTC().Format(consts.DefaultTimePattern),
|
||||
UpdatedAt: time.UnixMilli(credentialObj.UpdatedAt).UTC().Format(consts.DefaultTimePattern),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCredential creates or replaces the SCM credential of a provider
|
||||
//
|
||||
// @Summary Update code repository credential
|
||||
// @security BasicAuth
|
||||
// @Tags CodeRepository
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Router /coderepos/{provider}/credential [put]
|
||||
// @Param provider path string true "Update the SCM credential of the provider"
|
||||
// @Param body body api.UpdateCodeRepositoryCredentialRequest true "SCM credential"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} errcode.ErrCode
|
||||
// @Failure 409 {object} errcode.ErrCode
|
||||
// @Failure 500 {object} errcode.ErrCode
|
||||
func (h *handler) UpdateCredential(c *gin.Context, req *api.UpdateCodeRepositoryCredentialRequest) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
user, ok := utils.GetFromCtx[*models.User](c, consts.ContextUser)
|
||||
if !ok {
|
||||
slog.Error("get user from context failed")
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.CodeRepoSvc.UpsertCodeRepositoryCredential(ctx, user.ID, *req)
|
||||
if err != nil {
|
||||
if e, ok := errcode.AsType[errcode.ErrCode](err); ok {
|
||||
errcode.NewHTTPError(c, e)
|
||||
return
|
||||
}
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeInternalError)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteCredential clears the SCM credential of a provider
|
||||
//
|
||||
// @Summary Delete code repository credential
|
||||
// @security BasicAuth
|
||||
// @Tags CodeRepository
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Router /coderepos/{provider}/credential [delete]
|
||||
// @Param provider path string true "Delete the SCM credential of the provider"
|
||||
// @Success 204
|
||||
// @Failure 404 {object} errcode.ErrCode
|
||||
// @Failure 500 {object} errcode.ErrCode
|
||||
func (h *handler) DeleteCredential(c *gin.Context, req *api.DeleteCodeRepositoryCredentialRequest) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
user, ok := utils.GetFromCtx[*models.User](c, consts.ContextUser)
|
||||
if !ok {
|
||||
slog.Error("get user from context failed")
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.CodeRepoSvc.DeleteCodeRepositoryCredential(ctx, user.ID, req.Provider)
|
||||
if err != nil {
|
||||
if e, ok := errcode.AsType[errcode.ErrCode](err); ok {
|
||||
errcode.NewHTTPError(c, e)
|
||||
return
|
||||
}
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeInternalError)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ValidateCredential verifies the SCM credential of a provider without storing it
|
||||
//
|
||||
// @Summary Validate code repository credential
|
||||
// @security BasicAuth
|
||||
// @Tags CodeRepository
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Router /coderepos/{provider}/credential/validate [post]
|
||||
// @Param provider path string true "Validate the SCM credential of the provider"
|
||||
// @Param body body api.UpdateCodeRepositoryCredentialRequest true "SCM credential"
|
||||
// @Success 200 {object} api.ValidateCodeRepositoryCredentialResponse
|
||||
// @Failure 400 {object} errcode.ErrCode
|
||||
// @Failure 500 {object} errcode.ErrCode
|
||||
func (h *handler) ValidateCredential(c *gin.Context, req *api.UpdateCodeRepositoryCredentialRequest) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
resp, err := h.CodeRepoSvc.ValidateCodeRepositoryCredential(ctx, *req)
|
||||
if err != nil {
|
||||
if e, ok := errcode.AsType[errcode.ErrCode](err); ok {
|
||||
errcode.NewHTTPError(c, e)
|
||||
return
|
||||
}
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeInternalError)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// maskSecret returns a masked representation of a secret, keeping only its first and last
|
||||
// characters, so that the API never echoes a stored secret back.
|
||||
func maskSecret(secret *string) *string {
|
||||
if secret == nil || *secret == "" {
|
||||
return nil
|
||||
}
|
||||
runes := []rune(*secret)
|
||||
if len(runes) < 2*maskedSecretLength+1 {
|
||||
masked := "****"
|
||||
return &masked
|
||||
}
|
||||
masked := string(runes[:maskedSecretLength]) + "****" + string(runes[len(runes)-maskedSecretLength:])
|
||||
return &masked
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func (h *handler) Providers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user3rdPartyObjs, err := h.CodeRepoSvc.ListCodeRepositoryProviders(ctx, user.ID)
|
||||
providers, err := h.CodeRepoSvc.ListCodeRepositoryProviders(ctx, user.ID)
|
||||
if err != nil {
|
||||
if e, ok := errcode.AsType[errcode.ErrCode](err); ok {
|
||||
errcode.NewHTTPError(c, e)
|
||||
@@ -58,11 +58,9 @@ func (h *handler) Providers(c *gin.Context) {
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeInternalError, fmt.Sprintf("List providers failed: %v", err))
|
||||
return
|
||||
}
|
||||
resp := make([]any, 0, len(user3rdPartyObjs))
|
||||
for _, user3rdPartyObj := range user3rdPartyObjs {
|
||||
resp = append(resp, api.ListCodeRepositoryProvidersResponse{
|
||||
Provider: user3rdPartyObj.Provider,
|
||||
})
|
||||
resp := make([]any, 0, len(providers))
|
||||
for _, provider := range providers {
|
||||
resp = append(resp, provider)
|
||||
}
|
||||
c.JSON(http.StatusOK, api.CommonList{Total: int64(len(user3rdPartyObjs)), Items: resp})
|
||||
c.JSON(http.StatusOK, api.CommonList{Total: int64(len(providers)), Items: resp})
|
||||
}
|
||||
@@ -263,14 +263,16 @@ func TestProviders(t *testing.T) {
|
||||
serviceObj := codereposmocks.NewMockCodeRepositoryService(ctrl)
|
||||
serviceObj.EXPECT().
|
||||
ListCodeRepositoryProviders(gomock.Any(), "10").
|
||||
DoAndReturn(func(_ context.Context, userID string) ([]*models.User3rdParty, error) {
|
||||
DoAndReturn(func(_ context.Context, userID string) ([]api.ListCodeRepositoryProvidersResponse, error) {
|
||||
require.Equal(t, "10", userID)
|
||||
return []*models.User3rdParty{{
|
||||
ID: "1",
|
||||
UserID: "10",
|
||||
Provider: enums.ProviderGithub,
|
||||
CreatedAt: 1,
|
||||
UpdatedAt: 1,
|
||||
return []api.ListCodeRepositoryProvidersResponse{{
|
||||
Provider: enums.ProviderGithub,
|
||||
DisplayName: "GitHub",
|
||||
BaseURL: "https://github.com",
|
||||
OauthEnabled: true,
|
||||
Linked: true,
|
||||
AccountID: new("account-1"),
|
||||
CredentialType: enums.ScmCredentialTypeNone,
|
||||
}}, nil
|
||||
})
|
||||
|
||||
@@ -330,3 +332,96 @@ func newCodereposContext(t *testing.T) (*httptest.ResponseRecorder, *gin.Context
|
||||
c.Set(consts.ContextUser, &models.User{ID: "10", Role: enums.UserRoleAdmin})
|
||||
return recorder, c
|
||||
}
|
||||
|
||||
func TestGetCredential(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
serviceObj := codereposmocks.NewMockCodeRepositoryService(ctrl)
|
||||
serviceObj.EXPECT().
|
||||
GetCodeRepositoryCredential(gomock.Any(), "10", enums.ProviderGithub).
|
||||
Return(&models.CodeRepositoryCloneCredential{
|
||||
ID: "1",
|
||||
Type: enums.ScmCredentialTypeToken,
|
||||
Token: new("ghp_1234567890abcd"),
|
||||
CreatedAt: 1,
|
||||
UpdatedAt: 2,
|
||||
}, nil)
|
||||
|
||||
recorder, c := newCodereposContext(t)
|
||||
(&handler{CodeRepoSvc: serviceObj}).GetCredential(c, &api.GetCodeRepositoryCredentialRequest{Provider: enums.ProviderGithub})
|
||||
c.Writer.WriteHeaderNow()
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), `"token":"ghp_****abcd"`)
|
||||
require.NotContains(t, recorder.Body.String(), "ghp_1234567890abcd")
|
||||
}
|
||||
|
||||
func TestGetCredentialReturnsNotFound(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
serviceObj := codereposmocks.NewMockCodeRepositoryService(ctrl)
|
||||
serviceObj.EXPECT().
|
||||
GetCodeRepositoryCredential(gomock.Any(), "10", enums.ProviderGithub).
|
||||
Return(nil, errcode.HTTPErrCodeNotFound)
|
||||
|
||||
recorder, c := newCodereposContext(t)
|
||||
(&handler{CodeRepoSvc: serviceObj}).GetCredential(c, &api.GetCodeRepositoryCredentialRequest{Provider: enums.ProviderGithub})
|
||||
c.Writer.WriteHeaderNow()
|
||||
|
||||
require.Equal(t, http.StatusNotFound, recorder.Code)
|
||||
}
|
||||
|
||||
func TestUpdateCredential(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
serviceObj := codereposmocks.NewMockCodeRepositoryService(ctrl)
|
||||
serviceObj.EXPECT().
|
||||
UpsertCodeRepositoryCredential(gomock.Any(), "10", gomock.Any()).
|
||||
Return(nil)
|
||||
|
||||
recorder, c := newCodereposContext(t)
|
||||
(&handler{CodeRepoSvc: serviceObj}).UpdateCredential(c, &api.UpdateCodeRepositoryCredentialRequest{
|
||||
Provider: enums.ProviderGithub,
|
||||
Type: enums.ScmCredentialTypeToken,
|
||||
Token: new("ghp_sigma"),
|
||||
})
|
||||
c.Writer.WriteHeaderNow()
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
func TestDeleteCredential(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
serviceObj := codereposmocks.NewMockCodeRepositoryService(ctrl)
|
||||
serviceObj.EXPECT().
|
||||
DeleteCodeRepositoryCredential(gomock.Any(), "10", enums.ProviderGithub).
|
||||
Return(nil)
|
||||
|
||||
recorder, c := newCodereposContext(t)
|
||||
(&handler{CodeRepoSvc: serviceObj}).DeleteCredential(c, &api.DeleteCodeRepositoryCredentialRequest{Provider: enums.ProviderGithub})
|
||||
c.Writer.WriteHeaderNow()
|
||||
|
||||
require.Equal(t, http.StatusNoContent, recorder.Code)
|
||||
}
|
||||
|
||||
func TestValidateCredential(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
serviceObj := codereposmocks.NewMockCodeRepositoryService(ctrl)
|
||||
serviceObj.EXPECT().
|
||||
ValidateCodeRepositoryCredential(gomock.Any(), gomock.Any()).
|
||||
Return(&api.ValidateCodeRepositoryCredentialResponse{
|
||||
Provider: enums.ProviderGithub,
|
||||
Type: enums.ScmCredentialTypeToken,
|
||||
AccountID: "1",
|
||||
Username: "sigma",
|
||||
Validated: true,
|
||||
}, nil)
|
||||
|
||||
recorder, c := newCodereposContext(t)
|
||||
(&handler{CodeRepoSvc: serviceObj}).ValidateCredential(c, &api.UpdateCodeRepositoryCredentialRequest{
|
||||
Provider: enums.ProviderGithub,
|
||||
Type: enums.ScmCredentialTypeToken,
|
||||
Token: new("ghp_sigma"),
|
||||
})
|
||||
c.Writer.WriteHeaderNow()
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), `"account_id":"1"`)
|
||||
}
|
||||
@@ -43,6 +43,14 @@ type Handler interface {
|
||||
Providers(c *gin.Context)
|
||||
// User3rdParty get user 3rdparty
|
||||
User3rdParty(c *gin.Context, req *api.GetCodeRepositoryUser3rdPartyRequest)
|
||||
// GetCredential gets the SCM credential of the provider
|
||||
GetCredential(c *gin.Context, req *api.GetCodeRepositoryCredentialRequest)
|
||||
// UpdateCredential creates or replaces the SCM credential of the provider
|
||||
UpdateCredential(c *gin.Context, req *api.UpdateCodeRepositoryCredentialRequest)
|
||||
// DeleteCredential clears the SCM credential of the provider
|
||||
DeleteCredential(c *gin.Context, req *api.DeleteCodeRepositoryCredentialRequest)
|
||||
// ValidateCredential verifies the SCM credential of the provider
|
||||
ValidateCredential(c *gin.Context, req *api.UpdateCodeRepositoryCredentialRequest)
|
||||
}
|
||||
|
||||
var _ Handler = &handler{}
|
||||
@@ -62,6 +70,10 @@ func Initialize(e *gin.Engine, h handler) error {
|
||||
codereposGroup.GET("/:provider", server.WrapRequest(h.List))
|
||||
codereposGroup.GET("/:provider/repos/:repo_id", server.WrapRequest(h.Get))
|
||||
codereposGroup.GET("/:provider/user3rdparty", server.WrapRequest(h.User3rdParty))
|
||||
codereposGroup.GET("/:provider/credential", server.WrapRequest(h.GetCredential))
|
||||
codereposGroup.PUT("/:provider/credential", server.WrapRequest(h.UpdateCredential))
|
||||
codereposGroup.DELETE("/:provider/credential", server.WrapRequest(h.DeleteCredential))
|
||||
codereposGroup.POST("/:provider/credential/validate", server.WrapRequest(h.ValidateCredential))
|
||||
codereposGroup.GET("/:provider/resync", server.WrapRequest(h.Resync))
|
||||
codereposGroup.GET("/:provider/owners", server.WrapRequest(h.ListOwners))
|
||||
codereposGroup.GET("/:provider/repos/:repo_id/branches", server.WrapRequest(h.ListBranches))
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestInitialize(t *testing.T) {
|
||||
require.NoError(t, digCon.Invoke(Initialize))
|
||||
require.NoError(t, digCon.Invoke(func(engine *gin.Engine) {
|
||||
routes := engine.Routes()
|
||||
require.Len(t, routes, 8)
|
||||
require.Len(t, routes, 12)
|
||||
require.True(t, hasRoute(routes, http.MethodGet, "/api/v1/coderepos/providers"))
|
||||
require.True(t, hasRoute(routes, http.MethodGet, "/api/v1/coderepos/:provider/repos/:repo_id/branches/:name"))
|
||||
}))
|
||||
|
||||
@@ -26,12 +26,15 @@ import (
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
"github.com/go-sigma/sigma/pkg/config"
|
||||
"github.com/go-sigma/sigma/pkg/consts"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
"github.com/go-sigma/sigma/pkg/dal/query"
|
||||
repocoderepo "github.com/go-sigma/sigma/pkg/dal/repository/coderepo"
|
||||
repouser "github.com/go-sigma/sigma/pkg/dal/repository/user"
|
||||
"github.com/go-sigma/sigma/pkg/infra/workq"
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
svccredential "github.com/go-sigma/sigma/pkg/service/credential"
|
||||
"github.com/go-sigma/sigma/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -51,18 +54,30 @@ type Service interface {
|
||||
GetCodeRepositoryBranch(ctx context.Context, codeRepositoryID string, name string) (*models.CodeRepositoryBranch, error)
|
||||
// ResyncCodeRepositories resync all code repositories for the user's 3rd party (includes: update user status + produce daemon task).
|
||||
ResyncCodeRepositories(ctx context.Context, userID string, provider enums.Provider) error
|
||||
// ListCodeRepositoryProviders lists code repository providers (user 3rd party).
|
||||
ListCodeRepositoryProviders(ctx context.Context, userID string) ([]*models.User3rdParty, error)
|
||||
// ListCodeRepositoryProviders lists the configured source code providers together with
|
||||
// the caller's binding, credential and last sync state.
|
||||
ListCodeRepositoryProviders(ctx context.Context, userID string) ([]api.ListCodeRepositoryProvidersResponse, error)
|
||||
// GetCodeRepositoryUser3rdParty gets the user 3rd party by provider.
|
||||
GetCodeRepositoryUser3rdParty(ctx context.Context, userID string, provider enums.Provider) (*models.User3rdParty, error)
|
||||
// GetCodeRepositoryCredential gets the SCM credential metadata of a provider.
|
||||
GetCodeRepositoryCredential(ctx context.Context, userID string, provider enums.Provider) (*models.CodeRepositoryCloneCredential, error)
|
||||
// UpsertCodeRepositoryCredential validates and stores the SCM credential of a provider,
|
||||
// then triggers a sync.
|
||||
UpsertCodeRepositoryCredential(ctx context.Context, userID string, req api.UpdateCodeRepositoryCredentialRequest) error
|
||||
// DeleteCodeRepositoryCredential clears the SCM credential of a provider.
|
||||
DeleteCodeRepositoryCredential(ctx context.Context, userID string, provider enums.Provider) error
|
||||
// ValidateCodeRepositoryCredential verifies a credential without storing it.
|
||||
ValidateCodeRepositoryCredential(ctx context.Context, req api.UpdateCodeRepositoryCredentialRequest) (*api.ValidateCodeRepositoryCredentialResponse, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
dig.In
|
||||
|
||||
RepoCode repocoderepo.CodeRepositoryRepository
|
||||
RepoUser repouser.UserRepository
|
||||
Producer workq.Producer
|
||||
RepoCode repocoderepo.CodeRepositoryRepository
|
||||
RepoUser repouser.UserRepository
|
||||
Producer workq.Producer
|
||||
Config *config.Configuration
|
||||
SvcCredential svccredential.Service
|
||||
}
|
||||
|
||||
func NewService(params service) Service {
|
||||
@@ -184,13 +199,90 @@ func (s *service) ResyncCodeRepositories(ctx context.Context, userID string, pro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ListCodeRepositoryProviders(ctx context.Context, userID string) ([]*models.User3rdParty, error) {
|
||||
// ListCodeRepositoryProviders lists the configured source code providers together with the
|
||||
// caller's binding, credential and last sync state.
|
||||
//
|
||||
// Providers come from the configuration (not from the user's bindings), so a provider that
|
||||
// is configured but not linked yet is still listed: the web UI needs it to offer the
|
||||
// "configure access token" action.
|
||||
func (s *service) ListCodeRepositoryProviders(ctx context.Context, userID string) ([]api.ListCodeRepositoryProvidersResponse, error) {
|
||||
user3rdPartyObjs, err := s.RepoUser.ListUser3rdParty(ctx, userID)
|
||||
if err != nil {
|
||||
slog.Error("list providers failed", "err", err)
|
||||
return nil, errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("List providers failed: %v", err))
|
||||
}
|
||||
return user3rdPartyObjs, nil
|
||||
bindings := make(map[enums.Provider]*models.User3rdParty, len(user3rdPartyObjs))
|
||||
for _, user3rdPartyObj := range user3rdPartyObjs {
|
||||
bindings[user3rdPartyObj.Provider] = user3rdPartyObj
|
||||
}
|
||||
|
||||
scmProviders := s.Config.ResolveScmProviders()
|
||||
resp := make([]api.ListCodeRepositoryProvidersResponse, 0, len(scmProviders))
|
||||
for _, scmProvider := range scmProviders {
|
||||
provider := enums.Provider(scmProvider.Provider.String())
|
||||
item := api.ListCodeRepositoryProvidersResponse{
|
||||
Provider: provider,
|
||||
DisplayName: scmProviderDisplayName(scmProvider.Provider),
|
||||
BaseURL: scmProvider.BaseURL,
|
||||
OauthEnabled: s.oauthEnabled(provider),
|
||||
CredentialType: enums.ScmCredentialTypeNone,
|
||||
CrLastUpdateStatus: enums.TaskCommonStatusPending,
|
||||
CrLastUpdateTimestamp: time.Unix(0, 0).UTC().Format(consts.DefaultTimePattern),
|
||||
}
|
||||
|
||||
user3rdPartyObj, ok := bindings[provider]
|
||||
if !ok {
|
||||
resp = append(resp, item)
|
||||
continue
|
||||
}
|
||||
item.Linked = true
|
||||
item.AccountID = user3rdPartyObj.AccountID
|
||||
item.CrLastUpdateTimestamp = time.UnixMilli(user3rdPartyObj.CrLastUpdateTimestamp).UTC().Format(consts.DefaultTimePattern)
|
||||
item.CrLastUpdateStatus = user3rdPartyObj.CrLastUpdateStatus
|
||||
item.CrLastUpdateMessage = user3rdPartyObj.CrLastUpdateMessage
|
||||
|
||||
credentialObj, err := s.RepoCode.GetCloneCredential(ctx, user3rdPartyObj.ID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
slog.Error("get code repository credential failed", "err", err, "user_3rdparty_id", user3rdPartyObj.ID)
|
||||
return nil, errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get code repository credential failed: %v", err))
|
||||
}
|
||||
resp = append(resp, item)
|
||||
continue
|
||||
}
|
||||
if credentialObj.Type != enums.ScmCredentialTypeNone {
|
||||
item.CredentialType = credentialObj.Type
|
||||
item.HasCredential = true
|
||||
}
|
||||
resp = append(resp, item)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// oauthEnabled reports whether the provider can be used to sign in.
|
||||
func (s *service) oauthEnabled(provider enums.Provider) bool {
|
||||
switch provider {
|
||||
case enums.ProviderGithub:
|
||||
return s.Config.Auth.Oauth2.Github.Enabled
|
||||
case enums.ProviderGitlab:
|
||||
return s.Config.Auth.Oauth2.Gitlab.Enabled
|
||||
case enums.ProviderGitea:
|
||||
return s.Config.Auth.Oauth2.Gitea.Enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// scmProviderDisplayName returns the human readable name of a source code provider.
|
||||
func scmProviderDisplayName(provider enums.ScmProvider) string {
|
||||
switch provider {
|
||||
case enums.ScmProviderGithub:
|
||||
return "GitHub"
|
||||
case enums.ScmProviderGitlab:
|
||||
return "GitLab"
|
||||
case enums.ScmProviderGitea:
|
||||
return "Gitea"
|
||||
}
|
||||
return provider.String()
|
||||
}
|
||||
|
||||
func (s *service) GetCodeRepositoryUser3rdParty(ctx context.Context, userID string, provider enums.Provider) (*models.User3rdParty, error) {
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package coderepos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/google/go-github/v74/github"
|
||||
gitlab "gitlab.com/gitlab-org/api/client-go"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
"github.com/go-sigma/sigma/pkg/config"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
"github.com/go-sigma/sigma/pkg/dal/query"
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
"github.com/go-sigma/sigma/pkg/utils/ptr"
|
||||
"github.com/go-sigma/sigma/pkg/utils/uuid"
|
||||
)
|
||||
|
||||
// scmAccount is the provider account a credential resolves to.
|
||||
type scmAccount struct {
|
||||
ID string
|
||||
Username string
|
||||
}
|
||||
|
||||
// GetCodeRepositoryCredential gets the SCM credential metadata of a provider.
|
||||
//
|
||||
// Secrets are not decrypted here: the response only carries the credential type, a masked
|
||||
// token and "has" flags, so that the API never echoes a secret back.
|
||||
func (s *service) GetCodeRepositoryCredential(ctx context.Context, userID string, provider enums.Provider) (*models.CodeRepositoryCloneCredential, error) {
|
||||
user3rdPartyObj, err := s.RepoUser.GetUser3rdPartyByProvider(ctx, userID, provider)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errcode.HTTPErrCodeNotFound.Detail(fmt.Sprintf("No %s account is linked", provider.String()))
|
||||
}
|
||||
slog.Error("get user 3rdparty by provider failed", "err", err, "provider", provider.String())
|
||||
return nil, errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get user 3rdparty by provider failed: %v", err))
|
||||
}
|
||||
credentialObj, err := s.RepoCode.GetCloneCredential(ctx, user3rdPartyObj.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errcode.HTTPErrCodeNotFound.Detail(fmt.Sprintf("No credential is configured for %s", provider.String()))
|
||||
}
|
||||
slog.Error("get code repository credential failed", "err", err, "provider", provider.String())
|
||||
return nil, errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get code repository credential failed: %v", err))
|
||||
}
|
||||
return credentialObj, nil
|
||||
}
|
||||
|
||||
// UpsertCodeRepositoryCredential validates and stores the SCM credential of a provider, then triggers a sync.
|
||||
func (s *service) UpsertCodeRepositoryCredential(ctx context.Context, userID string, req api.UpdateCodeRepositoryCredentialRequest) error {
|
||||
scmProvider, err := s.mustScmProvider(req.Provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = validateCredentialRequest(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// A token is the only credential type that can list repositories, so it is the only
|
||||
// one verified against the provider and used to discover the linked account.
|
||||
var account *scmAccount
|
||||
if req.Type == enums.ScmCredentialTypeToken {
|
||||
account, err = fetchScmAccount(ctx, req.Provider, scmProvider.BaseURL, ptr.To(req.Token))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
user3rdPartyObj, err := s.ensureUser3rdParty(ctx, userID, req.Provider, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credentialObj := &models.CodeRepositoryCloneCredential{
|
||||
ID: uuid.NewV7String(),
|
||||
User3rdPartyID: user3rdPartyObj.ID,
|
||||
Type: req.Type,
|
||||
Username: req.Username,
|
||||
}
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
plain *string
|
||||
dst **string
|
||||
}{
|
||||
{name: "token", plain: req.Token, dst: &credentialObj.Token},
|
||||
{name: "password", plain: req.Password, dst: &credentialObj.Password},
|
||||
{name: "ssh key", plain: req.SshKey, dst: &credentialObj.SshKey},
|
||||
} {
|
||||
if field.plain == nil || *field.plain == "" {
|
||||
continue
|
||||
}
|
||||
sealed, err := s.SvcCredential.Seal(*field.plain)
|
||||
if err != nil {
|
||||
slog.Error("seal code repository credential failed", "err", err, "field", field.name)
|
||||
return errcode.HTTPErrCodeInternalError.Detail("Seal code repository credential failed")
|
||||
}
|
||||
*field.dst = &sealed
|
||||
}
|
||||
|
||||
err = s.RepoCode.UpsertCredential(ctx, credentialObj)
|
||||
if err != nil {
|
||||
slog.Error("upsert code repository credential failed", "err", err, "provider", req.Provider.String())
|
||||
return errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Upsert code repository credential failed: %v", err))
|
||||
}
|
||||
|
||||
// Only token credentials can list repositories; SSH and username/password are clone
|
||||
// credentials, so syncing them would only produce a failure message.
|
||||
if req.Type == enums.ScmCredentialTypeToken {
|
||||
s.publishSync(ctx, user3rdPartyObj.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCodeRepositoryCredential clears the SCM credential of a provider.
|
||||
func (s *service) DeleteCodeRepositoryCredential(ctx context.Context, userID string, provider enums.Provider) error {
|
||||
if _, err := s.mustScmProvider(provider); err != nil {
|
||||
return err
|
||||
}
|
||||
user3rdPartyObj, err := s.RepoUser.GetUser3rdPartyByProvider(ctx, userID, provider)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errcode.HTTPErrCodeNotFound.Detail(fmt.Sprintf("No %s account is linked", provider.String()))
|
||||
}
|
||||
slog.Error("get user 3rdparty by provider failed", "err", err, "provider", provider.String())
|
||||
return errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get user 3rdparty by provider failed: %v", err))
|
||||
}
|
||||
err = s.RepoCode.DeleteCredential(ctx, user3rdPartyObj.ID)
|
||||
if err != nil {
|
||||
slog.Error("delete code repository credential failed", "err", err, "provider", provider.String())
|
||||
return errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Delete code repository credential failed: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCodeRepositoryCredential verifies a credential without storing it.
|
||||
//
|
||||
// Only token credentials can be verified: SSH keys and username/password credentials are
|
||||
// used by the builder when cloning and are never sent to a provider API here.
|
||||
func (s *service) ValidateCodeRepositoryCredential(ctx context.Context, req api.UpdateCodeRepositoryCredentialRequest) (*api.ValidateCodeRepositoryCredentialResponse, error) {
|
||||
scmProvider, err := s.mustScmProvider(req.Provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = validateCredentialRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &api.ValidateCodeRepositoryCredentialResponse{
|
||||
Provider: req.Provider,
|
||||
Type: req.Type,
|
||||
}
|
||||
if req.Type != enums.ScmCredentialTypeToken {
|
||||
return resp, nil
|
||||
}
|
||||
account, err := fetchScmAccount(ctx, req.Provider, scmProvider.BaseURL, ptr.To(req.Token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.AccountID = account.ID
|
||||
resp.Username = account.Username
|
||||
resp.Validated = true
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// mustScmProvider returns the configuration of a provider, rejecting providers that the
|
||||
// administrator did not enable.
|
||||
func (s *service) mustScmProvider(provider enums.Provider) (config.ConfigurationScmProvider, error) {
|
||||
scmProvider, ok := s.Config.ScmProvider(provider)
|
||||
if !ok {
|
||||
return config.ConfigurationScmProvider{}, errcode.HTTPErrCodeBadRequest.Detail(fmt.Sprintf("scm provider %s is not configured", provider.String()))
|
||||
}
|
||||
return scmProvider, nil
|
||||
}
|
||||
|
||||
// validateCredentialRequest checks that the required fields of a credential type are present.
|
||||
func validateCredentialRequest(req api.UpdateCodeRepositoryCredentialRequest) error {
|
||||
switch req.Type {
|
||||
case enums.ScmCredentialTypeToken:
|
||||
if req.Token == nil || *req.Token == "" {
|
||||
return errcode.HTTPErrCodeBadRequest.Detail("token is required for a token credential")
|
||||
}
|
||||
case enums.ScmCredentialTypeSsh:
|
||||
if req.SshKey == nil || *req.SshKey == "" {
|
||||
return errcode.HTTPErrCodeBadRequest.Detail("ssh_key is required for an ssh credential")
|
||||
}
|
||||
case enums.ScmCredentialTypeUsername:
|
||||
if req.Username == nil || *req.Username == "" {
|
||||
return errcode.HTTPErrCodeBadRequest.Detail("username is required for a username credential")
|
||||
}
|
||||
if req.Password == nil || *req.Password == "" {
|
||||
return errcode.HTTPErrCodeBadRequest.Detail("password is required for a username credential")
|
||||
}
|
||||
case enums.ScmCredentialTypeNone:
|
||||
return errcode.HTTPErrCodeBadRequest.Detail("type none is not a credential, delete the credential instead")
|
||||
default:
|
||||
return errcode.HTTPErrCodeBadRequest.Detail(fmt.Sprintf("invalid credential type %s", req.Type.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureUser3rdParty returns the binding of the provider for the user, creating it from the
|
||||
// account resolved by the credential when the user never signed in with OAuth.
|
||||
func (s *service) ensureUser3rdParty(ctx context.Context, userID string, provider enums.Provider, account *scmAccount) (*models.User3rdParty, error) {
|
||||
user3rdPartyObj, err := s.RepoUser.GetUser3rdPartyByProvider(ctx, userID, provider)
|
||||
if err == nil {
|
||||
return user3rdPartyObj, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
slog.Error("get user 3rdparty by provider failed", "err", err, "provider", provider.String())
|
||||
return nil, errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get user 3rdparty by provider failed: %v", err))
|
||||
}
|
||||
|
||||
if account == nil || account.ID == "" {
|
||||
return nil, errcode.HTTPErrCodeBadRequest.Detail(fmt.Sprintf(
|
||||
"No %s account is linked yet, use a token credential to link it", provider.String()))
|
||||
}
|
||||
|
||||
if existing, err := s.RepoUser.GetUser3rdPartyByAccountID(ctx, provider, account.ID); err == nil {
|
||||
if existing.UserID != userID {
|
||||
slog.Error("account already bound to another user", "err", err, "provider", provider.String())
|
||||
return nil, errcode.HTTPErrCodeConflict.Detail("This account is already bound to another user")
|
||||
}
|
||||
return existing, nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
slog.Error("get user 3rdparty by account id failed", "err", err, "provider", provider.String())
|
||||
return nil, errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get user 3rdparty by account id failed: %v", err))
|
||||
}
|
||||
|
||||
created, err := s.RepoUser.UpsertUser3rdPartyByAccountID(ctx, &models.User3rdParty{
|
||||
ID: uuid.NewV7String(),
|
||||
UserID: userID,
|
||||
Provider: provider,
|
||||
AccountID: &account.ID,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("create user 3rdparty failed", "err", err, "provider", provider.String())
|
||||
return nil, errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Create user 3rdparty failed: %v", err))
|
||||
}
|
||||
if created.UserID != userID {
|
||||
return nil, errcode.HTTPErrCodeConflict.Detail("This account is already bound to another user")
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// publishSync marks the binding as syncing and queues a sync task.
|
||||
func (s *service) publishSync(ctx context.Context, user3rdPartyID string) {
|
||||
err := s.RepoUser.UpdateUser3rdParty(ctx, user3rdPartyID, map[string]any{
|
||||
query.User3rdParty.CrLastUpdateTimestamp.ColumnName().String(): time.Now().UnixMilli(),
|
||||
query.User3rdParty.CrLastUpdateStatus.ColumnName().String(): enums.TaskCommonStatusDoing,
|
||||
query.User3rdParty.CrLastUpdateMessage.ColumnName().String(): "",
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("update user 3rdparty sync status failed", "err", err, "user_3rdparty_id", user3rdPartyID)
|
||||
}
|
||||
err = s.Producer.Produce(ctx, enums.DaemonCodeRepository,
|
||||
api.DaemonCodeRepositoryPayload{User3rdPartyID: user3rdPartyID})
|
||||
if err != nil {
|
||||
slog.Error("publish sync code repository failed", "err", err, "user_3rdparty_id", user3rdPartyID)
|
||||
}
|
||||
}
|
||||
|
||||
// fetchScmAccount resolves the account a token belongs to.
|
||||
func fetchScmAccount(ctx context.Context, provider enums.Provider, baseURL, token string) (*scmAccount, error) {
|
||||
invalid := func(err error) error {
|
||||
slog.Error("validate scm credential failed", "err", err, "provider", provider.String())
|
||||
return errcode.HTTPErrCodeBadRequest.Detail(fmt.Sprintf("Credential is invalid: %v", err))
|
||||
}
|
||||
switch provider {
|
||||
case enums.ProviderGithub:
|
||||
userObj, _, err := github.NewTokenClient(ctx, token).Users.Get(ctx, "")
|
||||
if err != nil {
|
||||
return nil, invalid(err)
|
||||
}
|
||||
return &scmAccount{ID: strconv.FormatInt(userObj.GetID(), 10), Username: userObj.GetLogin()}, nil
|
||||
case enums.ProviderGitlab:
|
||||
client, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL))
|
||||
if err != nil {
|
||||
return nil, invalid(err)
|
||||
}
|
||||
userObj, _, err := client.Users.CurrentUser()
|
||||
if err != nil {
|
||||
return nil, invalid(err)
|
||||
}
|
||||
return &scmAccount{ID: strconv.FormatInt(userObj.ID, 10), Username: userObj.Username}, nil
|
||||
case enums.ProviderGitea:
|
||||
client, err := gitea.NewClient(baseURL, gitea.SetToken(token))
|
||||
if err != nil {
|
||||
return nil, invalid(err)
|
||||
}
|
||||
userObj, _, err := client.GetMyUserInfo()
|
||||
if err != nil {
|
||||
return nil, invalid(err)
|
||||
}
|
||||
return &scmAccount{ID: strconv.FormatInt(userObj.ID, 10), Username: userObj.UserName}, nil
|
||||
}
|
||||
return nil, errcode.HTTPErrCodeBadRequest.Detail(fmt.Sprintf("unsupported scm provider %s", provider.String()))
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package coderepos
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
"github.com/go-sigma/sigma/pkg/config"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
repocoderepomocks "github.com/go-sigma/sigma/pkg/dal/repository/coderepo/mocks"
|
||||
repousermocks "github.com/go-sigma/sigma/pkg/dal/repository/user/mocks"
|
||||
credentialmocks "github.com/go-sigma/sigma/pkg/service/credential/mocks"
|
||||
"github.com/go-sigma/sigma/pkg/utils/ptr"
|
||||
)
|
||||
|
||||
func scmConfig() *config.Configuration {
|
||||
return &config.Configuration{
|
||||
Scm: config.ConfigurationScm{Providers: []config.ConfigurationScmProvider{
|
||||
{Provider: enums.ScmProviderGithub, BaseURL: "https://github.com"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCodeRepositoryCredentialErrors(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
t.Run("account is not linked", func(t *testing.T) {
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByProvider(gomock.Any(), "user-1", enums.ProviderGithub).
|
||||
Return(nil, gorm.ErrRecordNotFound)
|
||||
|
||||
_, err := (&service{Config: scmConfig(), RepoUser: userRepository}).
|
||||
GetCodeRepositoryCredential(t.Context(), "user-1", enums.ProviderGithub)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("credential is not configured", func(t *testing.T) {
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByProvider(gomock.Any(), "user-1", enums.ProviderGithub).
|
||||
Return(&models.User3rdParty{ID: "3rdparty-1"}, nil)
|
||||
codeRepository := repocoderepomocks.NewMockCodeRepositoryRepository(ctrl)
|
||||
codeRepository.EXPECT().
|
||||
GetCloneCredential(gomock.Any(), "3rdparty-1").
|
||||
Return(nil, gorm.ErrRecordNotFound)
|
||||
|
||||
_, err := (&service{Config: scmConfig(), RepoUser: userRepository, RepoCode: codeRepository}).
|
||||
GetCodeRepositoryCredential(t.Context(), "user-1", enums.ProviderGithub)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateCodeRepositoryCredential(t *testing.T) {
|
||||
t.Run("provider is not configured", func(t *testing.T) {
|
||||
svc := &service{Config: &config.Configuration{}}
|
||||
_, err := svc.ValidateCodeRepositoryCredential(t.Context(), api.UpdateCodeRepositoryCredentialRequest{
|
||||
Provider: enums.ProviderGithub,
|
||||
Type: enums.ScmCredentialTypeToken,
|
||||
Token: new("ghp_sigma"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("missing field for the credential type", func(t *testing.T) {
|
||||
svc := &service{Config: scmConfig()}
|
||||
for _, req := range []api.UpdateCodeRepositoryCredentialRequest{
|
||||
{Provider: enums.ProviderGithub, Type: enums.ScmCredentialTypeToken},
|
||||
{Provider: enums.ProviderGithub, Type: enums.ScmCredentialTypeSsh},
|
||||
{Provider: enums.ProviderGithub, Type: enums.ScmCredentialTypeUsername, Username: new("sigma")},
|
||||
{Provider: enums.ProviderGithub, Type: enums.ScmCredentialTypeNone},
|
||||
} {
|
||||
_, err := svc.ValidateCodeRepositoryCredential(t.Context(), req)
|
||||
require.Error(t, err, "type %s must be rejected", req.Type.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ssh credential cannot be verified", func(t *testing.T) {
|
||||
svc := &service{Config: scmConfig()}
|
||||
resp, err := svc.ValidateCodeRepositoryCredential(t.Context(), api.UpdateCodeRepositoryCredentialRequest{
|
||||
Provider: enums.ProviderGithub,
|
||||
Type: enums.ScmCredentialTypeSsh,
|
||||
SshKey: new("-----BEGIN OPENSSH PRIVATE KEY-----"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, resp.Validated)
|
||||
require.Empty(t, resp.AccountID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpsertCodeRepositoryCredentialSealsSecret(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
codeRepository := repocoderepomocks.NewMockCodeRepositoryRepository(ctrl)
|
||||
svcCredential := credentialmocks.NewMockCredentialService(ctrl)
|
||||
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByProvider(gomock.Any(), "user-1", enums.ProviderGithub).
|
||||
Return(&models.User3rdParty{ID: "3rdparty-1", UserID: "user-1", Provider: enums.ProviderGithub}, nil)
|
||||
svcCredential.EXPECT().
|
||||
Seal("-----BEGIN OPENSSH PRIVATE KEY-----").
|
||||
Return("sealed-ssh-key", nil)
|
||||
codeRepository.EXPECT().
|
||||
UpsertCredential(gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(func(_ any, credential *models.CodeRepositoryCloneCredential) error {
|
||||
require.Equal(t, "3rdparty-1", credential.User3rdPartyID)
|
||||
require.Equal(t, enums.ScmCredentialTypeSsh, credential.Type)
|
||||
require.Equal(t, "sealed-ssh-key", ptr.To(credential.SshKey))
|
||||
require.Nil(t, credential.Token)
|
||||
return nil
|
||||
})
|
||||
|
||||
// An SSH credential cannot list repositories, so no sync task is queued here.
|
||||
svc := &service{Config: scmConfig(), RepoUser: userRepository, RepoCode: codeRepository, SvcCredential: svcCredential}
|
||||
require.NoError(t, svc.UpsertCodeRepositoryCredential(t.Context(), "user-1", api.UpdateCodeRepositoryCredentialRequest{
|
||||
Provider: enums.ProviderGithub,
|
||||
Type: enums.ScmCredentialTypeSsh,
|
||||
SshKey: new("-----BEGIN OPENSSH PRIVATE KEY-----"),
|
||||
}))
|
||||
}
|
||||
|
||||
func TestEnsureUser3rdPartyRejectsForeignAccount(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByProvider(gomock.Any(), "user-1", enums.ProviderGithub).
|
||||
Return(nil, gorm.ErrRecordNotFound)
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByAccountID(gomock.Any(), enums.ProviderGithub, "account-1").
|
||||
Return(&models.User3rdParty{ID: "3rdparty-1", UserID: "another-user"}, nil)
|
||||
|
||||
_, err := (&service{Config: scmConfig(), RepoUser: userRepository}).
|
||||
ensureUser3rdParty(t.Context(), "user-1", enums.ProviderGithub, &scmAccount{ID: "account-1"})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEnsureUser3rdPartyRequiresAccountForNewBinding(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByProvider(gomock.Any(), "user-1", enums.ProviderGithub).
|
||||
Return(nil, gorm.ErrRecordNotFound)
|
||||
|
||||
_, err := (&service{Config: scmConfig(), RepoUser: userRepository}).
|
||||
ensureUser3rdParty(t.Context(), "user-1", enums.ProviderGithub, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEnsureUser3rdPartyCreatesBinding(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByProvider(gomock.Any(), "user-1", enums.ProviderGithub).
|
||||
Return(nil, gorm.ErrRecordNotFound)
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByAccountID(gomock.Any(), enums.ProviderGithub, "account-1").
|
||||
Return(nil, gorm.ErrRecordNotFound)
|
||||
userRepository.EXPECT().
|
||||
UpsertUser3rdPartyByAccountID(gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(func(_ any, user3rdParty *models.User3rdParty) (*models.User3rdParty, error) {
|
||||
require.Equal(t, "user-1", user3rdParty.UserID)
|
||||
require.Equal(t, "account-1", ptr.To(user3rdParty.AccountID))
|
||||
return user3rdParty, nil
|
||||
})
|
||||
|
||||
got, err := (&service{Config: scmConfig(), RepoUser: userRepository}).
|
||||
ensureUser3rdParty(t.Context(), "user-1", enums.ProviderGithub, &scmAccount{ID: "account-1"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "user-1", got.UserID)
|
||||
}
|
||||
|
||||
func TestDeleteCodeRepositoryCredential(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
codeRepository := repocoderepomocks.NewMockCodeRepositoryRepository(ctrl)
|
||||
|
||||
svc := &service{Config: scmConfig(), RepoUser: userRepository, RepoCode: codeRepository}
|
||||
|
||||
// A provider that is not configured is rejected before touching the database.
|
||||
require.Error(t, svc.DeleteCodeRepositoryCredential(t.Context(), "user-1", enums.ProviderGitlab))
|
||||
|
||||
userRepository.EXPECT().
|
||||
GetUser3rdPartyByProvider(gomock.Any(), "user-1", enums.ProviderGithub).
|
||||
Return(&models.User3rdParty{ID: "3rdparty-1"}, nil)
|
||||
codeRepository.EXPECT().
|
||||
DeleteCredential(gomock.Any(), "3rdparty-1").
|
||||
Return(nil)
|
||||
|
||||
require.NoError(t, svc.DeleteCodeRepositoryCredential(t.Context(), "user-1", enums.ProviderGithub))
|
||||
}
|
||||
|
||||
func TestListCodeRepositoryProviders(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
userRepository := repousermocks.NewMockUserRepository(ctrl)
|
||||
codeRepository := repocoderepomocks.NewMockCodeRepositoryRepository(ctrl)
|
||||
|
||||
cfg := &config.Configuration{
|
||||
Auth: config.ConfigurationAuth{Oauth2: config.ConfigurationAuthOauth2{
|
||||
Github: config.ConfigurationAuthOauth2Github{Enabled: true},
|
||||
}},
|
||||
Scm: config.ConfigurationScm{Providers: []config.ConfigurationScmProvider{
|
||||
{Provider: enums.ScmProviderGithub, BaseURL: "https://github.com"},
|
||||
{Provider: enums.ScmProviderGitea, BaseURL: "https://gitea.example.com"},
|
||||
}},
|
||||
}
|
||||
|
||||
userRepository.EXPECT().
|
||||
ListUser3rdParty(gomock.Any(), "user-1").
|
||||
Return([]*models.User3rdParty{{
|
||||
ID: "3rdparty-1",
|
||||
UserID: "user-1",
|
||||
Provider: enums.ProviderGithub,
|
||||
AccountID: new("account-1"),
|
||||
CrLastUpdateStatus: enums.TaskCommonStatusFailed,
|
||||
}}, nil)
|
||||
codeRepository.EXPECT().
|
||||
GetCloneCredential(gomock.Any(), "3rdparty-1").
|
||||
Return(&models.CodeRepositoryCloneCredential{Type: enums.ScmCredentialTypeToken}, nil)
|
||||
|
||||
providers, err := (&service{Config: cfg, RepoUser: userRepository, RepoCode: codeRepository}).
|
||||
ListCodeRepositoryProviders(t.Context(), "user-1")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, providers, 2)
|
||||
|
||||
// GitHub is linked and has a credential.
|
||||
require.Equal(t, enums.ProviderGithub, providers[0].Provider)
|
||||
require.Equal(t, "GitHub", providers[0].DisplayName)
|
||||
require.True(t, providers[0].OauthEnabled)
|
||||
require.True(t, providers[0].Linked)
|
||||
require.True(t, providers[0].HasCredential)
|
||||
require.Equal(t, enums.ScmCredentialTypeToken, providers[0].CredentialType)
|
||||
require.Equal(t, enums.TaskCommonStatusFailed, providers[0].CrLastUpdateStatus)
|
||||
|
||||
// Gitea is configured but neither linked nor configured, and it is not an OAuth provider.
|
||||
require.Equal(t, enums.ProviderGitea, providers[1].Provider)
|
||||
require.False(t, providers[1].OauthEnabled)
|
||||
require.False(t, providers[1].Linked)
|
||||
require.False(t, providers[1].HasCredential)
|
||||
require.Equal(t, enums.ScmCredentialTypeNone, providers[1].CredentialType)
|
||||
}
|
||||
@@ -43,6 +43,20 @@ func (m *MockCodeRepositoryService) EXPECT() *MockCodeRepositoryServiceMockRecor
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// DeleteCodeRepositoryCredential mocks base method.
|
||||
func (m *MockCodeRepositoryService) DeleteCodeRepositoryCredential(ctx context.Context, userID string, provider enums.Provider) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteCodeRepositoryCredential", ctx, userID, provider)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteCodeRepositoryCredential indicates an expected call of DeleteCodeRepositoryCredential.
|
||||
func (mr *MockCodeRepositoryServiceMockRecorder) DeleteCodeRepositoryCredential(ctx, userID, provider any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCodeRepositoryCredential", reflect.TypeOf((*MockCodeRepositoryService)(nil).DeleteCodeRepositoryCredential), ctx, userID, provider)
|
||||
}
|
||||
|
||||
// GetCodeRepository mocks base method.
|
||||
func (m *MockCodeRepositoryService) GetCodeRepository(ctx context.Context, userID string, provider enums.Provider, id string) (*models.CodeRepository, []*models.CodeRepositoryOwner, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -74,6 +88,21 @@ func (mr *MockCodeRepositoryServiceMockRecorder) GetCodeRepositoryBranch(ctx, co
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCodeRepositoryBranch", reflect.TypeOf((*MockCodeRepositoryService)(nil).GetCodeRepositoryBranch), ctx, codeRepositoryID, name)
|
||||
}
|
||||
|
||||
// GetCodeRepositoryCredential mocks base method.
|
||||
func (m *MockCodeRepositoryService) GetCodeRepositoryCredential(ctx context.Context, userID string, provider enums.Provider) (*models.CodeRepositoryCloneCredential, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCodeRepositoryCredential", ctx, userID, provider)
|
||||
ret0, _ := ret[0].(*models.CodeRepositoryCloneCredential)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCodeRepositoryCredential indicates an expected call of GetCodeRepositoryCredential.
|
||||
func (mr *MockCodeRepositoryServiceMockRecorder) GetCodeRepositoryCredential(ctx, userID, provider any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCodeRepositoryCredential", reflect.TypeOf((*MockCodeRepositoryService)(nil).GetCodeRepositoryCredential), ctx, userID, provider)
|
||||
}
|
||||
|
||||
// GetCodeRepositoryUser3rdParty mocks base method.
|
||||
func (m *MockCodeRepositoryService) GetCodeRepositoryUser3rdParty(ctx context.Context, userID string, provider enums.Provider) (*models.User3rdParty, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -139,10 +168,10 @@ func (mr *MockCodeRepositoryServiceMockRecorder) ListCodeRepositoryOwners(ctx, u
|
||||
}
|
||||
|
||||
// ListCodeRepositoryProviders mocks base method.
|
||||
func (m *MockCodeRepositoryService) ListCodeRepositoryProviders(ctx context.Context, userID string) ([]*models.User3rdParty, error) {
|
||||
func (m *MockCodeRepositoryService) ListCodeRepositoryProviders(ctx context.Context, userID string) ([]api.ListCodeRepositoryProvidersResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListCodeRepositoryProviders", ctx, userID)
|
||||
ret0, _ := ret[0].([]*models.User3rdParty)
|
||||
ret0, _ := ret[0].([]api.ListCodeRepositoryProvidersResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -166,3 +195,32 @@ func (mr *MockCodeRepositoryServiceMockRecorder) ResyncCodeRepositories(ctx, use
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResyncCodeRepositories", reflect.TypeOf((*MockCodeRepositoryService)(nil).ResyncCodeRepositories), ctx, userID, provider)
|
||||
}
|
||||
|
||||
// UpsertCodeRepositoryCredential mocks base method.
|
||||
func (m *MockCodeRepositoryService) UpsertCodeRepositoryCredential(ctx context.Context, userID string, req api.UpdateCodeRepositoryCredentialRequest) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpsertCodeRepositoryCredential", ctx, userID, req)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpsertCodeRepositoryCredential indicates an expected call of UpsertCodeRepositoryCredential.
|
||||
func (mr *MockCodeRepositoryServiceMockRecorder) UpsertCodeRepositoryCredential(ctx, userID, req any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertCodeRepositoryCredential", reflect.TypeOf((*MockCodeRepositoryService)(nil).UpsertCodeRepositoryCredential), ctx, userID, req)
|
||||
}
|
||||
|
||||
// ValidateCodeRepositoryCredential mocks base method.
|
||||
func (m *MockCodeRepositoryService) ValidateCodeRepositoryCredential(ctx context.Context, req api.UpdateCodeRepositoryCredentialRequest) (*api.ValidateCodeRepositoryCredentialResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ValidateCodeRepositoryCredential", ctx, req)
|
||||
ret0, _ := ret[0].(*api.ValidateCodeRepositoryCredentialResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ValidateCodeRepositoryCredential indicates an expected call of ValidateCodeRepositoryCredential.
|
||||
func (mr *MockCodeRepositoryServiceMockRecorder) ValidateCodeRepositoryCredential(ctx, req any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateCodeRepositoryCredential", reflect.TypeOf((*MockCodeRepositoryService)(nil).ValidateCodeRepositoryCredential), ctx, req)
|
||||
}
|
||||
@@ -658,6 +658,29 @@ definitions:
|
||||
example: "2006-01-02 15:04:05"
|
||||
type: string
|
||||
type: object
|
||||
api.GetCodeRepositoryCredentialResponse:
|
||||
properties:
|
||||
created_at:
|
||||
example: "2006-01-02 15:04:05"
|
||||
type: string
|
||||
has_password:
|
||||
type: boolean
|
||||
has_ssh_key:
|
||||
type: boolean
|
||||
token:
|
||||
example: ghp_****abcd
|
||||
type: string
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/enums.ScmCredentialType'
|
||||
example: token
|
||||
updated_at:
|
||||
example: "2006-01-02 15:04:05"
|
||||
type: string
|
||||
username:
|
||||
example: sigma
|
||||
type: string
|
||||
type: object
|
||||
api.GetCodeRepositoryUser3rdPartyResponse:
|
||||
properties:
|
||||
account_id:
|
||||
@@ -829,6 +852,30 @@ definitions:
|
||||
type: object
|
||||
api.ListCodeRepositoryProvidersResponse:
|
||||
properties:
|
||||
account_id:
|
||||
type: string
|
||||
base_url:
|
||||
example: https://github.com
|
||||
type: string
|
||||
cr_last_update_message:
|
||||
type: string
|
||||
cr_last_update_status:
|
||||
$ref: '#/definitions/enums.TaskCommonStatus'
|
||||
cr_last_update_timestamp:
|
||||
type: string
|
||||
credential_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/enums.ScmCredentialType'
|
||||
example: none
|
||||
display_name:
|
||||
example: GitHub
|
||||
type: string
|
||||
has_credential:
|
||||
type: boolean
|
||||
linked:
|
||||
type: boolean
|
||||
oauth_enabled:
|
||||
type: boolean
|
||||
provider:
|
||||
allOf:
|
||||
- $ref: '#/definitions/enums.Provider'
|
||||
@@ -1536,6 +1583,34 @@ definitions:
|
||||
required:
|
||||
- buildkit_platforms
|
||||
type: object
|
||||
api.UpdateCodeRepositoryCredentialRequest:
|
||||
properties:
|
||||
password:
|
||||
example: sigma
|
||||
maxLength: 512
|
||||
type: string
|
||||
provider:
|
||||
$ref: '#/definitions/enums.Provider'
|
||||
ssh_key:
|
||||
example: '-----BEGIN OPENSSH PRIVATE KEY-----'
|
||||
maxLength: 8192
|
||||
type: string
|
||||
token:
|
||||
example: ghp_xxx
|
||||
maxLength: 1024
|
||||
type: string
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/enums.ScmCredentialType'
|
||||
example: token
|
||||
username:
|
||||
example: sigma
|
||||
maxLength: 256
|
||||
type: string
|
||||
required:
|
||||
- provider
|
||||
- type
|
||||
type: object
|
||||
api.UpdateGcArtifactRuleRequest:
|
||||
properties:
|
||||
cron_enabled:
|
||||
@@ -1684,6 +1759,29 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
api.ValidateCodeRepositoryCredentialResponse:
|
||||
properties:
|
||||
account_id:
|
||||
example: "1234567"
|
||||
type: string
|
||||
provider:
|
||||
allOf:
|
||||
- $ref: '#/definitions/enums.Provider'
|
||||
example: github
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/enums.ScmCredentialType'
|
||||
example: token
|
||||
username:
|
||||
example: sigma
|
||||
type: string
|
||||
validated:
|
||||
description: |-
|
||||
Validated reports whether the credential was verified against the provider API.
|
||||
SSH keys and username/password credentials are only usable for cloning, so they
|
||||
cannot be verified.
|
||||
type: boolean
|
||||
type: object
|
||||
api.ValidateCronRequest:
|
||||
properties:
|
||||
cron:
|
||||
@@ -2119,6 +2217,136 @@ paths:
|
||||
summary: List code repositories
|
||||
tags:
|
||||
- CodeRepository
|
||||
/coderepos/{provider}/credential:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: Delete the SCM credential of the provider
|
||||
in: path
|
||||
name: provider
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Delete code repository credential
|
||||
tags:
|
||||
- CodeRepository
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: Get the SCM credential of the provider
|
||||
in: path
|
||||
name: provider
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/api.GetCodeRepositoryCredentialResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Get code repository credential
|
||||
tags:
|
||||
- CodeRepository
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: Update the SCM credential of the provider
|
||||
in: path
|
||||
name: provider
|
||||
required: true
|
||||
type: string
|
||||
- description: SCM credential
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api.UpdateCodeRepositoryCredentialRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
"409":
|
||||
description: Conflict
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Update code repository credential
|
||||
tags:
|
||||
- CodeRepository
|
||||
/coderepos/{provider}/credential/validate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: Validate the SCM credential of the provider
|
||||
in: path
|
||||
name: provider
|
||||
required: true
|
||||
type: string
|
||||
- description: SCM credential
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api.UpdateCodeRepositoryCredentialRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/api.ValidateCodeRepositoryCredentialResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/errcode.ErrCode'
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Validate code repository credential
|
||||
tags:
|
||||
- CodeRepository
|
||||
/coderepos/{provider}/owners:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
Reference in new issue
Block a user