atc: Allow OPA integration to return reasons

Allow OPA documents to include an `allowed` and a `reasons` field that
gives a bit more feedback to the user about why something failed. This
allows OPA operators to write rules that are self explanatory as to why
things are getting denied.

Fly does not appear to print the full error message currently but this
works for the UI.

Signed-off-by: Andy Paine <andy.paine@engineerbetter.com>
This commit is contained in:
Andy Paine 2020-08-17 23:55:32 +01:00
parent 924d7d0809
commit 1e5c207bb2
14 changed files with 212 additions and 114 deletions

View File

@ -24,6 +24,7 @@ import (
"github.com/concourse/concourse/atc/db"
"github.com/concourse/concourse/atc/db/dbfakes"
"github.com/concourse/concourse/atc/gc/gcfakes"
"github.com/concourse/concourse/atc/policy"
"github.com/concourse/concourse/atc/worker/workerfakes"
"github.com/concourse/concourse/atc/wrappa"
@ -159,7 +160,7 @@ var _ = BeforeEach(func() {
checkWorkerTeamAccessHandlerFactory := auth.NewCheckWorkerTeamAccessHandlerFactory(dbWorkerFactory)
fakePolicyChecker = new(policycheckerfakes.FakePolicyChecker)
fakePolicyChecker.CheckReturns(true, nil)
fakePolicyChecker.CheckReturns(policy.PassedPolicyCheck(), nil)
apiWrapper := wrappa.MultiWrappa{
wrappa.NewPolicyCheckWrappa(logger, fakePolicyChecker),

View File

@ -2,10 +2,11 @@ package policychecker
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"sigs.k8s.io/yaml"
"encoding/json"
"github.com/concourse/concourse/atc/api/accessor"
"github.com/concourse/concourse/atc/policy"
@ -14,7 +15,7 @@ import (
//go:generate counterfeiter . PolicyChecker
type PolicyChecker interface {
Check(string, accessor.Access, *http.Request) (bool, error)
Check(string, accessor.Access, *http.Request) (policy.PolicyCheckOutput, error)
}
type checker struct {
@ -28,22 +29,22 @@ func NewApiPolicyChecker(policyChecker *policy.Checker) PolicyChecker {
return &checker{policyChecker: policyChecker}
}
func (c *checker) Check(action string, acc accessor.Access, req *http.Request) (bool, error) {
func (c *checker) Check(action string, acc accessor.Access, req *http.Request) (policy.PolicyCheckOutput, error) {
// Ignore self invoked API calls.
if acc.IsSystem() {
return true, nil
return policy.PassedPolicyCheck(), nil
}
// Actions in black will not go through policy check.
if c.policyChecker.ShouldSkipAction(action) {
return true, nil
return policy.PassedPolicyCheck(), nil
}
// Only actions with specified http method will go through policy check.
// But actions in white list will always go through policy check.
if !c.policyChecker.ShouldCheckHttpMethod(req.Method) &&
!c.policyChecker.ShouldCheckAction(action) {
return true, nil
return policy.PassedPolicyCheck(), nil
}
team := req.FormValue(":team_name")
@ -60,7 +61,7 @@ func (c *checker) Check(action string, acc accessor.Access, req *http.Request) (
case "application/json", "text/vnd.yaml", "text/yaml", "text/x-yaml", "application/x-yaml":
body, err := ioutil.ReadAll(req.Body)
if err != nil {
return false, err
return policy.FailedPolicyCheck(), err
} else if body != nil && len(body) > 0 {
if ct == "application/json" {
err = json.Unmarshal(body, &input.Data)
@ -68,7 +69,7 @@ func (c *checker) Check(action string, acc accessor.Access, req *http.Request) (
err = yaml.Unmarshal(body, &input.Data)
}
if err != nil {
return false, err
return policy.FailedPolicyCheck(), err
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))

View File

@ -22,7 +22,7 @@ var _ = Describe("PolicyChecker", func() {
policyFilter policy.Filter
fakeAccess *accessorfakes.FakeAccess
fakeRequest *http.Request
pass bool
result policy.PolicyCheckOutput
checkErr error
)
@ -42,7 +42,7 @@ var _ = Describe("PolicyChecker", func() {
policyCheck, err := policy.Initialize(testLogger, "some-cluster", "some-version", policyFilter)
Expect(err).ToNot(HaveOccurred())
Expect(policyCheck).ToNot(BeNil())
pass, checkErr = policychecker.NewApiPolicyChecker(policyCheck).Check("some-action", fakeAccess, fakeRequest)
result, checkErr = policychecker.NewApiPolicyChecker(policyCheck).Check("some-action", fakeAccess, fakeRequest)
})
Context("when system action", func() {
@ -51,7 +51,7 @@ var _ = Describe("PolicyChecker", func() {
})
It("should pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(result.Allowed).To(BeTrue())
})
It("Agent should not be called", func() {
Expect(fakePolicyAgent.CheckCallCount()).To(Equal(0))
@ -69,7 +69,7 @@ var _ = Describe("PolicyChecker", func() {
})
It("should pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(result.Allowed).To(BeTrue())
})
It("Agent should not be called", func() {
Expect(fakePolicyAgent.CheckCallCount()).To(Equal(0))
@ -83,7 +83,7 @@ var _ = Describe("PolicyChecker", func() {
})
It("should pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(result.Allowed).To(BeTrue())
})
It("Agent should not be called", func() {
Expect(fakePolicyAgent.CheckCallCount()).To(Equal(0))
@ -98,7 +98,7 @@ var _ = Describe("PolicyChecker", func() {
})
It("should pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(result.Allowed).To(BeTrue())
})
It("Agent should not be called", func() {
Expect(fakePolicyAgent.CheckCallCount()).To(Equal(0))
@ -121,7 +121,7 @@ var _ = Describe("PolicyChecker", func() {
It("should error", func() {
Expect(checkErr).To(HaveOccurred())
Expect(checkErr.Error()).To(Equal(`invalid character 'h' looking for beginning of value`))
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
})
It("Agent should not be called", func() {
Expect(fakePolicyAgent.CheckCallCount()).To(Equal(0))
@ -138,7 +138,7 @@ var _ = Describe("PolicyChecker", func() {
It("should error", func() {
Expect(checkErr).To(HaveOccurred())
Expect(checkErr.Error()).To(Equal(`error converting YAML to JSON: yaml: line 3: could not find expected ':'`))
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
})
It("Agent should not be called", func() {
Expect(fakePolicyAgent.CheckCallCount()).To(Equal(0))
@ -186,35 +186,39 @@ var _ = Describe("PolicyChecker", func() {
Context("when Agent says pass", func() {
BeforeEach(func() {
fakePolicyAgent.CheckReturns(true, nil)
fakePolicyAgent.CheckReturns(policy.PassedPolicyCheck(), nil)
})
It("it should pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(result.Allowed).To(BeTrue())
})
})
Context("when Agent says not-pass", func() {
BeforeEach(func() {
fakePolicyAgent.CheckReturns(false, nil)
fakePolicyAgent.CheckReturns(policy.PolicyCheckOutput{
Allowed: false,
Reasons: []string{"a policy says you can't do that"},
}, nil)
})
It("should not pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
Expect(result.Reasons).To(ConsistOf("a policy says you can't do that"))
})
})
Context("when Agent says error", func() {
BeforeEach(func() {
fakePolicyAgent.CheckReturns(false, errors.New("some-error"))
fakePolicyAgent.CheckReturns(policy.FailedPolicyCheck(), errors.New("some-error"))
})
It("should not pass", func() {
Expect(checkErr).To(HaveOccurred())
Expect(checkErr.Error()).To(Equal("some-error"))
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
})
})
})

View File

@ -7,6 +7,7 @@ import (
"code.cloudfoundry.org/lager"
"github.com/concourse/concourse/atc/api/accessor"
"github.com/concourse/concourse/atc/policy"
)
func NewHandler(
@ -34,15 +35,18 @@ func (h policyCheckingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
acc := accessor.GetAccessor(r)
if h.policyChecker != nil {
pass, err := h.policyChecker.Check(h.action, acc, r)
result, err := h.policyChecker.Check(h.action, acc, r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, fmt.Sprintf("policy check error: %s", err.Error()))
return
}
if !pass {
if !result.Allowed {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "policy check not pass")
policyCheckErr := policy.PolicyCheckNotPass{
Reasons: result.Reasons,
}
fmt.Fprintf(w, policyCheckErr.Error())
return
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/concourse/concourse/atc/api/policychecker"
"github.com/concourse/concourse/atc/api/policychecker/policycheckerfakes"
"github.com/concourse/concourse/atc/policy"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@ -63,7 +64,7 @@ var _ = Describe("Handler", func() {
Context("policy check passes", func() {
BeforeEach(func() {
fakePolicyChecker.CheckReturns(true, nil)
fakePolicyChecker.CheckReturns(policy.PassedPolicyCheck(), nil)
})
It("calls the inner handler", func() {
@ -73,7 +74,10 @@ var _ = Describe("Handler", func() {
Context("policy check doesn't pass", func() {
BeforeEach(func() {
fakePolicyChecker.CheckReturns(false, nil)
fakePolicyChecker.CheckReturns(policy.PolicyCheckOutput{
Allowed: false,
Reasons: []string{"a policy says you can't do that", "another policy also says you can't do that"},
}, nil)
})
It("return http forbidden", func() {
@ -81,7 +85,7 @@ var _ = Describe("Handler", func() {
msg, err := ioutil.ReadAll(responseWriter.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(msg)).To(Equal("policy check not pass"))
Expect(string(msg)).To(Equal("policy check failed: a policy says you can't do that, another policy also says you can't do that"))
})
It("not call the inner handler", func() {
@ -91,7 +95,7 @@ var _ = Describe("Handler", func() {
Context("policy check errors", func() {
BeforeEach(func() {
fakePolicyChecker.CheckReturns(false, errors.New("some-error"))
fakePolicyChecker.CheckReturns(policy.FailedPolicyCheck(), errors.New("some-error"))
})
It("return http bad request", func() {

View File

@ -7,10 +7,11 @@ import (
"github.com/concourse/concourse/atc/api/accessor"
"github.com/concourse/concourse/atc/api/policychecker"
"github.com/concourse/concourse/atc/policy"
)
type FakePolicyChecker struct {
CheckStub func(string, accessor.Access, *http.Request) (bool, error)
CheckStub func(string, accessor.Access, *http.Request) (policy.PolicyCheckOutput, error)
checkMutex sync.RWMutex
checkArgsForCall []struct {
arg1 string
@ -18,18 +19,18 @@ type FakePolicyChecker struct {
arg3 *http.Request
}
checkReturns struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}
checkReturnsOnCall map[int]struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakePolicyChecker) Check(arg1 string, arg2 accessor.Access, arg3 *http.Request) (bool, error) {
func (fake *FakePolicyChecker) Check(arg1 string, arg2 accessor.Access, arg3 *http.Request) (policy.PolicyCheckOutput, error) {
fake.checkMutex.Lock()
ret, specificReturn := fake.checkReturnsOnCall[len(fake.checkArgsForCall)]
fake.checkArgsForCall = append(fake.checkArgsForCall, struct {
@ -55,7 +56,7 @@ func (fake *FakePolicyChecker) CheckCallCount() int {
return len(fake.checkArgsForCall)
}
func (fake *FakePolicyChecker) CheckCalls(stub func(string, accessor.Access, *http.Request) (bool, error)) {
func (fake *FakePolicyChecker) CheckCalls(stub func(string, accessor.Access, *http.Request) (policy.PolicyCheckOutput, error)) {
fake.checkMutex.Lock()
defer fake.checkMutex.Unlock()
fake.CheckStub = stub
@ -68,28 +69,28 @@ func (fake *FakePolicyChecker) CheckArgsForCall(i int) (string, accessor.Access,
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakePolicyChecker) CheckReturns(result1 bool, result2 error) {
func (fake *FakePolicyChecker) CheckReturns(result1 policy.PolicyCheckOutput, result2 error) {
fake.checkMutex.Lock()
defer fake.checkMutex.Unlock()
fake.CheckStub = nil
fake.checkReturns = struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}{result1, result2}
}
func (fake *FakePolicyChecker) CheckReturnsOnCall(i int, result1 bool, result2 error) {
func (fake *FakePolicyChecker) CheckReturnsOnCall(i int, result1 policy.PolicyCheckOutput, result2 error) {
fake.checkMutex.Lock()
defer fake.checkMutex.Unlock()
fake.CheckStub = nil
if fake.checkReturnsOnCall == nil {
fake.checkReturnsOnCall = make(map[int]struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
})
}
fake.checkReturnsOnCall[i] = struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}{result1, result2}
}

View File

@ -10,10 +10,12 @@ import (
const ActionUseImage = "UseImage"
type PolicyCheckNotPass struct{}
type PolicyCheckNotPass struct {
Reasons []string
}
func (e PolicyCheckNotPass) Error() string {
return "policy check rejected"
return fmt.Sprintf("policy check failed: %s", strings.Join(e.Reasons, ", "))
}
type Filter struct {
@ -35,13 +37,34 @@ type PolicyCheckInput struct {
Data interface{} `json:"data,omitempty"`
}
type PolicyCheckOutput struct {
Allowed bool
Reasons []string
}
// FailedPolicyCheck creates a generic failed check
func FailedPolicyCheck() PolicyCheckOutput {
return PolicyCheckOutput{
Allowed: false,
Reasons: []string{},
}
}
// PassedPolicyCheck creates a generic passed check
func PassedPolicyCheck() PolicyCheckOutput {
return PolicyCheckOutput{
Allowed: true,
Reasons: []string{},
}
}
//go:generate counterfeiter . Agent
// Agent should be implemented by policy agents.
type Agent interface {
// Check returns true if passes policy check. If not goes through policy
// check, just return true.
Check(PolicyCheckInput) (bool, error)
Check(PolicyCheckInput) (PolicyCheckOutput, error)
}
//go:generate counterfeiter . AgentFactory
@ -137,7 +160,7 @@ func inArray(array []string, target string) bool {
return found
}
func (c *Checker) Check(input PolicyCheckInput) (bool, error) {
func (c *Checker) Check(input PolicyCheckInput) (PolicyCheckOutput, error) {
input.Service = "concourse"
input.ClusterName = clusterName
input.ClusterVersion = clusterVersion

View File

@ -75,14 +75,14 @@ var _ = Describe("Policy checker", func() {
Context("Check", func() {
var (
input policy.PolicyCheckInput
pass bool
output policy.PolicyCheckOutput
checkErr error
)
BeforeEach(func() {
input = policy.PolicyCheckInput{}
})
JustBeforeEach(func() {
pass, checkErr = checker.Check(input)
output, checkErr = checker.Check(input)
})
It("agent should be called", func() {
@ -99,35 +99,52 @@ var _ = Describe("Policy checker", func() {
Context("when agent says pass", func() {
BeforeEach(func() {
fakeAgent.CheckReturns(true, nil)
fakeAgent.CheckReturns(policy.PassedPolicyCheck(), nil)
})
It("it should pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(output.Allowed).To(BeTrue())
})
})
Context("when agent says not-pass", func() {
BeforeEach(func() {
fakeAgent.CheckReturns(false, nil)
fakeAgent.CheckReturns(policy.FailedPolicyCheck(), nil)
})
It("should not pass", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(pass).To(BeFalse())
Expect(output.Allowed).To(BeFalse())
})
})
Context("when agent includes reasons", func() {
BeforeEach(func() {
fakeAgent.CheckReturns(
policy.PolicyCheckOutput{
Allowed: false,
Reasons: []string{"a policy says you can't do that"},
},
nil,
)
})
It("should include reasons", func() {
Expect(checkErr).ToNot(HaveOccurred())
Expect(output.Reasons).To(ConsistOf("a policy says you can't do that"))
})
})
Context("when agent says error", func() {
BeforeEach(func() {
fakeAgent.CheckReturns(false, errors.New("some-error"))
fakeAgent.CheckReturns(policy.FailedPolicyCheck(), errors.New("some-error"))
})
It("should not pass", func() {
Expect(checkErr).To(HaveOccurred())
Expect(checkErr.Error()).To(Equal("some-error"))
Expect(pass).To(BeFalse())
Expect(output.Allowed).To(BeFalse())
})
})
})

View File

@ -33,8 +33,13 @@ type opaInput struct {
Input policy.PolicyCheckInput `json:"input"`
}
type opaOuptut struct {
Allowed *bool `json:"allowed,omitempty"`
Reasons []string `json:"reasons,omitempty"`
}
type opaResult struct {
Result *bool `json:"result,omitempty"`
Result *opaOuptut `json:"result,omitempty"`
}
type opa struct {
@ -42,18 +47,18 @@ type opa struct {
logger lager.Logger
}
func (c opa) Check(input policy.PolicyCheckInput) (bool, error) {
func (c opa) Check(input policy.PolicyCheckInput) (policy.PolicyCheckOutput, error) {
data := opaInput{input}
jsonBytes, err := json.Marshal(data)
if err != nil {
return false, err
return policy.FailedPolicyCheck(), nil
}
c.logger.Debug("opa-check", lager.Data{"input": string(jsonBytes)})
req, err := http.NewRequest("POST", c.config.URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return false, err
return policy.FailedPolicyCheck(), nil
}
req.Header.Set("Content-Type", "application/json")
@ -61,31 +66,34 @@ func (c opa) Check(input policy.PolicyCheckInput) (bool, error) {
client.Timeout = c.config.Timeout
resp, err := client.Do(req)
if err != nil {
return false, err
return policy.FailedPolicyCheck(), err
}
defer resp.Body.Close()
statusCode := resp.StatusCode
if statusCode != http.StatusOK {
return false, fmt.Errorf("opa returned status: %d", statusCode)
return policy.FailedPolicyCheck(), fmt.Errorf("opa returned status: %d", statusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("opa returned no response: %s", err.Error())
return policy.FailedPolicyCheck(), fmt.Errorf("opa returned no response: %s", err.Error())
}
result := &opaResult{}
err = json.Unmarshal(body, &result)
if err != nil {
return false, fmt.Errorf("opa returned bad response: %s", err.Error())
return policy.FailedPolicyCheck(), fmt.Errorf("opa returned bad response: %s", err.Error())
}
// If no result returned, meaning that the requested policy decision is
// undefined OPA, then consider as pass.
if result.Result == nil {
return true, nil
return policy.PassedPolicyCheck(), nil
}
return *result.Result, nil
return policy.PolicyCheckOutput{
Allowed: *result.Result.Allowed,
Reasons: result.Result.Reasons,
}, nil
}

View File

@ -1,14 +1,15 @@
package opa_test
import (
"code.cloudfoundry.org/lager/lagertest"
"fmt"
"github.com/concourse/concourse/atc/policy"
"github.com/concourse/concourse/atc/policy/opa"
"net/http"
"net/http/httptest"
"time"
"code.cloudfoundry.org/lager/lagertest"
"github.com/concourse/concourse/atc/policy"
"github.com/concourse/concourse/atc/policy/opa"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
@ -16,10 +17,10 @@ import (
var _ = Describe("Policy checker", func() {
var (
logger = lagertest.NewTestLogger("opa-test")
logger = lagertest.NewTestLogger("opa-test")
fakeOpa *httptest.Server
agent policy.Agent
err error
agent policy.Agent
err error
)
AfterEach(func() {
@ -30,7 +31,7 @@ var _ = Describe("Policy checker", func() {
JustBeforeEach(func() {
fakeOpa.Start()
agent, err = (&opa.OpaConfig{fakeOpa.URL, time.Second*2}).NewAgent(logger)
agent, err = (&opa.OpaConfig{fakeOpa.URL, time.Second * 2}).NewAgent(logger)
Expect(err).ToNot(HaveOccurred())
Expect(agent).ToNot(BeNil())
})
@ -42,38 +43,54 @@ var _ = Describe("Policy checker", func() {
}))
})
It("should pass", func() {
pass, err := agent.Check(policy.PolicyCheckInput{})
It("should be allowed", func() {
result, err := agent.Check(policy.PolicyCheckInput{})
Expect(err).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(result.Allowed).To(BeTrue())
})
})
Context("when OPA returns pass", func() {
Context("when OPA returns allowed", func() {
BeforeEach(func() {
fakeOpa = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"result": true}`)
fmt.Fprint(w, `{"result": {"allowed": true }}`)
}))
})
It("should pass", func() {
pass, err := agent.Check(policy.PolicyCheckInput{})
It("should be allowed", func() {
result, err := agent.Check(policy.PolicyCheckInput{})
Expect(err).ToNot(HaveOccurred())
Expect(pass).To(BeTrue())
Expect(result.Allowed).To(BeTrue())
})
})
Context("when OPA returns not-pass", func() {
Context("when OPA returns not-allowed", func() {
BeforeEach(func() {
fakeOpa = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"result": false}`)
fmt.Fprint(w, `{"result": {"allowed": false }}`)
}))
})
It("should not pass", func() {
pass, err := agent.Check(policy.PolicyCheckInput{})
It("should not be allowed and return reasons", func() {
result, err := agent.Check(policy.PolicyCheckInput{})
Expect(err).ToNot(HaveOccurred())
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
Expect(result.Reasons).To(BeEmpty())
})
})
Context("when OPA returns not-allowed with reasons", func() {
BeforeEach(func() {
fakeOpa = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"result": {"allowed": false, "reasons": ["a policy says you can't do that"]}}`)
}))
})
It("should not be allowed and return reasons", func() {
result, err := agent.Check(policy.PolicyCheckInput{})
Expect(err).ToNot(HaveOccurred())
Expect(result.Allowed).To(BeFalse())
Expect(result.Reasons).To(ConsistOf("a policy says you can't do that"))
})
})
@ -88,10 +105,10 @@ var _ = Describe("Policy checker", func() {
})
It("should return error", func() {
pass, err := agent.Check(policy.PolicyCheckInput{})
result, err := agent.Check(policy.PolicyCheckInput{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(MatchRegexp("connection refused"))
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
})
})
@ -101,10 +118,10 @@ var _ = Describe("Policy checker", func() {
})
It("should return error", func() {
pass, err := agent.Check(policy.PolicyCheckInput{})
result, err := agent.Check(policy.PolicyCheckInput{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("opa returned status: 404"))
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
})
})
@ -116,10 +133,10 @@ var _ = Describe("Policy checker", func() {
})
It("should return error", func() {
pass, err := agent.Check(policy.PolicyCheckInput{})
result, err := agent.Check(policy.PolicyCheckInput{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("opa returned bad response: invalid character 'h' looking for beginning of value"))
Expect(pass).To(BeFalse())
Expect(result.Allowed).To(BeFalse())
})
})
})

View File

@ -8,24 +8,24 @@ import (
)
type FakeAgent struct {
CheckStub func(policy.PolicyCheckInput) (bool, error)
CheckStub func(policy.PolicyCheckInput) (policy.PolicyCheckOutput, error)
checkMutex sync.RWMutex
checkArgsForCall []struct {
arg1 policy.PolicyCheckInput
}
checkReturns struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}
checkReturnsOnCall map[int]struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeAgent) Check(arg1 policy.PolicyCheckInput) (bool, error) {
func (fake *FakeAgent) Check(arg1 policy.PolicyCheckInput) (policy.PolicyCheckOutput, error) {
fake.checkMutex.Lock()
ret, specificReturn := fake.checkReturnsOnCall[len(fake.checkArgsForCall)]
fake.checkArgsForCall = append(fake.checkArgsForCall, struct {
@ -49,7 +49,7 @@ func (fake *FakeAgent) CheckCallCount() int {
return len(fake.checkArgsForCall)
}
func (fake *FakeAgent) CheckCalls(stub func(policy.PolicyCheckInput) (bool, error)) {
func (fake *FakeAgent) CheckCalls(stub func(policy.PolicyCheckInput) (policy.PolicyCheckOutput, error)) {
fake.checkMutex.Lock()
defer fake.checkMutex.Unlock()
fake.CheckStub = stub
@ -62,28 +62,28 @@ func (fake *FakeAgent) CheckArgsForCall(i int) policy.PolicyCheckInput {
return argsForCall.arg1
}
func (fake *FakeAgent) CheckReturns(result1 bool, result2 error) {
func (fake *FakeAgent) CheckReturns(result1 policy.PolicyCheckOutput, result2 error) {
fake.checkMutex.Lock()
defer fake.checkMutex.Unlock()
fake.CheckStub = nil
fake.checkReturns = struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}{result1, result2}
}
func (fake *FakeAgent) CheckReturnsOnCall(i int, result1 bool, result2 error) {
func (fake *FakeAgent) CheckReturnsOnCall(i int, result1 policy.PolicyCheckOutput, result2 error) {
fake.checkMutex.Lock()
defer fake.checkMutex.Unlock()
fake.CheckStub = nil
if fake.checkReturnsOnCall == nil {
fake.checkReturnsOnCall = make(map[int]struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
})
}
fake.checkReturnsOnCall[i] = struct {
result1 bool
result1 policy.PolicyCheckOutput
result2 error
}{result1, result2}
}

View File

@ -218,14 +218,14 @@ func (worker *gardenWorker) imagePolicyCheck(
metadata db.ContainerMetadata,
containerSpec ContainerSpec,
resourceTypes atc.VersionedResourceTypes,
) (bool, error) {
) (policy.PolicyCheckOutput, error) {
if worker.policyChecker == nil {
return true, nil
return policy.PassedPolicyCheck(), nil
}
// Actions in skip list will not go through policy check.
if !worker.policyChecker.ShouldCheckAction(policy.ActionUseImage) {
return true, nil
return policy.PassedPolicyCheck(), nil
}
imageSpec := containerSpec.ImageSpec
@ -248,17 +248,17 @@ func (worker *gardenWorker) imagePolicyCheck(
// If resource type not found, then it should be a built-in resource
// type, and could skip policy check.
if _, ok := imageInfo["image_type"]; !ok {
return true, nil
return policy.PassedPolicyCheck(), nil
}
} else {
// Ignore other images as policy checker cannot do much on them.
return true, nil
return policy.PassedPolicyCheck(), nil
}
if originalSource, ok := imageInfo["image_source"].(atc.Source); ok {
redactedSource, err := delegate.RedactImageSource(originalSource)
if err != nil {
return false, err
return policy.FailedPolicyCheck(), err
}
imageInfo["image_source"] = redactedSource
}
@ -292,12 +292,14 @@ func (worker *gardenWorker) FindOrCreateContainer(
err error
)
pass, err := worker.imagePolicyCheck(ctx, delegate, metadata, containerSpec, resourceTypes)
result, err := worker.imagePolicyCheck(ctx, delegate, metadata, containerSpec, resourceTypes)
if err != nil {
return nil, err
}
if !pass {
return nil, policy.PolicyCheckNotPass{}
if !result.Allowed {
return nil, policy.PolicyCheckNotPass{
Reasons: result.Reasons,
}
}
// ensure either creatingContainer or createdContainer exists

View File

@ -1,8 +1,24 @@
package concourse
# replace with 'false' to add rules
default allow = true
default decision = {"allowed": true}
# allow {
# input.action == "ListContainers"
# }
# uncomment to include deny rules
#decision = {"allowed": false, "reasons": reasons} {
# count(deny) > 0
# reasons := deny
#}
deny["cannot use docker-image types"] {
input.action == "UseImage"
input.data.image_type == "docker-image"
}
deny["cannot run privileged tasks"] {
input.action == "SaveConfig"
input.data.jobs[_].plan[_].privileged
}
deny["cannot use privileged resource types"] {
input.action == "SaveConfig"
input.data.resource_types[_].privileged
}

View File

@ -8,11 +8,11 @@ version: '3'
services:
web:
environment:
CONCOURSE_OPA_URL: http://opa:8181/v1/data/concourse/allow
CONCOURSE_OPA_URL: http://opa:8181/v1/data/concourse/decision
CONCOURSE_POLICY_CHECK_FILTER_HTTP_METHODS: PUT,POST
# uncomment to configure
# CONCOURSE_POLICY_CHECK_FILTER_ACTION: ListWorkers,ListContainers,UseImage
# CONCOURSE_POLICY_CHECK_FILTER_ACTION: ListWorkers,ListContainers,UseImage,SaveConfig
# CONCOURSE_POLICY_CHECK_FILTER_ACTION_SKIP: PausePipeline,UnpausePipeline
opa: