Open Policy Agent (OPA) Integration

⚠️ Preview Feature

OPA integration is currently an preview feature and may undergo breaking changes in future versions. Use with caution in production environments.

This page describes how to integrate Apache Polaris (Incubating) with Open Policy Agent (OPA) for external authorization.

Overview

Open Policy Agent (OPA) is a general-purpose policy engine that enables unified, context-aware policy enforcement across your stack. OPA provides a high-level declarative language (Rego) for authoring policies and APIs to offload policy decision-making from your software.

Key benefits of using OPA with Polaris:

  • Flexible policy language: Write authorization logic in Rego, a powerful declarative language
  • Centralized policy management: Manage all policies in a single location
  • Policy testing: Write unit tests for your authorization policies
  • Rich ecosystem: Integrate with policy bundles, decision logs, and management tools
  • Attribute-based access control: Make decisions based on user attributes, resource properties, and environmental context

Prerequisites

Before configuring OPA integration:

  1. OPA Server: Deploy and configure an OPA server accessible from Polaris
  2. Policy Definition: Write and deploy authorization policies to OPA
  3. Network Access: Ensure Polaris can reach the OPA server

Quick Start

1. Deploy OPA

Deploy OPA server with your policies. For example, using Docker:

docker run -d \
  --name opa \
  -p 8181:8181 \
  -v $(pwd)/policies:/policies \
  openpolicyagent/opa:latest \
  run --server --addr :8181 /policies

2. Create a Policy

Create a policy file (e.g., policies/polaris.rego):

package polaris.authz

import future.keywords.if

# Default deny
default allow := false

# Allow admins to do everything
allow if {
    "ADMIN" in input.actor.roles
}

# Allow read operations on tables in analytics catalogs
allow if {
    input.action == "LOAD_TABLE_WITH_READ_DELEGATION"
    some target in input.resource.targets
    some parent in target.parents
    parent.type == "CATALOG"
    startswith(parent.name, "analytics_")
}

3. Configure Polaris

Add the following to your Polaris configuration:

# Enable OPA authorization
polaris.authorization.type=opa

# OPA server endpoint
polaris.authorization.opa.policy-uri=http://localhost:8181/v1/data/polaris/authz/allow

4. Restart Polaris

Restart the Polaris service to apply the configuration.

Configuration Reference

Basic Configuration

PropertyRequiredDefaultDescription
polaris.authorization.typeYesinternalSet to opa to enable OPA authorization
polaris.authorization.opa.policy-uriYes-Full URI to the OPA policy decision endpoint (must be http or https)

HTTP Configuration

PropertyRequiredDefaultDescription
polaris.authorization.opa.http.timeoutNoPT2SHTTP request timeout (ISO-8601 duration format, e.g., PT2S, PT10S)
polaris.authorization.opa.http.verify-sslNotrueWhether to verify SSL certificates
polaris.authorization.opa.http.trust-store-pathNo-Path to the trust store containing CA certificates
polaris.authorization.opa.http.trust-store-passwordNo-Password for the trust store

Authentication Configuration

OPA integration supports two authentication modes:

No Authentication (default)

No authentication configuration needed if OPA server doesn’t require authentication:

polaris.authorization.opa.auth.type=none

Bearer Token Authentication

Static Bearer Token:

Use a static bearer token:

polaris.authorization.opa.auth.type=bearer
polaris.authorization.opa.auth.bearer.static-token.value=your-secret-token
PropertyRequiredDefaultDescription
polaris.authorization.opa.auth.typeNononeSet to bearer to enable bearer token authentication
polaris.authorization.opa.auth.bearer.static-token.valueYes*-The bearer token value (*when using static token)

File-Based Bearer Token with Auto-Refresh:

Use a bearer token from a file with automatic refresh (ideal for JWT tokens):

polaris.authorization.opa.auth.type=bearer
polaris.authorization.opa.auth.bearer.file-based.path=/var/secrets/token.txt
polaris.authorization.opa.auth.bearer.file-based.refresh-interval=PT5M
polaris.authorization.opa.auth.bearer.file-based.jwt-expiration-refresh=true
polaris.authorization.opa.auth.bearer.file-based.jwt-expiration-buffer=PT1M
PropertyRequiredDefaultDescription
polaris.authorization.opa.auth.typeNononeSet to bearer to enable bearer token authentication
polaris.authorization.opa.auth.bearer.file-based.pathYes*-Path to file containing the bearer token (*when using file-based token)
polaris.authorization.opa.auth.bearer.file-based.refresh-intervalNo-How often to refresh the token (ISO-8601 duration, e.g., PT5M for 5 minutes). If not set and JWT refresh is disabled, token won’t be refreshed
polaris.authorization.opa.auth.bearer.file-based.jwt-expiration-refreshNotrueAutomatically detect JWT tokens and refresh based on expiration time
polaris.authorization.opa.auth.bearer.file-based.jwt-expiration-bufferNoPT1MBuffer time before JWT expiration to trigger refresh (ISO-8601 duration)

JWT Auto-Refresh: When jwt-expiration-refresh is enabled (default), if the token file contains a valid JWT with an exp claim, Polaris will automatically refresh the token shortly before it expires based on the jwt-expiration-buffer setting.

Policy Development

Input Document Structure

Polaris sends the following input structure to OPA:

{
  "actor": {
    "principal": "user@example.com",
    "roles": ["role1", "role2"]
  },
  "action": "LOAD_TABLE_WITH_READ_DELEGATION",
  "resource": {
    "targets": [
      {
        "type": "TABLE",
        "name": "my_table",
        "parents": [
          {
            "type": "CATALOG",
            "name": "my_catalog"
          },
          {
            "type": "NAMESPACE",
            "name": "schema1"
          }
        ]
      }
    ],
    "secondaries": []
  },
  "context": {
    "request_id": "uuid"
  }
}

Actor Object

FieldTypeDescription
principalstringThe principal identifier (e.g., username, service account)
rolesarrayArray of role names assigned to the principal

Action Field

The action field contains the operation being attempted as a string value from the PolarisAuthorizableOperation enum.

For the complete list of available operations, see the PolarisAuthorizableOperation enum in the source code.

Common examples include:

  • Table operations: LOAD_TABLE_WITH_READ_DELEGATION, LOAD_TABLE_WITH_WRITE_DELEGATION, CREATE_TABLE_DIRECT, UPDATE_TABLE, DROP_TABLE_WITHOUT_PURGE
  • Catalog operations: CREATE_CATALOG, UPDATE_CATALOG, DELETE_CATALOG
  • Namespace operations: CREATE_NAMESPACE, UPDATE_NAMESPACE_PROPERTIES, DROP_NAMESPACE

⚠️ Important Policy Considerations

Handle All Data Operations Explicitly: When writing OPA policies, explicitly handle all PolarisAuthorizableOperation values that enable catalog, namespace and table operations. Operations not covered by your policy rules will fall through to your default decision. We recommend:

  • Setting default allow := false to deny by default
  • Explicitly allowing only operations your users need

Internal-Only Operations: Some operations like ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE and CREATE_POLICY manage Polaris’s internal privilege system. These should always be denied in OPA policies since privilege management should be handled through Polaris’s native authorization system, not external policies.

Resource Object

FieldTypeDescription
targetsarrayArray of target resource objects (primary resources being accessed)
secondariesarrayArray of secondary resource objects (related resources, if any)

Each resource object in targets or secondaries has:

FieldTypeDescription
typestringResource type (CATALOG, NAMESPACE, TABLE, VIEW, PRINCIPAL, CATALOG_ROLE, etc.)
namestringResource name
parentsarrayArray of parent resource objects in the hierarchy (e.g., catalog and namespace for a table)

Each parent object has:

FieldTypeDescription
typestringParent resource type
namestringParent resource name

Context Object

FieldTypeDescription
request_idstringUUID for correlating requests with logs

Policy Example

package polaris.authz

import future.keywords.if
import future.keywords.in

default allow := false

# Admin role can perform all catalog, namespace, and table operations
allow if {
    "ADMIN" in input.actor.roles
    input.action in [
        # Catalog operations
        "CREATE_CATALOG",
        "UPDATE_CATALOG",
        "DELETE_CATALOG",
        "LIST_CATALOGS",
        
        # Namespace operations
        "CREATE_NAMESPACE",
        "UPDATE_NAMESPACE_PROPERTIES",
        "DROP_NAMESPACE",
        "LIST_NAMESPACES",
        
        # Table operations
        "CREATE_TABLE_DIRECT",
        "LOAD_TABLE_WITH_READ_DELEGATION",
        "LOAD_TABLE_WITH_WRITE_DELEGATION",
        "UPDATE_TABLE",
        "DROP_TABLE_WITHOUT_PURGE",
        # Add other data operations as needed
    ]
}

# Data engineers can create/read/update tables and namespaces
allow if {
    "DATA_ENGINEER" in input.actor.roles
    input.action in [
        # Table operations
        "CREATE_TABLE_DIRECT",
        "LOAD_TABLE_WITH_READ_DELEGATION",
        "LOAD_TABLE_WITH_WRITE_DELEGATION",
        "UPDATE_TABLE",
        
        # Namespace operations
        "CREATE_NAMESPACE",
        "UPDATE_NAMESPACE_PROPERTIES",
        "LIST_NAMESPACES"
    ]
}

# Analysts can only read tables
allow if {
    "ANALYST" in input.actor.roles
    input.action == "LOAD_TABLE_WITH_READ_DELEGATION"
    some target in input.resource.targets
    target.type == "TABLE"
}

ℹ️ Best Practice

Explicit Allow Lists for Data Operations Only: The example above uses an explicit allow-list approach where only catalog, namespace, and table operations are permitted. Policy and grant operations (like ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE, CREATE_POLICY) are automatically denied by the default allow := false since they’re not in any allow rule. This ensures privilege management remains within Polaris’s native authorization system, and new operations added in future Polaris versions will be denied by default until you explicitly allow them.

Testing Policies

OPA supports policy testing using opa test. Create a test file (e.g., polaris_test.rego):

package polaris.authz

import future.keywords.if

test_admin_can_create_catalog if {
    allow with input as {
        "actor": {"principal": "admin", "roles": ["ADMIN"]},
        "action": "CREATE_CATALOG",
        "resource": {
            "targets": [{
                "type": "CATALOG",
                "name": "new_catalog",
                "parents": []
            }],
            "secondaries": []
        },
        "context": {"request_id": "test"}
    }
}

test_admin_cannot_grant_privileges if {
    not allow with input as {
        "actor": {"principal": "admin", "roles": ["ADMIN"]},
        "action": "ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE",
        "resource": {
            "targets": [{
                "type": "PRINCIPAL_ROLE",
                "name": "some_role",
                "parents": []
            }],
            "secondaries": []
        },
        "context": {"request_id": "test"}
    }
}

test_data_engineer_can_create_table if {
    allow with input as {
        "actor": {"principal": "engineer", "roles": ["DATA_ENGINEER"]},
        "action": "CREATE_TABLE_DIRECT",
        "resource": {
            "targets": [{
                "type": "TABLE",
                "name": "new_table",
                "parents": [
                    {"type": "CATALOG", "name": "prod"},
                    {"type": "NAMESPACE", "name": "schema1"}
                ]
            }],
            "secondaries": []
        },
        "context": {"request_id": "test"}
    }
}

test_data_engineer_cannot_delete_catalog if {
    not allow with input as {
        "actor": {"principal": "engineer", "roles": ["DATA_ENGINEER"]},
        "action": "DELETE_CATALOG",
        "resource": {
            "targets": [{
                "type": "CATALOG",
                "name": "prod",
                "parents": []
            }],
            "secondaries": []
        },
        "context": {"request_id": "test"}
    }
}

test_analyst_can_read_table if {
    allow with input as {
        "actor": {"principal": "analyst", "roles": ["ANALYST"]},
        "action": "LOAD_TABLE_WITH_READ_DELEGATION",
        "resource": {
            "targets": [{
                "type": "TABLE",
                "name": "data_table",
                "parents": [
                    {"type": "CATALOG", "name": "prod"},
                    {"type": "NAMESPACE", "name": "schema1"}
                ]
            }],
            "secondaries": []
        },
        "context": {"request_id": "test"}
    }
}

test_analyst_cannot_update_table if {
    not allow with input as {
        "actor": {"principal": "analyst", "roles": ["ANALYST"]},
        "action": "UPDATE_TABLE",
        "resource": {
            "targets": [{
                "type": "TABLE",
                "name": "data_table",
                "parents": [
                    {"type": "CATALOG", "name": "prod"},
                    {"type": "NAMESPACE", "name": "schema1"}
                ]
            }],
            "secondaries": []
        },
        "context": {"request_id": "test"}
    }
}

Run tests:

opa test policies/

Additional Resources