chore(policies): disable gRPC or event handlers by configuration + metrics

* add the ability to disable the gRPC API handler
   (POLICIES_GRPC_DISABLED)

 * add the ability to disable the Events API handler
   (POLICIES_EVENTS_DISABLED)

 * add metrics

 * add support for specifying rego files via the environment varirable
   POLICIES_ENGINE_FILES

 * for file paths specified in yaml or in POLICIES_ENGINE_FILES, support
   'config:' and 'data:' path prefixes

 * fix typos in the documentation, and try to make it more clear

 * add metrics to the documentation

 * add a section for testing in the documentation

 * introduces a new top-level package pkg/metrics/ with utilities for
   metrics that are backported from the groupware branch, with unit
   tests
This commit is contained in:
Pascal Bleser committed 2026-08-31 11:54:32 +02:00
1 parent 32a35f8653
commit 8821053ff8
16 files changed
+761 -102

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
package metrics
import (
"fmt"
"reflect"
"strings"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/version"
"github.com/prometheus/client_golang/prometheus"
)
type BuildInfoMetric = *prometheus.GaugeVec
// Create a BuildInfo metric for the specified namespace and subsystem.
func BuildInfo(namespace, subsystem string) BuildInfoMetric {
return prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "build_info",
Help: "Build information",
}, []string{"version"})
}
// Determine the fully qualified name of a metric.
//
// Beware that this requires storing a value into the metric in order to make it
// visible in a temporary registry.
// If the metric is a MetricVec, it will be Reset().
func describe(metric prometheus.Collector, initialize func() error) (string, error) {
reg := prometheus.NewRegistry()
if err := reg.Register(metric); err != nil {
return "", err
}
if err := initialize(); err != nil {
return "", err
}
if resettable, ok := metric.(*prometheus.MetricVec); ok {
defer resettable.Reset()
}
fams, err := reg.Gather()
if err != nil {
return "", err
}
if len(fams) == 0 {
return "", fmt.Errorf("no metric families gathered")
}
return fams[0].GetName(), nil
}
// Take a struct that contains metrics as attributes and register all of them
// with the specified Registerer.
func RegisterAll(registerer prometheus.Registerer, m any, logger *log.Logger) error {
// we go over all of them, use this to keep track of succeesses and failures
total := 0
succeeded := []string{}
failed := map[string]error{}
// we need to use reflection here to iterate over the public metric attributes
// that are contained in it
r := reflect.ValueOf(m)
if r.Kind() == reflect.Pointer {
r = r.Elem()
}
for i := 0; i < r.NumField(); i++ {
n := r.Type().Field(i).Name // the name of the attribute (not the name of the metric)
f := r.Field(i)
v := f.Interface()
switch c := v.(type) {
case prometheus.Collector:
total++
if err := registerer.Register(c); err != nil {
switch err.(type) {
case prometheus.AlreadyRegisteredError:
// silently ignore this error, as this case can happen when the suture service decides to restart
err = nil
succeeded = append(succeeded, n)
default:
failed[n] = err
}
} else {
succeeded = append(succeeded, n)
// special post-treatment for the BuildInfo metric, as we have that one pretty much
// everywhere: set its value with the current version so we don't need to do that every time
switch buildInfo := c.(type) {
case BuildInfoMetric:
if name, err := describe(buildInfo, func() error { buildInfo.WithLabelValues("0").Set(0.0); return nil }); err != nil {
failed[n] = err
} else if strings.HasSuffix(name, "_build_info") {
buildInfo.Reset()
buildInfo.WithLabelValues(version.GetString()).Set(1)
}
}
}
case *prometheus.Desc, prometheus.GaugeOpts, prometheus.CounterOpts, prometheus.UntypedOpts:
// skip these
default:
failed[n] = fmt.Errorf("unsupported metric '%s' of type %T", n, c)
}
}
if len(failed) > 0 {
failedMsgs := []string{}
for name, err := range failed {
failedMsgs = append(failedMsgs, fmt.Sprintf("'%s' (%v)", name, err))
}
msg := strings.Join(failedMsgs, ", ")
logger.Warn().Msgf("registered %d/%d metrics successfully (%d failed): %s", len(succeeded), total, len(failed), msg)
return fmt.Errorf("failed to register metrics: %s", msg)
} else {
logger.Debug().Msgf("registered %d/%d metrics successfully (%d failed)", len(succeeded), total, len(failed))
return nil
}
}
// Register all the metrics that are contained as public attributes in the struct,
// and log any errors that might occur while doing so.
func Register[M any](logger *log.Logger, m M) (M, error) {
reg := NewLoggingPrometheusRegisterer(prometheus.DefaultRegisterer, logger)
err := RegisterAll(reg, m, logger)
return m, err
}
// Prometheus Registerer wrapper that logs every error that occurs when registering
// a metric, and delegates to an actual Registerer.
type LoggingPrometheusRegisterer struct {
delegate prometheus.Registerer
logger *log.Logger
}
// Instantiate a Prometheus Registerer wrapper that logs every error that occurs when registering
// a metric, and that delegates to an actual Registerer specified here.
func NewLoggingPrometheusRegisterer(delegate prometheus.Registerer, logger *log.Logger) *LoggingPrometheusRegisterer {
return &LoggingPrometheusRegisterer{
delegate: delegate,
logger: logger,
}
}
func (r *LoggingPrometheusRegisterer) Register(c prometheus.Collector) error {
err := r.delegate.Register(c)
if err != nil {
switch err.(type) {
case prometheus.AlreadyRegisteredError:
// silently ignore this error, as this case can happen when the suture service decides to restart
err = nil
default:
r.logger.Warn().Err(err).Msgf("failed to register metric")
}
}
return err
}
func (r *LoggingPrometheusRegisterer) MustRegister(collectors ...prometheus.Collector) {
for _, c := range collectors {
if err := r.Register(c); err != nil {
r.logger.Error().Err(err).Msg("failed to register metrics collector")
}
}
}
func (r *LoggingPrometheusRegisterer) Unregister(c prometheus.Collector) bool {
return r.delegate.Unregister(c)
}
var _ prometheus.Registerer = &LoggingPrometheusRegisterer{}
+153
View File
@@ -0,0 +1,153 @@
package metrics
import (
"fmt"
"math/rand/v2"
"testing"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/version"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
func randName() string {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
n := 8 + rand.IntN(33)
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.IntN(len(letterBytes))]
}
return string(b)
}
func TestBuildInfo(t *testing.T) {
require := require.New(t)
namespace := "name-" + randName()
subsystem := "sub-" + randName()
expectedName := fmt.Sprintf("%s_%s_build_info", namespace, subsystem)
version := fmt.Sprintf("%d.%d.%d", rand.IntN(10), rand.IntN(10), rand.IntN(10))
g := BuildInfo(namespace, subsystem)
reg := prometheus.NewRegistry()
require.NoError(reg.Register(g))
{
mfs, err := reg.Gather()
require.NoError(err)
require.Len(mfs, 0)
}
g.WithLabelValues(version).Set(1)
{
mfs, err := reg.Gather()
require.NoError(err)
found := false
for _, mf := range mfs {
if mf.GetName() == expectedName {
found = true
ms := mf.GetMetric()
require.Len(ms, 1)
labels := ms[0].GetLabel()
require.Len(labels, 1)
require.NotNil(labels[0].Name)
require.Equal("version", *labels[0].Name)
require.NotNil(labels[0].Value)
require.Equal(version, *labels[0].Value)
require.Equal(1.0, ms[0].GetGauge().GetValue())
} else {
t.Fatalf("unexpected metric family %q", mf.GetName())
}
}
require.True(found, "failed to find metric %q", expectedName)
}
}
func TestRegisterAll(t *testing.T) {
require := require.New(t)
reg := prometheus.NewRegistry()
logger := log.NewLogger()
namespace := "name-" + randName()
subsystem := "sub-" + randName()
m := struct {
BuildInfo *prometheus.GaugeVec
Foo *prometheus.GaugeVec
Bar prometheus.Counter
}{
BuildInfo: BuildInfo(namespace, subsystem),
Foo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "foo",
ConstLabels: prometheus.Labels{
"f": "oo",
"fo": "o",
},
}, []string{"oof"}),
Bar: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "bar",
}),
}
expectedNameForBuildInfo := namespace + "_" + subsystem + "_build_info"
expectedNameForFoo := namespace + "_" + subsystem + "_foo"
expectedNameForBar := namespace + "_" + subsystem + "_bar"
{
mfs, err := reg.Gather()
require.NoError(err)
require.Len(mfs, 0)
}
require.NoError(RegisterAll(reg, m, &logger))
{
mfs, err := reg.Gather()
require.NoError(err)
require.Len(mfs, 3)
found := 0
for _, mf := range mfs {
switch mf.GetName() {
case expectedNameForBuildInfo:
found++
ms := mf.GetMetric()
require.Len(ms, 1)
labels := ms[0].GetLabel()
require.Len(labels, 1)
require.NotNil(labels[0].Name)
require.Equal("version", *labels[0].Name)
require.NotNil(labels[0].Value)
require.Equal(version.GetString(), *labels[0].Value)
require.Equal(1.0, ms[0].GetGauge().GetValue())
case expectedNameForFoo:
found++
ms := mf.GetMetric()
require.Len(ms, 1)
labels := ms[0].GetLabel()
require.Len(labels, 3)
require.NotNil(labels[0].Name)
require.Equal("f", *labels[0].Name)
require.NotNil(labels[0].Value)
require.Equal("oo", *labels[0].Value)
require.NotNil(labels[1].Name)
require.Equal("fo", *labels[1].Name)
require.NotNil(labels[1].Value)
require.Equal("o", *labels[1].Value)
require.Equal(0.0, ms[0].GetGauge().GetValue())
case expectedNameForBar:
found++
ms := mf.GetMetric()
require.Len(ms, 1)
labels := ms[0].GetLabel()
require.Len(labels, 0)
require.Equal(0.0, ms[0].GetGauge().GetValue())
default:
t.Fatalf("unexpected metric family %q", mf.GetName())
}
}
require.Equal(3, found, "failed to find expected metrics")
}
}
+146 -67
View File
@@ -1,14 +1,12 @@
# Policies
The policies service provides a new gRPC API which can be used to check whether a requested operation is allowed or not.
To do so, Open Policy Agent (OPA) is used to define the set of rules of what is permitted and what is not.
The policies service provides a gRPC and an Events API which can be used to check whether a requested operation is allowed or not. To do so, [Open Policy Agent (OPA)](https://www.openpolicyagent.org/) is used to define the set of rules of what is permitted and what is not.
Policies are written in the [rego query language](https://www.openpolicyagent.org/docs/latest/policy-language/). The
location of the rego files can be configured via yaml, a configuration via environment variables is not possible.
Policies are written in the [rego query language](https://www.openpolicyagent.org/docs/latest/policy-language/). The location of the rego files can be configured via yaml or via the environment variable `POLICIES_ENGINE_FILES`.
## General Information
The policies service consists of the following modules:
The policies service consists of the following modules that are able to process policy authorization requests for different API use-cases:
* Proxy authorization (middleware)
* Event authorization (async post-processing)
@@ -24,38 +22,40 @@ Note that each query setting defines the [Complete Rules](https://www.openpolicy
variable defined in the rego rule set the corresponding step uses for the evaluation. If the variable is mistyped or not
found, the evaluation defaults to deny. Individual query definitions can be defined for each module.
To activate the policies service for a module, it must be started with a yaml configuration that points to at least one
rego file that contains the complete rule variable to be queried. Note that if the service is scaled horizontally, each
instance should have access to the same rego files to avoid unpredictable results.
To activate it for a module, the `policies` service must be started with a yaml configuration or by setting the environment variable `POLICIES_ENGINE_FILES` that may contain a comma-separated list of file paths, either of which points to at least one rego file that contains the complete rule variable to be queried.
The rego files are read once when the service starts. A changed rule set takes effect after a restart, and a configured
file that is missing or does not parse keeps the service from starting.
file that is missing or does not parse keeps the service from starting, as it will abort with an error.
When using async post-processing via the postprocessing service, the value `policies` must be added to the
`POSTPROCESSING_STEPS` configuration in the order in which the evaluation should take place. Example: First check if a
file contains questionable content via policies. If it looks okay, continue to check for viruses.
Note that if the service is scaled horizontally, each instance should have access to the same rego files to avoid unpredictable results. If a file path has been configured but the file is not present or accessible, the evaluation defaults to deny.
For configuration examples, the [Example Policies](#example-policies) from below are used.
If a directory is specified in the list of paths, `.rego` files will be looked up and loaded recursively in that directory.
When using async post-processing, which is done via the `postprocessing` service, the value `policies` must be added to the `POSTPROCESSING_STEPS` configuration in the `postprocessing` service in the order in which the policies evaluation should take place.
Example: First check if a file contains questionable content via policies. If it looks okay, continue to check for viruses.
Configuration examples may be found in the [Example Policies](#example-policies) section below.
## Modules
### gRPC API
The gRPC API can be used by any other internal service. It can also be used for example by third parties to find out if
an action is allowed or not. This layer is already used by the proxy middleware. There is no configuration necessary,
because the query setting (complete rule variable) must be part of the request.
The gRPC API can be used by any other internal service. It can also be used for example by third parties to find out if an action is allowed or not. This layer is already used by the `proxy` middleware.
No configuration is necessary, because the query setting (complete rule variable) that should be evaluated is part of the request that is sent to the gRPC API.
Note that the gRPC API handler may be disabled via the configuration setting `disabled` in the `grpc` section, or via the environment variable `POLICIES_GRPC_DISABLED`, which ought to be set to `true` to be disabled, and defaults to `false`.
The purpose of disabling the gRPC API is to set up pools of `policies` service processes (or containers, or pods) that specialize in serving only the async Events API.
### Proxy Middleware
The proxy service already includes a middleware which uses the internal [gRPC API](#grpc-api) to evaluate the policies.
Since the proxy is in heavy use and every HTTP request is processed here, only simple and quick decisions should be
evaluated. More complex queries such as file content evaluation are _strongly_ discouraged.
The `proxy` service already includes a middleware which uses the internal [gRPC API](#grpc-api) to evaluate the policies. Since the proxy is in heavy use and every inbound HTTP request is processed by the `policies` service, only simple and quick decisions should be evaluated. More complex queries such as file content evaluation are _strongly_ discouraged.
The middleware only denies on the outcome of the policy, it makes no decision of its own. Where the file name is not
part of the request path, for example when a single shared resource is uploaded to by its id, the middleware stats the
resource to obtain it. If that stat fails, `input.resource.name` reaches the policy empty and the policy decides what
that means. Prefer policies that state which files are allowed over policies that list what is forbidden, a rule
matching on a specific extension does not match an empty name:
The `proxy` middleware only denies based on the outcome of the policy, it makes no decision of its own. Where the file name is not part of the request path, for example when a single shared resource is uploaded by its identifier, the middleware stats the resource to obtain it. If that stat fails, `input.resource.name` is an empty string and the policy may still decide what that means and how to proceed.
Prefer policies that state which files are allowed over policies that list what is forbidden, a rule matching on a specific extension does not match an empty name:
```rego
granted = false if {
@@ -63,8 +63,7 @@ granted = false if {
}
```
If the evaluation in the proxy results in a "denied" outcome, the response will return a `403 Permission Denied` with
the following response body
If the outcome of the evaluation in the `proxy` results in access being denied, the response will return a `403 Permission Denied` with the following response body:
```json
{
@@ -84,20 +83,28 @@ the following response body
### Event Service (Postprocessing)
This layer is event-based and part of the postprocessing service. Since processing at this point is asynchronous, the
operations can also take longer and be more expensive, like evaluating the contents of a file.
This layer is event-based and part of the `postprocessing` service. Since processing at this point is asynchronous, the operations can also take longer and be more expensive, like evaluating the contents of a file.
For processing asynchronous requests that come in through the Events API, one must set the `POLICIES_POSTPROCESSING_QUERY` environment variable, or the `query` string in the `postprocessing` section in the YAML configuration.
Note that the Events API handler may be disabled via the configuration setting `disabled` in the `events` section, or via the environment variable `POLICIES_EVENTS_DISABLED`, which ought to be set to `true` to be disabled, and defaults to `false`.
The purpose of disabling the Events API is to set up pools of `policies` service processes (or containers, or pods) that specialize in serving only the gRPC API.
## Defining Policies to Evaluate
Each module can have as many policy files as needed for evaluation. Files can also include other files if necessary. To
use policies, they have to be saved to a location that is accessible to the policies service. As a good starting point,
take the config directory and use a subdirectory collecting all the `.rego` files, though any other directory can be
defined. The config directory is already accessible by all services and usually is included in a xref:
maintenance/b-r/backup.adoc[backup] plan.
Each module can have as many policy files as needed for evaluation. Files can also include other files if necessary.
To use policies, they have to be saved to a location that is accessible to the policies service. As a good starting point, take the config directory and use a subdirectory collecting all the `.rego` files, though any other directory can be defined. The config directory is already accessible by all services and usually is included in a backup plan.
The list of files or directories specified there (or as comma-separated strings in the environment variable `POLICIES_ENGINE_FILES`) may make use of two magic prefixes that are resolved at runtime, for convenience:
* in paths starting with `config:`, that string is replaced by the OpenCloud configuration directory
* in paths starting with `data:`, that string is replaced by the OpenCloud base directory
If this is done, it's required to configure the policies service to use these files:
NOTE: It is important that *all* necessary files are added to the list of files the policies service uses.
NOTE: It is important that _all_ necessary files are added to the list of files the policies service uses.
```yaml
policies:
@@ -108,25 +115,35 @@ policies:
- your_path_to_policies/util.rego
```
Once the references to policy files are configured correctly, the `_QUERY` configuration needs to be defined for the
proxy middleware and for the events service.
Alternatively, using the environment variable:
```bash
POLICIES_ENGINE_FILES="data:policies/proxy.rego,data:policies/postprocessing.rego,data:policies/util.rego"
```
Although it would be more convenient to make use of the directory recuring support by just specifying the directory that contains all the `.rego` files instead:
```shell
export POLICIES_ENGINE_FILES="data:policies"
```
Once the references to policy files are configured correctly, the `_QUERY` configuration needs to be defined for the `proxy` middleware and for the events service.
## Setting the Query Configuration
To define a value for the query evaluation, the following scheme is necessary:
To define a value for the query evaluation, the following scheme must be used:
`data.<package-name>.<complete-rule-variable-name>`
* The keyword `data` is mandatory and must be present.
* The `package-name` is defined in one .rego file like `package postprocessing`. It is not related to the filename. For
more details, see the [packages](https://www.openpolicyagent.org/docs/latest/policy-language/#packages) documentation.
* The `package-name` is defined in one `.rego` file like `package postprocessing`. It is not related to the filename. For more details, see the [packages](https://www.openpolicyagent.org/docs/latest/policy-language/#packages) documentation.
* The `complete-rule-variable-name` is the variable providing the result of the evaluation.
* Exact one of the defined files, which is responsible for returning the evaluation result, must contain the combination
of `<package-name>` and `<complete-rule-variable-name>`.
### Proxy
Note that this setting has to be part of the proxy configuration.
Note that this setting has to be part of the configuration of the `proxy` service:
```yaml
proxy:
@@ -154,39 +171,34 @@ The same can be achieved by setting the following environment variable:
export POLICIES_POSTPROCESSING_QUERY=data.postprocessing.granted
```
As soon as that query is configured, the postprocessing service must be informed to use the policies step by setting the
environment variable:
As soon as that query is configured, the `postprocessing` service must be informed to use the policies step by setting the environment variable:
```shell
export POSTPROCESSING_STEPS=policies
```
Note that additional steps can be configured and their position in the list defines the order of processing. For details
see the postprocessing service documentation.
Note that additional steps can be configured and their position in the list defines the order of processing. For details see the `postprocessing` service documentation.
## Rego Key Match
To identify available keys for OPA, you need to look
at [engine.go](https://github.com/opencloud-eu/opencloud/blob/main/services/policies/pkg/engine/engine.go) and
the [policies.swagger.json](https://github.com/opencloud-eu/opencloud/blob/master/protogen/gen/opencloud/services/policies/v0/policies.swagger.json)
file. Note that which keys are available depends on from which module it is used.
To identify available keys for OPA, you need to look at [`engine.go`](https://github.com/opencloud-eu/opencloud/blob/main/services/policies/pkg/engine/engine.go) and the [`policies.swagger.json`](https://github.com/opencloud-eu/opencloud/blob/master/protogen/gen/opencloud/services/policies/v0/policies.swagger.json) file.
Note that which keys are available depends on the module it is used in.
## Rego Extensions
Besides the standard rego built-in functions, the following functions are added on top:
| Function | Result | Description |
|----------------------------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| `opencloud.mimetype.extensions("application/pdf")` | `[".pdf"]` | Lists the file extensions associated with a mimetype. See [Extend Mimetype File Extension Mapping](#extend-mimetype-file-extension-mapping). |
| `opencloud.mimetype.detect(bytes)` | `"application/pdf"` | Detects a mimetype from content. The list of known mimetypes is limited. |
| `opencloud.resource.download(input.resource.url)` | bytes | Downloads a resource. Available in the event service (postprocessing) where `input.resource.url` is set. |
| Function | Result | Description |
| -------- | ------ | ----------- |
| `opencloud.mimetype.extensions("application/pdf")` | `[".pdf"]` | Lists the file extensions associated with a mimetype. See [Extend Mimetype File Extension Mapping](#extend-mimetype-file-extension-mapping). |
| `opencloud.mimetype.detect(bytes)` | `"application/pdf"` | Detects a mimetype from content. The list of known mimetypes is limited. |
| `opencloud.resource.download(input.resource.url)` | bytes | Downloads a resource. Available in the event service (postprocessing) where `input.resource.url` is set. |
Rego has no byte type, so `opencloud.resource.download` hands the content to the policy as base64.
`opencloud.mimetype.detect` takes that value as it is, a policy working on the content itself has to run it through
`base64.decode` first.
`opencloud.mimetype.detect` takes that value as it is, a policy working on the content itself has to run it through `base64.decode` first.
Note that `opencloud.resource.download` performs an HTTP request and holds the whole resource in memory. Use it in
postprocessing policies only, not in policies evaluated by the proxy middleware.
Note that `opencloud.resource.download` performs an HTTP request and holds the whole resource in memory. Use it in postprocessing policies only, not in policies evaluated by the proxy middleware.
## Extend Mimetype File Extension Mapping
@@ -200,25 +212,92 @@ to extensions. The location for the file must be accessible by all instances of
use the directory where the OpenCloud configuration files are stored. Note that existing mappings from the host are
extended by the definitions from the mime types file, but not replaced.
The path to that file can be provided via a yaml configuration or an environment variable. Note to replace the
`OC_CONFIG_DIR` string by an existing path.
The path to that file can be provided via a yaml configuration or an environment variable. Note that the `config:` file path prefix is replaced by `$OC_CONFIG_DIR`.
```shell
export POLICIES_ENGINE_MIMES=OC_CONFIG_DIR/mime.types
export POLICIES_ENGINE_MIMES=config:mime.types
```
```yaml
policies:
engine:
mimes: OC_CONFIG_DIR/mime.types
mimes: config:mime.types
```
A good example of how such a file should be formatted can be found in
the [Apache svn repository](https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types).
A good example of how such a file should be formatted can be found in the [Apache SVN repository](https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types).
## Example Policies
The policies service contains a set of preconfigured example policies. See
the [devtools policie](https://github.com/opencloud-eu/opencloud/tree/main/devtools/deployments/service_policies/policies/)
directory for details. The contained policies disallow OpenCloud to create certain file types, both via the proxy
middleware and the events service via postprocessing.
The `policies` service contains a set of preconfigured example policies. See the [devtools policies](https://github.com/opencloud-eu/opencloud/tree/main/devtools/deployments/service_policies/policies/) directory for details. The contained policies disallow OpenCloud from creating certain file types, both via the `proxy` middleware and the events service via `postprocessing`.
## Metrics
The `policies` service provides the following metrics:
| Name | Description |
| ---- | ----------- |
| `opencloud_policies_build_info{version=...}` | Contains a label `version` that is set to the current version of the service, and always has a value of `1` |
| `opencloud_policies_events_enabled` | Is set to `1` if the Events API handler is enabled, or `0` if not |
| `opencloud_policies_grpc_enabled` | Is set to `1` if the gRPC API handler is enabled, or `0` if not |
| `opencloud_policies_policies_processed{result=...,origin=...}` | A counter with the number of policy rule evaluations. Has a label `result` that is set to `allowed` or `not-allowed` depending on the outcome, as well as a label `origin` that is set to `grpc` or `events` depending on how the request came in. |
| `opencloud_policies_policies_failures{origin=...}` | A counter with the number of policy rule evaluation errors. Has a label `origin` that is set to `grpc` or `events` depending on how the request came in. |
| `opencloud_policies_policies_requests{origin=...}` | A counter with the number of received requests for evaluating policies. Has a label `origin` that is set to `grpc` or `events` depending on how the request came in. |
## Testing
As a developer, to test the `policies` service, one approach may be to set the following environment variables:
```yaml
OC_ADD_RUN_SERVICES: "policies"
POLICIES_EVENTS_DISABLED: "false"
POLICIES_GRPC_DISABLED: "false"
POLICIES_ENGINE_TIMEOUT: "30s"
POLICIES_ENGINE_FILES: "data:policies"
PROXY_POLICIES_QUERY: "data.proxy.granted"
```
Create a directory `policies` under your `OC_CONFIG_DIR`, typically `~/.opencloud/policies/`, and then store the following file content in a file `proxy.rego` under that directory:
```rego
package proxy
import future.keywords.if
default granted := true
granted = false if {
input.user.username == "alan"
}
```
To inspect metrics, use the following request:
```shell
curl -sSLf http://localhost:9129/metrics
```
To also inspect the metrics using a Prometheus container (and possibly even e.g. Grafana), set the following environment variable:
```yaml
POLICIES_DEBUG_ADDR: "0.0.0.0:9129"
```
Create a `prometheus.yml` file with the following content:
```yaml
scrape_configs:
- job_name: 'opencloud'
scrape_interval: 5s
static_configs:
- targets:
- 'host.docker.internal:9129'
labels:
service: 'policies'
```
```shell
docker run --rm -p 9090:9090 \
-v "$PWD/prometheus.yml:/etc/prometheus/prometheus.yml" \
--add-host host.docker.internal=host-gateway \
prom/prometheus:latest
```
+31 -8
View File
@@ -6,8 +6,10 @@ import (
"os/signal"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/generators"
"github.com/opencloud-eu/opencloud/pkg/log"
ocmetrics "github.com/opencloud-eu/opencloud/pkg/metrics"
"github.com/opencloud-eu/opencloud/pkg/runner"
"github.com/opencloud-eu/opencloud/pkg/service/grpc"
"github.com/opencloud-eu/opencloud/pkg/tracing"
@@ -16,10 +18,12 @@ import (
"github.com/opencloud-eu/opencloud/services/policies/pkg/config"
"github.com/opencloud-eu/opencloud/services/policies/pkg/config/parser"
"github.com/opencloud-eu/opencloud/services/policies/pkg/engine/opa"
"github.com/opencloud-eu/opencloud/services/policies/pkg/metrics"
"github.com/opencloud-eu/opencloud/services/policies/pkg/server/debug"
svcEvent "github.com/opencloud-eu/opencloud/services/policies/pkg/service/event"
svcGRPC "github.com/opencloud-eu/opencloud/services/policies/pkg/service/grpc"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/cobra"
)
@@ -28,7 +32,7 @@ import (
func Server(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", "authz"),
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", "policies"),
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
@@ -47,13 +51,33 @@ func Server(cfg *config.Config) *cobra.Command {
return err
}
e, err := opa.NewOPA(cfg.Engine.Timeout, logger, cfg.Engine)
pathPrefixMap := map[string]func() string{
"config:": defaults.BaseConfigPath,
"data:": defaults.BaseDataPath,
}
e, err := opa.NewOPA(cfg.Engine.Timeout, logger, cfg.Engine, pathPrefixMap)
if err != nil {
return err
}
m, err := metrics.New(ocmetrics.NewLoggingPrometheusRegisterer(prometheus.DefaultRegisterer, &logger), &logger)
if err != nil {
return err
}
if cfg.GRPC.Disabled {
m.GrpcEnabled.Set(0)
} else {
m.GrpcEnabled.Set(1)
}
if cfg.Events.Disabled {
m.EventsEnabled.Set(0)
} else {
m.EventsEnabled.Set(1)
}
gr := runner.NewGroup()
{
if !cfg.GRPC.Disabled { // only run the GRPC consumer when enabled, https://github.com/opencloud-eu/opencloud/issues/1312
grpcClient, err := grpc.NewClient(
append(
grpc.GetClientOptions(cfg.GRPCClientTLS),
@@ -83,7 +107,7 @@ func Server(cfg *config.Config) *cobra.Command {
return err
}
grpcSvc, err := svcGRPC.New(e)
grpcSvc, err := svcGRPC.New(e, m)
if err != nil {
return err
}
@@ -98,15 +122,14 @@ func Server(cfg *config.Config) *cobra.Command {
gr.Add(runner.NewGoMicroGrpcServerRunner(cfg.Service.Name+".grpc", svc))
}
{
if !cfg.Events.Disabled { // only run the event consumer when enabled, https://github.com/opencloud-eu/opencloud/issues/1312
connName := generators.GenerateConnectionName(cfg.Service.Name, generators.NTypeBus)
bus, err := stream.NatsFromConfig(connName, false, stream.NatsConfig(cfg.Events))
bus, err := stream.NatsFromConfig(connName, false, cfg.Events.ToNatsConfig())
if err != nil {
return err
}
eventSvc, err := svcEvent.New(ctx, bus, logger, traceProvider, e, cfg.Postprocessing.Query)
eventSvc, err := svcEvent.New(ctx, bus, logger, traceProvider, e, cfg.Postprocessing.Query, m)
if err != nil {
return err
}
+16 -1
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
)
// Config combines all available configuration parts.
@@ -28,6 +29,7 @@ type Service struct {
// GRPC defines the available grpc configuration.
type GRPC struct {
Disabled bool `yaml:"disabled" env:"POLICIES_GRPC_DISABLED" desc:"Disables listening for GRPC API calls. Set this to true if the service should only handle requests through events." introductionVersion:"%NEXT%"`
Addr string `yaml:"addr" env:"POLICIES_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"1.0.0"`
Namespace string `yaml:"-"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
@@ -36,7 +38,7 @@ type GRPC struct {
// Engine configures the policy engine.
type Engine struct {
Timeout time.Duration `yaml:"timeout" env:"POLICIES_ENGINE_TIMEOUT" desc:"Sets the timeout the rego expression evaluation can take. Rules default to deny if the timeout was reached. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
Policies []string `yaml:"policies"`
Policies []string `yaml:"policies" env:"POLICIES_ENGINE_FILES" desc:"A list of filesystem paths to rego files or directories containing rego files." introductionVersion:"1.0.0"`
// Mimes file path, RFC 4288
Mimes string `yaml:"mimes" env:"POLICIES_ENGINE_MIMES" desc:"Sets the mimes file path which maps mimetypes to associated file extensions. See the text description for details." introductionVersion:"1.0.0"`
}
@@ -48,6 +50,7 @@ type Postprocessing struct {
// Events combines the configuration options for the event bus.
type Events struct {
Disabled bool `yaml:"disabled" env:"POLICIES_EVENTS_DISABLED" desc:"Disables listening for events. Set this to true if the service should only handle GRPC requests." introductionVersion:"%NEXT%"`
Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT;POLICIES_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"1.0.0"`
Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER;POLICIES_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"1.0.0"`
TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE;POLICIES_EVENTS_TLS_INSECURE" desc:"Whether the server should skip the client certificate verification during the TLS handshake." introductionVersion:"1.0.0"`
@@ -57,6 +60,18 @@ type Events struct {
AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD;POLICIES_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"`
}
func (e Events) ToNatsConfig() stream.NatsConfig {
return stream.NatsConfig{
Endpoint: e.Endpoint,
Cluster: e.Cluster,
TLSInsecure: e.TLSInsecure,
TLSRootCACertificate: e.TLSRootCACertificate,
EnableTLS: e.EnableTLS,
AuthUsername: e.AuthUsername,
AuthPassword: e.AuthPassword,
}
}
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"POLICIES_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
@@ -28,10 +28,12 @@ func DefaultConfig() *config.Config {
Zpages: false,
},
GRPC: config.GRPC{
Disabled: false,
Addr: "127.0.0.1:9125",
Namespace: "eu.opencloud.api",
},
Events: config.Events{
Disabled: false,
Endpoint: "127.0.0.1:9233",
Cluster: "opencloud-cluster",
EnableTLS: false,
@@ -33,5 +33,12 @@ func ParseConfig(cfg *config.Config) error {
}
func Validate(cfg *config.Config) error {
if cfg.GRPC.Disabled && cfg.Events.Disabled {
// might be debatable, but this situation should be treated as an error,
// as the process wouldn't be able to serve either API and would thus be
// completely useless -- in that case, just don't start this service
// in the first place (especially since it's optional)
return errors.New("both gRPC and events APIs are disabled by configuration; at least one must be enabled")
}
return nil
}
+24 -5
View File
@@ -4,6 +4,8 @@ import (
"context"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/open-policy-agent/opa/loader"
@@ -25,14 +27,25 @@ type OPA struct {
options []func(r *rego.Rego)
}
// NewOPA returns a ready to use opa engine.
func NewOPA(timeout time.Duration, logger log.Logger, conf config.Engine) (*OPA, error) {
var mtReader io.ReadCloser
func path(p string, pathPrefixMap map[string]func() string) string {
for prefix, mapper := range pathPrefixMap {
if pp, ok := strings.CutPrefix(p, prefix); ok {
p = filepath.Join(mapper(), pp)
}
}
return p
}
// NewOPA returns a ready to use opa engine.
func NewOPA(timeout time.Duration, logger log.Logger, conf config.Engine, pathPrefixMap map[string]func() string) (*OPA, error) {
var mtReader io.ReadCloser
mimesPath := ""
if conf.Mimes != "" {
mimesPath = path(conf.Mimes, pathPrefixMap)
var err error
mtReader, err = os.Open(conf.Mimes)
mtReader, err = os.Open(mimesPath)
if err != nil {
logger.Error().Err(err).Str("filename", mimesPath).Msgf("failed to load MIME type definitions file %q specified in 'mime'", mimesPath)
return nil, err
}
@@ -43,10 +56,16 @@ func NewOPA(timeout time.Duration, logger log.Logger, conf config.Engine) (*OPA,
rfMimetypeExtensions, err := RFMimetypeExtensions(mtReader)
if err != nil {
logger.Error().Err(err).Str("filename", conf.Mimes).Msgf("failed to parse MIME type definitions file %q specified in 'mime'", mimesPath)
return nil, err
}
policies, err := loader.NewFileLoader().WithProcessAnnotation(true).Filtered(conf.Policies, nil)
policyPaths := []string{}
for _, p := range conf.Policies {
policyPaths = append(policyPaths, path(p, pathPrefixMap))
}
policies, err := loader.NewFileLoader().WithProcessAnnotation(true).Filtered(policyPaths, nil)
if err != nil {
return nil, err
}
@@ -18,7 +18,7 @@ func benchEngine(tb testing.TB) engine.Engine {
e, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: []string{
filepath.Join("testdata", "bench", "proxy.rego"),
filepath.Join("testdata", "bench", "utils.rego"),
}})
}}, nil)
if err != nil {
tb.Fatal(err)
}
@@ -40,9 +40,8 @@ func BenchmarkEvaluate(b *testing.B) {
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for range b.N {
for b.Loop() {
granted, err := e.Evaluate(ctx, "data.proxy.granted", env)
if err != nil {
b.Fatal(err)
@@ -26,7 +26,7 @@ var _ = Describe("engine", func() {
path := filepath.Join(GinkgoT().TempDir(), "policy.rego")
Expect(os.WriteFile(path, []byte(source), 0o600)).To(Succeed())
e, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: []string{path}})
e, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: []string{path}}, nil)
Expect(err).ToNot(HaveOccurred())
return e, path
@@ -42,7 +42,7 @@ var _ = Describe("engine", func() {
}
start := func(policies ...string) engine.Engine {
e, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: policies})
e, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: policies}, nil)
Expect(err).ToNot(HaveOccurred())
return e
@@ -78,14 +78,14 @@ var _ = Describe("engine", func() {
It("refuses to start on a broken policy", func() {
install("broken.rego")
_, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: []string{path}})
_, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: []string{path}}, nil)
Expect(err).To(HaveOccurred())
})
It("refuses to start when a policy is missing", func() {
Expect(os.Remove(path)).To(Succeed())
_, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: []string{path}})
_, err := opa.NewOPA(10*time.Second, log.NopLogger(), config.Engine{Policies: []string{path}}, nil)
Expect(err).To(HaveOccurred())
})
@@ -45,7 +45,7 @@ var _ = Describe("the shipped example policies", func() {
filepath.Join(examplePolicyDir, "proxy.rego"),
filepath.Join(examplePolicyDir, "postprocessing.rego"),
filepath.Join(examplePolicyDir, "utils.rego"),
}})
}}, nil)
Expect(err).ToNot(HaveOccurred())
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -60,7 +60,7 @@ var _ = Describe("opa opencloud resource functions", func() {
source := "package download\n\nimport future.keywords.if\n\ndefault granted := true\n\ngranted = false if {\n opencloud.resource.download(input.resource.url)\n}\n"
Expect(os.WriteFile(path, []byte(source), 0o600)).To(Succeed())
e, err := opa.NewOPA(timeout, log.NopLogger(), config.Engine{Policies: []string{path}})
e, err := opa.NewOPA(timeout, log.NopLogger(), config.Engine{Policies: []string{path}}, nil)
Expect(err).ToNot(HaveOccurred())
start := time.Now()
+158
View File
@@ -0,0 +1,158 @@
package metrics
import (
"github.com/opencloud-eu/opencloud/pkg/log"
ocmetrics "github.com/opencloud-eu/opencloud/pkg/metrics"
"github.com/prometheus/client_golang/prometheus"
)
const (
// Namespace defines the namespace for the defines metrics.
Namespace = "opencloud"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "policies"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
BuildInfo *prometheus.GaugeVec
EventsEnabled prometheus.Gauge
GrpcEnabled prometheus.Gauge
GrpcEvaluationsThatAllow *prometheus.CounterVec
GrpcEvaluationsThatDontAllow *prometheus.CounterVec
FailedGrpcEvaluations *prometheus.CounterVec
EventEvaluationsThatAllow *prometheus.CounterVec
EventEvaluationsThatDontAllow *prometheus.CounterVec
FailedEventEvaluations *prometheus.CounterVec
EventsReceived *prometheus.CounterVec
GrpcCallsReceived *prometheus.CounterVec
}
var Labels = struct {
Origin string
Result string
}{
Origin: "origin",
Result: "result",
}
var Values = struct {
Origin struct {
GRPC string
Event string
}
Result struct {
Allowed string
NotAllowed string
}
}{
Origin: struct {
GRPC string
Event string
}{
GRPC: "grpc",
Event: "event",
},
Result: struct {
Allowed string
NotAllowed string
}{
Allowed: "allowed",
NotAllowed: "not-allowed",
},
}
func New(registerer prometheus.Registerer, logger *log.Logger) (*Metrics, error) {
return ocmetrics.Register(logger, &Metrics{
BuildInfo: ocmetrics.BuildInfo(Namespace, Subsystem),
EventsEnabled: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "events_enabled",
Help: "Whether this instance processes events (1) or not (0)",
}),
GrpcEnabled: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "grpc_enabled",
Help: "Whether this instance processes gRPC API calls (1) or not (0)",
}),
GrpcEvaluationsThatAllow: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_processed",
Help: "Number of evaluations of policies",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.GRPC,
Labels.Result: Values.Result.Allowed,
},
}, []string{}),
GrpcEvaluationsThatDontAllow: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_processed",
Help: "Number of evaluations of policies",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.GRPC,
Labels.Result: Values.Result.NotAllowed,
},
}, []string{}),
FailedGrpcEvaluations: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_failures",
Help: "Number of failed policy evaluations",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.GRPC,
},
}, []string{}),
EventEvaluationsThatAllow: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_processed",
Help: "Number of evaluations of policies",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.Event,
Labels.Result: Values.Result.Allowed,
},
}, []string{}),
EventEvaluationsThatDontAllow: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_processed",
Help: "Number of evaluations of policies",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.Event,
Labels.Result: Values.Result.NotAllowed,
},
}, []string{}),
FailedEventEvaluations: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_failures",
Help: "Number of failed policy evaluations",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.Event,
},
}, []string{}),
EventsReceived: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_requests",
Help: "Number of inbound policies requests, by origin",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.Event,
},
}, []string{}),
GrpcCallsReceived: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "policies_requests",
Help: "Number of inbound policies requests, by origin",
ConstLabels: prometheus.Labels{
Labels.Origin: Values.Origin.GRPC,
},
}, []string{}),
})
}
+16 -8
View File
@@ -18,13 +18,21 @@ func Server(opts ...Option) (*http.Server, error) {
WithLogger(options.Logger).
WithCheck("grpc reachability", checks.NewGRPCCheck(options.Config.GRPC.Addr))
secureOption := nats.Secure(
options.Config.Events.EnableTLS,
options.Config.Events.TLSInsecure,
options.Config.Events.TLSRootCACertificate,
)
readyHandlerConfiguration := healthHandlerConfiguration.
WithCheck("nats reachability", checks.NewNatsCheck(options.Config.Events.Endpoint, secureOption))
natsCheckFactory := func() debug.Option {
return func(o *debug.Options) { // do nothing
}
}
if !options.Config.Events.Disabled {
secureOption := nats.Secure(
options.Config.Events.EnableTLS,
options.Config.Events.TLSInsecure,
options.Config.Events.TLSRootCACertificate,
)
readyHandlerConfiguration := healthHandlerConfiguration.
WithCheck("nats reachability", checks.NewNatsCheck(options.Config.Events.Endpoint, secureOption))
natsCheckFactory = func() debug.Option { return debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)) }
}
return debug.NewService(
debug.Logger(options.Logger),
@@ -35,6 +43,6 @@ func Server(opts ...Option) (*http.Server, error) {
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(handlers.NewCheckHandler(healthHandlerConfiguration)),
debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)),
natsCheckFactory(),
), nil
}
+13 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/policies/pkg/engine"
"github.com/opencloud-eu/opencloud/services/policies/pkg/metrics"
"github.com/opencloud-eu/reva/v2/pkg/events"
"go.opentelemetry.io/otel/trace"
)
@@ -17,19 +18,21 @@ type Service struct {
log log.Logger
stream events.Stream
engine engine.Engine
metrics *metrics.Metrics
tp trace.TracerProvider
stopCh chan struct{}
stopped *atomic.Bool
}
// New returns a service implementation for Service.
func New(ctx context.Context, stream events.Stream, logger log.Logger, tp trace.TracerProvider, engine engine.Engine, query string) (Service, error) {
func New(ctx context.Context, stream events.Stream, logger log.Logger, tp trace.TracerProvider, engine engine.Engine, query string, metrics *metrics.Metrics) (Service, error) {
svc := Service{
ctx: ctx,
log: logger,
query: query,
tp: tp,
engine: engine,
metrics: metrics,
stream: stream,
stopCh: make(chan struct{}, 1),
stopped: new(atomic.Bool),
@@ -86,6 +89,8 @@ func (s Service) processEvent(e events.Event) error {
ctx, span := s.tp.Tracer("policies").Start(ctx, "processEvent")
defer span.End()
s.metrics.EventsReceived.WithLabelValues().Inc()
switch ev := e.Event.(type) {
case events.StartPostprocessingStep:
if ev.StepToStart != events.PPStepPolicies {
@@ -114,7 +119,14 @@ func (s Service) processEvent(e events.Event) error {
result, err := s.engine.Evaluate(context.TODO(), s.query, env)
if err != nil {
s.metrics.FailedEventEvaluations.WithLabelValues().Inc()
s.log.Error().Err(err).Msg("unable evaluate policy")
} else {
if result {
s.metrics.EventEvaluationsThatAllow.WithLabelValues().Inc()
} else {
s.metrics.EventEvaluationsThatDontAllow.WithLabelValues().Inc()
}
}
if !result {
+19 -3
View File
@@ -5,17 +5,20 @@ import (
v0 "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/policies/v0"
"github.com/opencloud-eu/opencloud/services/policies/pkg/engine"
"github.com/opencloud-eu/opencloud/services/policies/pkg/metrics"
)
// Service defines the service handlers.
type Service struct {
engine engine.Engine
engine engine.Engine
metrics *metrics.Metrics
}
// New returns a service implementation for Service.
func New(engine engine.Engine) (Service, error) {
func New(engine engine.Engine, metrics *metrics.Metrics) (Service, error) {
svc := Service{
engine: engine,
engine: engine,
metrics: metrics,
}
return svc, nil
@@ -23,13 +26,26 @@ func New(engine engine.Engine) (Service, error) {
// Evaluate exposes the engine policy evaluation.
func (s Service) Evaluate(ctx context.Context, request *v0.EvaluateRequest, response *v0.EvaluateResponse) error {
s.metrics.EventsReceived.WithLabelValues().Inc()
env, err := engine.NewEnvironmentFromPB(request.Environment)
if err != nil {
s.metrics.FailedGrpcEvaluations.WithLabelValues().Inc()
return err
}
result, err := s.engine.Evaluate(ctx, request.Query, env)
response.Result = result
if err != nil {
s.metrics.FailedGrpcEvaluations.WithLabelValues().Inc()
} else {
if result {
s.metrics.GrpcEvaluationsThatAllow.WithLabelValues().Inc()
} else {
s.metrics.GrpcEvaluationsThatDontAllow.WithLabelValues().Inc()
}
}
return err
}