diff --git a/app/api/controller/githook/pre_receive_process.go b/app/api/controller/githook/pre_receive_process.go index 60085f08a..26c411019 100644 --- a/app/api/controller/githook/pre_receive_process.go +++ b/app/api/controller/githook/pre_receive_process.go @@ -62,7 +62,18 @@ func (c *Controller) processObjects( return fmt.Errorf("failed to check settings for principal committer match: %w", err) } - if sizeLimit == 0 && !principalCommitterMatch { + gitLFSEnabled, err := settings.RepoGet( + ctx, + c.settings, + repo.ID, + settings.KeyGitLFSEnabled, + settings.DefaultGitLFSEnabled, + ) + if err != nil { + return fmt.Errorf("failed to check settings for Git LFS enabled: %w", err) + } + + if sizeLimit == 0 && !principalCommitterMatch && !gitLFSEnabled { return nil } @@ -85,7 +96,9 @@ func (c *Controller) processObjects( } } - preReceiveObjsIn.FindLFSPointersParams = &git.FindLFSPointersParams{} + if gitLFSEnabled { + preReceiveObjsIn.FindLFSPointersParams = &git.FindLFSPointersParams{} + } preReceiveObjsOut, err := c.git.ProcessPreReceiveObjects( ctx, diff --git a/app/api/controller/lfs/authenticate.go b/app/api/controller/lfs/authenticate.go index 566e72452..71b1c2aad 100644 --- a/app/api/controller/lfs/authenticate.go +++ b/app/api/controller/lfs/authenticate.go @@ -18,8 +18,10 @@ import ( "context" "fmt" + "github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/auth/authn" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/token" ) @@ -28,6 +30,26 @@ func (c *Controller) Authenticate( session *auth.Session, repoRef string, ) (*AuthenticateResponse, error) { + repo, err := c.repoFinder.FindByRef(ctx, repoRef) + if err != nil { + return nil, fmt.Errorf("failed to find repository: %w", err) + } + + gitLFSEnabled, err := settings.RepoGet( + ctx, + c.settings, + repo.ID, + settings.KeyGitLFSEnabled, + settings.DefaultGitLFSEnabled, + ) + if err != nil { + return nil, fmt.Errorf("failed to check settings for Git LFS enabled: %w", err) + } + + if !gitLFSEnabled { + return nil, usererror.ErrGitLFSDisabled + } + 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) diff --git a/app/api/controller/lfs/controller.go b/app/api/controller/lfs/controller.go index a111b80ef..c7715da55 100644 --- a/app/api/controller/lfs/controller.go +++ b/app/api/controller/lfs/controller.go @@ -24,6 +24,7 @@ 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/services/settings" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" "github.com/harness/gitness/blob" @@ -43,6 +44,7 @@ type Controller struct { blobStore blob.Store remoteAuth remoteauth.Service urlProvider url.Provider + settings *settings.Service } func NewController( @@ -53,6 +55,7 @@ func NewController( blobStore blob.Store, remoteAuth remoteauth.Service, urlProvider url.Provider, + settings *settings.Service, ) *Controller { return &Controller{ authorizer: authorizer, @@ -62,10 +65,11 @@ func NewController( blobStore: blobStore, remoteAuth: remoteAuth, urlProvider: urlProvider, + settings: settings, } } -func (c *Controller) getRepoCheckAccess( +func (c *Controller) getRepoCheckAccessAndSetting( ctx context.Context, session *auth.Session, repoRef string, @@ -89,6 +93,21 @@ func (c *Controller) getRepoCheckAccess( return nil, fmt.Errorf("access check failed: %w", err) } + gitLFSEnabled, err := settings.RepoGet( + ctx, + c.settings, + repo.ID, + settings.KeyGitLFSEnabled, + settings.DefaultGitLFSEnabled, + ) + if err != nil { + return nil, fmt.Errorf("failed to check settings for Git LFS enabled: %w", err) + } + + if !gitLFSEnabled { + return nil, usererror.ErrGitLFSDisabled + } + return repo, nil } diff --git a/app/api/controller/lfs/download.go b/app/api/controller/lfs/download.go index 452e2db60..f405293e8 100644 --- a/app/api/controller/lfs/download.go +++ b/app/api/controller/lfs/download.go @@ -41,7 +41,7 @@ func (c *Controller) Download(ctx context.Context, repoRef string, oid string, ) (*Content, error) { - repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) + repo, err := c.getRepoCheckAccessAndSetting(ctx, session, repoRef, enum.PermissionRepoView) if err != nil { return nil, fmt.Errorf("failed to acquire access to repo: %w", err) } diff --git a/app/api/controller/lfs/transfer.go b/app/api/controller/lfs/transfer.go index 201c20685..e36c0c9cc 100644 --- a/app/api/controller/lfs/transfer.go +++ b/app/api/controller/lfs/transfer.go @@ -37,7 +37,7 @@ func (c *Controller) LFSTransfer(ctx context.Context, reqPermission = enum.PermissionRepoPush } - repo, err := c.getRepoCheckAccess(ctx, session, repoRef, reqPermission) + repo, err := c.getRepoCheckAccessAndSetting(ctx, session, repoRef, reqPermission) if err != nil { return nil, err } diff --git a/app/api/controller/lfs/upload.go b/app/api/controller/lfs/upload.go index d847e5973..460dea80a 100644 --- a/app/api/controller/lfs/upload.go +++ b/app/api/controller/lfs/upload.go @@ -39,7 +39,7 @@ func (c *Controller) Upload(ctx context.Context, pointer Pointer, file io.Reader, ) (*UploadOut, error) { - repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush) + repo, err := c.getRepoCheckAccessAndSetting(ctx, session, repoRef, enum.PermissionRepoPush) if err != nil { return nil, fmt.Errorf("failed to acquire access to repo: %w", err) } diff --git a/app/api/controller/lfs/wire.go b/app/api/controller/lfs/wire.go index e10f820cc..3d7292460 100644 --- a/app/api/controller/lfs/wire.go +++ b/app/api/controller/lfs/wire.go @@ -18,6 +18,7 @@ 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/services/settings" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" "github.com/harness/gitness/blob" @@ -37,6 +38,16 @@ func ProvideController( blobStore blob.Store, remoteAuth remoteauth.Service, urlProvider url.Provider, + settings *settings.Service, ) *Controller { - return NewController(authorizer, repoFinder, principalStore, lfsStore, blobStore, remoteAuth, urlProvider) + return NewController( + authorizer, + repoFinder, + principalStore, + lfsStore, + blobStore, + remoteAuth, + urlProvider, + settings, + ) } diff --git a/app/api/controller/repo/raw.go b/app/api/controller/repo/raw.go index 4d5f1964f..497e45988 100644 --- a/app/api/controller/repo/raw.go +++ b/app/api/controller/repo/raw.go @@ -22,9 +22,12 @@ import ( "github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/services/settings" + "github.com/harness/gitness/errors" "github.com/harness/gitness/git" "github.com/harness/gitness/git/parser" "github.com/harness/gitness/git/sha" + gitness_store "github.com/harness/gitness/store" "github.com/harness/gitness/types/enum" ) @@ -89,32 +92,52 @@ func (c *Controller) Raw(ctx context.Context, return nil, fmt.Errorf("failed to read blob: %w", err) } - // check if blob is an LFS pointer - content, err := io.ReadAll(io.LimitReader(blobReader.Content, parser.LfsPointerMaxSize)) + gitLFSEnabled, err := settings.RepoGet( + ctx, + c.settings, + repo.ID, + settings.KeyGitLFSEnabled, + settings.DefaultGitLFSEnabled, + ) if err != nil { - return nil, fmt.Errorf("failed to read LFS file content: %w", err) + return nil, fmt.Errorf("failed to check settings for Git LFS enabled: %w", err) } - lfsInfo, ok := parser.IsLFSPointer(ctx, content, blobReader.Size) - if !ok { + if !gitLFSEnabled { return &RawContent{ - Data: &multiReadCloser{ - Reader: io.MultiReader(bytes.NewBuffer(content), blobReader.Content), - closeFunc: blobReader.Content.Close, - }, + Data: blobReader.Content, Size: blobReader.ContentSize, SHA: blobReader.SHA, }, nil } - file, err := c.lfsCtrl.DownloadNoAuth(ctx, repo.ID, lfsInfo.OID) + // check if blob is an LFS pointer + headerContent, err := io.ReadAll(io.LimitReader(blobReader.Content, parser.LfsPointerMaxSize)) if err != nil { - return nil, fmt.Errorf("failed to download LFS file: %w", err) + return nil, fmt.Errorf("failed to read content: %w", err) + } + + lfsInfo, ok := parser.IsLFSPointer(ctx, headerContent, blobReader.Size) + if ok { + lfsContent, err := c.lfsCtrl.DownloadNoAuth(ctx, repo.ID, lfsInfo.OID) + if err == nil { + return &RawContent{ + Data: lfsContent, + Size: lfsInfo.Size, + SHA: blobReader.SHA, + }, nil + } + if !errors.Is(err, gitness_store.ErrResourceNotFound) { + return nil, fmt.Errorf("failed to download LFS file: %w", err) + } } return &RawContent{ - Data: file, - Size: lfsInfo.Size, + Data: &multiReadCloser{ + Reader: io.MultiReader(bytes.NewBuffer(headerContent), blobReader.Content), + closeFunc: blobReader.Content.Close, + }, + Size: blobReader.ContentSize, SHA: blobReader.SHA, }, nil } diff --git a/app/api/controller/reposettings/general.go b/app/api/controller/reposettings/general.go index b1ebe6922..f2c51bc3b 100644 --- a/app/api/controller/reposettings/general.go +++ b/app/api/controller/reposettings/general.go @@ -23,17 +23,20 @@ import ( // GeneralSettings represent the general repository settings as exposed externally. type GeneralSettings struct { FileSizeLimit *int64 `json:"file_size_limit" yaml:"file_size_limit"` + GitLFSEnabled *bool `json:"git_lfs_enabled" yaml:"git_lfs_enabled"` } func GetDefaultGeneralSettings() *GeneralSettings { return &GeneralSettings{ FileSizeLimit: ptr.Int64(settings.DefaultFileSizeLimit), + GitLFSEnabled: ptr.Bool(settings.DefaultGitLFSEnabled), } } func GetGeneralSettingsMappings(s *GeneralSettings) []settings.SettingHandler { return []settings.SettingHandler{ settings.Mapping(settings.KeyFileSizeLimit, s.FileSizeLimit), + settings.Mapping(settings.KeyGitLFSEnabled, s.GitLFSEnabled), } } @@ -46,5 +49,13 @@ func GetGeneralSettingsAsKeyValues(s *GeneralSettings) []settings.KeyValue { Value: s.FileSizeLimit, }) } + + if s.GitLFSEnabled != nil { + kvs = append(kvs, settings.KeyValue{ + Key: settings.KeyGitLFSEnabled, + Value: s.GitLFSEnabled, + }) + } + return kvs } diff --git a/app/api/usererror/usererror.go b/app/api/usererror/usererror.go index 8fbdd854e..5c750b17d 100644 --- a/app/api/usererror/usererror.go +++ b/app/api/usererror/usererror.go @@ -92,6 +92,9 @@ var ( // ErrEmptyRepoNeedsBranch is returned if no branch found on the githook post receieve for empty repositories. ErrEmptyRepoNeedsBranch = New(http.StatusBadRequest, "Pushing to an empty repository requires at least one branch with commits.") + + // ErrGitLFSDisabled is returned if the Git LFS is disabled but LFS endpoint is requested. + ErrGitLFSDisabled = New(http.StatusBadRequest, "Git LFS is disabled") ) // Error represents a json-encoded API error. diff --git a/app/services/importer/repository.go b/app/services/importer/repository.go index 010e32a31..f9a203496 100644 --- a/app/services/importer/repository.go +++ b/app/services/importer/repository.go @@ -30,6 +30,7 @@ import ( "github.com/harness/gitness/app/services/keywordsearch" "github.com/harness/gitness/app/services/publicaccess" "github.com/harness/gitness/app/services/refcache" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/store" gitnessurl "github.com/harness/gitness/app/url" @@ -72,6 +73,7 @@ type Repository struct { publicAccess publicaccess.Service eventReporter *repoevents.Reporter auditService audit.Service + settings *settings.Service } var _ job.Handler = (*Repository)(nil) @@ -295,6 +297,11 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo } } + // revert this when import fetches LFS objects + if err := r.settings.RepoSet(ctx, repo.ID, settings.KeyGitLFSEnabled, false); err != nil { + log.Warn().Err(err).Msg("failed to disable Git LFS in repository settings") + } + log.Info().Msg("create git repository") gitUID, err := r.createGitRepository(ctx, &systemPrincipal, repo.ID) diff --git a/app/services/importer/wire.go b/app/services/importer/wire.go index 787ce4a3e..22b1ebdcc 100644 --- a/app/services/importer/wire.go +++ b/app/services/importer/wire.go @@ -19,6 +19,7 @@ import ( "github.com/harness/gitness/app/services/keywordsearch" "github.com/harness/gitness/app/services/publicaccess" "github.com/harness/gitness/app/services/refcache" + "github.com/harness/gitness/app/services/settings" "github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/store" "github.com/harness/gitness/app/url" @@ -53,6 +54,7 @@ func ProvideRepoImporter( publicAccess publicaccess.Service, eventReporter *repoevents.Reporter, auditService audit.Service, + settings *settings.Service, ) (*Repository, error) { importer := &Repository{ defaultBranch: config.Git.DefaultBranch, @@ -70,6 +72,7 @@ func ProvideRepoImporter( publicAccess: publicAccess, eventReporter: eventReporter, auditService: auditService, + settings: settings, } err := executor.Register(jobType, importer) diff --git a/app/services/settings/settings.go b/app/services/settings/settings.go index d9a3a4b75..ab818898c 100644 --- a/app/services/settings/settings.go +++ b/app/services/settings/settings.go @@ -26,4 +26,6 @@ var ( DefaultInstallID = string("") KeyPrincipalCommitterMatch Key = "principal_committer_match" DefaultPrincipalCommitterMatch = false + KeyGitLFSEnabled Key = "git_lfs_enabled" + DefaultGitLFSEnabled = true ) diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 84e9997ad..5caa6afe9 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -260,7 +260,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } auditService := audit.ProvideAuditService() - repository, err := importer.ProvideRepoImporter(config, provider, gitInterface, transactor, repoStore, pipelineStore, triggerStore, repoFinder, encrypter, jobScheduler, executor, streamer, indexer, publicaccessService, eventsReporter, auditService) + repository, err := importer.ProvideRepoImporter(config, provider, gitInterface, transactor, repoStore, pipelineStore, triggerStore, repoFinder, encrypter, jobScheduler, executor, streamer, indexer, publicaccessService, eventsReporter, auditService, settingsService) if err != nil { return nil, err } @@ -296,7 +296,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } remoteauthService := remoteauth.ProvideRemoteAuth(tokenStore, principalStore) - lfsController := lfs.ProvideController(authorizer, repoFinder, principalStore, lfsObjectStore, blobStore, remoteauthService, provider) + lfsController := lfs.ProvideController(authorizer, repoFinder, principalStore, lfsObjectStore, blobStore, remoteauthService, provider, settingsService) repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, checkStore, pullReqStore, settingsService, principalInfoCache, protectionManager, gitInterface, spaceFinder, repoFinder, repository, codeownersService, eventsReporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, searchService, rulesService, streamer, lfsController) reposettingsController := reposettings.ProvideController(authorizer, repoFinder, settingsService, auditService) stageStore := database.ProvideStageStore(db) diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index ef2a4d64b..2f7b7c473 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -464,6 +464,9 @@ export interface StringsMap { findOrCreateBranch: string firstTimeTitle: string general: string + 'generalSetting.features': string + 'generalSetting.gitLFSEnable': string + 'generalSetting.gitLFSEnableDesc': string generate: string generateCloneCred: string generateCloneText: string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index 5e2a79411..2043df3b8 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -397,6 +397,10 @@ prReview: targetBranchChange: '{user} changed the target branch from {old} to {new}' webhookListingContent: 'create,delete,deployment ...' general: 'General' +generalSetting: + features: Features + gitLFSEnable: Git Large File Storage (LFS) + gitLFSEnableDesc: Manage large files using Git LFS. webhooks: 'Webhooks' open: Open merged: Merged diff --git a/web/src/pages/RepositorySettings/GeneralSettingsContent/GeneralSettingsContent.tsx b/web/src/pages/RepositorySettings/GeneralSettingsContent/GeneralSettingsContent.tsx index dd9035ece..7a28f9069 100644 --- a/web/src/pages/RepositorySettings/GeneralSettingsContent/GeneralSettingsContent.tsx +++ b/web/src/pages/RepositorySettings/GeneralSettingsContent/GeneralSettingsContent.tsx @@ -32,7 +32,7 @@ import cx from 'classnames' import { Color, FontVariation, Intent } from '@harnessio/design-system' import { Icon } from '@harnessio/icons' import { noop } from 'lodash-es' -import { useMutate } from 'restful-react' +import { useGet, useMutate } from 'restful-react' import { Render } from 'react-jsx-match' import { ACCESS_MODES, getErrorMessage, permissionProps, voidFn } from 'utils/Utils' import { useStrings } from 'framework/strings' @@ -64,7 +64,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => { const [defaultBranch, setDefaultBranch] = useState(ACCESS_MODES.VIEW) const { openModal: openDefaultBranchModal } = useDefaultBranchModal({ currentGitRef, setDefaultBranch, refetch }) const { showError, showSuccess } = useToaster() - const { standalone, hooks } = useAppContext() + const { standalone, hooks, routingId } = useAppContext() const space = useGetSpaceParam() const { allowPublicResourceCreation } = usePublicResourceConfig() const { getString } = useStrings() @@ -83,6 +83,17 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => { path: `/api/v1/repos/${repoMetadata?.path}/+/public-access` }) + const { data: generalSettingsData, refetch: refetchSettings } = useGet({ + path: `/api/v1/repos/${repoMetadata?.path}/+/settings/general`, + queryParams: { routingId: routingId } + }) + + const { mutate: updateGeneralSettings } = useMutate({ + verb: 'PATCH', + path: `/api/v1/repos/${repoMetadata?.path}/+/settings/general`, + queryParams: { routingId: routingId } + }) + const permEditResult = hooks?.usePermissionTranslate?.( { resource: { @@ -178,12 +189,14 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => { return ( {formik => { @@ -412,6 +425,56 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => { + + + + + {getString('generalSetting.features')} + + + + + + + + {getString('generalSetting.gitLFSEnable')} + + + {getString('generalSetting.gitLFSEnableDesc')} + + + + {generalSettingsData?.git_lfs_enabled !== formik.values.gitLFSEnabled ? ( +