Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions server/cmd/api/api/chromium.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"

Expand All @@ -26,6 +27,21 @@ var nameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]{1,255}$`)
type extensionZipItem struct {
zipTemp string
name string
pinned bool
}

// parseExtensionPinned interprets the optional extensions.pinned multipart field.
// An empty value is treated as false; otherwise standard bool spellings are accepted.
func parseExtensionPinned(raw string) (bool, error) {
s := strings.TrimSpace(raw)
if s == "" {
return false, nil
}
v, err := strconv.ParseBool(s)
if err != nil {
return false, fmt.Errorf("invalid extensions.pinned value %q", raw)
}
return v, nil
}

// chromiumFlagsPath is the runtime flags file read by the chromium-launcher at startup.
Expand Down Expand Up @@ -62,11 +78,14 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap
zipTemp string
name string
zipReceived bool
pinned bool
pinnedSet bool
}
// Process consecutive pairs of fields:
// Process consecutive groups of fields per extension:
// extensions.name (text)
// extensions.zip_file (file)
// Order may be name then zip or zip then name, but they must be consecutive.
// extensions.pinned (text, optional)
// Order within a group is flexible, but the fields must be grouped per extension.
items := []pending{}
var current *pending

Expand All @@ -79,11 +98,11 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap
log.Error("read form part", "error", err)
return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "failed to read form part"}}, nil
}
if current == nil {
current = &pending{}
}
switch part.FormName() {
case "extensions.zip_file":
if current == nil {
current = &pending{}
}
tmp, err := os.CreateTemp("", "ext-*.zip")
if err != nil {
log.Error("failed to create temporary file", "error", err)
Expand All @@ -105,6 +124,9 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap
current.zipTemp = tmp.Name()
current.zipReceived = true
case "extensions.name":
if current == nil {
current = &pending{}
}
b, err := io.ReadAll(part)
if err != nil {
log.Error("failed to read name", "error", err)
Expand All @@ -118,10 +140,33 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap
return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "duplicate name in pair"}}, nil
}
current.name = name
case "extensions.pinned":
b, err := io.ReadAll(part)
if err != nil {
log.Error("failed to read pinned", "error", err)
return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to read pinned"}}, nil
}
pv, perr := parseExtensionPinned(string(b))
if perr != nil {
return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: perr.Error()}}, nil
}
// pinned may arrive after the zip+name pair has already been finalized;
// attach it to the most recently finalized item in that case.
if current != nil {
if current.pinnedSet {
return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "duplicate pinned in pair"}}, nil
}
current.pinned = pv
current.pinnedSet = true
} else if n := len(items); n > 0 {
items[n-1].pinned = pv
} else {
current = &pending{pinned: pv, pinnedSet: true}
}
default:
return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("invalid field: %s", part.FormName())}}, nil
}
// If we have both fields, finalize this item
// Once both required fields are present, finalize this item.
if current != nil && current.zipReceived && current.name != "" {
items = append(items, *current)
current = nil
Expand All @@ -142,7 +187,7 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap
if !p.zipReceived || p.name == "" {
return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "each item must include zip_file and name"}}, nil
}
extItems = append(extItems, extensionZipItem{zipTemp: p.zipTemp, name: p.name})
extItems = append(extItems, extensionZipItem{zipTemp: p.zipTemp, name: p.name, pinned: p.pinned})
}

reqMsg, err := s.applyExtensionZipItems(ctx, extItems)
Expand Down Expand Up @@ -279,7 +324,7 @@ func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensi
pathsNeedingFlags = append(pathsNeedingFlags, extensionPath)
}

if err := s.policy.AddExtension(extensionName, chromeExtensionID, extensionPath, requiresEntPolicy); err != nil {
if err := s.policy.AddExtension(extensionName, chromeExtensionID, extensionPath, requiresEntPolicy, p.pinned); err != nil {
log.Error("failed to update enterprise policy", "error", err, "extension", extensionName)
return "", fmt.Errorf("failed to update enterprise policy for %s: %w", extensionName, err)
}
Expand Down
32 changes: 28 additions & 4 deletions server/cmd/api/api/chromium_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,11 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) err
}

type pend struct {
zipTmp string
name string
gotZip bool
zipTmp string
name string
gotZip bool
pinned bool
gotPinned bool
}
var cur *pend
var gotDisplay, gotChromiumFlags, gotChromePolicies, gotStripComponents, gotProfileArchive, gotStartURL bool
Expand Down Expand Up @@ -519,11 +521,33 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) err
return cfgParseBadRequest("duplicate extensions.name in pair")
}
cur.name = nm
case "extensions.pinned":
b, err := io.ReadAll(part)
if err != nil {
return cfgParseInternal("read extensions.pinned")
}
pv, perr := parseExtensionPinned(string(b))
if perr != nil {
return cfgParseBadRequest(perr.Error())
}
// pinned may arrive after the zip+name pair has already been finalized;
// attach it to the most recently finalized item in that case.
if cur != nil {
if cur.gotPinned {
return cfgParseBadRequest("duplicate extensions.pinned in pair")
}
cur.pinned = pv
cur.gotPinned = true
} else if n := len(st.extItems); n > 0 {
st.extItems[n-1].pinned = pv
} else {
cur = &pend{pinned: pv, gotPinned: true}
}
default:
return cfgParseBadRequest(fmt.Sprintf("unknown form field %q", name))
}
if cur != nil && cur.gotZip && cur.name != "" {
st.extItems = append(st.extItems, extensionZipItem{zipTemp: cur.zipTmp, name: cur.name})
st.extItems = append(st.extItems, extensionZipItem{zipTemp: cur.zipTmp, name: cur.name, pinned: cur.pinned})
cur = nil
}
}
Expand Down
78 changes: 78 additions & 0 deletions server/cmd/api/api/chromium_configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,84 @@ func TestChromiumCfgParseMultipartValidation(t *testing.T) {
}
}

func TestParseExtensionPinned(t *testing.T) {
cases := []struct {
in string
want bool
wantErr bool
}{
{"true", true, false},
{"false", false, false},
{"1", true, false},
{"0", false, false},
{"", false, false},
{" true ", true, false},
{"maybe", false, true},
}
for _, tc := range cases {
got, err := parseExtensionPinned(tc.in)
if tc.wantErr {
require.Error(t, err, "input %q", tc.in)
continue
}
require.NoError(t, err, "input %q", tc.in)
require.Equal(t, tc.want, got, "input %q", tc.in)
}
}

func TestChromiumCfgParseMultipartPinned(t *testing.T) {
// pinned after the pair completes (attaches to the finalized item),
// and pinned interleaved within a second pair.
buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf)

part, err := w.CreateFormFile("extensions.zip_file", "one.zip")
require.NoError(t, err)
_, err = io.WriteString(part, "not validated by parser")
require.NoError(t, err)
require.NoError(t, w.WriteField("extensions.name", "one"))
require.NoError(t, w.WriteField("extensions.pinned", "true"))

part, err = w.CreateFormFile("extensions.zip_file", "two.zip")
require.NoError(t, err)
_, err = io.WriteString(part, "not validated by parser")
require.NoError(t, err)
require.NoError(t, w.WriteField("extensions.pinned", "false"))
require.NoError(t, w.WriteField("extensions.name", "two"))
require.NoError(t, w.Close())

st := &chromiumConfigureState{}
err = chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st)
defer st.cleanup()
require.NoError(t, err)
require.Len(t, st.extItems, 2)
require.Equal(t, "one", st.extItems[0].name)
require.True(t, st.extItems[0].pinned)
require.Equal(t, "two", st.extItems[1].name)
require.False(t, st.extItems[1].pinned)
}

func TestChromiumCfgParseMultipartPinnedInvalid(t *testing.T) {
buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf)

part, err := w.CreateFormFile("extensions.zip_file", "one.zip")
require.NoError(t, err)
_, err = io.WriteString(part, "x")
require.NoError(t, err)
require.NoError(t, w.WriteField("extensions.name", "one"))
require.NoError(t, w.WriteField("extensions.pinned", "maybe"))
require.NoError(t, w.Close())

st := &chromiumConfigureState{}
err = chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st)
defer st.cleanup()
require.Error(t, err)
var parseErr chromiumCfgParseError
require.True(t, errors.As(err, &parseErr))
require.False(t, parseErr.internal)
}

func TestChromiumCfgParseMultipartMultipleExtensionPairs(t *testing.T) {
buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf)
Expand Down
Loading
Loading