blob: 65a737a62cddf3f1612c5431ceaa18e3f7b99a1a [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package policyutil
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"chromiumos/tast/common/policy"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/testing"
)
// A DUTPolicy represents the information about a single policy as returned by
// the getAllEnterprisePolicies API.
// Example JSON: {"scope": "user", "level": "mandatory", "source": "cloud",
// "value": false, "error": "This policy has been deprecated."}
type DUTPolicy struct {
Level string
Scope string
Source string
Status string
ValueJSON json.RawMessage `json:"value"`
Error string
}
// DUTPolicies represents the format returned from the getAllEnterprisePolicies API.
// Each member map matches a string policy name (as shown in chrome://policy,
// not a device policy field name) to a DUTPolicy struct of information on that
// policy.
type DUTPolicies struct {
Chrome map[string]*DUTPolicy `json:"chromePolicies"`
DeviceLocal map[string]*DUTPolicy `json:"deviceLocalAccountPolicies"`
Extension map[string]*DUTPolicy `json:"extensionPolicies"`
}
// String turns a DUTPolicy struct into a human readable string.
func (dp *DUTPolicy) String() string {
return fmt.Sprintf("{level: %s, scope: %s, source: %s, status: %s, value: %s, error: %s}",
dp.Level, dp.Scope, dp.Source, dp.Status, string(dp.ValueJSON), dp.Error)
}
// Constant values as returned by getAllEnterprisePolicies API.
// These constants are for DUTPolicy members as indicated.
// See policy_conversions.cc in chrome/browser/policy/.
const (
// Sources (kPolicySources)
dutSourceDefault = "sourceEnterpriseDefault"
dutSourceCloud = "cloud"
dutSourceAD = "sourceActiveDirectory"
dutSourceLocalOverride = "sourceDeviceLocalAccountOverride"
dutSourcePlatform = "platform"
dutSourcePriorityCloud = "priorityCloud"
dutSourceMerged = "merged"
// Scopes
dutScopeUser = "user"
dutScopeDevice = "machine"
// Levels
dutLevelMandatory = "mandatory"
dutLevelRecommended = "recommended"
)
// mismatch represents an error found while comparing Policies to DUTPolicies.
type mismatch struct {
Err error
Act *DUTPolicy
Exp policy.Policy
}
// Error implements the error interface.
func (m *mismatch) Error() string {
return fmt.Sprintf("%s: %v", m.Exp.Name(), m.Err)
}
// Dump returns detailed information about a mismatch.
func (m *mismatch) Dump() string {
r := fmt.Sprintf("%s: %s\n", m.Exp.Name(), m.Err)
if m.Act == nil {
r += fmt.Sprintf("No matching policy found on DUT")
} else {
r += fmt.Sprintf("Policy read from DUT: %s\n", m.Act)
}
if expVal, err := json.Marshal(m.Exp.UntypedV()); err != nil {
r += fmt.Sprintf("Could not read expected policy: %v\n", err)
} else {
r += fmt.Sprintf("Expected policy: {value: %s, status: %s}\n",
expVal, m.Exp.Status())
}
r += "\n\n" // Add extra newlines as a spacer for easier human reading.
return r
}
// Verify takes a TestAPIConn struct and slice of Policies and
// ensures that Chrome has the given policies are set correctly. Only the first
// error is returned.
//
// Policies with StatusUnset or StatusDefault will be verified as not set or
// set with default source, respectively.
// This function does NOT ensure that other policies are not set on the DUT.
// Only policies passed in are considered, preventing test failures due
// to unrelated default policies.
func Verify(ctx context.Context, tconn *chrome.TestConn, ps []policy.Policy) error {
var ms []*mismatch
addM := func(a *DUTPolicy, e policy.Policy, problem string) {
ms = append(ms, &mismatch{Act: a, Exp: e, Err: errors.New(problem)})
}
dps, err := PoliciesFromDUT(ctx, tconn)
if err != nil {
return errors.Wrap(err, "could not get policies to compare against")
}
// Check only the policies given, ignoring anything else set on the DUT.
for _, expected := range ps {
actual, ok := dps.Chrome[expected.Name()]
if !ok {
if expected.Status() == policy.StatusUnset {
// Policy is correctly unset.
// Skip any further checking since there's nothing to compare.
continue
}
// Policy is unset when it should be set.
addM(nil, expected, "policy was not set on DUT")
continue
}
if expected.Status() == policy.StatusUnset {
// Policy is set when it should be unset.
addM(actual, expected, "policy should not have been set on DUT")
continue
}
// Flag any set policies with an error value, e.g. schema violations.
if actual.Error != "" {
addM(actual, expected, "policy error:"+actual.Error)
continue
}
// Compare status/source.
switch expected.Status() {
case policy.StatusSet, policy.StatusSetRecommended:
if actual.Source != dutSourceCloud {
addM(actual, expected, fmt.Sprintf("saw a source of %s, not %s",
actual.Source, dutSourceCloud))
}
case policy.StatusDefault:
if actual.Source != dutSourceDefault {
addM(actual, expected, fmt.Sprintf("saw a source of %s, not %s",
actual.Source, dutSourceDefault))
}
}
// Compare status/level.
switch expected.Status() {
case policy.StatusSet, policy.StatusDefault:
if actual.Level != dutLevelMandatory {
addM(actual, expected, fmt.Sprintf("saw a level of %s, not %s",
actual.Level, dutLevelMandatory))
}
case policy.StatusSetRecommended:
if actual.Level != dutLevelRecommended {
addM(actual, expected, fmt.Sprintf("saw a level of %s, not %s",
actual.Level, dutLevelRecommended))
}
}
// Compare scope.
if (expected.Scope() == policy.ScopeUser && actual.Scope != dutScopeUser) ||
(expected.Scope() == policy.ScopeDevice && actual.Scope != dutScopeDevice) {
addM(actual, expected, fmt.Sprintf("saw scope of %s, not %s",
actual.Scope, expected.Scope()))
}
// Compare policy value.
actualValue, err := expected.UnmarshalAs(actual.ValueJSON)
if err != nil {
addM(actual, expected, fmt.Sprintf("value unmarshal error: %v", err))
continue
}
if !expected.Equal(actualValue) {
addM(actual, expected, "actual value did not match expected")
}
}
if len(ms) == 0 {
return nil
}
// Write detailed information about all errors to file.
dir, ok := testing.ContextOutDir(ctx)
if !ok {
return errors.Wrap(ms[0],
"found policy errors but couldn't open OutDir for more info - first error")
}
const logName = "policy_errors.log"
logPath := filepath.Join(dir, logName)
var logs string
for _, m := range ms {
logs += m.Dump()
}
if err := ioutil.WriteFile(logPath, []byte(logs), 0644); err != nil {
return errors.Wrapf(ms[0],
"found policy errors but could not write to logs (%v) - first error", err)
}
// Tailor return error based on how many errors were found.
if len(ms) == 1 {
return errors.Wrapf(ms[0], "found a policy mismatch (see %s for more info)",
logName)
}
return errors.Wrapf(ms[0], "found %d policy mismatches (see %s for full list) - first error",
len(ms), logName)
}
// PoliciesFromDUT uses the passed in TestAPIConn to call autotestPrivate's
// getAllEnterprisePolicies function.
// For example data, see the Export to JSON button on chrome://policy.
// Note that a DUTPolicy contains a json.RawMessage value, not an unmarshalled value.
func PoliciesFromDUT(ctx context.Context, tconn *chrome.TestConn) (*DUTPolicies, error) {
var dps DUTPolicies
if err := tconn.Eval(ctx, `tast.promisify(chrome.autotestPrivate.getAllEnterprisePolicies)()`, &dps); err != nil {
return nil, errors.Wrap(err, "could not get policies from DUT")
}
return &dps, nil
}