| // 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 |
| } |