mirror of https://github.com/harness/drone.git
feat: [CODE-2528]: Support Git LFS (#3506)
* Apply suggestion from code review * Merge branch 'feature/GitLFSv1' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into feature/GitLFSv1 * pr comments * Apply suggestion from code review * PR comments and lints * lints * pr comments * self review-code cleaning * feat: [CODE-2528]: implement gcs download func (#3505) * revert ui changes that sneaked into my pr * rely on repoCore * merge main into feature branch * fix: parsing LFS OIDs in git (#3470) * Detect missing lfs objects on pre-receive (#3378) * use principal type to generate token for remote auth (#3385) * Revert "feat: [PIPE-24548]: Add label creation to pullreq creation (#3276)" This reverts commit 6391117c6137a574934b9adb57b46ca7d7feaa19. * feat: [CODE-2528] Git LFS Over SSH (#3279) * feat: [PIPE-24548]: Add label creation to pullreq creation (#3276) * Refactor label select base const and its use * Add created labels to create pr response * Merge remote-tracking branch 'origin/main' into ddtry-new-ui
parent
6da9821bc7
commit
0a573a566c
|
@ -56,6 +56,7 @@ type Controller struct {
|
||||||
updateExtender UpdateExtender
|
updateExtender UpdateExtender
|
||||||
postReceiveExtender PostReceiveExtender
|
postReceiveExtender PostReceiveExtender
|
||||||
sseStreamer sse.Streamer
|
sseStreamer sse.Streamer
|
||||||
|
lfsStore store.LFSObjectStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(
|
func NewController(
|
||||||
|
@ -75,6 +76,7 @@ func NewController(
|
||||||
updateExtender UpdateExtender,
|
updateExtender UpdateExtender,
|
||||||
postReceiveExtender PostReceiveExtender,
|
postReceiveExtender PostReceiveExtender,
|
||||||
sseStreamer sse.Streamer,
|
sseStreamer sse.Streamer,
|
||||||
|
lfsStore store.LFSObjectStore,
|
||||||
) *Controller {
|
) *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
authorizer: authorizer,
|
authorizer: authorizer,
|
||||||
|
@ -93,6 +95,7 @@ func NewController(
|
||||||
updateExtender: updateExtender,
|
updateExtender: updateExtender,
|
||||||
postReceiveExtender: postReceiveExtender,
|
postReceiveExtender: postReceiveExtender,
|
||||||
sseStreamer: sseStreamer,
|
sseStreamer: sseStreamer,
|
||||||
|
lfsStore: lfsStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,4 +36,5 @@ type RestrictedGIT interface {
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
params *git.FindOversizeFilesParams,
|
params *git.FindOversizeFilesParams,
|
||||||
) (*git.FindOversizeFilesOutput, error)
|
) (*git.FindOversizeFilesOutput, error)
|
||||||
|
ListLFSPointers(ctx context.Context, params *git.ListLFSPointersParams) (*git.ListLFSPointersOutput, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,14 @@ func (c *Controller) PreReceive(
|
||||||
return hook.Output{}, err
|
return hook.Output{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.checkLFSObjects(ctx, rgit, repo, in, &output)
|
||||||
|
if output.Error != nil {
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return hook.Output{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,15 +34,7 @@ func (c *Controller) checkFileSizeLimit(
|
||||||
output *hook.Output,
|
output *hook.Output,
|
||||||
) error {
|
) error {
|
||||||
// return if all new refs are nil refs
|
// return if all new refs are nil refs
|
||||||
allNilRefs := true
|
if isAllRefDeletions(in.RefUpdates) {
|
||||||
for _, refUpdate := range in.RefUpdates {
|
|
||||||
if refUpdate.New.IsNil() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
allNilRefs = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if allNilRefs {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
// 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 githook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/git"
|
||||||
|
"github.com/harness/gitness/git/hook"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
|
||||||
|
"github.com/gotidy/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Controller) checkLFSObjects(
|
||||||
|
ctx context.Context,
|
||||||
|
rgit RestrictedGIT,
|
||||||
|
repo *types.RepositoryCore,
|
||||||
|
in types.GithookPreReceiveInput,
|
||||||
|
output *hook.Output,
|
||||||
|
) error {
|
||||||
|
// return if all new refs are nil refs
|
||||||
|
if isAllRefDeletions(in.RefUpdates) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := rgit.ListLFSPointers(ctx,
|
||||||
|
&git.ListLFSPointersParams{
|
||||||
|
ReadParams: git.ReadParams{
|
||||||
|
RepoUID: repo.GitUID,
|
||||||
|
AlternateObjectDirs: in.Environment.AlternateObjectDirs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list lfs pointers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.LFSInfos) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oids := make([]string, len(res.LFSInfos))
|
||||||
|
for i := range res.LFSInfos {
|
||||||
|
oids[i] = res.LFSInfos[i].OID
|
||||||
|
}
|
||||||
|
|
||||||
|
existingObjs, err := c.lfsStore.FindMany(ctx, in.RepoID, oids)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find lfs objects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
if len(existingObjs) != len(oids) {
|
||||||
|
output.Error = ptr.String(
|
||||||
|
"Changes blocked by missing lfs objects. Please try `git lfs push --all` or check if LFS is setup properly.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllRefDeletions(refUpdates []hook.ReferenceUpdate) bool {
|
||||||
|
for _, refUpdate := range refUpdates {
|
||||||
|
if !refUpdate.New.IsNil() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ func ProvideController(
|
||||||
updateExtender UpdateExtender,
|
updateExtender UpdateExtender,
|
||||||
postReceiveExtender PostReceiveExtender,
|
postReceiveExtender PostReceiveExtender,
|
||||||
sseStreamer sse.Streamer,
|
sseStreamer sse.Streamer,
|
||||||
|
lfsStore store.LFSObjectStore,
|
||||||
) *Controller {
|
) *Controller {
|
||||||
ctrl := NewController(
|
ctrl := NewController(
|
||||||
authorizer,
|
authorizer,
|
||||||
|
@ -78,6 +79,7 @@ func ProvideController(
|
||||||
updateExtender,
|
updateExtender,
|
||||||
postReceiveExtender,
|
postReceiveExtender,
|
||||||
sseStreamer,
|
sseStreamer,
|
||||||
|
lfsStore,
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: improve wiring if possible
|
// TODO: improve wiring if possible
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/app/auth/authn"
|
||||||
|
"github.com/harness/gitness/app/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Controller) Authenticate(
|
||||||
|
ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
repoRef string,
|
||||||
|
) (*AuthenticateResponse, error) {
|
||||||
|
jwt, err := c.remoteAuth.GenerateToken(ctx, session.Principal.ID, session.Principal.Type, repoRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate auth token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthenticateResponse{
|
||||||
|
Header: map[string]string{
|
||||||
|
"Authorization": authn.HeaderTokenPrefixRemoteAuth + jwt,
|
||||||
|
},
|
||||||
|
HRef: c.urlProvider.GenerateGITCloneURL(ctx, repoRef) + "/info/lfs",
|
||||||
|
ExpiresIn: token.RemoteAuthTokenLifeTime,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apiauth "github.com/harness/gitness/app/api/auth"
|
||||||
|
"github.com/harness/gitness/app/api/usererror"
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/app/auth/authz"
|
||||||
|
"github.com/harness/gitness/app/services/refcache"
|
||||||
|
"github.com/harness/gitness/app/services/remoteauth"
|
||||||
|
"github.com/harness/gitness/app/store"
|
||||||
|
"github.com/harness/gitness/app/url"
|
||||||
|
"github.com/harness/gitness/blob"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
lfsObjectsPathFormat = "lfs/%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
authorizer authz.Authorizer
|
||||||
|
repoFinder refcache.RepoFinder
|
||||||
|
principalStore store.PrincipalStore
|
||||||
|
lfsStore store.LFSObjectStore
|
||||||
|
blobStore blob.Store
|
||||||
|
remoteAuth remoteauth.Service
|
||||||
|
urlProvider url.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(
|
||||||
|
authorizer authz.Authorizer,
|
||||||
|
repoFinder refcache.RepoFinder,
|
||||||
|
principalStore store.PrincipalStore,
|
||||||
|
lfsStore store.LFSObjectStore,
|
||||||
|
blobStore blob.Store,
|
||||||
|
remoteAuth remoteauth.Service,
|
||||||
|
urlProvider url.Provider,
|
||||||
|
) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
authorizer: authorizer,
|
||||||
|
repoFinder: repoFinder,
|
||||||
|
principalStore: principalStore,
|
||||||
|
lfsStore: lfsStore,
|
||||||
|
blobStore: blobStore,
|
||||||
|
remoteAuth: remoteAuth,
|
||||||
|
urlProvider: urlProvider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) getRepoCheckAccess(
|
||||||
|
ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
repoRef string,
|
||||||
|
reqPermission enum.Permission,
|
||||||
|
allowedRepoStates ...enum.RepoState,
|
||||||
|
) (*types.RepositoryCore, error) {
|
||||||
|
if repoRef == "" {
|
||||||
|
return nil, usererror.BadRequest("A valid repository reference must be provided.")
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := c.repoFinder.FindByRef(ctx, repoRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find repository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apiauth.CheckRepoState(ctx, session, repo, reqPermission, allowedRepoStates...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, reqPermission); err != nil {
|
||||||
|
return nil, fmt.Errorf("access check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLFSObjectPath(oid string) string {
|
||||||
|
return fmt.Sprintf(lfsObjectsPathFormat, oid)
|
||||||
|
}
|
|
@ -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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Controller) Download(ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
repoRef string,
|
||||||
|
oid string,
|
||||||
|
) (io.ReadCloser, error) {
|
||||||
|
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to acquire access to repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.lfsStore.Find(ctx, repo.ID, oid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find the oid %q for the repo: %w", oid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objPath := getLFSObjectPath(oid)
|
||||||
|
file, err := c.blobStore.Download(ctx, objPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to download file from blobstore: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
var (
|
||||||
|
// These are per-object errors when returned status code is 200.
|
||||||
|
errNotFound = ObjectError{
|
||||||
|
Code: 404,
|
||||||
|
Message: "The object does not exist on the server.",
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,136 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/api/usererror"
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/app/url"
|
||||||
|
"github.com/harness/gitness/store"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Controller) LFSTransfer(ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
repoRef string,
|
||||||
|
in *TransferInput,
|
||||||
|
) (*TransferOutput, error) {
|
||||||
|
reqPermission := enum.PermissionRepoView
|
||||||
|
if in.Operation == enum.GitLFSOperationTypeUpload {
|
||||||
|
reqPermission = enum.PermissionRepoPush
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, reqPermission)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check if server supports client's transfer adapters
|
||||||
|
var objResponses []ObjectResponse
|
||||||
|
switch {
|
||||||
|
case in.Operation == enum.GitLFSOperationTypeDownload:
|
||||||
|
for _, obj := range in.Objects {
|
||||||
|
var objResponse = ObjectResponse{
|
||||||
|
Pointer: Pointer{
|
||||||
|
OId: obj.OId,
|
||||||
|
Size: obj.Size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
object, err := c.lfsStore.Find(ctx, repo.ID, obj.OId)
|
||||||
|
if errors.Is(err, store.ErrResourceNotFound) {
|
||||||
|
objResponse.Error = &errNotFound
|
||||||
|
objResponses = append(objResponses, objResponse)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// size is not a required query param for download hence nil
|
||||||
|
downloadURL := getRedirectRef(ctx, c.urlProvider, repoRef, obj.OId, nil)
|
||||||
|
|
||||||
|
objResponse = ObjectResponse{
|
||||||
|
Pointer: Pointer{
|
||||||
|
OId: object.OID,
|
||||||
|
Size: object.Size,
|
||||||
|
},
|
||||||
|
Actions: map[string]Action{
|
||||||
|
"download": {
|
||||||
|
Href: downloadURL,
|
||||||
|
Header: map[string]string{"Content-Type": "application/octet-stream"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
objResponses = append(objResponses, objResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
case in.Operation == enum.GitLFSOperationTypeUpload:
|
||||||
|
for _, obj := range in.Objects {
|
||||||
|
objResponse := ObjectResponse{
|
||||||
|
Pointer: Pointer{
|
||||||
|
OId: obj.OId,
|
||||||
|
Size: obj.Size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// we dont create the object in lfs store here as the upload might fail in blob store.
|
||||||
|
_, err := c.lfsStore.Find(ctx, repo.ID, obj.OId)
|
||||||
|
if err == nil {
|
||||||
|
// no need to re-upload existing LFS objects
|
||||||
|
objResponses = append(objResponses, objResponse)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, store.ErrResourceNotFound) {
|
||||||
|
return nil, fmt.Errorf("failed to find object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadURL := getRedirectRef(ctx, c.urlProvider, repoRef, obj.OId, &obj.Size)
|
||||||
|
|
||||||
|
objResponse.Actions = map[string]Action{
|
||||||
|
"upload": {
|
||||||
|
Href: uploadURL,
|
||||||
|
Header: map[string]string{"Content-Type": "application/octet-stream"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
objResponses = append(objResponses, objResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, usererror.BadRequestf("git-lfs operation %q is not supported", in.Operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TransferOutput{
|
||||||
|
Transfer: enum.GitLFSTransferTypeBasic,
|
||||||
|
Objects: objResponses,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRedirectRef(ctx context.Context, urlProvider url.Provider, repoPath, oID string, size *int64) string {
|
||||||
|
baseGitURL := urlProvider.GenerateGITCloneURL(ctx, repoPath)
|
||||||
|
queryParams := "oid=" + oID
|
||||||
|
if size != nil {
|
||||||
|
queryParams += "&size=" + strconv.FormatInt(*size, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseGitURL + "/info/lfs/objects/?" + queryParams
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reference struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer contains LFS pointer data.
|
||||||
|
type Pointer struct {
|
||||||
|
OId string `json:"oid"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferInput struct {
|
||||||
|
Operation enum.GitLFSOperationType `json:"operation"`
|
||||||
|
Transfers []enum.GitLFSTransferType `json:"transfers,omitempty"`
|
||||||
|
Ref *Reference `json:"ref,omitempty"`
|
||||||
|
Objects []Pointer `json:"objects"`
|
||||||
|
HashAlgo string `json:"hash_algo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectError defines the JSON structure returned to the client in case of an error.
|
||||||
|
type ObjectError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action provides a structure with information about next actions fo the object.
|
||||||
|
type Action struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Header map[string]string `json:"header,omitempty"`
|
||||||
|
ExpiresIn *time.Duration `json:"expires_in,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectResponse is object metadata as seen by clients of the LFS server.
|
||||||
|
type ObjectResponse struct {
|
||||||
|
Pointer
|
||||||
|
Authenticated *bool `json:"authenticated,omitempty"`
|
||||||
|
Actions map[string]Action `json:"actions"`
|
||||||
|
Error *ObjectError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferOutput struct {
|
||||||
|
Transfer enum.GitLFSTransferType `json:"transfer"`
|
||||||
|
Objects []ObjectResponse `json:"objects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticateResponse struct {
|
||||||
|
Header map[string]string `json:"header"`
|
||||||
|
HRef string `json:"href"`
|
||||||
|
ExpiresIn time.Duration `json:"expires_in"`
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/api/usererror"
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/store"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploadOut struct {
|
||||||
|
ObjectPath string `json:"object_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) Upload(ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
repoRef string,
|
||||||
|
pointer Pointer,
|
||||||
|
file io.Reader,
|
||||||
|
) (*UploadOut, error) {
|
||||||
|
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to acquire access to repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file == nil {
|
||||||
|
return nil, usererror.BadRequest("no file or content provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
bufReader := bufio.NewReader(file)
|
||||||
|
objPath := getLFSObjectPath(pointer.OId)
|
||||||
|
|
||||||
|
err = c.blobStore.Upload(ctx, bufReader, objPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to upload file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
object := &types.LFSObject{
|
||||||
|
OID: pointer.OId,
|
||||||
|
Size: pointer.Size,
|
||||||
|
Created: now.UnixMilli(),
|
||||||
|
CreatedBy: session.Principal.ID,
|
||||||
|
RepoID: repo.ID,
|
||||||
|
}
|
||||||
|
// create the object in lfs store after successful upload to the blob store.
|
||||||
|
err = c.lfsStore.Create(ctx, object)
|
||||||
|
if err != nil && !errors.Is(err, store.ErrDuplicate) {
|
||||||
|
return nil, fmt.Errorf("failed to find object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UploadOut{
|
||||||
|
ObjectPath: objPath,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/harness/gitness/app/auth/authz"
|
||||||
|
"github.com/harness/gitness/app/services/refcache"
|
||||||
|
"github.com/harness/gitness/app/services/remoteauth"
|
||||||
|
"github.com/harness/gitness/app/store"
|
||||||
|
"github.com/harness/gitness/app/url"
|
||||||
|
"github.com/harness/gitness/blob"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WireSet = wire.NewSet(
|
||||||
|
ProvideController,
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideController(
|
||||||
|
authorizer authz.Authorizer,
|
||||||
|
repoFinder refcache.RepoFinder,
|
||||||
|
principalStore store.PrincipalStore,
|
||||||
|
lfsStore store.LFSObjectStore,
|
||||||
|
blobStore blob.Store,
|
||||||
|
remoteAuth remoteauth.Service,
|
||||||
|
urlProvider url.Provider,
|
||||||
|
) *Controller {
|
||||||
|
return NewController(authorizer, repoFinder, principalStore, lfsStore, blobStore, remoteAuth, urlProvider)
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/harness/gitness/app/auth"
|
"github.com/harness/gitness/app/auth"
|
||||||
"github.com/harness/gitness/blob"
|
"github.com/harness/gitness/blob"
|
||||||
|
@ -38,7 +39,7 @@ func (c *Controller) Download(
|
||||||
|
|
||||||
fileBucketPath := getFileBucketPath(repo.ID, filePath)
|
fileBucketPath := getFileBucketPath(repo.ID, filePath)
|
||||||
|
|
||||||
signedURL, err := c.blobStore.GetSignedURL(ctx, fileBucketPath)
|
signedURL, err := c.blobStore.GetSignedURL(ctx, fileBucketPath, time.Now().Add(1*time.Hour))
|
||||||
if err != nil && !errors.Is(err, blob.ErrNotSupported) {
|
if err != nil && !errors.Is(err, blob.ErrNotSupported) {
|
||||||
return "", nil, fmt.Errorf("failed to get signed URL: %w", err)
|
return "", nil, fmt.Errorf("failed to get signed URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,7 @@ package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/harness/gitness/app/api/usererror"
|
"github.com/harness/gitness/app/api/usererror"
|
||||||
"github.com/harness/gitness/app/token"
|
"github.com/harness/gitness/app/token"
|
||||||
|
@ -69,10 +65,8 @@ func (c *Controller) Login(
|
||||||
return nil, usererror.ErrNotFound
|
return nil, usererror.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenIdentifier, err := GenerateSessionTokenIdentifier()
|
tokenIdentifier := token.GenerateIdentifier("login")
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
token, jwtToken, err := token.CreateUserSession(ctx, c.tokenStore, user, tokenIdentifier)
|
token, jwtToken, err := token.CreateUserSession(ctx, c.tokenStore, user, tokenIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -80,11 +74,3 @@ func (c *Controller) Login(
|
||||||
|
|
||||||
return &types.TokenResponse{Token: *token, AccessToken: jwtToken}, nil
|
return &types.TokenResponse{Token: *token, AccessToken: jwtToken}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateSessionTokenIdentifier() (string, error) {
|
|
||||||
r, err := rand.Int(rand.Reader, big.NewInt(10000))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to generate random number: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("login-%d-%04d", time.Now().Unix(), r.Int64()), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
apiauth "github.com/harness/gitness/app/api/auth"
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
|
"github.com/harness/gitness/app/api/render"
|
||||||
|
"github.com/harness/gitness/app/api/request"
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/app/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleLFSDownload(controller *lfs.Controller, urlProvider url.Provider) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
session, _ := request.AuthSessionFrom(ctx)
|
||||||
|
repoRef, err := request.GetRepoRefFromPath(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oid, err := request.GetObjectIDFromQuery(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := controller.Download(ctx, session, repoRef, oid)
|
||||||
|
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
||||||
|
render.GitBasicAuth(ctx, w, urlProvider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
// apply max byte size
|
||||||
|
render.Reader(ctx, w, http.StatusOK, file)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
apiauth "github.com/harness/gitness/app/api/auth"
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
|
"github.com/harness/gitness/app/api/render"
|
||||||
|
"github.com/harness/gitness/app/api/request"
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/app/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleLFSTransfer(lfsCtrl *lfs.Controller, urlProvider url.Provider) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
session, _ := request.AuthSessionFrom(ctx)
|
||||||
|
repoRef, err := request.GetRepoRefFromPath(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
in := new(lfs.TransferInput)
|
||||||
|
err = json.NewDecoder(r.Body).Decode(in)
|
||||||
|
if err != nil {
|
||||||
|
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.git-lfs+json")
|
||||||
|
out, err := lfsCtrl.LFSTransfer(ctx, session, repoRef, in)
|
||||||
|
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
||||||
|
render.GitBasicAuth(ctx, w, urlProvider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
// 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 lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
apiauth "github.com/harness/gitness/app/api/auth"
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
|
"github.com/harness/gitness/app/api/render"
|
||||||
|
"github.com/harness/gitness/app/api/request"
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/app/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleLFSUpload(controller *lfs.Controller, urlProvider url.Provider) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
session, _ := request.AuthSessionFrom(ctx)
|
||||||
|
repoRef, err := request.GetRepoRefFromPath(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oid, err := request.GetObjectIDFromQuery(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := request.GetObjectSizeFromQuery(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply max byte size from the request body
|
||||||
|
|
||||||
|
res, err := controller.Upload(ctx, session, repoRef, lfs.Pointer{OId: oid, Size: size}, r.Body)
|
||||||
|
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
||||||
|
render.GitBasicAuth(ctx, w, urlProvider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, http.StatusCreated, res)
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,7 @@ func HandleGitInfoRefs(repoCtrl *repo.Controller, urlProvider url.Provider) http
|
||||||
|
|
||||||
err = repoCtrl.GitInfoRefs(ctx, session, repoRef, service, gitProtocol, w)
|
err = repoCtrl.GitInfoRefs(ctx, session, repoRef, service, gitProtocol, w)
|
||||||
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
||||||
renderBasicAuth(ctx, w, urlProvider)
|
render.GitBasicAuth(ctx, w, urlProvider)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -67,14 +67,6 @@ func HandleGitInfoRefs(repoCtrl *repo.Controller, urlProvider url.Provider) http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderBasicAuth renders a response that indicates that the client (GIT) requires basic authentication.
|
|
||||||
// This is required in order to tell git CLI to query user credentials.
|
|
||||||
func renderBasicAuth(ctx context.Context, w http.ResponseWriter, urlProvider url.Provider) {
|
|
||||||
// Git doesn't seem to handle "realm" - so it doesn't seem to matter for basic user CLI interactions.
|
|
||||||
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, urlProvider.GetAPIHostname(ctx)))
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pktError(ctx context.Context, w http.ResponseWriter, err error) {
|
func pktError(ctx context.Context, w http.ResponseWriter, err error) {
|
||||||
terr := usererror.Translate(ctx, err)
|
terr := usererror.Translate(ctx, err)
|
||||||
w.WriteHeader(terr.Status)
|
w.WriteHeader(terr.Status)
|
||||||
|
|
|
@ -80,7 +80,7 @@ func HandleGitServicePack(
|
||||||
Protocol: gitProtocol,
|
Protocol: gitProtocol,
|
||||||
})
|
})
|
||||||
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
if errors.Is(err, apiauth.ErrNotAuthorized) && auth.IsAnonymousSession(session) {
|
||||||
renderBasicAuth(ctx, w, urlProvider)
|
render.GitBasicAuth(ctx, w, urlProvider)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -17,6 +17,7 @@ package render
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -24,6 +25,7 @@ import (
|
||||||
|
|
||||||
"github.com/harness/gitness/app/api/usererror"
|
"github.com/harness/gitness/app/api/usererror"
|
||||||
"github.com/harness/gitness/app/services/protection"
|
"github.com/harness/gitness/app/services/protection"
|
||||||
|
"github.com/harness/gitness/app/url"
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
"github.com/harness/gitness/git/api"
|
"github.com/harness/gitness/git/api"
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
|
@ -136,6 +138,14 @@ func Violations(w http.ResponseWriter, violations []types.RuleViolations) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GitBasicAuth renders a response that indicates that the client (GIT) requires basic authentication.
|
||||||
|
// This is required in order to tell git CLI to query user credentials.
|
||||||
|
func GitBasicAuth(ctx context.Context, w http.ResponseWriter, urlProvider url.Provider) {
|
||||||
|
// Git doesn't seem to handle "realm" - so it doesn't seem to matter for basic user CLI interactions.
|
||||||
|
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, urlProvider.GetAPIHostname(ctx)))
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
func setCommonHeaders(w http.ResponseWriter) {
|
func setCommonHeaders(w http.ResponseWriter) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
// 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 request
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
const (
|
||||||
|
QueryParamObjectID = "oid"
|
||||||
|
QueryParamObjectSize = "size"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetObjectIDFromQuery(r *http.Request) (string, error) {
|
||||||
|
return QueryParamOrError(r, QueryParamObjectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetObjectSizeFromQuery(r *http.Request) (int64, error) {
|
||||||
|
return QueryParamAsPositiveInt64OrError(r, QueryParamObjectSize)
|
||||||
|
}
|
|
@ -30,6 +30,12 @@ import (
|
||||||
gojwt "github.com/golang-jwt/jwt"
|
gojwt "github.com/golang-jwt/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerTokenPrefixBearer = "Bearer "
|
||||||
|
//nolint:gosec // wrong flagging
|
||||||
|
HeaderTokenPrefixRemoteAuth = "RemoteAuth "
|
||||||
|
)
|
||||||
|
|
||||||
var _ Authenticator = (*JWTAuthenticator)(nil)
|
var _ Authenticator = (*JWTAuthenticator)(nil)
|
||||||
|
|
||||||
// JWTAuthenticator uses the provided JWT to authenticate the caller.
|
// JWTAuthenticator uses the provided JWT to authenticate the caller.
|
||||||
|
@ -162,8 +168,11 @@ func extractToken(r *http.Request, cookieName string) string {
|
||||||
_, pwd, _ := r.BasicAuth()
|
_, pwd, _ := r.BasicAuth()
|
||||||
return pwd
|
return pwd
|
||||||
// strip bearer prefix if present
|
// strip bearer prefix if present
|
||||||
case strings.HasPrefix(headerToken, "Bearer "):
|
case strings.HasPrefix(headerToken, headerTokenPrefixBearer):
|
||||||
return headerToken[7:]
|
return headerToken[len(headerTokenPrefixBearer):]
|
||||||
|
// for ssh git-lfs-authenticate the returned token prefix would be RemoteAuth of type JWT
|
||||||
|
case strings.HasPrefix(headerToken, HeaderTokenPrefixRemoteAuth):
|
||||||
|
return headerToken[len(HeaderTokenPrefixRemoteAuth):]
|
||||||
// otherwise use value as is
|
// otherwise use value as is
|
||||||
case headerToken != "":
|
case headerToken != "":
|
||||||
return headerToken
|
return headerToken
|
||||||
|
|
|
@ -18,7 +18,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
"github.com/harness/gitness/app/api/controller/repo"
|
"github.com/harness/gitness/app/api/controller/repo"
|
||||||
|
handlerlfs "github.com/harness/gitness/app/api/handler/lfs"
|
||||||
handlerrepo "github.com/harness/gitness/app/api/handler/repo"
|
handlerrepo "github.com/harness/gitness/app/api/handler/repo"
|
||||||
middlewareauthn "github.com/harness/gitness/app/api/middleware/authn"
|
middlewareauthn "github.com/harness/gitness/app/api/middleware/authn"
|
||||||
middlewareauthz "github.com/harness/gitness/app/api/middleware/authz"
|
middlewareauthz "github.com/harness/gitness/app/api/middleware/authz"
|
||||||
|
@ -45,6 +47,7 @@ func NewGitHandler(
|
||||||
authenticator authn.Authenticator,
|
authenticator authn.Authenticator,
|
||||||
repoCtrl *repo.Controller,
|
repoCtrl *repo.Controller,
|
||||||
usageSender usage.Sender,
|
usageSender usage.Sender,
|
||||||
|
lfsCtrl *lfs.Controller,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
// maxRepoDepth depends on config
|
// maxRepoDepth depends on config
|
||||||
maxRepoDepth := check.MaxRepoPathDepth
|
maxRepoDepth := check.MaxRepoPathDepth
|
||||||
|
@ -98,6 +101,9 @@ func NewGitHandler(
|
||||||
r.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", stubGitHandler())
|
r.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", stubGitHandler())
|
||||||
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", stubGitHandler())
|
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", stubGitHandler())
|
||||||
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", stubGitHandler())
|
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", stubGitHandler())
|
||||||
|
|
||||||
|
// Git LFS API
|
||||||
|
GitLFSHandler(r, lfsCtrl, urlProvider)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -111,3 +117,14 @@ func stubGitHandler() http.HandlerFunc {
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GitLFSHandler(r chi.Router, lfsCtrl *lfs.Controller, urlProvider url.Provider) {
|
||||||
|
r.Route("/info/lfs", func(r chi.Router) {
|
||||||
|
r.Route("/objects", func(r chi.Router) {
|
||||||
|
r.Post("/batch", handlerlfs.HandleLFSTransfer(lfsCtrl, urlProvider))
|
||||||
|
// direct download and upload handlers for lfs objects
|
||||||
|
r.Put("/", handlerlfs.HandleLFSUpload(lfsCtrl, urlProvider))
|
||||||
|
r.Get("/", handlerlfs.HandleLFSDownload(lfsCtrl, urlProvider))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/harness/gitness/app/api/controller/gitspace"
|
"github.com/harness/gitness/app/api/controller/gitspace"
|
||||||
"github.com/harness/gitness/app/api/controller/infraprovider"
|
"github.com/harness/gitness/app/api/controller/infraprovider"
|
||||||
"github.com/harness/gitness/app/api/controller/keywordsearch"
|
"github.com/harness/gitness/app/api/controller/keywordsearch"
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
"github.com/harness/gitness/app/api/controller/logs"
|
"github.com/harness/gitness/app/api/controller/logs"
|
||||||
"github.com/harness/gitness/app/api/controller/migrate"
|
"github.com/harness/gitness/app/api/controller/migrate"
|
||||||
"github.com/harness/gitness/app/api/controller/pipeline"
|
"github.com/harness/gitness/app/api/controller/pipeline"
|
||||||
|
@ -110,6 +111,7 @@ func ProvideRouter(
|
||||||
openapi openapi.Service,
|
openapi openapi.Service,
|
||||||
registryRouter router.AppRouter,
|
registryRouter router.AppRouter,
|
||||||
usageSender usage.Sender,
|
usageSender usage.Sender,
|
||||||
|
lfsCtrl *lfs.Controller,
|
||||||
) *Router {
|
) *Router {
|
||||||
routers := make([]Interface, 4)
|
routers := make([]Interface, 4)
|
||||||
|
|
||||||
|
@ -120,6 +122,7 @@ func ProvideRouter(
|
||||||
authenticator,
|
authenticator,
|
||||||
repoCtrl,
|
repoCtrl,
|
||||||
usageSender,
|
usageSender,
|
||||||
|
lfsCtrl,
|
||||||
)
|
)
|
||||||
routers[0] = NewGitRouter(gitHandler, gitRoutingHost)
|
routers[0] = NewGitRouter(gitHandler, gitRoutingHost)
|
||||||
routers[1] = router.NewRegistryRouter(registryRouter)
|
routers[1] = router.NewRegistryRouter(registryRouter)
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
// 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 remoteauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/store"
|
||||||
|
"github.com/harness/gitness/app/token"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
// GenerateToken generates a jwt for the given principle to access the resource (for git-lfs-authenticate response)
|
||||||
|
GenerateToken(
|
||||||
|
ctx context.Context,
|
||||||
|
principalID int64,
|
||||||
|
principalType enum.PrincipalType,
|
||||||
|
resource string,
|
||||||
|
) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(tokenStore store.TokenStore, principalStore store.PrincipalStore) LocalService {
|
||||||
|
return LocalService{
|
||||||
|
tokenStore: tokenStore,
|
||||||
|
principalStore: principalStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalService struct {
|
||||||
|
tokenStore store.TokenStore
|
||||||
|
principalStore store.PrincipalStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s LocalService) GenerateToken(
|
||||||
|
ctx context.Context,
|
||||||
|
principalID int64,
|
||||||
|
_ enum.PrincipalType,
|
||||||
|
_ string,
|
||||||
|
) (string, error) {
|
||||||
|
identifier := token.GenerateIdentifier("remoteAuth")
|
||||||
|
|
||||||
|
principal, err := s.principalStore.Find(ctx, principalID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to find principal %d: %w", principalID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, jwt, err := token.CreateRemoteAuthToken(ctx, s.tokenStore, principal, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create a remote auth token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt, nil
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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 remoteauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/harness/gitness/app/store"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WireSet = wire.NewSet(
|
||||||
|
ProvideRemoteAuth,
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideRemoteAuth(
|
||||||
|
tokenStore store.TokenStore,
|
||||||
|
principalStore store.PrincipalStore,
|
||||||
|
) Service {
|
||||||
|
return NewService(tokenStore, principalStore)
|
||||||
|
}
|
|
@ -1299,6 +1299,15 @@ type (
|
||||||
) (map[int64][]*types.LabelPullReqAssignmentInfo, error)
|
) (map[int64][]*types.LabelPullReqAssignmentInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LFSObjectStore interface {
|
||||||
|
// Find finds an LFS object with a specified oid and repo-id.
|
||||||
|
Find(ctx context.Context, repoID int64, oid string) (*types.LFSObject, error)
|
||||||
|
// FindMany finds LFS objects for a specified repo.
|
||||||
|
FindMany(ctx context.Context, repoID int64, oids []string) ([]*types.LFSObject, error)
|
||||||
|
// Create creates an LFS object.
|
||||||
|
Create(ctx context.Context, lfsObject *types.LFSObject) error
|
||||||
|
}
|
||||||
|
|
||||||
InfraProviderTemplateStore interface {
|
InfraProviderTemplateStore interface {
|
||||||
FindByIdentifier(ctx context.Context, spaceID int64, identifier string) (*types.InfraProviderTemplate, error)
|
FindByIdentifier(ctx context.Context, spaceID int64, identifier string) (*types.InfraProviderTemplate, error)
|
||||||
Find(ctx context.Context, id int64) (*types.InfraProviderTemplate, error)
|
Find(ctx context.Context, id int64) (*types.InfraProviderTemplate, error)
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
// 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 database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/store"
|
||||||
|
"github.com/harness/gitness/store/database"
|
||||||
|
"github.com/harness/gitness/store/database/dbtx"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ store.LFSObjectStore = (*LFSObjectStore)(nil)
|
||||||
|
|
||||||
|
func NewLFSObjectStore(db *sqlx.DB) *LFSObjectStore {
|
||||||
|
return &LFSObjectStore{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LFSObjectStore struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type lfsObject struct {
|
||||||
|
ID int64 `db:"lfs_object_id"`
|
||||||
|
OID string `db:"lfs_object_oid"`
|
||||||
|
Size int64 `db:"lfs_object_size"`
|
||||||
|
Created int64 `db:"lfs_object_created"`
|
||||||
|
CreatedBy int64 `db:"lfs_object_created_by"`
|
||||||
|
RepoID int64 `db:"lfs_object_repo_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
lfsObjectColumns = `
|
||||||
|
lfs_object_id
|
||||||
|
,lfs_object_oid
|
||||||
|
,lfs_object_size
|
||||||
|
,lfs_object_created
|
||||||
|
,lfs_object_created_by
|
||||||
|
,lfs_object_repo_id`
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *LFSObjectStore) Find(
|
||||||
|
ctx context.Context,
|
||||||
|
repoID int64,
|
||||||
|
oid string,
|
||||||
|
) (*types.LFSObject, error) {
|
||||||
|
stmt := database.Builder.
|
||||||
|
Select(lfsObjectColumns).
|
||||||
|
From("lfs_objects").
|
||||||
|
Where("lfs_object_repo_id = ? AND lfs_object_oid = ?", repoID, oid)
|
||||||
|
|
||||||
|
sql, args, err := stmt.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert query to sql: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db := dbtx.GetAccessor(ctx, s.db)
|
||||||
|
|
||||||
|
dst := &lfsObject{}
|
||||||
|
if err := db.GetContext(ctx, dst, sql, args...); err != nil {
|
||||||
|
return nil, database.ProcessSQLErrorf(ctx, err, "Select query failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapLFSObject(dst), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LFSObjectStore) FindMany(
|
||||||
|
ctx context.Context,
|
||||||
|
repoID int64,
|
||||||
|
oids []string,
|
||||||
|
) ([]*types.LFSObject, error) {
|
||||||
|
stmt := database.Builder.
|
||||||
|
Select(lfsObjectColumns).
|
||||||
|
From("lfs_objects").
|
||||||
|
Where("lfs_object_repo_id = ?", repoID).
|
||||||
|
Where(squirrel.Eq{"lfs_object_oid": oids})
|
||||||
|
|
||||||
|
sql, args, err := stmt.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert query to sql: %w", err)
|
||||||
|
}
|
||||||
|
db := dbtx.GetAccessor(ctx, s.db)
|
||||||
|
|
||||||
|
var dst []*lfsObject
|
||||||
|
if err := db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
||||||
|
return nil, database.ProcessSQLErrorf(ctx, err, "Select query failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapLFSObjects(dst), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LFSObjectStore) Create(ctx context.Context, obj *types.LFSObject) error {
|
||||||
|
const sqlQuery = `
|
||||||
|
INSERT INTO lfs_objects (
|
||||||
|
lfs_object_oid
|
||||||
|
,lfs_object_size
|
||||||
|
,lfs_object_created
|
||||||
|
,lfs_object_created_by
|
||||||
|
,lfs_object_repo_id
|
||||||
|
) VALUES (
|
||||||
|
:lfs_object_oid
|
||||||
|
,:lfs_object_size
|
||||||
|
,:lfs_object_created
|
||||||
|
,:lfs_object_created_by
|
||||||
|
,:lfs_object_repo_id
|
||||||
|
) RETURNING lfs_object_id`
|
||||||
|
|
||||||
|
db := dbtx.GetAccessor(ctx, s.db)
|
||||||
|
query, args, err := db.BindNamed(sqlQuery, mapInternalLFSObject(obj))
|
||||||
|
if err != nil {
|
||||||
|
return database.ProcessSQLErrorf(ctx, err, "Failed to bind query")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.QueryRowContext(ctx, query, args...).Scan(&obj.ID); err != nil {
|
||||||
|
return database.ProcessSQLErrorf(ctx, err, "Failed to create LFS object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapInternalLFSObject(obj *types.LFSObject) *lfsObject {
|
||||||
|
return &lfsObject{
|
||||||
|
ID: obj.ID,
|
||||||
|
OID: obj.OID,
|
||||||
|
Size: obj.Size,
|
||||||
|
Created: obj.Created,
|
||||||
|
CreatedBy: obj.CreatedBy,
|
||||||
|
RepoID: obj.RepoID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapLFSObject(obj *lfsObject) *types.LFSObject {
|
||||||
|
return &types.LFSObject{
|
||||||
|
ID: obj.ID,
|
||||||
|
OID: obj.OID,
|
||||||
|
Size: obj.Size,
|
||||||
|
Created: obj.Created,
|
||||||
|
CreatedBy: obj.CreatedBy,
|
||||||
|
RepoID: obj.RepoID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapLFSObjects(objs []*lfsObject) []*types.LFSObject {
|
||||||
|
res := make([]*types.LFSObject, len(objs))
|
||||||
|
for i := range objs {
|
||||||
|
res[i] = mapLFSObject(objs[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
DROP INDEX lfs_objects_oid;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS lfs_objects;
|
|
@ -0,0 +1,19 @@
|
||||||
|
CREATE TABLE lfs_objects (
|
||||||
|
lfs_object_id SERIAL PRIMARY KEY
|
||||||
|
,lfs_object_oid TEXT NOT NULL
|
||||||
|
,lfs_object_size BIGINT NOT NULL
|
||||||
|
,lfs_object_created BIGINT NOT NULL
|
||||||
|
,lfs_object_created_by INTEGER NOT NULL
|
||||||
|
,lfs_object_repo_id INTEGER
|
||||||
|
,CONSTRAINT fk_lfs_object_repo_id FOREIGN KEY (lfs_object_repo_id)
|
||||||
|
REFERENCES repositories (repo_id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE SET NULL
|
||||||
|
,CONSTRAINT fk_lfs_object_created_by FOREIGN KEY (lfs_object_created_by)
|
||||||
|
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX lfs_objects_oid
|
||||||
|
ON lfs_objects(lfs_object_repo_id, lfs_object_oid);
|
|
@ -0,0 +1,3 @@
|
||||||
|
DROP INDEX lfs_objects_oid;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS lfs_objects;
|
|
@ -0,0 +1,19 @@
|
||||||
|
CREATE TABLE lfs_objects (
|
||||||
|
lfs_object_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||||
|
,lfs_object_oid TEXT NOT NULL
|
||||||
|
,lfs_object_size BIGINT NOT NULL
|
||||||
|
,lfs_object_created BIGINT NOT NULL
|
||||||
|
,lfs_object_created_by INTEGER NOT NULL
|
||||||
|
,lfs_object_repo_id INTEGER
|
||||||
|
,CONSTRAINT fk_lfs_object_repo_id FOREIGN KEY (lfs_object_repo_id)
|
||||||
|
REFERENCES repositories (repo_id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE SET NULL
|
||||||
|
,CONSTRAINT fk_lfs_object_created_by FOREIGN KEY (lfs_object_created_by)
|
||||||
|
REFERENCES principals (principal_id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
ON DELETE NO ACTION
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX lfs_objects_oid
|
||||||
|
ON lfs_objects(lfs_object_repo_id, lfs_object_oid);
|
|
@ -70,6 +70,7 @@ var WireSet = wire.NewSet(
|
||||||
ProvideLabelStore,
|
ProvideLabelStore,
|
||||||
ProvideLabelValueStore,
|
ProvideLabelValueStore,
|
||||||
ProvidePullReqLabelStore,
|
ProvidePullReqLabelStore,
|
||||||
|
ProvideLFSObjectStore,
|
||||||
ProvideInfraProviderTemplateStore,
|
ProvideInfraProviderTemplateStore,
|
||||||
ProvideInfraProvisionedStore,
|
ProvideInfraProvisionedStore,
|
||||||
ProvideUsageMetricStore,
|
ProvideUsageMetricStore,
|
||||||
|
@ -336,6 +337,11 @@ func ProvidePullReqLabelStore(db *sqlx.DB) store.PullReqLabelAssignmentStore {
|
||||||
return NewPullReqLabelStore(db)
|
return NewPullReqLabelStore(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideLFSObjectStore provides an lfs object store.
|
||||||
|
func ProvideLFSObjectStore(db *sqlx.DB) store.LFSObjectStore {
|
||||||
|
return NewLFSObjectStore(db)
|
||||||
|
}
|
||||||
|
|
||||||
// ProvideInfraProviderTemplateStore provides a infraprovider template store.
|
// ProvideInfraProviderTemplateStore provides a infraprovider template store.
|
||||||
func ProvideInfraProviderTemplateStore(db *sqlx.DB) store.InfraProviderTemplateStore {
|
func ProvideInfraProviderTemplateStore(db *sqlx.DB) store.InfraProviderTemplateStore {
|
||||||
return NewInfraProviderTemplateStore(db)
|
return NewInfraProviderTemplateStore(db)
|
||||||
|
|
|
@ -17,6 +17,7 @@ package token
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/harness/gitness/app/jwt"
|
"github.com/harness/gitness/app/jwt"
|
||||||
|
@ -32,6 +33,7 @@ const (
|
||||||
// NOTE: Users can list / delete session tokens via rest API if they want to cleanup earlier.
|
// NOTE: Users can list / delete session tokens via rest API if they want to cleanup earlier.
|
||||||
userSessionTokenLifeTime time.Duration = 30 * 24 * time.Hour // 30 days.
|
userSessionTokenLifeTime time.Duration = 30 * 24 * time.Hour // 30 days.
|
||||||
sessionTokenWithAccessPermissionsLifeTime time.Duration = 24 * time.Hour // 24 hours.
|
sessionTokenWithAccessPermissionsLifeTime time.Duration = 24 * time.Hour // 24 hours.
|
||||||
|
RemoteAuthTokenLifeTime time.Duration = 15 * time.Minute // 15 minutes.
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateUserWithAccessPermissions(
|
func CreateUserWithAccessPermissions(
|
||||||
|
@ -102,6 +104,29 @@ func CreateSAT(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateRemoteAuthToken(
|
||||||
|
ctx context.Context,
|
||||||
|
tokenStore store.TokenStore,
|
||||||
|
principal *types.Principal,
|
||||||
|
identifier string,
|
||||||
|
) (*types.Token, string, error) {
|
||||||
|
return create(
|
||||||
|
ctx,
|
||||||
|
tokenStore,
|
||||||
|
enum.TokenTypeRemoteAuth,
|
||||||
|
principal,
|
||||||
|
principal,
|
||||||
|
identifier,
|
||||||
|
ptr.Duration(RemoteAuthTokenLifeTime),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateIdentifier(prefix string) string {
|
||||||
|
//nolint:gosec // math/rand is sufficient for this use case
|
||||||
|
r := rand.IntN(0x10000)
|
||||||
|
return fmt.Sprintf("%s-%08x-%04x", prefix, time.Now().Unix(), r)
|
||||||
|
}
|
||||||
|
|
||||||
func create(
|
func create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tokenStore store.TokenStore,
|
tokenStore store.TokenStore,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -81,7 +82,7 @@ func (c FileSystemStore) Upload(ctx context.Context,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c FileSystemStore) GetSignedURL(_ context.Context, _ string) (string, error) {
|
func (c FileSystemStore) GetSignedURL(context.Context, string, time.Time) (string, error) {
|
||||||
return "", ErrNotSupported
|
return "", ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
blob/gcs.go
29
blob/gcs.go
|
@ -16,6 +16,7 @@ package blob
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -65,7 +66,7 @@ func NewGCSStore(ctx context.Context, cfg Config) (Store, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GCSStore) Upload(ctx context.Context, file io.Reader, filePath string) error {
|
func (c *GCSStore) Upload(ctx context.Context, file io.Reader, filePath string) error {
|
||||||
gcsClient, err := c.getLatestClient(ctx)
|
gcsClient, err := c.getClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to retrieve latest client: %w", err)
|
return fmt.Errorf("failed to retrieve latest client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -93,8 +94,8 @@ func (c *GCSStore) Upload(ctx context.Context, file io.Reader, filePath string)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GCSStore) GetSignedURL(ctx context.Context, filePath string) (string, error) {
|
func (c *GCSStore) GetSignedURL(ctx context.Context, filePath string, expire time.Time) (string, error) {
|
||||||
gcsClient, err := c.getLatestClient(ctx)
|
gcsClient, err := c.getClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to retrieve latest client: %w", err)
|
return "", fmt.Errorf("failed to retrieve latest client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -102,7 +103,7 @@ func (c *GCSStore) GetSignedURL(ctx context.Context, filePath string) (string, e
|
||||||
bkt := gcsClient.Bucket(c.config.Bucket)
|
bkt := gcsClient.Bucket(c.config.Bucket)
|
||||||
signedURL, err := bkt.SignedURL(filePath, &storage.SignedURLOptions{
|
signedURL, err := bkt.SignedURL(filePath, &storage.SignedURLOptions{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Expires: time.Now().Add(1 * time.Hour),
|
Expires: expire,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create signed URL for file %q: %w", filePath, err)
|
return "", fmt.Errorf("failed to create signed URL for file %q: %w", filePath, err)
|
||||||
|
@ -110,8 +111,22 @@ func (c *GCSStore) GetSignedURL(ctx context.Context, filePath string) (string, e
|
||||||
return signedURL, nil
|
return signedURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GCSStore) Download(_ context.Context, _ string) (io.ReadCloser, error) {
|
func (c *GCSStore) Download(ctx context.Context, filePath string) (io.ReadCloser, error) {
|
||||||
return nil, fmt.Errorf("not implemented")
|
gcsClient, err := c.getClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve latest client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bkt := gcsClient.Bucket(c.config.Bucket)
|
||||||
|
rc, err := bkt.Object(filePath).NewReader(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrObjectNotExist) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to create reader for file %q in bucket %q: %w", filePath, c.config.Bucket, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNewImpersonatedClient(ctx context.Context, cfg Config) (*storage.Client, error) {
|
func createNewImpersonatedClient(ctx context.Context, cfg Config) (*storage.Client, error) {
|
||||||
|
@ -138,7 +153,7 @@ func createNewImpersonatedClient(ctx context.Context, cfg Config) (*storage.Clie
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GCSStore) getLatestClient(ctx context.Context) (*storage.Client, error) {
|
func (c *GCSStore) getClient(ctx context.Context) (*storage.Client, error) {
|
||||||
err := c.checkAndRefreshToken(ctx)
|
err := c.checkAndRefreshToken(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to refresh token: %w", err)
|
return nil, fmt.Errorf("failed to refresh token: %w", err)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -30,7 +31,7 @@ type Store interface {
|
||||||
Upload(ctx context.Context, file io.Reader, filePath string) error
|
Upload(ctx context.Context, file io.Reader, filePath string) error
|
||||||
|
|
||||||
// GetSignedURL returns the URL for a file in the blob store.
|
// GetSignedURL returns the URL for a file in the blob store.
|
||||||
GetSignedURL(ctx context.Context, filePath string) (string, error)
|
GetSignedURL(ctx context.Context, filePath string, expire time.Time) (string, error)
|
||||||
|
|
||||||
// Download returns a reader for a file in the blob store.
|
// Download returns a reader for a file in the blob store.
|
||||||
Download(ctx context.Context, filePath string) (io.ReadCloser, error)
|
Download(ctx context.Context, filePath string) (io.ReadCloser, error)
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
gitspaceCtrl "github.com/harness/gitness/app/api/controller/gitspace"
|
gitspaceCtrl "github.com/harness/gitness/app/api/controller/gitspace"
|
||||||
infraproviderCtrl "github.com/harness/gitness/app/api/controller/infraprovider"
|
infraproviderCtrl "github.com/harness/gitness/app/api/controller/infraprovider"
|
||||||
controllerkeywordsearch "github.com/harness/gitness/app/api/controller/keywordsearch"
|
controllerkeywordsearch "github.com/harness/gitness/app/api/controller/keywordsearch"
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
"github.com/harness/gitness/app/api/controller/limiter"
|
"github.com/harness/gitness/app/api/controller/limiter"
|
||||||
controllerlogs "github.com/harness/gitness/app/api/controller/logs"
|
controllerlogs "github.com/harness/gitness/app/api/controller/logs"
|
||||||
"github.com/harness/gitness/app/api/controller/migrate"
|
"github.com/harness/gitness/app/api/controller/migrate"
|
||||||
|
@ -99,6 +100,7 @@ import (
|
||||||
"github.com/harness/gitness/app/services/publickey"
|
"github.com/harness/gitness/app/services/publickey"
|
||||||
pullreqservice "github.com/harness/gitness/app/services/pullreq"
|
pullreqservice "github.com/harness/gitness/app/services/pullreq"
|
||||||
"github.com/harness/gitness/app/services/refcache"
|
"github.com/harness/gitness/app/services/refcache"
|
||||||
|
"github.com/harness/gitness/app/services/remoteauth"
|
||||||
reposervice "github.com/harness/gitness/app/services/repo"
|
reposervice "github.com/harness/gitness/app/services/repo"
|
||||||
"github.com/harness/gitness/app/services/rules"
|
"github.com/harness/gitness/app/services/rules"
|
||||||
secretservice "github.com/harness/gitness/app/services/secret"
|
secretservice "github.com/harness/gitness/app/services/secret"
|
||||||
|
@ -253,6 +255,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
|
||||||
audit.WireSet,
|
audit.WireSet,
|
||||||
ssh.WireSet,
|
ssh.WireSet,
|
||||||
publickey.WireSet,
|
publickey.WireSet,
|
||||||
|
remoteauth.WireSet,
|
||||||
migrate.WireSet,
|
migrate.WireSet,
|
||||||
scm.WireSet,
|
scm.WireSet,
|
||||||
platformconnector.WireSet,
|
platformconnector.WireSet,
|
||||||
|
@ -274,6 +277,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
|
||||||
docker.ProvideReporter,
|
docker.ProvideReporter,
|
||||||
secretservice.WireSet,
|
secretservice.WireSet,
|
||||||
runarg.WireSet,
|
runarg.WireSet,
|
||||||
|
lfs.WireSet,
|
||||||
usage.WireSet,
|
usage.WireSet,
|
||||||
registryevents.WireSet,
|
registryevents.WireSet,
|
||||||
registrywebhooks.WireSet,
|
registrywebhooks.WireSet,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
gitspace2 "github.com/harness/gitness/app/api/controller/gitspace"
|
gitspace2 "github.com/harness/gitness/app/api/controller/gitspace"
|
||||||
infraprovider3 "github.com/harness/gitness/app/api/controller/infraprovider"
|
infraprovider3 "github.com/harness/gitness/app/api/controller/infraprovider"
|
||||||
keywordsearch2 "github.com/harness/gitness/app/api/controller/keywordsearch"
|
keywordsearch2 "github.com/harness/gitness/app/api/controller/keywordsearch"
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
"github.com/harness/gitness/app/api/controller/limiter"
|
"github.com/harness/gitness/app/api/controller/limiter"
|
||||||
logs2 "github.com/harness/gitness/app/api/controller/logs"
|
logs2 "github.com/harness/gitness/app/api/controller/logs"
|
||||||
migrate2 "github.com/harness/gitness/app/api/controller/migrate"
|
migrate2 "github.com/harness/gitness/app/api/controller/migrate"
|
||||||
|
@ -90,6 +91,7 @@ import (
|
||||||
"github.com/harness/gitness/app/services/publickey"
|
"github.com/harness/gitness/app/services/publickey"
|
||||||
"github.com/harness/gitness/app/services/pullreq"
|
"github.com/harness/gitness/app/services/pullreq"
|
||||||
"github.com/harness/gitness/app/services/refcache"
|
"github.com/harness/gitness/app/services/refcache"
|
||||||
|
"github.com/harness/gitness/app/services/remoteauth"
|
||||||
repo2 "github.com/harness/gitness/app/services/repo"
|
repo2 "github.com/harness/gitness/app/services/repo"
|
||||||
"github.com/harness/gitness/app/services/rules"
|
"github.com/harness/gitness/app/services/rules"
|
||||||
secret3 "github.com/harness/gitness/app/services/secret"
|
secret3 "github.com/harness/gitness/app/services/secret"
|
||||||
|
@ -409,7 +411,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
githookController := githook.ProvideController(authorizer, principalStore, repoStore, repoFinder, reporter5, reporter, gitInterface, pullReqStore, provider, protectionManager, clientFactory, resourceLimiter, settingsService, preReceiveExtender, updateExtender, postReceiveExtender, streamer)
|
lfsObjectStore := database.ProvideLFSObjectStore(db)
|
||||||
|
githookController := githook.ProvideController(authorizer, principalStore, repoStore, repoFinder, reporter5, reporter, gitInterface, pullReqStore, provider, protectionManager, clientFactory, resourceLimiter, settingsService, preReceiveExtender, updateExtender, postReceiveExtender, streamer, lfsObjectStore)
|
||||||
serviceaccountController := serviceaccount.NewController(principalUID, authorizer, principalStore, spaceStore, repoStore, tokenStore)
|
serviceaccountController := serviceaccount.NewController(principalUID, authorizer, principalStore, spaceStore, repoStore, tokenStore)
|
||||||
principalController := principal.ProvideController(principalStore, authorizer)
|
principalController := principal.ProvideController(principalStore, authorizer)
|
||||||
usergroupController := usergroup2.ProvideController(userGroupStore, spaceStore, authorizer, searchService)
|
usergroupController := usergroup2.ProvideController(userGroupStore, spaceStore, authorizer, searchService)
|
||||||
|
@ -504,10 +507,12 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
||||||
handler4 := router.PackageHandlerProvider(packagesHandler, mavenHandler, genericHandler, pypiHandler)
|
handler4 := router.PackageHandlerProvider(packagesHandler, mavenHandler, genericHandler, pypiHandler)
|
||||||
appRouter := router.AppRouterProvider(registryOCIHandler, apiHandler, handler2, handler3, handler4)
|
appRouter := router.AppRouterProvider(registryOCIHandler, apiHandler, handler2, handler3, handler4)
|
||||||
sender := usage.ProvideMediator(ctx, config, spaceFinder, usageMetricStore)
|
sender := usage.ProvideMediator(ctx, config, spaceFinder, usageMetricStore)
|
||||||
routerRouter := router2.ProvideRouter(ctx, config, authenticator, repoController, reposettingsController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, usergroupController, checkController, systemController, uploadController, keywordsearchController, infraproviderController, gitspaceController, migrateController, provider, openapiService, appRouter, sender)
|
remoteauthService := remoteauth.ProvideRemoteAuth(tokenStore, principalStore)
|
||||||
|
lfsController := lfs.ProvideController(authorizer, repoFinder, principalStore, lfsObjectStore, blobStore, remoteauthService, provider)
|
||||||
|
routerRouter := router2.ProvideRouter(ctx, config, authenticator, repoController, reposettingsController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, usergroupController, checkController, systemController, uploadController, keywordsearchController, infraproviderController, gitspaceController, migrateController, provider, openapiService, appRouter, sender, lfsController)
|
||||||
serverServer := server2.ProvideServer(config, routerRouter)
|
serverServer := server2.ProvideServer(config, routerRouter)
|
||||||
publickeyService := publickey.ProvidePublicKey(publicKeyStore, principalInfoCache)
|
publickeyService := publickey.ProvidePublicKey(publicKeyStore, principalInfoCache)
|
||||||
sshServer := ssh.ProvideServer(config, publickeyService, repoController)
|
sshServer := ssh.ProvideServer(config, publickeyService, repoController, lfsController)
|
||||||
executionManager := manager.ProvideExecutionManager(config, executionStore, pipelineStore, provider, streamer, fileService, converterService, logStore, logStream, checkStore, repoStore, schedulerScheduler, secretStore, stageStore, stepStore, principalStore, publicaccessService, reporter3)
|
executionManager := manager.ProvideExecutionManager(config, executionStore, pipelineStore, provider, streamer, fileService, converterService, logStore, logStream, checkStore, repoStore, schedulerScheduler, secretStore, stageStore, stepStore, principalStore, publicaccessService, reporter3)
|
||||||
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)
|
||||||
|
|
92
git/blob.go
92
git/blob.go
|
@ -16,12 +16,20 @@ package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/errors"
|
||||||
"github.com/harness/gitness/git/api"
|
"github.com/harness/gitness/git/api"
|
||||||
|
"github.com/harness/gitness/git/parser"
|
||||||
"github.com/harness/gitness/git/sha"
|
"github.com/harness/gitness/git/sha"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// lfsPointerMaxSize is the maximum size for an LFS pointer file.
|
||||||
|
// This is used to identify blobs that are too large to be valid LFS pointers.
|
||||||
|
// lfs-pointer specification ref: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer
|
||||||
|
const lfsPointerMaxSize = 200
|
||||||
|
|
||||||
type GetBlobParams struct {
|
type GetBlobParams struct {
|
||||||
ReadParams
|
ReadParams
|
||||||
SHA string
|
SHA string
|
||||||
|
@ -64,3 +72,87 @@ func (s *Service) GetBlob(ctx context.Context, params *GetBlobParams) (*GetBlobO
|
||||||
Content: reader.Content,
|
Content: reader.Content,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListLFSPointersParams struct {
|
||||||
|
ReadParams
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLFSPointersOutput struct {
|
||||||
|
LFSInfos []LFSInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type LFSInfo struct {
|
||||||
|
OID string `json:"oid"`
|
||||||
|
SHA sha.SHA `json:"sha"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListLFSPointers(
|
||||||
|
ctx context.Context,
|
||||||
|
params *ListLFSPointersParams,
|
||||||
|
) (*ListLFSPointersOutput, error) {
|
||||||
|
if params.RepoUID == "" {
|
||||||
|
return nil, api.ErrRepositoryPathEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||||
|
|
||||||
|
var lfsInfos []LFSInfo
|
||||||
|
var candidateObjects []parser.BatchCheckObject
|
||||||
|
// first get the sha of the objects that could be lfs pointers
|
||||||
|
for _, gitObjDir := range params.AlternateObjectDirs {
|
||||||
|
objects, err := catFileBatchCheckAllObjects(ctx, repoPath, gitObjDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range objects {
|
||||||
|
if obj.Type == string(TreeNodeTypeBlob) && obj.Size <= lfsPointerMaxSize {
|
||||||
|
candidateObjects = append(candidateObjects, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidateObjects) == 0 {
|
||||||
|
return &ListLFSPointersOutput{LFSInfos: lfsInfos}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the short-listed objects for lfs-pointers content
|
||||||
|
stdIn, stdOut, cancel := api.CatFileBatch(ctx, repoPath, params.AlternateObjectDirs)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
for _, obj := range candidateObjects {
|
||||||
|
line := obj.SHA.String() + "\n"
|
||||||
|
|
||||||
|
_, err := stdIn.Write([]byte(line))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write blob sha to git stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// first line is always the object type, sha, and size
|
||||||
|
_, err = stdOut.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read the git cat-file output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := io.ReadAll(io.LimitReader(stdOut, obj.Size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read the git cat-file output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oid, err := parser.GetLFSOID(content)
|
||||||
|
if err != nil && !errors.Is(err, parser.ErrInvalidLFSPointer) {
|
||||||
|
return nil, fmt.Errorf("failed to scan git cat-file output for %s: %w", obj.SHA, err)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
lfsInfos = append(lfsInfos, LFSInfo{OID: oid, SHA: obj.SHA})
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip the trailing new line
|
||||||
|
_, err = stdOut.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read trailing newline after object: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ListLFSPointersOutput{LFSInfos: lfsInfos}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ type Interface interface {
|
||||||
GetRef(ctx context.Context, params GetRefParams) (GetRefResponse, error)
|
GetRef(ctx context.Context, params GetRefParams) (GetRefResponse, error)
|
||||||
PathsDetails(ctx context.Context, params PathsDetailsParams) (PathsDetailsOutput, error)
|
PathsDetails(ctx context.Context, params PathsDetailsParams) (PathsDetailsOutput, error)
|
||||||
Summary(ctx context.Context, params SummaryParams) (SummaryOutput, error)
|
Summary(ctx context.Context, params SummaryParams) (SummaryOutput, error)
|
||||||
|
ListLFSPointers(ctx context.Context, params *ListLFSPointersParams) (*ListLFSPointersOutput, error)
|
||||||
|
|
||||||
// GetRepositorySize calculates the size of a repo in KiB.
|
// GetRepositorySize calculates the size of a repo in KiB.
|
||||||
GetRepositorySize(ctx context.Context, params *GetRepositorySizeParams) (*GetRepositorySizeOutput, error)
|
GetRepositorySize(ctx context.Context, params *GetRepositorySizeParams) (*GetRepositorySizeOutput, error)
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
// 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 parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const lfsPointerVersionPrefix = "version https://git-lfs.github.com/spec"
|
||||||
|
|
||||||
|
var (
|
||||||
|
regexLFSOID = regexp.MustCompile(`(?m)^oid sha256:([a-f0-9]{64})$`)
|
||||||
|
regexLFSSize = regexp.MustCompile(`(?m)^size [0-9]+$`)
|
||||||
|
|
||||||
|
ErrInvalidLFSPointer = errors.New("invalid lfs pointer")
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetLFSOID(content []byte) (string, error) {
|
||||||
|
if !bytes.HasPrefix(content, []byte(lfsPointerVersionPrefix)) {
|
||||||
|
return "", ErrInvalidLFSPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
oidMatch := regexLFSOID.FindSubmatch(content)
|
||||||
|
if oidMatch == nil {
|
||||||
|
return "", ErrInvalidLFSPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
if !regexLFSSize.Match(content) {
|
||||||
|
return "", ErrInvalidLFSPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(oidMatch[1]), nil
|
||||||
|
}
|
1
go.mod
1
go.mod
|
@ -152,6 +152,7 @@ require (
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -691,8 +691,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
|
||||||
github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY=
|
github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY=
|
||||||
github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ=
|
github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
@ -1053,6 +1053,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -30,6 +31,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
"github.com/harness/gitness/app/api/controller/repo"
|
"github.com/harness/gitness/app/api/controller/repo"
|
||||||
"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"
|
||||||
|
@ -53,6 +55,8 @@ var (
|
||||||
allowedCommands = []string{
|
allowedCommands = []string{
|
||||||
"git-upload-pack",
|
"git-upload-pack",
|
||||||
"git-receive-pack",
|
"git-receive-pack",
|
||||||
|
"git-lfs-authenticate",
|
||||||
|
"git-lfs-transfer",
|
||||||
}
|
}
|
||||||
defaultCiphers = []string{
|
defaultCiphers = []string{
|
||||||
"chacha20-poly1305@openssh.com",
|
"chacha20-poly1305@openssh.com",
|
||||||
|
@ -97,6 +101,7 @@ type Server struct {
|
||||||
|
|
||||||
Verifier publickey.Service
|
Verifier publickey.Service
|
||||||
RepoCtrl *repo.Controller
|
RepoCtrl *repo.Controller
|
||||||
|
LFSCtrl *lfs.Controller
|
||||||
|
|
||||||
ServerKeyPath string
|
ServerKeyPath string
|
||||||
}
|
}
|
||||||
|
@ -225,12 +230,70 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// first part is git service pack command: git-upload-pack, git-receive-pack
|
// first part is git service pack command: git-upload-pack, git-receive-pack
|
||||||
|
// of git-lfs client command: git-lfs-authenticate, git-lfs-transfer
|
||||||
gitCommand := parts[0]
|
gitCommand := parts[0]
|
||||||
if !slices.Contains(allowedCommands, gitCommand) {
|
if !slices.Contains(allowedCommands, gitCommand) {
|
||||||
_, _ = fmt.Fprintf(session.Stderr(), "command not supported: %q\n", command)
|
_, _ = fmt.Fprintf(session.Stderr(), "command not supported: %q\n", command)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle git-lfs commands
|
||||||
|
//nolint:nestif
|
||||||
|
if strings.HasPrefix(gitCommand, "git-lfs-") {
|
||||||
|
gitLFSservice, err := enum.ParseGitLFSServiceType(gitCommand)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(session.Stderr(), "failed to parse git-lfs service command: %q\n", gitCommand)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
repoRef := getRepoRefFromCommand(parts[1])
|
||||||
|
|
||||||
|
// when git-lfs-transfer not supported, git-lfs client uses git-lfs-authenticate
|
||||||
|
// to gain a token from server and continue with http transfer APIs
|
||||||
|
if gitLFSservice == enum.GitLFSServiceTypeTransfer {
|
||||||
|
_, _ = fmt.Fprint(session.Stderr(), "git-lfs-transfer is not supported.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handling git-lfs-authenticate
|
||||||
|
principal := types.Principal{
|
||||||
|
ID: principal.ID,
|
||||||
|
UID: principal.UID,
|
||||||
|
Email: principal.Email,
|
||||||
|
Type: principal.Type,
|
||||||
|
DisplayName: principal.DisplayName,
|
||||||
|
Created: principal.Created,
|
||||||
|
Updated: principal.Updated,
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(session.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
response, err := s.LFSCtrl.Authenticate(
|
||||||
|
ctx,
|
||||||
|
&auth.Session{
|
||||||
|
Principal: principal,
|
||||||
|
},
|
||||||
|
repoRef)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("git lfs authenticate failed")
|
||||||
|
writeErrorToSession(session, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJSON, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to marshal lfs authenticate response")
|
||||||
|
writeErrorToSession(session, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := session.Write(responseJSON); err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to write response of git lfs authenticate")
|
||||||
|
writeErrorToSession(session, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle git service pack commands
|
||||||
gitServicePack := strings.TrimPrefix(gitCommand, "git-")
|
gitServicePack := strings.TrimPrefix(gitCommand, "git-")
|
||||||
service, err := enum.ParseGitServiceType(gitServicePack)
|
service, err := enum.ParseGitServiceType(gitServicePack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -241,12 +304,7 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||||
// git command args
|
// git command args
|
||||||
gitArgs := parts[1:]
|
gitArgs := parts[1:]
|
||||||
|
|
||||||
// first git service pack cmd arg is path: 'space/repository.git' so we need to remove
|
repoRef := getRepoRefFromCommand(gitArgs[0])
|
||||||
// single quotes.
|
|
||||||
repoRef := strings.Trim(gitArgs[0], "'")
|
|
||||||
// remove .git suffix
|
|
||||||
repoRef = strings.TrimSuffix(repoRef, ".git")
|
|
||||||
|
|
||||||
gitProtocol := ""
|
gitProtocol := ""
|
||||||
for _, key := range session.Environ() {
|
for _, key := range session.Environ() {
|
||||||
if strings.HasPrefix(key, "GIT_PROTOCOL=") {
|
if strings.HasPrefix(key, "GIT_PROTOCOL=") {
|
||||||
|
@ -432,3 +490,19 @@ func GenerateKeyPair(keyPath string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRepoRefFromCommand(gitArg string) string {
|
||||||
|
// first git service pack cmd arg is path: 'space/repository.git' so we need to remove
|
||||||
|
// single quotes.
|
||||||
|
repoRef := strings.Trim(gitArg, "'")
|
||||||
|
// remove .git suffix
|
||||||
|
repoRef = strings.TrimSuffix(repoRef, ".git")
|
||||||
|
|
||||||
|
return repoRef
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeErrorToSession(session ssh.Session, message string) {
|
||||||
|
if _, err := io.Copy(session.Stderr(), strings.NewReader(message+"\n")); err != nil {
|
||||||
|
log.Printf("error writing to session stderr: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/harness/gitness/app/api/controller/lfs"
|
||||||
"github.com/harness/gitness/app/api/controller/repo"
|
"github.com/harness/gitness/app/api/controller/repo"
|
||||||
"github.com/harness/gitness/app/services/publickey"
|
"github.com/harness/gitness/app/services/publickey"
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
|
@ -30,6 +31,7 @@ func ProvideServer(
|
||||||
config *types.Config,
|
config *types.Config,
|
||||||
verifier publickey.Service,
|
verifier publickey.Service,
|
||||||
repoctrl *repo.Controller,
|
repoctrl *repo.Controller,
|
||||||
|
lfsCtrl *lfs.Controller,
|
||||||
) *Server {
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
Host: config.SSH.Host,
|
Host: config.SSH.Host,
|
||||||
|
@ -44,6 +46,7 @@ func ProvideServer(
|
||||||
KeepAliveInterval: config.SSH.KeepAliveInterval,
|
KeepAliveInterval: config.SSH.KeepAliveInterval,
|
||||||
Verifier: verifier,
|
Verifier: verifier,
|
||||||
RepoCtrl: repoctrl,
|
RepoCtrl: repoctrl,
|
||||||
|
LFSCtrl: lfsCtrl,
|
||||||
ServerKeyPath: config.SSH.ServerKeyPath,
|
ServerKeyPath: config.SSH.ServerKeyPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
// 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 enum
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GitLFSTransferType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GitLFSTransferTypeBasic GitLFSTransferType = "basic"
|
||||||
|
GitLFSTransferTypeSSH GitLFSTransferType = "ssh"
|
||||||
|
// TODO GitLFSTransferTypeMultipart
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseGitLFSTransferType(s string) (GitLFSTransferType, error) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case string(GitLFSTransferTypeBasic):
|
||||||
|
return GitLFSTransferTypeBasic, nil
|
||||||
|
case string(GitLFSTransferTypeSSH):
|
||||||
|
return GitLFSTransferTypeSSH, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown git-lfs transfer type provided: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitLFSOperationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GitLFSOperationTypeDownload GitLFSOperationType = "download"
|
||||||
|
GitLFSOperationTypeUpload GitLFSOperationType = "upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseGitLFSOperationType(s string) (GitLFSOperationType, error) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case string(GitLFSOperationTypeDownload):
|
||||||
|
return GitLFSOperationTypeDownload, nil
|
||||||
|
case string(GitLFSOperationTypeUpload):
|
||||||
|
return GitLFSOperationTypeUpload, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown git-lfs operation type provided: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitLFSServiceType represents the different types of services git-lfs client sends over ssh.
|
||||||
|
type GitLFSServiceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// GitLFSServiceTypeTransfer is sent by git lfs client for transfer LFS objects.
|
||||||
|
GitLFSServiceTypeTransfer GitLFSServiceType = "git-lfs-transfer"
|
||||||
|
// GitLFSServiceTypeAuthenticate is sent by git lfs client for authentication.
|
||||||
|
GitLFSServiceTypeAuthenticate GitLFSServiceType = "git-lfs-authenticate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseGitLFSServiceType(s string) (GitLFSServiceType, error) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case string(GitLFSServiceTypeTransfer):
|
||||||
|
return GitLFSServiceTypeTransfer, nil
|
||||||
|
case string(GitLFSServiceTypeAuthenticate):
|
||||||
|
return GitLFSServiceTypeAuthenticate, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown git-lfs service type provided: %q", s)
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,4 +26,7 @@ const (
|
||||||
|
|
||||||
// TokenTypeSAT is a service account access token.
|
// TokenTypeSAT is a service account access token.
|
||||||
TokenTypeSAT TokenType = "sat"
|
TokenTypeSAT TokenType = "sat"
|
||||||
|
|
||||||
|
// TokenTypeRemoteAuth is the token returned during ssh git-lfs-authenticate.
|
||||||
|
TokenTypeRemoteAuth TokenType = "remoteAuth"
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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 types
|
||||||
|
|
||||||
|
type LFSObject struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
OID string `json:"oid"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
CreatedBy int64 `json:"created_by"`
|
||||||
|
RepoID int64 `json:"repo_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LFSLock struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
RepoID int64 `json:"repo_id"`
|
||||||
|
}
|
Loading…
Reference in New Issue