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:
parent
924d7d0809
commit
1e5c207bb2
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue