mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
- As part of merging support for Template RBAC and user groups a permission check on reading files was relaxed. With the addition of admin roles on individual templates, regular users are now able to push template versions if they have inherited the 'admin' role for a template. In order to do so they need to be able to create and read their own files. Since collisions on hash in the past were ignored, this means that a regular user who pushes a template version with a file hash that collides with an existing hash will not be able to read the file (since it belongs to another user). This commit fixes the underlying problem which was that the files table had a primary key on the 'hash' column. This was not a problem at the time because only template admins and other users with similar elevated roles were able to read all files regardless of ownership. To fix this a new column and primary key 'id' has been introduced to the files table. The unique constraint has been updated to be hash+created_by. Tables (provisioner_jobs) that referenced files.hash have been updated to reference files.id. Relevant API endpoints have also been updated.
124 lines
3.1 KiB
Go
124 lines
3.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/cli/cliui"
|
|
"github.com/coder/coder/codersdk"
|
|
)
|
|
|
|
func templatePull() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "pull <name> [destination]",
|
|
Short: "Download the latest version of a template to a path.",
|
|
Args: cobra.RangeArgs(1, 2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var (
|
|
ctx = cmd.Context()
|
|
templateName = args[0]
|
|
dest string
|
|
)
|
|
|
|
if len(args) > 1 {
|
|
dest = args[1]
|
|
}
|
|
|
|
client, err := CreateClient(cmd)
|
|
if err != nil {
|
|
return xerrors.Errorf("create client: %w", err)
|
|
}
|
|
|
|
// TODO(JonA): Do we need to add a flag for organization?
|
|
organization, err := currentOrganization(cmd, client)
|
|
if err != nil {
|
|
return xerrors.Errorf("current organization: %w", err)
|
|
}
|
|
|
|
template, err := client.TemplateByName(ctx, organization.ID, templateName)
|
|
if err != nil {
|
|
return xerrors.Errorf("template by name: %w", err)
|
|
}
|
|
|
|
// Pull the versions for the template. We'll find the latest
|
|
// one and download the source.
|
|
versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
|
|
TemplateID: template.ID,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("template versions by template: %w", err)
|
|
}
|
|
|
|
if len(versions) == 0 {
|
|
return xerrors.Errorf("no template versions for template %q", templateName)
|
|
}
|
|
|
|
// Sort the slice from newest to oldest template.
|
|
sort.SliceStable(versions, func(i, j int) bool {
|
|
return versions[i].CreatedAt.After(versions[j].CreatedAt)
|
|
})
|
|
|
|
latest := versions[0]
|
|
|
|
// Download the tar archive.
|
|
raw, ctype, err := client.Download(ctx, latest.Job.FileID)
|
|
if err != nil {
|
|
return xerrors.Errorf("download template: %w", err)
|
|
}
|
|
|
|
if ctype != codersdk.ContentTypeTar {
|
|
return xerrors.Errorf("unexpected Content-Type %q, expecting %q", ctype, codersdk.ContentTypeTar)
|
|
}
|
|
|
|
// If the destination is empty then we write to stdout
|
|
// and bail early.
|
|
if dest == "" {
|
|
_, err = cmd.OutOrStdout().Write(raw)
|
|
if err != nil {
|
|
return xerrors.Errorf("write stdout: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Stat the destination to ensure nothing exists already.
|
|
fi, err := os.Stat(dest)
|
|
if err != nil && !xerrors.Is(err, fs.ErrNotExist) {
|
|
return xerrors.Errorf("stat destination: %w", err)
|
|
}
|
|
|
|
if fi != nil && fi.IsDir() {
|
|
// If the destination is a directory we just bail.
|
|
return xerrors.Errorf("%q already exists.", dest)
|
|
}
|
|
|
|
// If a file exists at the destination prompt the user
|
|
// to ensure we don't overwrite something valuable.
|
|
if fi != nil {
|
|
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
|
Text: fmt.Sprintf("%q already exists, do you want to overwrite it?", dest),
|
|
IsConfirm: true,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("parse prompt: %w", err)
|
|
}
|
|
}
|
|
|
|
err = os.WriteFile(dest, raw, 0600)
|
|
if err != nil {
|
|
return xerrors.Errorf("write to path: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cliui.AllowSkipPrompt(cmd)
|
|
|
|
return cmd
|
|
}
|