gitlabhq/gitlab-shell

View on GitHub
internal/command/lfstransfer/gitlab_backend.go

Summary

Maintainability
A
2 hrs
Test Coverage
package lfstransfer

import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "time"

    "github.com/charmbracelet/git-lfs-transfer/transfer"
    "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
    "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
    "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/lfstransfer"
)

type errCustom struct {
    err     error
    message string
}

func (e *errCustom) Error() string {
    return e.message
}

func (e *errCustom) Is(err error) bool {
    return err == e.err || err == e
}

func newErrUnsupported(operation string) error {
    return &errCustom{
        err:     transfer.ErrNotAllowed,
        message: fmt.Sprintf("%s is not yet supported by git-lfs-transfer. See https://gitlab.com/groups/gitlab-org/-/epics/11872 to track progress.", operation),
    }
}

type GitlabAuthentication struct {
    href string
    auth string
}

type GitlabBackend struct {
    ctx    context.Context
    config *config.Config
    args   *commandargs.Shell
    auth   *GitlabAuthentication
    client *lfstransfer.Client
}

type idData struct {
    Operation string            `json:"operation"`
    Oid       string            `json:"oid"`
    Href      string            `json:"href"`
    Headers   map[string]string `json:"headers,omitempty"`
}

func NewGitlabBackend(ctx context.Context, config *config.Config, args *commandargs.Shell, auth *GitlabAuthentication) (*GitlabBackend, error) {
    client, err := lfstransfer.NewClient(config, args, auth.href, auth.auth)
    if err != nil {
        return nil, err
    }

    return &GitlabBackend{
        ctx,
        config,
        args,
        auth,
        client,
    }, nil
}

func (b *GitlabBackend) issueBatchArgs(op string, oid string, href string, headers map[string]string) (args transfer.Args, err error) {
    data := &idData{
        Operation: op,
        Oid:       oid,
        Href:      href,
        Headers:   headers,
    }

    args = transfer.Args{
        "id":    "",
        "token": "",
    }
    dataBinary, err := json.Marshal(data)
    if err != nil {
        return args, err
    }

    h := hmac.New(sha256.New, []byte(b.config.Secret))
    _, err = h.Write(dataBinary)
    if err != nil {
        return args, err
    }

    args["id"] = base64.StdEncoding.EncodeToString(dataBinary)
    args["token"] = base64.StdEncoding.EncodeToString(h.Sum(nil))

    return args, nil
}

func (b *GitlabBackend) Batch(op string, pointers []transfer.BatchItem, args transfer.Args) ([]transfer.BatchItem, error) {
    reqObjects := make([]*lfstransfer.BatchObject, 0)

    for _, pointer := range pointers {
        reqObject := &lfstransfer.BatchObject{
            Oid:  pointer.Oid,
            Size: pointer.Size,
        }
        reqObjects = append(reqObjects, reqObject)
    }

    refName := args["refname"]
    hashAlgo := args["hash-algo"]

    res, err := b.client.Batch(op, reqObjects, refName, hashAlgo)
    if err != nil {
        return nil, err
    }

    items := make([]transfer.BatchItem, 0)

    for _, retObject := range res.Objects {
        var present bool
        var action *lfstransfer.BatchAction
        var args transfer.Args

        if action, present = retObject.Actions[op]; present {
            args, err = b.issueBatchArgs(op, retObject.Oid, action.Href, action.Header)
            if err != nil {
                return nil, err
            }
        }

        if op == "upload" {
            present = !present
        }

        batchItem := transfer.BatchItem{
            Pointer: transfer.Pointer{
                Oid:  retObject.Oid,
                Size: retObject.Size,
            },
            Present: present,
            Args:    args,
        }
        items = append(items, batchItem)
    }

    return items, nil
}

func (b *GitlabBackend) parseAndCheckBatchArgs(op, oid, id, token string) (href string, headers map[string]string, err error) {
    if id == "" {
        return "", nil, &errCustom{
            err:     transfer.ErrParseError,
            message: "missing id",
        }
    }
    if token == "" {
        return "", nil, &errCustom{
            err:     transfer.ErrUnauthorized,
            message: "missing token",
        }
    }
    idBinary, err := base64.StdEncoding.DecodeString(id)
    if err != nil {
        return "", nil, &errCustom{
            err:     transfer.ErrParseError,
            message: "invalid id",
        }
    }
    tokenBinary, err := base64.StdEncoding.DecodeString(token)
    if err != nil {
        return "", nil, &errCustom{
            err:     transfer.ErrParseError,
            message: "invalid token",
        }
    }
    h := hmac.New(sha256.New, []byte(b.config.Secret))
    h.Write(idBinary)
    if !hmac.Equal(tokenBinary, h.Sum(nil)) {
        return "", nil, &errCustom{
            err:     transfer.ErrForbidden,
            message: "token hash mismatch",
        }
    }

    idData := &idData{}
    err = json.Unmarshal(idBinary, idData)
    if err != nil {
        return "", nil, &errCustom{
            err:     transfer.ErrParseError,
            message: "invalid id",
        }
    }
    if idData.Operation != op {
        return "", nil, &errCustom{
            err:     transfer.ErrForbidden,
            message: "invalid operation",
        }
    }
    if idData.Oid != oid {
        return "", nil, &errCustom{
            err:     transfer.ErrForbidden,
            message: "invalid oid",
        }
    }

    return idData.Href, idData.Headers, nil
}

func (b *GitlabBackend) Upload(oid string, _ int64, r io.Reader, args transfer.Args) error {
    href, headers, err := b.parseAndCheckBatchArgs("upload", oid, args["id"], args["token"])
    if err != nil {
        _, _ = io.Copy(io.Discard, r)
        return err
    }
    return b.client.PutObject(oid, href, headers, r)
}

func (b *GitlabBackend) Verify(_ string, _ int64, _ transfer.Args) (transfer.Status, error) {
    // Not needed, all verification is done in upload step.
    return transfer.SuccessStatus(), nil
}

func (b *GitlabBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) {
    href, headers, err := b.parseAndCheckBatchArgs("download", oid, args["id"], args["token"])
    if err != nil {
        return nil, 0, err
    }
    return b.client.GetObject(oid, href, headers)
}

func (b *GitlabBackend) LockBackend(args transfer.Args) transfer.LockBackend {
    return &gitlabLockBackend{
        auth:   b.auth,
        client: b.client,
        args:   args,
    }
}

type gitlabLock struct {
    *gitlabLockBackend
    id        string
    path      string
    timestamp time.Time
    owner     string
    ownerid   string
}

func (l *gitlabLock) Unlock() error {
    lock, err := l.gitlabLockBackend.client.Unlock(l.id, l.gitlabLockBackend.args["force"] == "true", l.gitlabLockBackend.args["refname"])
    if err != nil {
        return err
    }
    l.id = lock.ID
    l.path = lock.Path
    l.timestamp = lock.LockedAt
    if lock.Owner != nil {
        l.owner = lock.Owner.Name
    }
    return nil
}

func (l *gitlabLock) AsArguments() []string {
    return []string{
        fmt.Sprintf("id=%s", l.id),
        fmt.Sprintf("path=%s", l.path),
        fmt.Sprintf("locked-at=%s", l.timestamp.Format(time.RFC3339)),
        fmt.Sprintf("ownername=%s", l.owner),
    }
}

func (l *gitlabLock) AsLockSpec(useOwnerID bool) ([]string, error) {
    spec := []string{
        fmt.Sprintf("lock %s", l.id),
        fmt.Sprintf("path %s %s", l.id, l.path),
        fmt.Sprintf("locked-at %s %s", l.id, l.timestamp.Format(time.RFC3339)),
        fmt.Sprintf("ownername %s %s", l.id, l.owner),
    }
    if useOwnerID {
        spec = append(spec, fmt.Sprintf("owner %s %s", l.id, l.ownerid))
    }
    return spec, nil
}

func (l *gitlabLock) FormattedTimestamp() string {
    return l.timestamp.Format("")
}

func (l *gitlabLock) ID() string {
    return l.id
}

func (l *gitlabLock) OwnerName() string {
    return l.owner
}

func (l *gitlabLock) Path() string {
    return l.path
}

type gitlabLockBackend struct {
    auth   *GitlabAuthentication
    client *lfstransfer.Client
    args   map[string]string
}

func (b *gitlabLockBackend) Create(path string, refname string) (transfer.Lock, error) {
    l, err := b.client.Lock(path, refname)
    var lock *gitlabLock
    if l != nil {
        lock = &gitlabLock{
            gitlabLockBackend: b,
            id:                l.ID,
            path:              l.Path,
            timestamp:         l.LockedAt,
            owner:             l.Owner.Name,
        }
    }
    return lock, err
}

func (b *gitlabLockBackend) Unlock(_ transfer.Lock) error {
    return newErrUnsupported("unlock")
}

func (b *gitlabLockBackend) FromPath(path string) (transfer.Lock, error) {
    res, err := b.client.ListLocksVerify(path, "", "", 1, "")
    if err != nil {
        return nil, err
    }
    var lock *lfstransfer.Lock
    var owner string
    switch {
    case len(res.Ours) == 1 && len(res.Theirs) == 0:
        lock = res.Ours[0]
        owner = "ours"
    case len(res.Ours) == 0 && len(res.Theirs) == 1:
        lock = res.Theirs[0]
        owner = "theirs"
    case len(res.Ours) == 0 && len(res.Theirs) == 0:
        return nil, nil
    default:
        return nil, errors.New("internal error")
    }
    return &gitlabLock{
        gitlabLockBackend: b,
        id:                lock.ID,
        path:              lock.Path,
        timestamp:         lock.LockedAt,
        owner:             lock.Owner.Name,
        ownerid:           owner,
    }, nil
}

func (b *gitlabLockBackend) FromID(id string) (transfer.Lock, error) {
    return &gitlabLock{
        gitlabLockBackend: b,
        id:                id,
    }, nil
}

func (b *gitlabLockBackend) Range(cursor string, limit int, iter func(transfer.Lock) error) (string, error) {
    res, err := b.client.ListLocksVerify(b.args["path"], b.args["id"], cursor, limit, b.args["refname"])
    if err != nil {
        return "", err
    }
    for _, lock := range res.Ours {
        tlock := &gitlabLock{
            gitlabLockBackend: b,
            id:                lock.ID,
            path:              lock.Path,
            timestamp:         lock.LockedAt,
            owner:             lock.Owner.Name,
            ownerid:           "ours",
        }
        err = iter(tlock)
        if err != nil {
            return "", err
        }
    }
    for _, lock := range res.Theirs {
        tlock := &gitlabLock{
            gitlabLockBackend: b,
            id:                lock.ID,
            path:              lock.Path,
            timestamp:         lock.LockedAt,
            owner:             lock.Owner.Name,
            ownerid:           "theirs",
        }
        err = iter(tlock)
        if err != nil {
            return "", err
        }
    }

    return res.NextCursor, nil
}