mirror of
https://github.com/harness/drone.git
synced 2025-05-17 09:30:00 +08:00
[code-1946] initial work on ssh server (#2075)
This commit is contained in:
parent
ef18a7aff2
commit
2f8900e463
5
.gitignore
vendored
5
.gitignore
vendored
@ -19,4 +19,7 @@ web/cypress/node_modules
|
|||||||
# ignore any executables we build
|
# ignore any executables we build
|
||||||
/gitness
|
/gitness
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
ssh/gitness.rsa
|
||||||
|
ssh/gitness.rsa.pub
|
@ -147,6 +147,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea
|
|||||||
|
|
||||||
// backfil GitURL
|
// backfil GitURL
|
||||||
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
||||||
|
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
|
||||||
|
|
||||||
repoOutput := &RepositoryOutput{
|
repoOutput := &RepositoryOutput{
|
||||||
Repository: *repo,
|
Repository: *repo,
|
||||||
|
@ -36,6 +36,7 @@ func (c *Controller) Find(ctx context.Context, session *auth.Session, repoRef st
|
|||||||
|
|
||||||
// backfill clone url
|
// backfill clone url
|
||||||
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
||||||
|
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
|
||||||
|
|
||||||
return GetRepoOutput(ctx, c.publicAccess, repo)
|
return GetRepoOutput(ctx, c.publicAccess, repo)
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,11 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/harness/gitness/app/api/controller"
|
"github.com/harness/gitness/app/api/controller"
|
||||||
"github.com/harness/gitness/app/auth"
|
"github.com/harness/gitness/app/auth"
|
||||||
"github.com/harness/gitness/git"
|
"github.com/harness/gitness/git"
|
||||||
|
"github.com/harness/gitness/git/api"
|
||||||
"github.com/harness/gitness/types/enum"
|
"github.com/harness/gitness/types/enum"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,15 +30,12 @@ func (c *Controller) GitServicePack(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
session *auth.Session,
|
session *auth.Session,
|
||||||
repoRef string,
|
repoRef string,
|
||||||
service enum.GitServiceType,
|
options api.ServicePackOptions,
|
||||||
gitProtocol string,
|
|
||||||
r io.Reader,
|
|
||||||
w io.Writer,
|
|
||||||
) error {
|
) error {
|
||||||
isWriteOperation := false
|
isWriteOperation := false
|
||||||
permission := enum.PermissionRepoView
|
permission := enum.PermissionRepoView
|
||||||
// receive-pack is the server receiving data - aka the client pushing data.
|
// receive-pack is the server receiving data - aka the client pushing data.
|
||||||
if service == enum.GitServiceTypeReceivePack {
|
if options.Service == enum.GitServiceTypeReceivePack {
|
||||||
isWriteOperation = true
|
isWriteOperation = true
|
||||||
permission = enum.PermissionRepoPush
|
permission = enum.PermissionRepoPush
|
||||||
}
|
}
|
||||||
@ -50,10 +47,7 @@ func (c *Controller) GitServicePack(
|
|||||||
|
|
||||||
params := &git.ServicePackParams{
|
params := &git.ServicePackParams{
|
||||||
// TODO: git shouldn't take a random string here, but instead have accepted enum values.
|
// TODO: git shouldn't take a random string here, but instead have accepted enum values.
|
||||||
Service: string(service),
|
ServicePackOptions: options,
|
||||||
Data: r,
|
|
||||||
Options: nil,
|
|
||||||
GitProtocol: gitProtocol,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup read/writeparams depending on whether it's a write operation
|
// setup read/writeparams depending on whether it's a write operation
|
||||||
@ -69,8 +63,8 @@ func (c *Controller) GitServicePack(
|
|||||||
params.ReadParams = &readParams
|
params.ReadParams = &readParams
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.git.ServicePack(ctx, w, params); err != nil {
|
if err = c.git.ServicePack(ctx, params); err != nil {
|
||||||
return fmt.Errorf("failed service pack operation %q on git: %w", service, err)
|
return fmt.Errorf("failed service pack operation %q on git: %w", options.Service, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -98,6 +98,7 @@ func (c *Controller) Import(ctx context.Context, session *auth.Session, in *Impo
|
|||||||
}
|
}
|
||||||
|
|
||||||
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
||||||
|
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
|
||||||
|
|
||||||
err = c.auditService.Log(ctx,
|
err = c.auditService.Log(ctx,
|
||||||
session.Principal,
|
session.Principal,
|
||||||
|
@ -131,6 +131,7 @@ func (c *Controller) Move(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
||||||
|
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
|
||||||
|
|
||||||
return GetRepoOutput(ctx, c.publicAccess, repo)
|
return GetRepoOutput(ctx, c.publicAccess, repo)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ func (c *Controller) Update(ctx context.Context,
|
|||||||
|
|
||||||
// backfill repo url
|
// backfill repo url
|
||||||
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
||||||
|
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
|
||||||
|
|
||||||
return GetRepoOutput(ctx, c.publicAccess, repo)
|
return GetRepoOutput(ctx, c.publicAccess, repo)
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,7 @@ func (c *Controller) UpdatePublicAccess(ctx context.Context,
|
|||||||
|
|
||||||
// backfill GitURL
|
// backfill GitURL
|
||||||
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
||||||
|
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
|
||||||
|
|
||||||
err = c.auditService.Log(ctx,
|
err = c.auditService.Log(ctx,
|
||||||
session.Principal,
|
session.Principal,
|
||||||
|
@ -82,6 +82,7 @@ func (c *Controller) ListRepositoriesNoAuth(
|
|||||||
for _, repo := range repos {
|
for _, repo := range repos {
|
||||||
// backfill URLs
|
// backfill URLs
|
||||||
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
|
||||||
|
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
|
||||||
|
|
||||||
repoOut, err := repoCtrl.GetRepoOutput(ctx, c.publicAccess, repo)
|
repoOut, err := repoCtrl.GetRepoOutput(ctx, c.publicAccess, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/harness/gitness/app/api/request"
|
"github.com/harness/gitness/app/api/request"
|
||||||
"github.com/harness/gitness/app/auth"
|
"github.com/harness/gitness/app/auth"
|
||||||
"github.com/harness/gitness/app/url"
|
"github.com/harness/gitness/app/url"
|
||||||
|
"github.com/harness/gitness/git/api"
|
||||||
"github.com/harness/gitness/types/enum"
|
"github.com/harness/gitness/types/enum"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@ -71,7 +72,13 @@ func HandleGitServicePack(
|
|||||||
render.NoCache(w)
|
render.NoCache(w)
|
||||||
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
|
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
|
||||||
|
|
||||||
err = repoCtrl.GitServicePack(ctx, session, repoRef, service, gitProtocol, dataReader, w)
|
err = repoCtrl.GitServicePack(ctx, session, repoRef, api.ServicePackOptions{
|
||||||
|
Service: service,
|
||||||
|
StatelessRPC: true,
|
||||||
|
Stdout: w,
|
||||||
|
Stdin: dataReader,
|
||||||
|
Protocol: gitProtocol,
|
||||||
|
})
|
||||||
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
||||||
renderBasicAuth(w, urlProvider)
|
renderBasicAuth(w, urlProvider)
|
||||||
return
|
return
|
||||||
|
@ -87,6 +87,7 @@ type RepositoryInfo struct {
|
|||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
DefaultBranch string `json:"default_branch"`
|
DefaultBranch string `json:"default_branch"`
|
||||||
GitURL string `json:"git_url"`
|
GitURL string `json:"git_url"`
|
||||||
|
GitSSHURL string `json:"git_ssh_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO [CODE-1363]: remove after identifier migration.
|
// TODO [CODE-1363]: remove after identifier migration.
|
||||||
@ -110,6 +111,7 @@ func repositoryInfoFrom(repo *types.Repository, urlProvider url.Provider) Reposi
|
|||||||
Identifier: repo.Identifier,
|
Identifier: repo.Identifier,
|
||||||
DefaultBranch: repo.DefaultBranch,
|
DefaultBranch: repo.DefaultBranch,
|
||||||
GitURL: urlProvider.GenerateGITCloneURL(repo.Path),
|
GitURL: urlProvider.GenerateGITCloneURL(repo.Path),
|
||||||
|
GitSSHURL: urlProvider.GenerateGITCloneSSHURL(repo.Path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,10 @@ type Provider interface {
|
|||||||
// NOTE: url is guaranteed to not have any trailing '/'.
|
// NOTE: url is guaranteed to not have any trailing '/'.
|
||||||
GenerateGITCloneURL(repoPath string) string
|
GenerateGITCloneURL(repoPath string) string
|
||||||
|
|
||||||
|
// GenerateGITCloneSSHURL generates the public git clone URL for the provided repo path.
|
||||||
|
// NOTE: url is guaranteed to not have any trailing '/'.
|
||||||
|
GenerateGITCloneSSHURL(repoPath string) string
|
||||||
|
|
||||||
// GenerateUIRepoURL returns the url for the UI screen of a repository.
|
// GenerateUIRepoURL returns the url for the UI screen of a repository.
|
||||||
GenerateUIRepoURL(repoPath string) string
|
GenerateUIRepoURL(repoPath string) string
|
||||||
|
|
||||||
@ -87,6 +91,9 @@ type provider struct {
|
|||||||
// NOTE: we store it as url.URL so we can derive clone URLS without errors.
|
// NOTE: we store it as url.URL so we can derive clone URLS without errors.
|
||||||
gitURL *url.URL
|
gitURL *url.URL
|
||||||
|
|
||||||
|
SSHDefaultUser string
|
||||||
|
gitSSHURL *url.URL
|
||||||
|
|
||||||
// uiURL stores the raw URL to the ui endpoints.
|
// uiURL stores the raw URL to the ui endpoints.
|
||||||
uiURL *url.URL
|
uiURL *url.URL
|
||||||
}
|
}
|
||||||
@ -96,6 +103,8 @@ func NewProvider(
|
|||||||
containerURLRaw string,
|
containerURLRaw string,
|
||||||
apiURLRaw string,
|
apiURLRaw string,
|
||||||
gitURLRaw,
|
gitURLRaw,
|
||||||
|
gitSSHURLRaw string,
|
||||||
|
sshDefaultUser string,
|
||||||
uiURLRaw string,
|
uiURLRaw string,
|
||||||
) (Provider, error) {
|
) (Provider, error) {
|
||||||
// remove trailing '/' to make usage easier
|
// remove trailing '/' to make usage easier
|
||||||
@ -103,6 +112,7 @@ func NewProvider(
|
|||||||
containerURLRaw = strings.TrimRight(containerURLRaw, "/")
|
containerURLRaw = strings.TrimRight(containerURLRaw, "/")
|
||||||
apiURLRaw = strings.TrimRight(apiURLRaw, "/")
|
apiURLRaw = strings.TrimRight(apiURLRaw, "/")
|
||||||
gitURLRaw = strings.TrimRight(gitURLRaw, "/")
|
gitURLRaw = strings.TrimRight(gitURLRaw, "/")
|
||||||
|
gitSSHURLRaw = strings.TrimRight(gitSSHURLRaw, "/")
|
||||||
uiURLRaw = strings.TrimRight(uiURLRaw, "/")
|
uiURLRaw = strings.TrimRight(uiURLRaw, "/")
|
||||||
|
|
||||||
internalURL, err := url.Parse(internalURLRaw)
|
internalURL, err := url.Parse(internalURLRaw)
|
||||||
@ -125,17 +135,24 @@ func NewProvider(
|
|||||||
return nil, fmt.Errorf("provided gitURLRaw '%s' is invalid: %w", gitURLRaw, err)
|
return nil, fmt.Errorf("provided gitURLRaw '%s' is invalid: %w", gitURLRaw, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitSSHURL, err := url.Parse(gitSSHURLRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("provided gitSSHURLRaw '%s' is invalid: %w", gitSSHURLRaw, err)
|
||||||
|
}
|
||||||
|
|
||||||
uiURL, err := url.Parse(uiURLRaw)
|
uiURL, err := url.Parse(uiURLRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("provided uiURLRaw '%s' is invalid: %w", uiURLRaw, err)
|
return nil, fmt.Errorf("provided uiURLRaw '%s' is invalid: %w", uiURLRaw, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &provider{
|
return &provider{
|
||||||
internalURL: internalURL,
|
internalURL: internalURL,
|
||||||
containerURL: containerURL,
|
containerURL: containerURL,
|
||||||
apiURL: apiURL,
|
apiURL: apiURL,
|
||||||
gitURL: gitURL,
|
gitURL: gitURL,
|
||||||
uiURL: uiURL,
|
gitSSHURL: gitSSHURL,
|
||||||
|
SSHDefaultUser: sshDefaultUser,
|
||||||
|
uiURL: uiURL,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,6 +178,15 @@ func (p *provider) GenerateGITCloneURL(repoPath string) string {
|
|||||||
return p.gitURL.JoinPath(repoPath).String()
|
return p.gitURL.JoinPath(repoPath).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *provider) GenerateGITCloneSSHURL(repoPath string) string {
|
||||||
|
repoPath = path.Clean(repoPath)
|
||||||
|
if !strings.HasSuffix(repoPath, GITSuffix) {
|
||||||
|
repoPath += GITSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s@%s:%s", p.SSHDefaultUser, p.gitSSHURL.String(), repoPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *provider) GenerateUIBuildURL(repoPath, pipelineIdentifier string, seqNumber int64) string {
|
func (p *provider) GenerateUIBuildURL(repoPath, pipelineIdentifier string, seqNumber int64) string {
|
||||||
return p.uiURL.JoinPath(repoPath, "pipelines",
|
return p.uiURL.JoinPath(repoPath, "pipelines",
|
||||||
pipelineIdentifier, "execution", strconv.Itoa(int(seqNumber))).String()
|
pipelineIdentifier, "execution", strconv.Itoa(int(seqNumber))).String()
|
||||||
|
@ -29,6 +29,8 @@ func ProvideURLProvider(config *types.Config) (Provider, error) {
|
|||||||
config.URL.Container,
|
config.URL.Container,
|
||||||
config.URL.API,
|
config.URL.API,
|
||||||
config.URL.Git,
|
config.URL.Git,
|
||||||
|
config.URL.GitSSH,
|
||||||
|
config.SSH.DefaultUser,
|
||||||
config.URL.UI,
|
config.URL.UI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,13 @@ func (c *command) run(*kingpin.ParseContext) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.SSH.Enable {
|
||||||
|
g.Go(func() error {
|
||||||
|
log.Err(system.sshServer.ListenAndServe()).Send()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Int("port", config.Server.HTTP.Port).
|
Int("port", config.Server.HTTP.Port).
|
||||||
Str("revision", version.GitCommit).
|
Str("revision", version.GitCommit).
|
||||||
@ -152,6 +159,12 @@ func (c *command) run(*kingpin.ParseContext) error {
|
|||||||
log.Err(sErr).Msg("failed to shutdown http server gracefully")
|
log.Err(sErr).Msg("failed to shutdown http server gracefully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.SSH.Enable {
|
||||||
|
if err := system.sshServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Err(err).Msg("failed to shutdown ssh server gracefully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
system.services.JobScheduler.WaitJobsDone(shutdownCtx)
|
system.services.JobScheduler.WaitJobsDone(shutdownCtx)
|
||||||
|
|
||||||
log.Info().Msg("wait for subroutines to complete")
|
log.Info().Msg("wait for subroutines to complete")
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/harness/gitness/app/pipeline/resolver"
|
"github.com/harness/gitness/app/pipeline/resolver"
|
||||||
"github.com/harness/gitness/app/server"
|
"github.com/harness/gitness/app/server"
|
||||||
"github.com/harness/gitness/app/services"
|
"github.com/harness/gitness/app/services"
|
||||||
|
"github.com/harness/gitness/ssh"
|
||||||
|
|
||||||
"github.com/drone/runner-go/poller"
|
"github.com/drone/runner-go/poller"
|
||||||
)
|
)
|
||||||
@ -27,17 +28,25 @@ import (
|
|||||||
type System struct {
|
type System struct {
|
||||||
bootstrap bootstrap.Bootstrap
|
bootstrap bootstrap.Bootstrap
|
||||||
server *server.Server
|
server *server.Server
|
||||||
|
sshServer *ssh.Server
|
||||||
resolverManager *resolver.Manager
|
resolverManager *resolver.Manager
|
||||||
poller *poller.Poller
|
poller *poller.Poller
|
||||||
services services.Services
|
services services.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSystem returns a new system structure.
|
// NewSystem returns a new system structure.
|
||||||
func NewSystem(bootstrap bootstrap.Bootstrap, server *server.Server, poller *poller.Poller,
|
func NewSystem(
|
||||||
resolverManager *resolver.Manager, services services.Services) *System {
|
bootstrap bootstrap.Bootstrap,
|
||||||
|
server *server.Server,
|
||||||
|
sshServer *ssh.Server,
|
||||||
|
poller *poller.Poller,
|
||||||
|
resolverManager *resolver.Manager,
|
||||||
|
services services.Services,
|
||||||
|
) *System {
|
||||||
return &System{
|
return &System{
|
||||||
bootstrap: bootstrap,
|
bootstrap: bootstrap,
|
||||||
server: server,
|
server: server,
|
||||||
|
sshServer: sshServer,
|
||||||
poller: poller,
|
poller: poller,
|
||||||
resolverManager: resolverManager,
|
resolverManager: resolverManager,
|
||||||
services: services,
|
services: services,
|
||||||
|
@ -64,6 +64,7 @@ import (
|
|||||||
"github.com/harness/gitness/app/services/notification/mailer"
|
"github.com/harness/gitness/app/services/notification/mailer"
|
||||||
"github.com/harness/gitness/app/services/protection"
|
"github.com/harness/gitness/app/services/protection"
|
||||||
"github.com/harness/gitness/app/services/publicaccess"
|
"github.com/harness/gitness/app/services/publicaccess"
|
||||||
|
"github.com/harness/gitness/app/services/publickey"
|
||||||
pullreqservice "github.com/harness/gitness/app/services/pullreq"
|
pullreqservice "github.com/harness/gitness/app/services/pullreq"
|
||||||
reposervice "github.com/harness/gitness/app/services/repo"
|
reposervice "github.com/harness/gitness/app/services/repo"
|
||||||
"github.com/harness/gitness/app/services/settings"
|
"github.com/harness/gitness/app/services/settings"
|
||||||
@ -88,6 +89,7 @@ import (
|
|||||||
"github.com/harness/gitness/livelog"
|
"github.com/harness/gitness/livelog"
|
||||||
"github.com/harness/gitness/lock"
|
"github.com/harness/gitness/lock"
|
||||||
"github.com/harness/gitness/pubsub"
|
"github.com/harness/gitness/pubsub"
|
||||||
|
"github.com/harness/gitness/ssh"
|
||||||
"github.com/harness/gitness/store/database/dbtx"
|
"github.com/harness/gitness/store/database/dbtx"
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
"github.com/harness/gitness/types/check"
|
"github.com/harness/gitness/types/check"
|
||||||
@ -193,6 +195,8 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
|
|||||||
openapi.WireSet,
|
openapi.WireSet,
|
||||||
repo.ProvideRepoCheck,
|
repo.ProvideRepoCheck,
|
||||||
audit.WireSet,
|
audit.WireSet,
|
||||||
|
ssh.WireSet,
|
||||||
|
publickey.WireSet,
|
||||||
)
|
)
|
||||||
return &cliserver.System{}, nil
|
return &cliserver.System{}, nil
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ import (
|
|||||||
"github.com/harness/gitness/app/services/notification/mailer"
|
"github.com/harness/gitness/app/services/notification/mailer"
|
||||||
"github.com/harness/gitness/app/services/protection"
|
"github.com/harness/gitness/app/services/protection"
|
||||||
"github.com/harness/gitness/app/services/publicaccess"
|
"github.com/harness/gitness/app/services/publicaccess"
|
||||||
|
"github.com/harness/gitness/app/services/publickey"
|
||||||
"github.com/harness/gitness/app/services/pullreq"
|
"github.com/harness/gitness/app/services/pullreq"
|
||||||
repo2 "github.com/harness/gitness/app/services/repo"
|
repo2 "github.com/harness/gitness/app/services/repo"
|
||||||
"github.com/harness/gitness/app/services/settings"
|
"github.com/harness/gitness/app/services/settings"
|
||||||
@ -87,6 +88,7 @@ import (
|
|||||||
"github.com/harness/gitness/livelog"
|
"github.com/harness/gitness/livelog"
|
||||||
"github.com/harness/gitness/lock"
|
"github.com/harness/gitness/lock"
|
||||||
"github.com/harness/gitness/pubsub"
|
"github.com/harness/gitness/pubsub"
|
||||||
|
"github.com/harness/gitness/ssh"
|
||||||
"github.com/harness/gitness/store/database/dbtx"
|
"github.com/harness/gitness/store/database/dbtx"
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
"github.com/harness/gitness/types/check"
|
"github.com/harness/gitness/types/check"
|
||||||
@ -309,6 +311,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||||||
webHandler := router.ProvideWebHandler(config, openapiService)
|
webHandler := router.ProvideWebHandler(config, openapiService)
|
||||||
routerRouter := router.ProvideRouter(apiHandler, gitHandler, webHandler, provider)
|
routerRouter := router.ProvideRouter(apiHandler, gitHandler, webHandler, provider)
|
||||||
serverServer := server2.ProvideServer(config, routerRouter)
|
serverServer := server2.ProvideServer(config, routerRouter)
|
||||||
|
publickeyService := publickey.ProvidePublicKey(publicKeyStore, principalInfoCache)
|
||||||
|
sshServer := ssh.ProvideServer(config, publickeyService, repoController)
|
||||||
executionManager := manager.ProvideExecutionManager(config, executionStore, pipelineStore, provider, streamer, fileService, converterService, logStore, logStream, checkStore, repoStore, schedulerScheduler, secretStore, stageStore, stepStore, principalStore, publicaccessService)
|
executionManager := manager.ProvideExecutionManager(config, executionStore, pipelineStore, provider, streamer, fileService, converterService, logStore, logStream, checkStore, repoStore, schedulerScheduler, secretStore, stageStore, stepStore, principalStore, publicaccessService)
|
||||||
client := manager.ProvideExecutionClient(executionManager, provider, config)
|
client := manager.ProvideExecutionClient(executionManager, provider, config)
|
||||||
resolverManager := resolver.ProvideResolver(config, pluginStore, templateStore, executionStore, repoStore)
|
resolverManager := resolver.ProvideResolver(config, pluginStore, templateStore, executionStore, repoStore)
|
||||||
@ -356,6 +360,6 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
servicesServices := services.ProvideServices(webhookService, pullreqService, triggerService, jobScheduler, collector, sizeCalculator, repoService, cleanupService, notificationService, keywordsearchService)
|
servicesServices := services.ProvideServices(webhookService, pullreqService, triggerService, jobScheduler, collector, sizeCalculator, repoService, cleanupService, notificationService, keywordsearchService)
|
||||||
serverSystem := server.NewSystem(bootstrapBootstrap, serverServer, poller, resolverManager, servicesServices)
|
serverSystem := server.NewSystem(bootstrapBootstrap, serverServer, sshServer, poller, resolverManager, servicesServices)
|
||||||
return serverSystem, nil
|
return serverSystem, nil
|
||||||
}
|
}
|
||||||
|
@ -18,15 +18,19 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
"github.com/harness/gitness/git/command"
|
"github.com/harness/gitness/git/command"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
|
||||||
|
|
||||||
func (g *Git) InfoRefs(
|
func (g *Git) InfoRefs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
repoPath string,
|
repoPath string,
|
||||||
@ -61,27 +65,44 @@ func (g *Git) InfoRefs(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServicePackOptions struct {
|
||||||
|
Service enum.GitServiceType
|
||||||
|
Timeout int // seconds
|
||||||
|
StatelessRPC bool
|
||||||
|
Stdout io.Writer
|
||||||
|
Stdin io.Reader
|
||||||
|
Stderr io.Writer
|
||||||
|
Env []string
|
||||||
|
Protocol string
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Git) ServicePack(
|
func (g *Git) ServicePack(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
repoPath string,
|
repoPath string,
|
||||||
service string,
|
options ServicePackOptions,
|
||||||
stdin io.Reader,
|
|
||||||
stdout io.Writer,
|
|
||||||
env ...string,
|
|
||||||
) error {
|
) error {
|
||||||
cmd := command.New(service,
|
cmd := command.New(string(options.Service),
|
||||||
command.WithFlag("--stateless-rpc"),
|
|
||||||
command.WithArg(repoPath),
|
command.WithArg(repoPath),
|
||||||
command.WithEnv("SSH_ORIGINAL_COMMAND", service),
|
command.WithEnv("SSH_ORIGINAL_COMMAND", string(options.Service)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if options.StatelessRPC {
|
||||||
|
cmd.Add(command.WithFlag("--stateless-rpc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Protocol != "" && safeGitProtocolHeader.MatchString(options.Protocol) {
|
||||||
|
cmd.Add(command.WithEnv("GIT_PROTOCOL", options.Protocol))
|
||||||
|
}
|
||||||
|
|
||||||
err := cmd.Run(ctx,
|
err := cmd.Run(ctx,
|
||||||
command.WithDir(repoPath),
|
command.WithDir(repoPath),
|
||||||
command.WithStdout(stdout),
|
command.WithStdout(options.Stdout),
|
||||||
command.WithStdin(stdin),
|
command.WithStdin(options.Stdin),
|
||||||
command.WithEnvs(env...),
|
command.WithStderr(options.Stderr),
|
||||||
|
command.WithEnvs(options.Env...),
|
||||||
)
|
)
|
||||||
if err != nil && err.Error() != "signal: killed" {
|
if err != nil && err.Error() != "signal: killed" {
|
||||||
log.Ctx(ctx).Err(err).Msgf("Fail to serve RPC(%s) in %s: %v", service, repoPath, err)
|
log.Ctx(ctx).Err(err).Msgf("Fail to serve RPC(%s) in %s: %v", options.Service, repoPath, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
@ -70,7 +70,7 @@ type Interface interface {
|
|||||||
* Git Cli Service
|
* Git Cli Service
|
||||||
*/
|
*/
|
||||||
GetInfoRefs(ctx context.Context, w io.Writer, params *InfoRefsParams) error
|
GetInfoRefs(ctx context.Context, w io.Writer, params *InfoRefsParams) error
|
||||||
ServicePack(ctx context.Context, w io.Writer, params *ServicePackParams) error
|
ServicePack(ctx context.Context, params *ServicePackParams) error
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Diff services
|
* Diff services
|
||||||
|
@ -18,13 +18,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
|
"github.com/harness/gitness/git/api"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
)
|
)
|
||||||
|
|
||||||
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
|
|
||||||
|
|
||||||
type InfoRefsParams struct {
|
type InfoRefsParams struct {
|
||||||
ReadParams
|
ReadParams
|
||||||
Service string
|
Service string
|
||||||
@ -53,10 +52,7 @@ func (s *Service) GetInfoRefs(ctx context.Context, w io.Writer, params *InfoRefs
|
|||||||
type ServicePackParams struct {
|
type ServicePackParams struct {
|
||||||
*ReadParams
|
*ReadParams
|
||||||
*WriteParams
|
*WriteParams
|
||||||
Service string
|
api.ServicePackOptions
|
||||||
GitProtocol string
|
|
||||||
Data io.Reader
|
|
||||||
Options []string // (key, value) pair
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ServicePackParams) Validate() error {
|
func (p *ServicePackParams) Validate() error {
|
||||||
@ -66,37 +62,28 @@ func (p *ServicePackParams) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ServicePack(ctx context.Context, w io.Writer, params *ServicePackParams) error {
|
func (s *Service) ServicePack(ctx context.Context, params *ServicePackParams) error {
|
||||||
if err := params.Validate(); err != nil {
|
if err := params.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var repoPath string
|
||||||
var (
|
|
||||||
repoPath string
|
|
||||||
env []string
|
|
||||||
)
|
|
||||||
|
|
||||||
switch params.Service {
|
switch params.Service {
|
||||||
case "upload-pack":
|
case enum.GitServiceTypeUploadPack:
|
||||||
if err := params.ReadParams.Validate(); err != nil {
|
if err := params.ReadParams.Validate(); err != nil {
|
||||||
return errors.InvalidArgument("upload-pack requires ReadParams")
|
return errors.InvalidArgument("upload-pack requires ReadParams")
|
||||||
}
|
}
|
||||||
repoPath = getFullPathForRepo(s.reposRoot, params.ReadParams.RepoUID)
|
repoPath = getFullPathForRepo(s.reposRoot, params.ReadParams.RepoUID)
|
||||||
case "receive-pack":
|
case enum.GitServiceTypeReceivePack:
|
||||||
if err := params.WriteParams.Validate(); err != nil {
|
if err := params.WriteParams.Validate(); err != nil {
|
||||||
return errors.InvalidArgument("receive-pack requires WriteParams")
|
return errors.InvalidArgument("receive-pack requires WriteParams")
|
||||||
}
|
}
|
||||||
env = CreateEnvironmentForPush(ctx, *params.WriteParams)
|
params.Env = append(params.Env, CreateEnvironmentForPush(ctx, *params.WriteParams)...)
|
||||||
repoPath = getFullPathForRepo(s.reposRoot, params.WriteParams.RepoUID)
|
repoPath = getFullPathForRepo(s.reposRoot, params.WriteParams.RepoUID)
|
||||||
default:
|
default:
|
||||||
return errors.InvalidArgument("unsupported service provided: %s", params.Service)
|
return errors.InvalidArgument("unsupported service provided: %s", params.Service)
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.GitProtocol != "" && safeGitProtocolHeader.MatchString(params.GitProtocol) {
|
err := s.git.ServicePack(ctx, repoPath, params.ServicePackOptions)
|
||||||
env = append(env, "GIT_PROTOCOL="+params.GitProtocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.git.ServicePack(ctx, repoPath, params.Service, params.Data, w, env...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to execute git %s: %w", params.Service, err)
|
return fmt.Errorf("failed to execute git %s: %w", params.Service, err)
|
||||||
}
|
}
|
404
ssh/server.go
Normal file
404
ssh/server.go
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
// Copyright 2023 Harness, Inc.
|
||||||
|
//
|
||||||
|
// 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 ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/api/controller/repo"
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/app/services/publickey"
|
||||||
|
"github.com/harness/gitness/errors"
|
||||||
|
"github.com/harness/gitness/git/api"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
|
||||||
|
"github.com/gliderlabs/ssh"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const principalKey = contextKey("principalKey")
|
||||||
|
|
||||||
|
var (
|
||||||
|
allowedCommands = []string{
|
||||||
|
"git-upload-pack",
|
||||||
|
"git-receive-pack",
|
||||||
|
}
|
||||||
|
defaultCiphers = []string{
|
||||||
|
"chacha20-poly1305@openssh.com",
|
||||||
|
"aes128-ctr",
|
||||||
|
"aes192-ctr",
|
||||||
|
"aes256-ctr",
|
||||||
|
"aes128-gcm@openssh.com",
|
||||||
|
"aes256-gcm@openssh.com",
|
||||||
|
}
|
||||||
|
defaultKeyExchanges = []string{
|
||||||
|
"curve25519-sha256",
|
||||||
|
"curve25519-sha256@libssh.org",
|
||||||
|
"ecdh-sha2-nistp256",
|
||||||
|
"ecdh-sha2-nistp384",
|
||||||
|
"ecdh-sha2-nistp521",
|
||||||
|
"diffie-hellman-group14-sha256",
|
||||||
|
"diffie-hellman-group14-sha1",
|
||||||
|
}
|
||||||
|
defaultMACs = []string{
|
||||||
|
"hmac-sha2-256-etm@openssh.com",
|
||||||
|
"hmac-sha2-512-etm@openssh.com",
|
||||||
|
"hmac-sha2-256",
|
||||||
|
"hmac-sha2-512",
|
||||||
|
}
|
||||||
|
defaultServerKeys = []string{"ssh/gitness.rsa"}
|
||||||
|
KeepAliveMsg = "keepalive@openssh.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrHostKeysAreRequired = errors.New("host keys are required")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
internal *ssh.Server
|
||||||
|
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
DefaultUser string
|
||||||
|
|
||||||
|
TrustedUserCAKeys []string
|
||||||
|
TrustedUserCAKeysParsed []gossh.PublicKey
|
||||||
|
Ciphers []string
|
||||||
|
KeyExchanges []string
|
||||||
|
MACs []string
|
||||||
|
HostKeys []string
|
||||||
|
KeepAliveInterval int
|
||||||
|
|
||||||
|
Verifier publickey.Service
|
||||||
|
RepoCtrl *repo.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sanitize() error {
|
||||||
|
if s.Port == 0 {
|
||||||
|
s.Port = 22
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.Ciphers) == 0 {
|
||||||
|
s.Ciphers = defaultCiphers
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.KeyExchanges) == 0 {
|
||||||
|
s.KeyExchanges = defaultKeyExchanges
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.MACs) == 0 {
|
||||||
|
s.MACs = defaultMACs
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.HostKeys) == 0 {
|
||||||
|
s.HostKeys = defaultServerKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.KeepAliveInterval == 0 {
|
||||||
|
s.KeepAliveInterval = 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RepoCtrl == nil {
|
||||||
|
return errors.InvalidArgument("repository controller is needed to run git service pack commands")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListenAndServe() error {
|
||||||
|
err := s.sanitize()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sanitize server defaults: %w", err)
|
||||||
|
}
|
||||||
|
s.internal = &ssh.Server{
|
||||||
|
Addr: net.JoinHostPort(s.Host, strconv.Itoa(s.Port)),
|
||||||
|
Handler: s.sessionHandler,
|
||||||
|
PublicKeyHandler: s.publicKeyHandler,
|
||||||
|
PtyCallback: func(ssh.Context, ssh.Pty) bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
ConnectionFailedCallback: sshConnectionFailed,
|
||||||
|
ServerConfigCallback: func(ssh.Context) *gossh.ServerConfig {
|
||||||
|
config := &gossh.ServerConfig{}
|
||||||
|
config.KeyExchanges = s.KeyExchanges
|
||||||
|
config.MACs = s.MACs
|
||||||
|
config.Ciphers = s.Ciphers
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.setupHostKeys()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to setup host keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("starting ssh service....: %v", s.internal.Addr)
|
||||||
|
err = s.internal.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ssh service not running: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) setupHostKeys() error {
|
||||||
|
if len(s.HostKeys) == 0 {
|
||||||
|
return ErrHostKeysAreRequired
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(s.HostKeys))
|
||||||
|
// check if file exists and append to slice keys
|
||||||
|
for _, key := range s.HostKeys {
|
||||||
|
_, err := os.Stat(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msgf("unable to check if %s exists", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no keys found then create one from HostKeys field
|
||||||
|
if len(keys) == 0 {
|
||||||
|
fullpath := s.HostKeys[0]
|
||||||
|
filePath := filepath.Dir(fullpath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filePath, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("failed to create dir %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := GenerateKeyPair(fullpath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate private key: %w", err)
|
||||||
|
}
|
||||||
|
keys = append(keys, fullpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set keys to internal ssh server
|
||||||
|
for _, key := range keys {
|
||||||
|
err := s.internal.SetOption(ssh.HostKeyFile(key))
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failed to set host key to ssh server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
|
log.Debug().Msgf("stopping ssh service: %v", s.internal.Addr)
|
||||||
|
err := s.internal.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stop ssh service: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sessionHandler(session ssh.Session) {
|
||||||
|
command := session.RawCommand()
|
||||||
|
|
||||||
|
principal, ok := session.Context().Value(principalKey).(*types.PrincipalInfo)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(session.Stderr(), "principal not found or empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Fields(command)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
_, _ = fmt.Fprintf(session.Stderr(), "command %q must have an argument\n", command)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// first part is git service pack command: git-upload-pack, git-receive-pack
|
||||||
|
gitCommand := parts[0]
|
||||||
|
if !slices.Contains(allowedCommands, gitCommand) {
|
||||||
|
_, _ = fmt.Fprintf(session.Stderr(), "command not supported: %q\n", command)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gitServicePack := strings.TrimPrefix(gitCommand, "git-")
|
||||||
|
service, err := enum.ParseGitServiceType(gitServicePack)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(session.Stderr(), "failed to parse service pack: %q\n", gitServicePack)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// git command args
|
||||||
|
gitArgs := parts[1:]
|
||||||
|
|
||||||
|
// first git service pack cmd arg is path: 'space/repository.git' so we need to remove
|
||||||
|
// single quotes.
|
||||||
|
repoRef := strings.Trim(gitArgs[0], "'")
|
||||||
|
// remove .git suffix
|
||||||
|
repoRef = strings.TrimSuffix(repoRef, ".git")
|
||||||
|
|
||||||
|
gitProtocol := ""
|
||||||
|
for _, key := range session.Environ() {
|
||||||
|
if strings.HasPrefix(key, "GIT_PROTOCOL=") {
|
||||||
|
gitProtocol = key[len("GIT_PROTOCOL="):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(session.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// set keep alive connection
|
||||||
|
if s.KeepAliveInterval > 0 {
|
||||||
|
go s.sendKeepAliveMsg(ctx, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.RepoCtrl.GitServicePack(
|
||||||
|
ctx,
|
||||||
|
&auth.Session{
|
||||||
|
Principal: types.Principal{
|
||||||
|
ID: principal.ID,
|
||||||
|
UID: principal.UID,
|
||||||
|
Email: principal.Email,
|
||||||
|
Type: principal.Type,
|
||||||
|
DisplayName: principal.DisplayName,
|
||||||
|
Created: principal.Created,
|
||||||
|
Updated: principal.Updated,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
repoRef,
|
||||||
|
api.ServicePackOptions{
|
||||||
|
Service: service,
|
||||||
|
Stdout: session,
|
||||||
|
Stdin: session,
|
||||||
|
Stderr: session.Stderr(),
|
||||||
|
Protocol: gitProtocol,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("git service pack failed")
|
||||||
|
_, err = io.Copy(session.Stderr(), strings.NewReader(err.Error()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("error writing to session stderr")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendKeepAliveMsg(ctx context.Context, session ssh.Session) {
|
||||||
|
ticker := time.NewTicker(time.Duration(s.KeepAliveInterval))
|
||||||
|
defer ticker.Stop()
|
||||||
|
log.Ctx(ctx).Debug().Str("remote_addr", session.RemoteAddr().String()).Msgf("sendKeepAliveMsg")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
log.Ctx(ctx).Debug().Msg("connection: sendKeepAliveMsg: send keepalive message to a client")
|
||||||
|
_, _ = session.SendRequest(KeepAliveMsg, true, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||||
|
if slices.Contains(publickey.DisallowedTypes, key.Type()) {
|
||||||
|
log.Warn().Msgf("public key type not supported: %s", key.Type())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.DefaultUser != "" && ctx.User() != s.DefaultUser {
|
||||||
|
log.Warn().Msgf("invalid SSH username %s - must use %s for all git operations via ssh",
|
||||||
|
ctx.User(), s.DefaultUser)
|
||||||
|
log.Warn().Msgf("failed authentication attempt from %s", ctx.RemoteAddr())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
principal, err := s.Verifier.ValidateKey(ctx, key, enum.PublicKeyUsageAuth)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to validate public key")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have a certificate
|
||||||
|
if cert, ok := key.(*gossh.Certificate); ok {
|
||||||
|
if len(s.TrustedUserCAKeys) == 0 {
|
||||||
|
log.Warn().Msg("Certificate Rejected: No trusted certificate authorities for this server")
|
||||||
|
log.Warn().Msgf("Failed authentication attempt from %s", ctx.RemoteAddr())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert.CertType != gossh.UserCert {
|
||||||
|
log.Warn().Msg("Certificate Rejected: Not a user certificate")
|
||||||
|
log.Warn().Msgf("Failed authentication attempt from %s", ctx.RemoteAddr())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
certChecker := &gossh.CertChecker{}
|
||||||
|
if err := certChecker.CheckCert(principal.UID, cert); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetValue(principalKey, principal)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshConnectionFailed(conn net.Conn, err error) {
|
||||||
|
log.Err(err).Msgf("failed connection from %s with error: %v", conn.RemoteAddr(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeyPair make a pair of public and private keys for SSH access.
|
||||||
|
func GenerateKeyPair(keyPath string) error {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
|
||||||
|
f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := pem.Encode(f, privateKeyPEM); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate public key
|
||||||
|
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
public := gossh.MarshalAuthorizedKey(pub)
|
||||||
|
p, err := os.OpenFile(keyPath+".pub", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
_, err = p.Write(public)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to public key: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
48
ssh/wire.go
Normal file
48
ssh/wire.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2023 Harness, Inc.
|
||||||
|
//
|
||||||
|
// 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 ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/harness/gitness/app/api/controller/repo"
|
||||||
|
"github.com/harness/gitness/app/services/publickey"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WireSet = wire.NewSet(
|
||||||
|
ProvideServer,
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideServer(
|
||||||
|
config *types.Config,
|
||||||
|
vierifier publickey.Service,
|
||||||
|
repoctrl *repo.Controller,
|
||||||
|
) *Server {
|
||||||
|
return &Server{
|
||||||
|
Host: config.SSH.Host,
|
||||||
|
Port: config.SSH.Port,
|
||||||
|
DefaultUser: config.SSH.DefaultUser,
|
||||||
|
Ciphers: config.SSH.Ciphers,
|
||||||
|
KeyExchanges: config.SSH.KeyExchanges,
|
||||||
|
MACs: config.SSH.MACs,
|
||||||
|
HostKeys: config.SSH.ServerHostKeys,
|
||||||
|
TrustedUserCAKeys: config.SSH.TrustedUserCAKeys,
|
||||||
|
TrustedUserCAKeysParsed: config.SSH.TrustedUserCAKeysParsed,
|
||||||
|
KeepAliveInterval: config.SSH.KeepAliveInterval,
|
||||||
|
Verifier: vierifier,
|
||||||
|
RepoCtrl: repoctrl,
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,8 @@ import (
|
|||||||
gitenum "github.com/harness/gitness/git/enum"
|
gitenum "github.com/harness/gitness/git/enum"
|
||||||
"github.com/harness/gitness/lock"
|
"github.com/harness/gitness/lock"
|
||||||
"github.com/harness/gitness/pubsub"
|
"github.com/harness/gitness/pubsub"
|
||||||
|
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config stores the system configuration.
|
// Config stores the system configuration.
|
||||||
@ -63,6 +65,9 @@ type Config struct {
|
|||||||
// Value is derived from Base unless explicitly specified (e.g. http://localhost:3000/git).
|
// Value is derived from Base unless explicitly specified (e.g. http://localhost:3000/git).
|
||||||
Git string `envconfig:"GITNESS_URL_GIT"`
|
Git string `envconfig:"GITNESS_URL_GIT"`
|
||||||
|
|
||||||
|
// GitSSH defines the external URL via which the GIT SSH server is reachable.
|
||||||
|
GitSSH string `envconfig:"GITNESS_URL_GIT_SSH" default:"localhost"`
|
||||||
|
|
||||||
// API defines the external URL via which the rest API is reachable.
|
// API defines the external URL via which the rest API is reachable.
|
||||||
// NOTE: for routing to work properly, the request path reaching gitness has to end with `/api`
|
// NOTE: for routing to work properly, the request path reaching gitness has to end with `/api`
|
||||||
// (this could be after proxy path rewrite).
|
// (this could be after proxy path rewrite).
|
||||||
@ -132,6 +137,24 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SSH struct {
|
||||||
|
Enable bool `envconfig:"GITNESS_SSH_ENABLE" default:"true"`
|
||||||
|
Host string `envconfig:"GITNESS_SSH_HOST"`
|
||||||
|
Port int `envconfig:"GITNESS_SSH_PORT" default:"22"`
|
||||||
|
// DefaultUser holds value for generating urls {user}@host:path and force check
|
||||||
|
// no other user can authenticate unless it is empty then any username is allowed
|
||||||
|
DefaultUser string `envconfig:"GITNESS_SSH_DEFAULT_USER" default:"git"`
|
||||||
|
Ciphers []string `envconfig:"GITNESS_SSH_CIPHERS"`
|
||||||
|
KeyExchanges []string `envconfig:"GITNESS_SSH_KEY_EXCHANGES"`
|
||||||
|
MACs []string `envconfig:"GITNESS_SSH_MACS"`
|
||||||
|
ServerHostKeys []string `envconfig:"GITNESS_SSH_HOST_KEYS"`
|
||||||
|
KeygenPath string `envconfig:"GITNESS_SSH_KEYGEN_PATH"`
|
||||||
|
TrustedUserCAKeys []string `envconfig:"GITNESS_SSH_TRUSTED_USER_CA_KEYS"`
|
||||||
|
TrustedUserCAKeysFile string `envconfig:"GITNESS_SSH_TRUSTED_USER_CA_KEYS_FILENAME"`
|
||||||
|
TrustedUserCAKeysParsed []gossh.PublicKey
|
||||||
|
KeepAliveInterval int `envconfig:"GITNESS_SSH_KEEP_ALIVE_INTERVAL" default:"5000"`
|
||||||
|
}
|
||||||
|
|
||||||
// CI defines configuration related to build executions.
|
// CI defines configuration related to build executions.
|
||||||
CI struct {
|
CI struct {
|
||||||
ParallelWorkers int `envconfig:"GITNESS_CI_PARALLEL_WORKERS" default:"2"`
|
ParallelWorkers int `envconfig:"GITNESS_CI_PARALLEL_WORKERS" default:"2"`
|
||||||
|
@ -111,6 +111,6 @@ func ParseGitServiceType(s string) (GitServiceType, error) {
|
|||||||
case string(GitServiceTypeUploadPack):
|
case string(GitServiceTypeUploadPack):
|
||||||
return GitServiceTypeUploadPack, nil
|
return GitServiceTypeUploadPack, nil
|
||||||
default:
|
default:
|
||||||
return GitServiceType(""), fmt.Errorf("unknown git service type provided: %q", s)
|
return "", fmt.Errorf("unknown git service type provided: %q", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,8 @@ type Repository struct {
|
|||||||
IsEmpty bool `json:"is_empty,omitempty" yaml:"is_empty"`
|
IsEmpty bool `json:"is_empty,omitempty" yaml:"is_empty"`
|
||||||
|
|
||||||
// git urls
|
// git urls
|
||||||
GitURL string `json:"git_url" yaml:"-"`
|
GitURL string `json:"git_url" yaml:"-"`
|
||||||
|
GitSSHURL string `json:"git_ssh_url" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone makes deep copy of repository object.
|
// Clone makes deep copy of repository object.
|
||||||
|
@ -30,9 +30,10 @@ import css from './CloneButtonTooltip.module.scss'
|
|||||||
|
|
||||||
interface CloneButtonTooltipProps {
|
interface CloneButtonTooltipProps {
|
||||||
httpsURL: string
|
httpsURL: string
|
||||||
|
sshURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CloneButtonTooltip({ httpsURL }: CloneButtonTooltipProps) {
|
export function CloneButtonTooltip({ httpsURL, sshURL }: CloneButtonTooltipProps) {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const [flag, setFlag] = useState(false)
|
const [flag, setFlag] = useState(false)
|
||||||
const { isCurrentSessionPublic } = useAppContext()
|
const { isCurrentSessionPublic } = useAppContext()
|
||||||
@ -54,6 +55,7 @@ export function CloneButtonTooltip({ httpsURL }: CloneButtonTooltipProps) {
|
|||||||
<Text font={{ variation: FontVariation.H4 }}>{getString('cloneHTTPS')}</Text>
|
<Text font={{ variation: FontVariation.H4 }}>{getString('cloneHTTPS')}</Text>
|
||||||
|
|
||||||
<Container padding={{ top: 'small' }}>
|
<Container padding={{ top: 'small' }}>
|
||||||
|
<Text font={{ variation: FontVariation.BODY2_SEMI }}>HTTP</Text>
|
||||||
<Layout.Horizontal className={css.layout}>
|
<Layout.Horizontal className={css.layout}>
|
||||||
<Text className={css.url}>{httpsURL}</Text>
|
<Text className={css.url}>{httpsURL}</Text>
|
||||||
|
|
||||||
@ -61,6 +63,15 @@ export function CloneButtonTooltip({ httpsURL }: CloneButtonTooltipProps) {
|
|||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
<Container padding={{ top: 'small' }}>
|
||||||
|
<Text font={{ variation: FontVariation.BODY2_SEMI }}>SSH</Text>
|
||||||
|
<Layout.Horizontal className={css.layout}>
|
||||||
|
<Text className={css.url}>{sshURL}</Text>
|
||||||
|
|
||||||
|
<CopyButton content={sshURL} id={css.cloneCopyButton} icon={CodeIcon.Copy} iconProps={{ size: 14 }} />
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<Render when={!isCurrentSessionPublic}>
|
<Render when={!isCurrentSessionPublic}>
|
||||||
<Button
|
<Button
|
||||||
width={300}
|
width={300}
|
||||||
|
@ -96,6 +96,7 @@ export const EmptyRepositoryInfo: React.FC<Pick<GitInfoProps, 'repoMetadata'>> =
|
|||||||
</Text>
|
</Text>
|
||||||
<Layout.Horizontal>
|
<Layout.Horizontal>
|
||||||
<Container padding={{ bottom: 'medium' }} width={400} margin={{ right: 'small' }}>
|
<Container padding={{ bottom: 'medium' }} width={400} margin={{ right: 'small' }}>
|
||||||
|
<Text>HTTP</Text>
|
||||||
<Layout.Horizontal className={css.layout}>
|
<Layout.Horizontal className={css.layout}>
|
||||||
<Text className={css.url}>{repoMetadata.git_url}</Text>
|
<Text className={css.url}>{repoMetadata.git_url}</Text>
|
||||||
<FlexExpander />
|
<FlexExpander />
|
||||||
@ -106,16 +107,26 @@ export const EmptyRepositoryInfo: React.FC<Pick<GitInfoProps, 'repoMetadata'>> =
|
|||||||
iconProps={{ size: 14 }}
|
iconProps={{ size: 14 }}
|
||||||
/>
|
/>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
|
<Text>SSH</Text>
|
||||||
|
<Layout.Horizontal className={css.layout}>
|
||||||
|
<Text className={css.url}>{repoMetadata.git_ssh_url}</Text>
|
||||||
|
<FlexExpander />
|
||||||
|
<CopyButton
|
||||||
|
content={repoMetadata?.git_ssh_url as string}
|
||||||
|
id={css.cloneCopyButton}
|
||||||
|
icon={CodeIcon.Copy}
|
||||||
|
iconProps={{ size: 14 }}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setFlag(true)
|
|
||||||
}}
|
|
||||||
variation={ButtonVariation.SECONDARY}>
|
|
||||||
{getString('generateCloneCred')}
|
|
||||||
</Button>
|
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFlag(true)
|
||||||
|
}}
|
||||||
|
variation={ButtonVariation.SECONDARY}>
|
||||||
|
{getString('generateCloneCred')}
|
||||||
|
</Button>
|
||||||
<Text font={{ variation: FontVariation.BODY, size: 'small' }}>
|
<Text font={{ variation: FontVariation.BODY, size: 'small' }}>
|
||||||
<StringSubstitute
|
<StringSubstitute
|
||||||
str={getString('manageCredText')}
|
str={getString('manageCredText')}
|
||||||
|
@ -135,7 +135,12 @@ export function ContentHeader({
|
|||||||
variation={ButtonVariation.SECONDARY}
|
variation={ButtonVariation.SECONDARY}
|
||||||
icon={CodeIcon.Clone}
|
icon={CodeIcon.Clone}
|
||||||
className={css.btnColorFix}
|
className={css.btnColorFix}
|
||||||
tooltip={<CloneButtonTooltip httpsURL={repoMetadata.git_url as string} />}
|
tooltip={
|
||||||
|
<CloneButtonTooltip
|
||||||
|
httpsURL={repoMetadata.git_url as string}
|
||||||
|
sshURL={repoMetadata.git_ssh_url as string}
|
||||||
|
/>
|
||||||
|
}
|
||||||
tooltipProps={{
|
tooltipProps={{
|
||||||
interactionKind: 'click',
|
interactionKind: 'click',
|
||||||
minimal: true,
|
minimal: true,
|
||||||
|
@ -894,6 +894,7 @@ export interface TypesRepository {
|
|||||||
description?: string
|
description?: string
|
||||||
fork_id?: number
|
fork_id?: number
|
||||||
git_url?: string
|
git_url?: string
|
||||||
|
git_ssh_url?: string
|
||||||
id?: number
|
id?: number
|
||||||
importing?: boolean
|
importing?: boolean
|
||||||
is_public?: boolean
|
is_public?: boolean
|
||||||
|
Loading…
Reference in New Issue
Block a user