Preparing for v1 API

Moving (most) source to v1 root package to prepare for v0/v1 API separation.

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
This commit is contained in:
Johan Fylling
2024-11-21 16:35:26 +01:00
parent b4e2909bda
commit 7bb6dbe36b
5064 changed files with 1332 additions and 1428 deletions
-84
View File
@@ -1,84 +0,0 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package download
import (
"fmt"
"time"
"github.com/open-policy-agent/opa/plugins"
)
const (
defaultMinDelaySeconds = int64(60)
defaultMaxDelaySeconds = int64(120)
// deltaBundleMode indicates that OPA supports delta bundle processing
deltaBundleMode = "delta"
// defaultBundleMode indicates that OPA supports snapshot bundle processing
defaultBundleMode = "snapshot"
)
// PollingConfig represents polling configuration for the downloader.
type PollingConfig struct {
MinDelaySeconds *int64 `json:"min_delay_seconds,omitempty"` // min amount of time to wait between successful poll attempts
MaxDelaySeconds *int64 `json:"max_delay_seconds,omitempty"` // max amount of time to wait between poll attempts
LongPollingTimeoutSeconds *int64 `json:"long_polling_timeout_seconds,omitempty"` // max amount of time the server should wait before issuing a timeout if there's no update available
}
// Config represents the configuration for the downloader.
type Config struct {
Trigger *plugins.TriggerMode `json:"trigger,omitempty"`
Polling PollingConfig `json:"polling"`
}
// ValidateAndInjectDefaults checks for configuration errors and ensures all
// values are set on the Config object.
func (c *Config) ValidateAndInjectDefaults() error {
if c.Trigger == nil {
t := plugins.DefaultTriggerMode
c.Trigger = &t
}
switch *c.Trigger {
case plugins.TriggerPeriodic, plugins.TriggerManual:
break
default:
return fmt.Errorf("invalid trigger mode %q (want %q or %q)", *c.Trigger, plugins.TriggerPeriodic, plugins.TriggerManual)
}
min := defaultMinDelaySeconds
max := defaultMaxDelaySeconds
// reject bad min/max values
if c.Polling.MaxDelaySeconds != nil && c.Polling.MinDelaySeconds != nil {
if *c.Polling.MaxDelaySeconds < *c.Polling.MinDelaySeconds {
return fmt.Errorf("max polling delay must be >= min polling delay")
}
min = *c.Polling.MinDelaySeconds
max = *c.Polling.MaxDelaySeconds
} else if c.Polling.MaxDelaySeconds == nil && c.Polling.MinDelaySeconds != nil {
return fmt.Errorf("polling configuration missing 'max_delay_seconds'")
} else if c.Polling.MinDelaySeconds == nil && c.Polling.MaxDelaySeconds != nil {
return fmt.Errorf("polling configuration missing 'min_delay_seconds'")
}
// scale to seconds
minSeconds := int64(time.Duration(min) * time.Second)
c.Polling.MinDelaySeconds = &minSeconds
maxSeconds := int64(time.Duration(max) * time.Second)
c.Polling.MaxDelaySeconds = &maxSeconds
if c.Polling.LongPollingTimeoutSeconds != nil {
if *c.Polling.LongPollingTimeoutSeconds < 1 {
return fmt.Errorf("'long_polling_timeout_seconds' must be at least 1")
}
}
return nil
}
-100
View File
@@ -1,100 +0,0 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package download
import (
"encoding/json"
"testing"
"time"
)
func TestConfigValidation(t *testing.T) {
tests := []struct {
note string
input string
wantErr bool
expMin time.Duration
expMax time.Duration
}{
{
note: "min > max",
input: `{
"polling": {
"min_delay_seconds": 10,
"max_delay_seconds": 1
}
}`,
wantErr: true,
},
{
note: "empty",
input: `{}`,
expMin: time.Second * time.Duration(defaultMinDelaySeconds),
expMax: time.Second * time.Duration(defaultMaxDelaySeconds),
},
{
note: "min missing",
input: `{
"polling": {
"max_delay_seconds": 10
}
}`,
wantErr: true,
},
{
note: "max missing",
input: `{
"polling": {
"min_delay_seconds": 1
}
}`,
wantErr: true,
},
{
note: "user supplied",
input: `{
"polling": {
"min_delay_seconds": 10,
"max_delay_seconds": 30
}
}`,
expMin: time.Second * 10,
expMax: time.Second * 30,
},
{
note: "long polling timeout < 1",
input: `{
"polling": {
"long_polling_timeout_seconds": 0
}
}`,
wantErr: true,
},
}
for _, test := range tests {
var config Config
if err := json.Unmarshal([]byte(test.input), &config); err != nil {
t.Fatal(err)
}
err := config.ValidateAndInjectDefaults()
if err != nil && !test.wantErr {
t.Errorf("Unexpected error on: %v, err: %v", test.input, err)
}
if err == nil {
if time.Duration(*config.Polling.MinDelaySeconds) != test.expMin {
t.Errorf("For %q expected min %v but got %v", test.note, test.expMin, time.Duration(*config.Polling.MinDelaySeconds))
}
if time.Duration(*config.Polling.MaxDelaySeconds) != test.expMax {
t.Errorf("For %q expected min %v but got %v", test.note, test.expMax, time.Duration(*config.Polling.MaxDelaySeconds))
}
}
}
}
-454
View File
@@ -1,454 +0,0 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package download implements low-level OPA bundle downloading.
package download
import (
"bytes"
"context"
"fmt"
"io"
"math/rand"
"net/http"
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/logging"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/plugins"
"github.com/open-policy-agent/opa/plugins/rest"
"github.com/open-policy-agent/opa/util"
)
const (
minRetryDelay = time.Millisecond * 100
)
// Update contains the result of a download. If an error occurred, the Error
// field will be non-nil. If a new bundle is available, the Bundle field will
// be non-nil.
type Update struct {
ETag string
Bundle *bundle.Bundle
Error error
Metrics metrics.Metrics
Raw io.Reader
Size int
}
// Downloader implements low-level OPA bundle downloading. Downloader can be
// started and stopped. After starting, the downloader will request bundle
// updates from the remote HTTP endpoint that the client is configured to
// connect to.
type Downloader struct {
config Config // downloader configuration for tuning polling and other downloader behaviour
client rest.Client // HTTP client to use for bundle downloading
path string // path to use in bundle download request
trigger chan chan struct{} // channel to signal downloads when manual triggering is enabled
stop chan chan struct{} // used to signal plugin to stop running
f func(context.Context, Update) // callback function invoked when download updates occur
etag string // HTTP Etag for caching purposes
sizeLimitBytes *int64 // max bundle file size in bytes (passed to reader)
bvc *bundle.VerificationConfig
respHdrTimeoutSec int64
wg sync.WaitGroup
logger logging.Logger
mtx sync.Mutex
stopped bool
persist bool
longPollingEnabled bool
lazyLoadingMode bool
bundleName string
bundleParserOpts ast.ParserOptions
}
type downloaderResponse struct {
b *bundle.Bundle
raw io.Reader
etag string
longPoll bool
size int
}
// New returns a new Downloader that can be started.
func New(config Config, client rest.Client, path string) *Downloader {
return &Downloader{
config: config,
client: client,
path: path,
trigger: make(chan chan struct{}),
stop: make(chan chan struct{}),
logger: client.Logger(),
longPollingEnabled: config.Polling.LongPollingTimeoutSeconds != nil,
}
}
// WithCallback registers a function f to be called when download updates occur.
func (d *Downloader) WithCallback(f func(context.Context, Update)) *Downloader {
d.f = f
return d
}
// WithLogAttrs sets an optional set of key/value pair attributes to include in
// log messages emitted by the downloader.
func (d *Downloader) WithLogAttrs(attrs map[string]interface{}) *Downloader {
d.logger = d.logger.WithFields(attrs)
return d
}
// WithBundleVerificationConfig sets the key configuration used to verify a signed bundle
func (d *Downloader) WithBundleVerificationConfig(config *bundle.VerificationConfig) *Downloader {
d.bvc = config
return d
}
// WithSizeLimitBytes sets the file size limit for bundles read by this downloader.
func (d *Downloader) WithSizeLimitBytes(n int64) *Downloader {
d.sizeLimitBytes = &n
return d
}
// WithBundlePersistence specifies if the downloaded bundle will eventually be persisted to disk.
func (d *Downloader) WithBundlePersistence(persist bool) *Downloader {
d.persist = persist
return d
}
// WithLazyLoadingMode specifies how the downloaded bundle should be read.
// If true, data files in the bundle will not be deserialized
// and the check to validate that the bundle data does not contain paths
// outside the bundle's roots will not be performed while reading the bundle.
func (d *Downloader) WithLazyLoadingMode(yes bool) *Downloader {
d.lazyLoadingMode = yes
return d
}
// WithBundleName specifies the name of the downloaded bundle.
func (d *Downloader) WithBundleName(bundleName string) *Downloader {
d.bundleName = bundleName
return d
}
// WithBundleParserOpts specifies the parser options to use when parsing downloaded bundles.
func (d *Downloader) WithBundleParserOpts(opts ast.ParserOptions) *Downloader {
d.bundleParserOpts = opts
return d
}
// ClearCache is deprecated. Use SetCache instead.
func (d *Downloader) ClearCache() {
d.etag = ""
}
// SetCache sets the given etag value on the downloader.
func (d *Downloader) SetCache(etag string) {
d.etag = etag
}
// Trigger can be used to control when the downloader attempts to download
// a new bundle in manual triggering mode.
func (d *Downloader) Trigger(ctx context.Context) error {
done := make(chan error)
go func() {
err := d.oneShot(ctx)
if err != nil {
d.logger.Error("Bundle download failed: %v.", err)
if ctx.Err() == nil {
done <- err
}
}
close(done)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
// Start tells the Downloader to begin downloading bundles.
func (d *Downloader) Start(ctx context.Context) {
if *d.config.Trigger == plugins.TriggerPeriodic {
go d.doStart(ctx)
}
}
func (d *Downloader) doStart(context.Context) {
// We'll revisit context passing/usage later.
ctx, cancel := context.WithCancel(context.Background())
d.wg.Add(1)
go d.loop(ctx)
done := <-d.stop // blocks until there's something to read
cancel()
d.wg.Wait()
d.stopped = true
close(done)
}
// Stop tells the Downloader to stop downloading bundles.
func (d *Downloader) Stop(context.Context) {
if *d.config.Trigger == plugins.TriggerManual {
return
}
d.mtx.Lock()
defer d.mtx.Unlock()
if d.stopped {
return
}
done := make(chan struct{})
d.stop <- done
<-done
}
func (d *Downloader) loop(ctx context.Context) {
defer d.wg.Done()
var retry int
for {
var delay time.Duration
err := d.oneShot(ctx)
if ctx.Err() != nil {
return
}
if err != nil {
delay = util.DefaultBackoff(float64(minRetryDelay), float64(*d.config.Polling.MaxDelaySeconds), retry)
} else {
if !d.longPollingEnabled || d.config.Polling.LongPollingTimeoutSeconds == nil {
// revert the response header timeout value on the http client's transport
if *d.client.Config().ResponseHeaderTimeoutSeconds == 0 {
d.client = d.client.SetResponseHeaderTimeout(&d.respHdrTimeoutSec)
}
min := float64(*d.config.Polling.MinDelaySeconds)
max := float64(*d.config.Polling.MaxDelaySeconds)
delay = time.Duration(((max - min) * rand.Float64()) + min)
}
}
d.logger.Debug("Waiting %v before next download/retry.", delay)
timer, timerCancel := util.TimerWithCancel(delay)
select {
case <-timer.C:
if err != nil {
retry++
} else {
retry = 0
}
case <-ctx.Done():
timerCancel() // explicitly cancel the timer.
return
}
}
}
func (d *Downloader) oneShot(ctx context.Context) error {
m := metrics.New()
resp, err := d.download(ctx, m)
if err != nil {
d.etag = ""
if d.f != nil {
d.f(ctx, Update{ETag: "", Bundle: nil, Error: err, Metrics: m, Raw: nil})
}
return err
}
d.etag = resp.etag
d.longPollingEnabled = resp.longPoll
if d.f != nil {
d.f(ctx, Update{ETag: resp.etag, Bundle: resp.b, Error: nil, Metrics: m, Raw: resp.raw, Size: resp.size})
}
return nil
}
func (d *Downloader) download(ctx context.Context, m metrics.Metrics) (*downloaderResponse, error) {
d.logger.Debug("Download starting.")
d.client = d.client.WithHeader("If-None-Match", d.etag)
preferences := []string{fmt.Sprintf("modes=%v,%v", defaultBundleMode, deltaBundleMode)}
if d.longPollingEnabled && d.config.Polling.LongPollingTimeoutSeconds != nil {
wait := fmt.Sprintf("wait=%s", strconv.FormatInt(*d.config.Polling.LongPollingTimeoutSeconds, 10))
preferences = append(preferences, wait)
// fetch existing response header timeout value on the http client's transport and
// clear it for the long poll request
current := d.client.Config().ResponseHeaderTimeoutSeconds
if *current != 0 {
d.respHdrTimeoutSec = *current
t := int64(0)
d.client = d.client.SetResponseHeaderTimeout(&t)
}
}
preferValue := fmt.Sprintf("%v", strings.Join(preferences, ";"))
d.client = d.client.WithHeader("Prefer", preferValue)
m.Timer(metrics.BundleRequest).Start()
resp, err := d.client.Do(ctx, "GET", d.path)
m.Timer(metrics.BundleRequest).Stop()
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer util.Close(resp)
switch resp.StatusCode {
case http.StatusOK:
var buf bytes.Buffer
if resp.Body != nil {
d.logger.Debug("Download in progress.")
m.Timer(metrics.RegoLoadBundles).Start()
defer m.Timer(metrics.RegoLoadBundles).Stop()
baseURL := path.Join(d.client.Config().URL, d.path)
cnt := &count{}
r := io.TeeReader(resp.Body, cnt)
var loader bundle.DirectoryLoader
if d.persist {
tee := io.TeeReader(r, &buf)
loader = bundle.NewTarballLoaderWithBaseURL(tee, baseURL)
} else {
loader = bundle.NewTarballLoaderWithBaseURL(r, baseURL)
}
// Setting the size limit on the loader allows early exit in the case
// of any file exceeding the limit, without the file getting loaded
if d.sizeLimitBytes != nil {
loader = loader.WithSizeLimitBytes(*d.sizeLimitBytes)
}
etag := resp.Header.Get("ETag")
reader := bundle.NewCustomReader(loader).
WithRegoVersion(d.bundleParserOpts.RegoVersion).
WithMetrics(m).
WithBundleVerificationConfig(d.bvc).
WithBundleEtag(etag).
WithLazyLoadingMode(d.lazyLoadingMode).
WithBundleName(d.bundleName).
WithBundlePersistence(d.persist)
if d.sizeLimitBytes != nil {
reader = reader.WithSizeLimitBytes(*d.sizeLimitBytes)
}
if d.logger.GetLevel() >= logging.Debug {
expectedBundleContentType := []string{
"application/gzip",
"application/octet-stream",
"application/vnd.openpolicyagent.bundles",
}
contentType := resp.Header.Get("content-type")
if !contains(contentType, expectedBundleContentType) {
d.logger.Debug("Content-Type response header set to %v. Expected one of %v. "+
"Possibly not a bundle being downloaded.",
contentType,
expectedBundleContentType,
)
}
}
b, err := reader.Read()
if err != nil {
return nil, err
}
return &downloaderResponse{
b: &b,
raw: &buf,
etag: etag,
longPoll: isLongPollSupported(resp.Header),
size: cnt.Bytes(),
}, nil
}
d.logger.Debug("Server replied with empty body.")
return &downloaderResponse{
b: nil,
raw: nil,
etag: "",
longPoll: isLongPollSupported(resp.Header),
}, nil
case http.StatusNotModified:
etag := resp.Header.Get("ETag")
if etag == "" {
etag = d.etag
}
return &downloaderResponse{
b: nil,
raw: nil,
etag: etag,
longPoll: d.longPollingEnabled,
}, nil
default:
if d.logger.GetLevel() == logging.Debug && resp.Body != nil {
body, err := io.ReadAll(resp.Body)
if err == nil {
d.logger.Debug("bundle download error response with response body: %s", body)
}
}
return nil, HTTPError{StatusCode: resp.StatusCode}
}
}
type count struct {
total int
}
func (c *count) Write(p []byte) (n int, err error) {
n = len(p)
c.total += n
return
}
func (c *count) Bytes() int {
return c.total
}
func isLongPollSupported(header http.Header) bool {
return header.Get("Content-Type") == "application/vnd.openpolicyagent.bundles"
}
type HTTPError struct {
StatusCode int
}
func (e HTTPError) Error() string {
return fmt.Sprintf("server replied with %s", http.StatusText(e.StatusCode))
}
func contains(s string, strings []string) bool {
for _, str := range strings {
if s == str {
return true
}
}
return false
}
File diff suppressed because it is too large Load Diff
-460
View File
@@ -1,460 +0,0 @@
//go:build !opa_no_oci
package download
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/containerd/errdefs"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
oraslib "oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/oci"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/logging"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/plugins"
"github.com/open-policy-agent/opa/plugins/rest"
"github.com/open-policy-agent/opa/util"
)
// NewOCI returns a new Downloader that can be started.
func NewOCI(config Config, client rest.Client, path, storePath string) *OCIDownloader {
localstore, err := oci.New(storePath)
if err != nil {
panic(err)
}
return &OCIDownloader{
config: config,
path: path,
localStorePath: storePath,
client: client,
trigger: make(chan chan struct{}),
stop: make(chan chan struct{}),
logger: client.Logger(),
store: localstore,
}
}
// WithCallback registers a function f to be called when download updates occur.
func (d *OCIDownloader) WithCallback(f func(context.Context, Update)) *OCIDownloader {
d.f = f
return d
}
// WithLogAttrs sets an optional set of key/value pair attributes to include in
// log messages emitted by the downloader.
func (d *OCIDownloader) WithLogAttrs(attrs map[string]interface{}) *OCIDownloader {
d.logger = d.logger.WithFields(attrs)
return d
}
// WithBundleVerificationConfig sets the key configuration used to verify a signed bundle
func (d *OCIDownloader) WithBundleVerificationConfig(config *bundle.VerificationConfig) *OCIDownloader {
d.bvc = config
return d
}
// WithSizeLimitBytes sets the file size limit for bundles read by this downloader.
func (d *OCIDownloader) WithSizeLimitBytes(n int64) *OCIDownloader {
d.sizeLimitBytes = &n
return d
}
// WithBundlePersistence specifies if the downloaded bundle will eventually be persisted to disk.
func (d *OCIDownloader) WithBundlePersistence(persist bool) *OCIDownloader {
d.persist = persist
return d
}
// WithBundleParserOpts specifies the parser options to use when parsing downloaded bundles.
func (d *OCIDownloader) WithBundleParserOpts(opts ast.ParserOptions) *OCIDownloader {
d.bundleParserOpts = opts
return d
}
// ClearCache is deprecated. Use SetCache instead.
func (d *OCIDownloader) ClearCache() {
}
// SetCache sets the etag value to the SHA of the loaded bundle
func (d *OCIDownloader) SetCache(etag string) {
d.etag = etag
}
// Trigger can be used to control when the downloader attempts to download
// a new bundle in manual triggering mode.
func (d *OCIDownloader) Trigger(ctx context.Context) error {
done := make(chan error)
go func() {
err := d.oneShot(ctx)
if err != nil {
d.logger.Error("OCI - Bundle download failed: %v.", err)
if ctx.Err() == nil {
done <- err
}
}
close(done)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
// Start tells the Downloader to begin downloading bundles.
func (d *OCIDownloader) Start(ctx context.Context) {
if *d.config.Trigger == plugins.TriggerPeriodic {
go d.doStart(ctx)
}
}
// Stop tells the Downloader to stop downloading bundles.
func (d *OCIDownloader) Stop(context.Context) {
if *d.config.Trigger == plugins.TriggerManual {
return
}
d.mtx.Lock()
defer d.mtx.Unlock()
if d.stopped {
return
}
done := make(chan struct{})
d.stop <- done
<-done
}
func (d *OCIDownloader) doStart(context.Context) {
// We'll revisit context passing/usage later.
ctx, cancel := context.WithCancel(context.Background())
d.wg.Add(1)
go d.loop(ctx)
done := <-d.stop // blocks until there's something to read
cancel()
d.wg.Wait()
d.stopped = true
close(done)
}
func (d *OCIDownloader) loop(ctx context.Context) {
defer d.wg.Done()
var retry int
for {
var delay time.Duration
err := d.oneShot(ctx)
if ctx.Err() != nil {
return
}
if err != nil {
delay = util.DefaultBackoff(float64(minRetryDelay), float64(*d.config.Polling.MaxDelaySeconds), retry)
} else {
// revert the response header timeout value on the http client's transport
min := float64(*d.config.Polling.MinDelaySeconds)
max := float64(*d.config.Polling.MaxDelaySeconds)
delay = time.Duration(((max - min) * rand.Float64()) + min)
}
d.logger.Debug("OCI - Waiting %v before next download/retry.", delay)
timer, timerCancel := util.TimerWithCancel(delay)
select {
case <-timer.C:
if err != nil {
retry++
} else {
retry = 0
}
case <-ctx.Done():
timerCancel() // explicitly cancel the timer.
return
}
}
}
func (d *OCIDownloader) oneShot(ctx context.Context) error {
m := metrics.New()
resp, err := d.download(ctx, m)
if err != nil {
if d.f != nil {
d.f(ctx, Update{ETag: "", Bundle: nil, Error: err, Metrics: m, Raw: nil})
}
return err
}
d.SetCache(resp.etag) // set the current etag sha to the cache
if d.f != nil {
d.f(ctx, Update{ETag: resp.etag, Bundle: resp.b, Error: nil, Metrics: m, Raw: resp.raw, Size: resp.size})
}
return nil
}
func (d *OCIDownloader) download(ctx context.Context, m metrics.Metrics) (*downloaderResponse, error) {
d.logger.Debug("OCI - Download starting.")
var buf bytes.Buffer
preferences := []string{fmt.Sprintf("modes=%v,%v", defaultBundleMode, deltaBundleMode)}
preferValue := fmt.Sprintf("%v", strings.Join(preferences, ";"))
d.client = d.client.WithHeader("Prefer", preferValue)
m.Timer(metrics.BundleRequest).Start()
desc, err := d.pull(ctx, d.path)
if err != nil {
return &downloaderResponse{}, fmt.Errorf("failed to pull %s: %w", d.path, err)
}
manifest, err := manifestFromDesc(ctx, d.store, desc)
if err != nil {
return nil, err
}
tarballDescriptor := ocispec.Descriptor{}
for _, descriptor := range manifest.Layers {
if descriptor.MediaType == "application/vnd.oci.image.layer.v1.tar+gzip" {
tarballDescriptor = descriptor
break
}
}
if tarballDescriptor.MediaType == "" {
return nil, fmt.Errorf("no tarball descriptor found in the layers")
}
etag := tarballDescriptor.Digest.Hex()
bundleFilePath := filepath.Join(d.localStorePath, "blobs", "sha256", etag)
// if the downloader etag sha is the same with digest of the tarball it was already loaded
if d.etag == etag {
return &downloaderResponse{
b: nil,
raw: nil,
etag: etag,
longPoll: false,
}, nil
}
fileReader, err := os.Open(bundleFilePath)
cnt := &count{}
r := io.TeeReader(fileReader, cnt)
tee := io.TeeReader(r, &buf)
if err != nil {
return nil, err
}
loader := bundle.NewTarballLoaderWithBaseURL(tee, d.localStorePath)
reader := bundle.NewCustomReader(loader).
WithMetrics(m).
WithBundleVerificationConfig(d.bvc).
WithBundleEtag(etag).
WithRegoVersion(d.bundleParserOpts.RegoVersion)
bundleInfo, err := reader.Read()
if err != nil {
return &downloaderResponse{}, fmt.Errorf("unexpected error %w", err)
}
m.Timer(metrics.BundleRequest).Stop()
return &downloaderResponse{
b: &bundleInfo,
raw: &buf,
etag: etag,
longPoll: false,
size: cnt.Bytes(),
}, nil
}
func (d *OCIDownloader) pull(ctx context.Context, ref string) (*ocispec.Descriptor, error) {
lookup := d.client.AuthPluginLookup()
plugin, err := d.client.Config().AuthPlugin(lookup)
if err != nil {
return nil, fmt.Errorf("failed to look up auth plugin: %w", err)
}
d.logger.Debug("OCIDownloader: using auth plugin: %T", plugin)
resolver, err := dockerResolver(plugin, d.client.Config(), d.logger)
if err != nil {
return nil, fmt.Errorf("invalid host url %s: %w", d.client.Config().URL, err)
}
target := remoteManager{
resolver: resolver,
srcRef: ref,
}
manifestDescriptor, err := oraslib.Copy(ctx, &target, ref, d.store, "", oraslib.DefaultCopyOptions)
if err != nil {
return nil, fmt.Errorf("download for '%s' failed: %w", ref, err)
}
return &manifestDescriptor, nil
}
func dockerResolver(plugin rest.HTTPAuthPlugin, config *rest.Config, logger logging.Logger) (remotes.Resolver, error) {
client, err := plugin.NewClient(*config)
if err != nil {
return nil, fmt.Errorf("failed to create auth client: %w", err)
}
urlInfo, err := url.Parse(config.URL)
if err != nil {
return nil, fmt.Errorf("failed to parse url: %w", err)
}
authorizer := pluginAuthorizer{
plugin: plugin,
client: client,
logger: logger,
}
registryHost := docker.RegistryHost{
Host: urlInfo.Host,
Scheme: urlInfo.Scheme,
Capabilities: docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush,
Client: client,
Path: "/v2",
Authorizer: &authorizer,
}
opts := docker.ResolverOptions{
Hosts: func(string) ([]docker.RegistryHost, error) {
return []docker.RegistryHost{registryHost}, nil
},
}
return docker.NewResolver(opts), nil
}
type pluginAuthorizer struct {
plugin rest.HTTPAuthPlugin
client *http.Client
// authorizer will be populated by the first call to pluginAuthorizer.Prepare
// since it requires a first pass through the plugin.Prepare method.
authorizer docker.Authorizer
logger logging.Logger
}
var _ docker.Authorizer = &pluginAuthorizer{}
func (a *pluginAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
return a.authorizer.AddResponses(ctx, responses)
}
// Authorize uses a rest.HTTPAuthPlugin to Prepare a request before passing it on
// to the docker.Authorizer.
func (a *pluginAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
if err := a.plugin.Prepare(req); err != nil {
err = fmt.Errorf("failed to prepare docker request: %w", err)
// Make sure to log this before passing the error back to docker
a.logger.Error(err.Error())
return err
}
if a.authorizer == nil {
// Some registry authentication implementations require a token fetch from
// a separate authenticated token server. This flow is described in the
// docker token auth spec:
// https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
//
// Unfortunately, the containerd implementation does not use the Prepare
// mechanism to authenticate these token requests and we need to add
// auth information in form of a static docker.WithAuthHeader.
//
// Since rest.HTTPAuthPlugins will set the auth header on the request
// passed to HTTPAuthPlugin.Prepare, we can use it afterwards to build
// our docker.Authorizer.
a.authorizer = docker.NewDockerAuthorizer(
docker.WithAuthHeader(req.Header),
docker.WithAuthClient(a.client),
)
}
return a.authorizer.Authorize(ctx, req)
}
func manifestFromDesc(ctx context.Context, target oraslib.Target, desc *ocispec.Descriptor) (*ocispec.Manifest, error) {
var manifest ocispec.Manifest
descReader, err := target.Fetch(ctx, *desc)
if err != nil {
return nil, fmt.Errorf("unable to fetch descriptor with digest %q: %w", desc.Digest, err)
}
defer descReader.Close()
descBytes, err := io.ReadAll(descReader)
if err != nil {
return nil, fmt.Errorf("unable to read bytes from descriptor: %w", err)
}
if err = json.Unmarshal(descBytes, &manifest); err != nil {
return nil, fmt.Errorf("unable to unmarshal manifest: %w", err)
}
if len(manifest.Layers) < 1 {
return nil, fmt.Errorf("no layers in manifest")
}
return &manifest, nil
}
type remoteManager struct {
resolver remotes.Resolver
srcRef string
}
func (r *remoteManager) Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error) {
_, desc, err := r.resolver.Resolve(ctx, ref)
if err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}
func (r *remoteManager) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
fetcher, err := r.resolver.Fetcher(ctx, r.srcRef)
if err != nil {
return nil, err
}
return fetcher.Fetch(ctx, target)
}
func (r *remoteManager) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
_, err := r.Fetch(ctx, target)
if err == nil {
return true, nil
}
return !errdefs.IsNotFound(err), err
}
-461
View File
@@ -1,461 +0,0 @@
//go:build slow
// +build slow
package download
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"reflect"
"strings"
"testing"
"time"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/keys"
"github.com/open-policy-agent/opa/plugins/rest"
)
// when changed the layer hash & size should be updated in .manifest files
//go:generate go run github.com/open-policy-agent/opa build -b --signing-alg HS256 testdata/latest_bundle_data --output testdata/latest.tar.gz
//go:generate go run github.com/open-policy-agent/opa build -b --signing-alg HS256 --signing-key secret testdata/signed_bundle_data --output testdata/signed.tar.gz
//go:generate go run github.com/open-policy-agent/opa build --v1-compatible -b --signing-alg HS256 --signing-key secret testdata/rego_v1_bundle_data --output testdata/rego_v1.tar.gz
func TestOCIDownloaderWithBundleVerificationConfig(t *testing.T) {
vc := bundle.NewVerificationConfig(map[string]*bundle.KeyConfig{"default": {Key: "secret", Algorithm: "HS256"}}, "", "", nil)
ctx := context.Background()
fixture := newTestFixture(t)
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"
updates := make(chan *Update)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
d := NewOCI(config, fixture.client, "ghcr.io/org/repo:signed", "/tmp/opa/").WithCallback(func(_ context.Context, u Update) {
if u.Error != nil {
t.Fatalf("expected no error but got: %v", u.Error)
}
updates <- &u
}).WithBundleVerificationConfig(vc)
d.Start(ctx)
// Give time for some download events to occur
time.Sleep(1 * time.Second)
u1 := <-updates
if u1.Bundle == nil || len(u1.Bundle.Modules) == 0 {
t.Fatal("expected bundle with at least one module but got:", u1)
}
if !strings.HasSuffix(u1.Bundle.Modules[0].URL, u1.Bundle.Modules[0].Path) {
t.Fatalf("expected URL to have path as suffix but got %v and %v", u1.Bundle.Modules[0].URL, u1.Bundle.Modules[0].Path)
}
d.Stop(ctx)
}
func TestOCIDownloaderWithRegoV1Bundle(t *testing.T) {
tests := []struct {
note string
regoVersion ast.RegoVersion
expErr string
}{
// The bundle contains a v1 rego_version attr, so we expect no errors regardless of parser regoVersion.
{
note: "non-1.0 compatible OCI downloader",
},
{
note: "1.0 compatible OCI downloader",
regoVersion: ast.RegoV1,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
vc := bundle.NewVerificationConfig(map[string]*bundle.KeyConfig{"default": {Key: "secret", Algorithm: "HS256"}}, "", "", nil)
ctx := context.Background()
fixture := newTestFixture(t)
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"
// We might get multiple updates, a buffered channel will make sure we save the first one.
updates := make(chan *Update, 1)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
d := NewOCI(config, fixture.client, "ghcr.io/org/repo:rego_v1", "/tmp/opa/").
WithBundleParserOpts(ast.ParserOptions{RegoVersion: tc.regoVersion}).
WithCallback(func(_ context.Context, u Update) {
// We might get multiple updates before the test ends, and we don't want to block indefinitely.
select {
case updates <- &u:
}
}).WithBundleVerificationConfig(vc)
d.Start(ctx)
// Give time for some download events to occur
time.Sleep(1 * time.Second)
// We only care about the first update
u1 := <-updates
if tc.expErr != "" {
if u1.Error == nil {
t.Fatalf("expected error but got: %v", u1)
} else {
if !strings.Contains(u1.Error.Error(), tc.expErr) {
t.Fatalf("expected error:\n\n%v\n\nbut got:\n\n%v", tc.expErr, u1.Error)
}
}
} else {
if u1.Error != nil {
t.Fatalf("expected no error but got: %v", u1.Error)
}
if u1.Bundle == nil || len(u1.Bundle.Modules) == 0 {
t.Fatal("expected bundle with at least one module but got:", u1)
}
if !strings.HasSuffix(u1.Bundle.Modules[0].URL, u1.Bundle.Modules[0].Path) {
t.Fatalf("expected URL to have path as suffix but got %v and %v", u1.Bundle.Modules[0].URL, u1.Bundle.Modules[0].Path)
}
}
d.Stop(ctx)
})
}
}
func TestOCIStartStop(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t)
fixture.server.expEtag = "sha256:cc09b0f5ac97b11637c96ff1b0fbbc287c5ba0169813edaa71fe58424e95f0b7"
updates := make(chan *Update)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
d := NewOCI(config, fixture.client, "ghcr.io/org/repo:latest", "/tmp/opa/").WithCallback(func(_ context.Context, u Update) {
updates <- &u
})
d.Start(ctx)
// Give time for some download events to occur
time.Sleep(1 * time.Second)
u1 := <-updates
if u1.Bundle == nil || len(u1.Bundle.Modules) == 0 {
t.Fatal("expected bundle with at least one module but got:", u1)
}
if !strings.HasSuffix(u1.Bundle.Modules[0].URL, u1.Bundle.Modules[0].Path) {
t.Fatalf("expected URL to have path as suffix but got %v and %v", u1.Bundle.Modules[0].URL, u1.Bundle.Modules[0].Path)
}
d.Stop(ctx)
}
func TestOCIBearerAuthPlugin(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t)
plainToken := "secret"
token := base64.StdEncoding.EncodeToString([]byte(plainToken)) // token should be base64 encoded
fixture.server.expAuth = fmt.Sprintf("Bearer %s", token) // test on private repository
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"
restConf := fmt.Sprintf(`{
"url": %q,
"type": "oci",
"credentials": {
"bearer": {
"token": %q
}
}
}`, fixture.server.server.URL, plainToken)
client, err := rest.New([]byte(restConf), map[string]*keys.Config{})
if err != nil {
t.Fatal(err)
}
fixture.setClient(client)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
d := NewOCI(config, fixture.client, "ghcr.io/org/repo:latest", "/tmp/oci")
if err := d.oneShot(ctx); err != nil {
t.Fatal(err)
}
}
func TestOCIFailureAuthn(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t)
fixture.server.expAuth = "Bearer badsecret"
defer fixture.server.stop()
d := NewOCI(Config{}, fixture.client, "ghcr.io/org/repo:latest", "/tmp/oci")
err := d.oneShot(ctx)
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "401 Unauthorized") {
t.Fatal("expected 401 Unauthorized message")
}
}
func TestOCIEtag(t *testing.T) {
fixture := newTestFixture(t)
token := base64.StdEncoding.EncodeToString([]byte("secret")) // token should be base64 encoded
fixture.server.expAuth = fmt.Sprintf("Bearer %s", token) // test on private repository
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"
restConfig := []byte(fmt.Sprintf(`{
"url": %q,
"type": "oci",
"credentials": {
"bearer": {
"token": "secret"
}
}
}`, fixture.server.server.URL))
client, err := rest.New(restConfig, map[string]*keys.Config{})
if err != nil {
t.Fatal(err)
}
fixture.setClient(client)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
firstResponse := Update{ETag: ""}
d := NewOCI(config, fixture.client, "ghcr.io/org/repo:latest", "/tmp/oci").WithCallback(func(_ context.Context, u Update) {
if firstResponse.ETag == "" {
firstResponse = u
return
}
if u.ETag != firstResponse.ETag || u.Bundle != nil {
t.Fatal("expected nil bundle and same etag but got:", u)
}
})
// fill firstResponse
if err := d.oneShot(context.Background()); err != nil {
t.Fatal(err)
}
// Give time for some download events to occur
time.Sleep(1 * time.Second)
// second call to verify if nil bundle is returned and same etag
err = d.oneShot(context.Background())
if err != nil {
t.Fatal(err)
}
}
// TestOCIPublicRegistryAuth tests the registry `token` auth
// that is implemented by public registries (more details are
// in the doc comment of withPublicRegistryAuth).
//
// Other tests that don't explicitly set an authentication method
// implicitly test no authentication - this is different from
// the mechanism used by public registries.
func TestOCIPublicRegistryAuth(t *testing.T) {
fixture := newTestFixture(t, withPublicRegistryAuth())
restConfig := []byte(fmt.Sprintf(`{
"url": %q,
"type": "oci"
}`, fixture.server.server.URL))
client, err := rest.New(restConfig, map[string]*keys.Config{})
if err != nil {
t.Fatalf("failed to create rest client: %s", err)
}
fixture.client = client
d := NewOCI(Config{}, fixture.client, "ghcr.io/org/repo:latest", t.TempDir())
if err := d.oneShot(context.Background()); err != nil {
t.Fatalf("Unexpected error: %s", err)
}
}
// TestOCITokenAuth tests the registry `token` auth that is used for some registries (f.e. gitlab).
// After the initial fetch the token has to be added to the request that fetches the temporary token.
// This test verifies that the token is added to the second token request.
func TestOCITokenAuth(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t, withAuthenticatedTokenAuth())
plainToken := "secret"
token := base64.StdEncoding.EncodeToString([]byte(plainToken)) // token should be base64 encoded
fixture.server.expAuth = fmt.Sprintf("Bearer %s", token) // test on private repository
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"
restConf := fmt.Sprintf(`{
"url": %q,
"type": "oci",
"credentials": {
"bearer": {
"token": %q
}
}
}`, fixture.server.server.URL, plainToken)
client, err := rest.New([]byte(restConf), map[string]*keys.Config{})
if err != nil {
t.Fatalf("failed to create rest client: %s", err)
}
fixture.setClient(client)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
d := NewOCI(Config{}, fixture.client, "ghcr.io/org/repo:latest", t.TempDir())
if err := d.oneShot(ctx); err != nil {
t.Fatalf("Unexpected error: %s", err)
}
}
func TestOCICustomAuthPlugin(t *testing.T) {
fixture := newTestFixture(t)
defer fixture.server.stop()
restConfig := []byte(fmt.Sprintf(`{
"url": %q,
"credentials": {
"plugin": "my_plugin"
}
}`, fixture.server.server.URL))
client, err := rest.New(
restConfig,
map[string]*keys.Config{},
rest.AuthPluginLookup(mockAuthPluginLookup),
)
if err != nil {
t.Fatal(err)
}
fixture.setClient(client)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
tmpDir := t.TempDir()
d := NewOCI(config, fixture.client, "ghcr.io/org/repo:latest", tmpDir)
if err := d.oneShot(context.Background()); err != nil {
t.Fatal(err)
}
}
func TestOCIValidateAndInjectDefaults(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t)
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"
updates := make(chan *Update)
config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}
d := NewOCI(config, fixture.client, "ghcr.io/org/repo:latest", t.TempDir()).WithCallback(func(_ context.Context, u Update) {
updates <- &u
}).WithBundlePersistence(true)
d.Start(ctx)
// Give time for some download events to occur
time.Sleep(1 * time.Second)
u1 := <-updates
if u1.Size == 0 {
t.Fatal("expected non-0 size")
}
if u1.Raw == nil {
t.Fatal("expected bundle reader to be non-nil")
}
r := bundle.NewReader(u1.Raw)
b, err := r.Read()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(b.Data, u1.Bundle.Data) {
t.Fatal("expected the bundle object and reader to have the same data")
}
if len(b.Modules) != len(u1.Bundle.Modules) {
t.Fatal("expected the bundle object and reader to have the same number of bundle modules")
}
d.Stop(ctx)
}
func mockAuthPluginLookup(string) rest.HTTPAuthPlugin {
return &mockAuthPlugin{}
}
type mockAuthPlugin struct{}
func (p *mockAuthPlugin) NewClient(c rest.Config) (*http.Client, error) {
tlsConfig, err := rest.DefaultTLSConfig(c)
if err != nil {
return nil, err
}
timeoutSec := 10
client := rest.DefaultRoundTripperClient(
tlsConfig,
int64(timeoutSec),
)
return client, nil
}
func (*mockAuthPlugin) Prepare(r *http.Request) error {
r.Header.Set("Authorization", "Bearer secret")
return nil
}
-59
View File
@@ -1,59 +0,0 @@
//go:build opa_no_oci
package download
import (
"context"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/plugins/rest"
)
func NewOCI(Config, rest.Client, string, string) *OCIDownloader {
panic("built without OCI support")
}
func (d *OCIDownloader) WithCallback(f func(context.Context, Update)) *OCIDownloader {
panic("built without OCI support")
}
func (d *OCIDownloader) WithLogAttrs(map[string]interface{}) *OCIDownloader {
panic("built without OCI support")
}
func (d *OCIDownloader) WithBundleVerificationConfig(*bundle.VerificationConfig) *OCIDownloader {
panic("built without OCI support")
}
func (d *OCIDownloader) WithSizeLimitBytes(int64) *OCIDownloader {
panic("built without OCI support")
}
func (d *OCIDownloader) WithBundlePersistence(bool) *OCIDownloader {
panic("built without OCI support")
}
func (d *OCIDownloader) ClearCache() {
panic("built without OCI support")
}
func (d *OCIDownloader) SetCache(string) {
panic("built without OCI support")
}
func (d *OCIDownloader) Trigger(context.Context) error {
panic("built without OCI support")
}
func (d *OCIDownloader) Start(context.Context) {
panic("built without OCI support")
}
func (d *OCIDownloader) Stop(context.Context) {
panic("built without OCI support")
}
func (*OCIDownloader) WithBundleParserOpts(ast.ParserOptions) *OCIDownloader {
panic("built without OCI support")
}
-32
View File
@@ -1,32 +0,0 @@
package download
import (
"context"
"sync"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/logging"
"github.com/open-policy-agent/opa/plugins/rest"
"oras.land/oras-go/v2/content/oci"
)
type OCIDownloader struct {
config Config // downloader configuration for tuning polling and other downloader behaviour
client rest.Client // HTTP client to use for bundle downloading
path string // path for OCI image as <registry>/<org>/<repo>:<tag>
localStorePath string // path for the local OCI storage
trigger chan chan struct{} // channel to signal downloads when manual triggering is enabled
stop chan chan struct{} // used to signal plugin to stop running
f func(context.Context, Update) // callback function invoked when download updates occur
sizeLimitBytes *int64 // max bundle file size in bytes (passed to reader)
bvc *bundle.VerificationConfig
wg sync.WaitGroup
logger logging.Logger
mtx sync.Mutex
stopped bool
persist bool
store *oci.Store
etag string
bundleParserOpts ast.ParserOptions
}
-1
View File
@@ -1 +0,0 @@
{}
-19
View File
@@ -1,19 +0,0 @@
{
"schemaVersion":2,
"config":{
"mediaType":"application/vnd.oci.image.config.v1+json",
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"size":2
},
"layers":[
{
"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip",
"digest":"sha256:d85a3b7072e295a091f4ec50e85fefcd5285a1e2c60c298c0b87c498f1cb0613",
"size":610,
"annotations":{
"org.opencontainers.image.created":"2022-02-11T09:00:07Z",
"org.opencontainers.image.title":"dani/testpol"
}
}
]
}
BIN
View File
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
{"revision":"","roots":["peoplefinder"]}
-1
View File
@@ -1 +0,0 @@
{}
@@ -1,23 +0,0 @@
package peoplefinder.DELETE.api.users.__id
import rego.v1
import input.user.attributes.properties as user_props
default allowed = false
default visible = false
default enabled = false
allowed if {
user_props.department == "Operations"
user_props.title == "IT Manager"
}
visible if {
user_props.department == "Operations"
}
enabled if {
allowed
}
@@ -1,7 +0,0 @@
package peoplefinder.GET.api.users.__id
default allowed = true
default visible = true
default enabled = true
@@ -1,18 +0,0 @@
package peoplefinder.POST.api.users.__id
import rego.v1
import input.user.attributes.properties as user_props
default allowed = false
default visible = true
default enabled = false
allowed if {
user_props.department == "Operations"
}
enabled if {
allowed
}
@@ -1,18 +0,0 @@
package peoplefinder.PUT.api.users.__id
import rego.v1
import input.user.attributes.properties as user_props
default allowed = false
default visible = true
default enabled = true
allowed if {
user_props.department == "Operations"
}
allowed if {
input.user.id == input.resource.id
}
@@ -1,7 +0,0 @@
package peoplefinder.GET.api.users
default allowed = true
default visible = true
default enabled = true
@@ -1,23 +0,0 @@
package peoplefinder.POST.api.users
import rego.v1
import input.user.attributes.properties as user_props
default allowed = false
default visible = false
default enabled = false
allowed if {
user_props.department == "Operations"
user_props.title == "IT Manager"
}
visible if {
allowed
}
enabled if {
allowed
}
-19
View File
@@ -1,19 +0,0 @@
{
"schemaVersion":2,
"config":{
"mediaType":"application/vnd.oci.image.config.v1+json",
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"size":2
},
"layers":[
{
"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip",
"digest":"sha256:0f93a2c5964d7c8b676e3b507b6bc3b771c086428dece6f84256b9948cb3f256",
"size":830,
"annotations":{
"org.opencontainers.image.created":"2022-02-11T09:00:07Z",
"org.opencontainers.image.title":"dani/testpol"
}
}
]
}
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
[1,2,3]
@@ -1,9 +0,0 @@
package example
violations contains msg if {
msg := "hello"
}
allow if {
count(violations) == 0
}
-19
View File
@@ -1,19 +0,0 @@
{
"schemaVersion":2,
"config":{
"mediaType":"application/vnd.oci.image.config.v1+json",
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"size":2
},
"layers":[
{
"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip",
"digest":"sha256:7fccf82798e6e627afd04144889570d966583788473db2888f0d0d325904d273",
"size":764,
"annotations":{
"org.opencontainers.image.created":"2022-02-11T09:00:07Z",
"org.opencontainers.image.title":"dani/testpol"
}
}
]
}
BIN
View File
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
[1,2,3]
@@ -1 +0,0 @@
package example
-557
View File
@@ -1,557 +0,0 @@
//go:build slow
// +build slow
package download
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/keys"
"github.com/open-policy-agent/opa/plugins/rest"
)
var errUnauthorized = errors.New("401 Unauthorized")
type testFixture struct {
d *Downloader
client rest.Client
server *testServer
updates []Update
mockBundleActivationError bool
etags map[string]string
}
func newTestFixture(t *testing.T, opts ...fixtureOpt) testFixture {
t.Helper()
ts := newTestServer(t)
ts.start()
restConfig := []byte(fmt.Sprintf(`{"url": %q}`, ts.server.URL))
client, err := rest.New(restConfig, map[string]*keys.Config{})
if err != nil {
t.Fatal(err)
}
fixture := testFixture{
server: ts,
client: client,
etags: make(map[string]string),
}
for i, opt := range opts {
if err := opt(&fixture); err != nil {
t.Fatalf("Failed applying option #%d: %s", i, err)
}
}
return fixture
}
type fixtureOpt func(*testFixture) error
// withPublicRegistryAuth sets up a token auth flow according to
// the spec https://docs.docker.com/registry/spec/auth/token/.
//
// This authentication method is implemented by public
// repositories of Github Container Registry, Docker Hub and
// AWS ECR (and likely others) and corresponds with the auth
// method `token` of the github.com/distribution/distribution
// registry project.
// See https://docs.docker.com/registry/configuration/#token.
//
// The token issuing and validation differs between providers
// and we only use a minimal version for testing.
func withPublicRegistryAuth() fixtureOpt {
const token = "some-test-token"
tokenServer := httptest.NewServer(tokenHandler(token))
const wwwAuthenticateFmt = "Bearer realm=%q service=%q scope=%q"
tokenServiceURL := tokenServer.URL + "/token"
wwwAuthenticate := fmt.Sprintf(wwwAuthenticateFmt,
tokenServiceURL,
"testRegistry.io",
"[pull]")
return func(tf *testFixture) error {
tf.server.customAuth = func(w http.ResponseWriter, r *http.Request) error {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("no authorization header: %w", errUnauthorized)
}
if !strings.HasPrefix(authHeader, "Bearer ") {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("expects bearer scheme: %w", errUnauthorized)
}
bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
if bearerToken != token {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("token %q doesn't match %q: %w", bearerToken, token, errUnauthorized)
}
return nil
}
return nil
}
}
// withAuthenticatedTokenAuth sets up a token auth flow according to
// the spec https://docs.docker.com/registry/spec/auth/token/.
//
// The flow is the same as for public registries but additionally
// the request for fetching the token also has to be authenticated.
// Used for example with gitlab registries.
//
// The token issuing and validation differs between providers,
// and we only use a minimal version for testing.
func withAuthenticatedTokenAuth() fixtureOpt {
const token = "some-test-token"
tokenServer := httptest.NewServer(tokenHandlerAuth("c2VjcmV0", token))
const wwwAuthenticateFmt = "Bearer realm=%q service=%q scope=%q"
tokenServiceURL := tokenServer.URL + "/token"
wwwAuthenticate := fmt.Sprintf(wwwAuthenticateFmt,
tokenServiceURL,
"testRegistry.io",
"[pull]")
return func(tf *testFixture) error {
tf.server.customAuth = func(w http.ResponseWriter, r *http.Request) error {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("no authorization header: %w", errUnauthorized)
}
if !strings.HasPrefix(authHeader, "Bearer ") {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("expects bearer scheme: %w", errUnauthorized)
}
bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
if bearerToken != token {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("token %q doesn't match %q: %w", bearerToken, token, errUnauthorized)
}
return nil
}
return nil
}
}
// tokenHandler returns an http.Handler that responds with the
// specified token to GET /token requests.
func tokenHandler(issuedToken string) http.HandlerFunc {
return tokenHandlerAuth("", issuedToken)
}
// tokenHandlerAuth returns an http.Handler that responds with the
// specified token to GET /token requests.
//
// If expectedToken is not empty, the handler will check that the
// Authorization header matches the expected token.
func tokenHandlerAuth(expectedToken, issuedToken string) http.HandlerFunc {
tokenResponse := struct {
Token string `json:"token"`
}{
Token: issuedToken,
}
responseBody, err := json.Marshal(tokenResponse)
if err != nil {
panic("failed to marshal token response: " + err.Error())
}
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if r.URL.Path != "/token" {
w.WriteHeader(http.StatusBadRequest)
return
}
// If no expected token is set, we don't check the Authorization header.
if expectedToken == "" {
_, _ = w.Write(responseBody)
return
}
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
if !strings.HasPrefix(authHeader, "Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
return
}
bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
if bearerToken != expectedToken {
w.WriteHeader(http.StatusUnauthorized)
return
}
_, _ = w.Write(responseBody)
}
}
func (t *testFixture) setClient(client rest.Client) {
t.client = client
}
func (t *testFixture) oneShot(ctx context.Context, u Update) {
t.updates = append(t.updates, u)
if u.Error != nil {
etag := t.etags["test/bundle1"]
t.d.SetCache(etag)
return
}
if u.Bundle != nil {
if t.mockBundleActivationError {
etag := t.etags["test/bundle1"]
t.d.SetCache(etag)
return
}
}
t.etags["test/bundle1"] = u.ETag
}
type fileInfo struct {
name string
length int64
}
type testServer struct {
t *testing.T
customAuth func(http.ResponseWriter, *http.Request) error
expCode int
expResp string
expEtag string
expAuth string
bundles map[string]bundle.Bundle
server *httptest.Server
etagInResponse bool
longPoll bool
testdataHashes map[string]fileInfo
}
func newTestServer(t *testing.T) *testServer {
return &testServer{
t: t,
bundles: map[string]bundle.Bundle{
"test/bundle1": {
Manifest: bundle.Manifest{
Revision: "quickbrownfaux",
},
Data: map[string]interface{}{
"foo": map[string]interface{}{
"bar": json.Number("1"),
"baz": "qux",
},
},
Modules: []bundle.ModuleFile{
{
Path: `/example.rego`,
Raw: []byte("package foo\n\ncorge=1"),
},
},
},
"test/bundle2": {
Manifest: bundle.Manifest{
Revision: deltaBundleMode,
},
Patch: bundle.Patch{Data: []bundle.PatchOperation{
{
Op: "upsert",
Path: "/a/c/d",
Value: []string{"foo", "bar"},
},
}},
},
"test/v1compat/no_imports": {
Manifest: bundle.Manifest{
Revision: "quickbrownfaux",
},
Data: map[string]interface{}{},
Modules: []bundle.ModuleFile{
{
Path: `/example.rego`,
Raw: []byte(`package test
import data.foo
import data.bar as foo
p contains 1 if {
input.x == 2
}`),
},
},
},
"test/v1compat/rego_import": {
Manifest: bundle.Manifest{
Revision: "quickbrownfaux",
},
Data: map[string]interface{}{},
Modules: []bundle.ModuleFile{
{
Path: `/example.rego`,
Raw: []byte(`package test
import rego.v1
import data.foo
import data.bar as foo
p contains 1 if {
input.x == 2
}`),
},
},
},
"test/v1compat/keywords_not_used": {
Manifest: bundle.Manifest{
Revision: "quickbrownfaux",
},
Data: map[string]interface{}{},
Modules: []bundle.ModuleFile{
{
Path: `/example.rego`,
Raw: []byte(`package test
p[1] {
input.x == 2
}`),
},
},
},
},
}
}
func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
if t.longPoll {
var timeout time.Duration
wait := getPreferHeaderField(r, "wait")
if wait != "" {
waitTime, err := strconv.Atoi(wait)
if err != nil {
panic(err)
}
timeout = time.Duration(waitTime) * time.Second
}
// simulate long operation
time.Sleep(timeout)
}
if t.expCode != 0 {
w.WriteHeader(t.expCode)
if t.expResp != "" {
w.Write([]byte(t.expResp))
}
return
}
if t.customAuth != nil {
if err := t.customAuth(w, r); err != nil {
t.t.Logf("Failed authorization: %s", err)
if errors.Is(err, errUnauthorized) {
w.WriteHeader(401)
return
}
w.WriteHeader(500)
return
}
} else if t.expAuth != "" {
if r.Header.Get("Authorization") != t.expAuth {
w.WriteHeader(401)
return
}
}
var buf bytes.Buffer
if strings.HasPrefix(r.URL.Path, "/v2/org/repo/") {
// build test data to hash map to serve testdata files by hash
if t.testdataHashes == nil {
t.testdataHashes = make(map[string]fileInfo)
files, err := os.ReadDir("testdata")
if err != nil {
t.t.Fatalf("failed to read testdata directory: %s", err)
}
for _, file := range files {
if file.IsDir() {
continue
}
hash, length, err := getFileSHAandSize("testdata/" + file.Name())
if err != nil {
t.t.Fatalf("failed to read testdata file: %s", err)
}
t.testdataHashes[fmt.Sprintf("%x", hash)] = fileInfo{name: file.Name(), length: length}
}
}
}
if strings.HasPrefix(r.URL.Path, "/v2/org/repo/blobs/sha256:") || strings.HasPrefix(r.URL.Path, "/v2/org/repo/manifests/sha256:") {
sha := strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, "/v2/org/repo/blobs/sha256:"), "/v2/org/repo/manifests/sha256:")
if fileInfo, ok := t.testdataHashes[sha]; ok {
w.Header().Add("Content-Length", strconv.Itoa(int(fileInfo.length)))
w.Header().Add("Content-Type", "application/gzip")
w.Header().Add("Docker-Content-Digest", "sha256:"+sha)
w.WriteHeader(200)
bs, err := os.ReadFile("testdata/" + fileInfo.name)
if err != nil {
w.WriteHeader(404)
return
}
buf.WriteString(string(bs))
w.Write(buf.Bytes())
return
}
w.WriteHeader(404)
return
}
if strings.HasPrefix(r.URL.Path, "/v2/org/repo/manifests/") {
sha, size, err := getFileSHAandSize("testdata/" + strings.TrimPrefix(r.URL.Path, "/v2/org/repo/manifests/") + ".manifest")
if err != nil {
w.WriteHeader(404)
return
}
w.Header().Add("Content-Length", strconv.Itoa(int(size)))
w.Header().Add("Content-Type", "application/vnd.oci.image.manifest.v1+json")
w.Header().Add("Docker-Content-Digest", "sha256:"+fmt.Sprintf("%x", sha))
w.WriteHeader(200)
return
}
name := strings.TrimPrefix(r.URL.Path, "/bundles/")
b, ok := t.bundles[name]
if !ok {
w.WriteHeader(404)
return
}
// check to verify if server can send a delta bundle to OPA
if b.Manifest.Revision == deltaBundleMode {
modes := strings.Split(getPreferHeaderField(r, "modes"), ",")
found := false
for _, m := range modes {
if m == deltaBundleMode {
found = true
break
}
}
if !found {
panic("delta bundle requested but OPA does not support it")
}
}
contentTypeShouldBeSend := true
if t.expEtag != "" {
etag := r.Header.Get("If-None-Match")
if etag == t.expEtag {
contentTypeShouldBeSend = false
if t.etagInResponse {
w.Header().Add("Etag", t.expEtag)
}
w.WriteHeader(304)
return
}
}
if t.longPoll && contentTypeShouldBeSend {
// in 304 Content-Type is not send according https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
w.Header().Add("Content-Type", "application/vnd.openpolicyagent.bundles")
} else {
if r.URL.Path == "/bundles/not-a-bundle" {
w.Header().Add("Content-Type", "text/html")
} else {
w.Header().Add("Content-Type", "application/gzip")
}
}
if t.expEtag != "" {
w.Header().Add("Etag", t.expEtag)
}
w.WriteHeader(200)
if err := bundle.Write(&buf, b); err != nil {
w.WriteHeader(500)
}
if _, err := w.Write(buf.Bytes()); err != nil {
panic(err)
}
}
func (t *testServer) start() {
t.server = httptest.NewServer(http.HandlerFunc(t.handle))
}
func (t *testServer) stop() {
t.server.Close()
}
func getPreferHeaderField(r *http.Request, field string) string {
for _, line := range r.Header.Values("prefer") {
for _, part := range strings.Split(line, ";") {
preference := strings.Split(strings.TrimSpace(part), "=")
if len(preference) == 2 {
if strings.ToLower(preference[0]) == field {
return preference[1]
}
}
}
}
return ""
}
func getFileSHAandSize(filePath string) ([]byte, int64, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, 0, err
}
defer f.Close()
hash := sha256.New()
w, err := io.Copy(hash, f)
if err != nil {
return nil, w, err
}
return hash.Sum(nil), w, nil
}