diff --git a/Dockerfile b/Dockerfile
index a68191078..c08429daa 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@ FROM cgr.dev/chainguard/go:latest AS builder
ARG TARGETOS TARGETARCH
WORKDIR /app
-# dependencies, add local,dependant package here
+
COPY protocol/ protocol/
COPY sdk/ sdk/
COPY lib/ocrypto lib/ocrypto
@@ -16,7 +16,7 @@ RUN cd service \
&& go mod verify
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o opentdf ./service
-FROM cgr.dev/chainguard/glibc-dynamic
+FROM cgr.dev/chainguard/glibc-dynamic:latest
COPY --from=builder /app/opentdf /usr/bin/
diff --git a/docker-compose.yaml b/docker-compose.yaml
index a1a15f9b8..f7301c9fd 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,69 +1,8 @@
networks:
default:
name: opentdf_platform
+
services:
- keycloak:
- volumes:
- - ./keys/localhost.crt:/etc/x509/tls/localhost.crt
- - ./keys/localhost.key:/etc/x509/tls/localhost.key
- - ./keys/ca.jks:/truststore/truststore.jks
- # This is kc 24.0.1 with opentdf protocol mapper on board
- image: cgr.dev/chainguard/keycloak@sha256:37895558d2e0e93ffff75da5900f9ae7e79ec6d1c390b18b2ecea6cee45ec26f
- restart: always
- command:
- - "start-dev"
- - "--verbose"
- - "-Djavax.net.ssl.trustStorePassword=password"
- - "-Djavax.net.ssl.HostnameVerifier=AllowAll"
- - "-Djavax.net.ssl.trustStore=/truststore/truststore.jks"
- - "--spi-truststore-file-hostname-verification-policy=ANY"
- environment:
- KC_PROXY: edge
- KC_HTTP_RELATIVE_PATH: /auth
- KC_DB_VENDOR: postgres
- KC_DB_URL_HOST: keycloakdb
- KC_DB_URL_PORT: 5432
- KC_DB_URL_DATABASE: keycloak
- KC_DB_USERNAME: keycloak
- KC_DB_PASSWORD: changeme
- KC_HOSTNAME_STRICT: "false"
- KC_HOSTNAME_STRICT_BACKCHANNEL: "false"
- KC_HOSTNAME_STRICT_HTTPS: "false"
- KC_HTTP_ENABLED: "true"
- KC_HTTP_PORT: "8888"
- KC_HTTPS_PORT: "8443"
- KEYCLOAK_ADMIN: admin
- KEYCLOAK_ADMIN_PASSWORD: changeme
- #KC_HOSTNAME_URL: http://localhost:8888/auth
- KC_FEATURES: "preview,token-exchange"
- KC_HEALTH_ENABLED: "true"
- KC_HTTPS_KEY_STORE_PASSWORD: "password"
- KC_HTTPS_KEY_STORE_FILE: "/truststore/truststore.jks"
- KC_HTTPS_CERTIFICATE_FILE: "/etc/x509/tls/localhost.crt"
- KC_HTTPS_CERTIFICATE_KEY_FILE: "/etc/x509/tls/localhost.key"
- KC_HTTPS_CLIENT_AUTH: "request"
- ports:
- - "8888:8888"
- - "8443:8443"
- healthcheck:
- test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:8888/auth/health/live']
- interval: 5s
- timeout: 10s
- retries: 3
- start_period: 2m
- keycloakdb:
- image: postgres:15-alpine
- restart: always
- user: postgres
- environment:
- POSTGRES_PASSWORD: changeme
- POSTGRES_USER: postgres
- POSTGRES_DB: keycloak
- healthcheck:
- test: ["CMD-SHELL", "pg_isready"]
- interval: 5s
- timeout: 5s
- retries: 10
opentdfdb:
image: postgres:15-alpine
restart: always
@@ -73,9 +12,26 @@ services:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: opentdf
healthcheck:
- test: ["CMD-SHELL", "pg_isready"]
+ test: [ "CMD-SHELL", "pg_isready" ]
interval: 5s
timeout: 5s
retries: 10
ports:
- "5432:5432"
+
+ opentdf:
+ image: custom-opentdf:latest
+ restart: always
+ volumes:
+ - "./keys:/keys" # Mount your keys directory
+ - "./opentdf.yaml:/home/nonroot/.opentdf/opentdf.yaml" # Mount opentdf.yaml
+ - "./keys/local-dsp.virtru.com.pem:/usr/local/share/ca-certificates/ca.crt" # Mount the cert file
+ networks:
+ - default
+ ports:
+ - "8080:8080" # Expose the service on port 8080
+ environment:
+ SSL_CERT_DIR: "/usr/local/share/ca-certificates" # Ensure the cert dir is set
+ entrypoint: ["/usr/bin/opentdf","start"]
+ extra_hosts:
+ - "local-dsp.virtru.com:192.168.1.195" # Add custom host entries
diff --git a/docs/grpc/index.html b/docs/grpc/index.html
index e303c4fc4..40fa7bbce 100644
--- a/docs/grpc/index.html
+++ b/docs/grpc/index.html
@@ -2910,7 +2910,7 @@
Methods with HTTP bindings
GetEntitlements |
POST |
/v1/entitlements |
- |
+ * |
diff --git a/docs/openapi/authorization/authorization.swagger.json b/docs/openapi/authorization/authorization.swagger.json
index 1d5ec3853..cf1fa7dc2 100644
--- a/docs/openapi/authorization/authorization.swagger.json
+++ b/docs/openapi/authorization/authorization.swagger.json
@@ -67,27 +67,13 @@
},
"parameters": [
{
- "name": "scope.resourceAttributesId",
- "in": "query",
- "required": false,
- "type": "string"
- },
- {
- "name": "scope.attributeValueFqns",
- "in": "query",
- "required": false,
- "type": "array",
- "items": {
- "type": "string"
- },
- "collectionFormat": "multi"
- },
- {
- "name": "withComprehensiveHierarchy",
- "description": "optional parameter to return a full list of entitlements - returns lower hierarchy attributes",
- "in": "query",
- "required": false,
- "type": "boolean"
+ "name": "body",
+ "description": "Example: Get entitlements for bob and alice (both represented using an email address\n\n{\n\"entities\": [\n{\n\"id\": \"e1\",\n\"emailAddress\": \"bob@example.org\"\n},\n{\n\"id\": \"e2\",\n\"emailAddress\": \"alice@example.org\"\n}\n],\n\"scope\": {\n\"attributeFqns\": [\n\"https://example.net/attr/attr1/value/value1\",\n\"https://example.net/attr/attr1/value/value2\"\n]\n}\n}",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authorizationGetEntitlementsRequest"
+ }
}
],
"tags": [
@@ -316,6 +302,29 @@
}
}
},
+ "authorizationGetEntitlementsRequest": {
+ "type": "object",
+ "properties": {
+ "entities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "$ref": "#/definitions/authorizationEntity"
+ },
+ "title": "list of requested entities"
+ },
+ "scope": {
+ "$ref": "#/definitions/authorizationResourceAttribute",
+ "title": "optional attribute fqn as a scope"
+ },
+ "withComprehensiveHierarchy": {
+ "type": "boolean",
+ "title": "optional parameter to return a full list of entitlements - returns lower hierarchy attributes"
+ }
+ },
+ "description": "Example: Get entitlements for bob and alice (both represented using an email address\n\n{\n\"entities\": [\n{\n\"id\": \"e1\",\n\"emailAddress\": \"bob@example.org\"\n},\n{\n\"id\": \"e2\",\n\"emailAddress\": \"alice@example.org\"\n}\n],\n\"scope\": {\n\"attributeFqns\": [\n\"https://example.net/attr/attr1/value/value1\",\n\"https://example.net/attr/attr1/value/value2\"\n]\n}\n}",
+ "title": "Request to get entitlements for one or more entities for an optional attribute scope"
+ },
"authorizationGetEntitlementsResponse": {
"type": "object",
"properties": {
diff --git a/protocol/go/authorization/authorization.pb.go b/protocol/go/authorization/authorization.pb.go
index f81676fbc..210f7213f 100644
--- a/protocol/go/authorization/authorization.pb.go
+++ b/protocol/go/authorization/authorization.pb.go
@@ -1384,7 +1384,7 @@ var file_authorization_authorization_proto_rawDesc = []byte{
0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65,
0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x11,
0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x73, 0x32, 0x99, 0x03, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
+ 0x73, 0x32, 0x9c, 0x03, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x75, 0x0a, 0x0c, 0x47, 0x65,
0x74, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x2e, 0x61, 0x75, 0x74,
0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65,
@@ -1402,26 +1402,26 @@ var file_authorization_authorization_proto_rawDesc = []byte{
0x73, 0x42, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
- 0x6e, 0x12, 0x7a, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d,
+ 0x6e, 0x12, 0x7d, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75,
0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45,
0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x22, 0x10, 0x2f, 0x76, 0x31,
- 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0xb2, 0x01,
- 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
- 0x69, 0x6f, 0x6e, 0x42, 0x12, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69,
- 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75,
- 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c,
- 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f,
- 0x67, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
- 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
- 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
- 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x19, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
- 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
- 0x74, 0x61, 0xea, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69,
- 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, 0x10,
+ 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73,
+ 0x42, 0xb2, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
+ 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x12, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x35, 0x67, 0x69,
+ 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66,
+ 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+ 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68,
+ 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xca, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68,
+ 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0xe2, 0x02, 0x19, 0x41, 0x75, 0x74, 0x68,
+ 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
+ 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
diff --git a/protocol/go/authorization/authorization.pb.gw.go b/protocol/go/authorization/authorization.pb.gw.go
index 3f98bab43..036c77456 100644
--- a/protocol/go/authorization/authorization.pb.gw.go
+++ b/protocol/go/authorization/authorization.pb.gw.go
@@ -93,18 +93,11 @@ func local_request_AuthorizationService_GetDecisionsByToken_0(ctx context.Contex
}
-var (
- filter_AuthorizationService_GetEntitlements_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
-)
-
func request_AuthorizationService_GetEntitlements_0(ctx context.Context, marshaler runtime.Marshaler, client AuthorizationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetEntitlementsRequest
var metadata runtime.ServerMetadata
- if err := req.ParseForm(); err != nil {
- return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
- }
- if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AuthorizationService_GetEntitlements_0); err != nil {
+ if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
@@ -117,10 +110,7 @@ func local_request_AuthorizationService_GetEntitlements_0(ctx context.Context, m
var protoReq GetEntitlementsRequest
var metadata runtime.ServerMetadata
- if err := req.ParseForm(); err != nil {
- return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
- }
- if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AuthorizationService_GetEntitlements_0); err != nil {
+ if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
diff --git a/service/authorization/authorization.go b/service/authorization/authorization.go
index 19cfe09e2..9497d0122 100644
--- a/service/authorization/authorization.go
+++ b/service/authorization/authorization.go
@@ -2,10 +2,11 @@ package authorization
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"log/slog"
- "os"
+ "os"
"strings"
"google.golang.org/grpc/codes"
@@ -188,6 +189,26 @@ func (as *AuthorizationService) GetDecisions(ctx context.Context, req *authoriza
for _, ra := range dr.GetResourceAttributes() {
as.logger.DebugContext(ctx, "getting resource attributes", slog.String("FQNs", strings.Join(ra.GetAttributeValueFqns(), ", ")))
+ filteredFQNs := []string{}
+ for _, fqn := range ra.GetAttributeValueFqns() {
+ if strings.Contains(fqn, "temporal/") {
+ // This FQN is part of the temporal attribute system, which can have dynamic or time-based values
+ // (e.g., /temporal/value/after::2024-11-05T12:00:00Z). Temporal attributes are handled separately
+ // by the accessPdp and do not require further processing here.
+ // Skipping these attributes avoids unnecessary handling as they do not affect other parts
+ // of the decision logic.
+ as.logger.DebugContext(ctx, "ignoring temporal FQN", slog.String("FQN", fqn))
+ continue
+ }
+ filteredFQNs = append(filteredFQNs, fqn)
+ }
+
+ if len(filteredFQNs) == 0 {
+ as.logger.DebugContext(ctx, "no valid FQNs left after filtering")
+ }
+
+ ra.AttributeValueFqns = filteredFQNs
+
// get attribute definition/value combinations
dataAttrDefsAndVals, err := retrieveAttributeDefinitions(ctx, ra, as.sdk)
if err != nil {
@@ -440,7 +461,12 @@ func makeScopeMap(scope *authorization.ResourceAttribute) map[string]bool {
}
func (as *AuthorizationService) GetEntitlements(ctx context.Context, req *authorization.GetEntitlementsRequest) (*authorization.GetEntitlementsResponse, error) {
- as.logger.DebugContext(ctx, "getting entitlements")
+ as.logger.DebugContext(ctx, "Preparing to retrieve entitlements")
+ as.logger.DebugContext(ctx, "getting entitlements with request", slog.String("Request Body", req.String()))
+
+ reqJSON, _ := json.Marshal(req)
+ as.logger.DebugContext(ctx, "getting entitlements", slog.String("Request Body", string(reqJSON)))
+
attrsRes, err := as.sdk.Attributes.ListAttributes(ctx, &attr.ListAttributesRequest{})
if err != nil {
as.logger.ErrorContext(ctx, "failed to list attributes", slog.String("error", err.Error()))
@@ -464,7 +490,12 @@ func (as *AuthorizationService) GetEntitlements(ctx context.Context, req *author
as.logger.DebugContext(ctx, fmt.Sprintf("retrieved %d subject mappings", len(subjectMappings)))
// TODO: this could probably be moved to proto validation https://github.com/opentdf/platform/issues/1057
if req.Entities == nil {
- as.logger.ErrorContext(ctx, "requires entities")
+ as.logger.ErrorContext(
+ ctx,
+ "invalid request: missing entities field in GetEntitlementsRequest",
+ slog.String("method", "GetEntitlements"),
+ slog.Any("request", req),
+ )
return nil, status.Error(codes.InvalidArgument, "requires entities")
}
rsp := &authorization.GetEntitlementsResponse{
diff --git a/service/authorization/authorization.proto b/service/authorization/authorization.proto
index 6e048e4f3..e0e5f29cd 100644
--- a/service/authorization/authorization.proto
+++ b/service/authorization/authorization.proto
@@ -297,6 +297,9 @@ service AuthorizationService {
option (google.api.http) = {post: "/v1/token/authorization"};
}
rpc GetEntitlements(GetEntitlementsRequest) returns (GetEntitlementsResponse) {
- option (google.api.http) = {post: "/v1/entitlements"};
+ option (google.api.http) = {
+ post: "/v1/entitlements",
+ body:"*"
+ };
}
}
diff --git a/service/buf.lock b/service/buf.lock
index 0e5e89005..ddd811982 100644
--- a/service/buf.lock
+++ b/service/buf.lock
@@ -4,15 +4,15 @@ deps:
- remote: buf.build
owner: bufbuild
repository: protovalidate
- commit: e097f827e65240ac9fd4b1158849a8fc
- digest: shake256:f19252436fd9ded945631e2ffaaed28247a92c9015ccf55ae99db9fb3d9600c4fdb00fd2d3bd7701026ec2fd4715c5129e6ae517c25a59ba690020cfe80bf8ad
+ commit: 5a7b106cbb87462d9a8c9ffecdbd2e38
+ digest: shake256:2f7efa5a904668219f039d4f6eeb51e871f8f7f5966055a10663cba335bd65f76cac84da3fa758ab7b5dcb489ec599521390ce3951d119fb56df1fc2def16bb0
- remote: buf.build
owner: googleapis
repository: googleapis
- commit: a86849a25cc04f4dbe9b15ddddfbc488
- digest: shake256:e19143328f8cbfe13fc226aeee5e63773ca494693a72740a7560664270039a380d94a1344234b88c7691311460df9a9b1c2982190d0a2612eae80368718e1943
+ commit: e7f8d366f5264595bcc4cd4139af9973
+ digest: shake256:e5e5f1c12f82e028ea696faa43b4f9dc6258a6d1226282962a8c8b282e10946281d815884f574bd279ebd9cd7588629beb3db17b892af6c33b56f92f8f67f509
- remote: buf.build
owner: grpc-ecosystem
repository: grpc-gateway
- commit: 3f42134f4c564983838425bc43c7a65f
- digest: shake256:3d11d4c0fe5e05fda0131afefbce233940e27f0c31c5d4e385686aea58ccd30f72053f61af432fa83f1fc11cda57f5f18ca3da26a29064f73c5a0d076bba8d92
+ commit: a48fcebcf8f140dd9d09359b9bb185a4
+ digest: shake256:a926173f0ec3e1a929462c350acda846e546134b5ce2bb83fe44f02f9330a42b1c9b292f64b951b06a4d2c47e2ce4d477d6a2cb31502a15637ada35ecedefcf6
diff --git a/service/cmd/keycloak_data.yaml b/service/cmd/keycloak_data.yaml
index 006a326b2..cb459422b 100644
--- a/service/cmd/keycloak_data.yaml
+++ b/service/cmd/keycloak_data.yaml
@@ -1,5 +1,5 @@
-baseUrl: &baseUrl http://localhost:8888
-serverBaseUrl: &serverBaseUrl http://localhost:8080
+baseUrl: &baseUrl http://local-dsp.virtru.com:8888
+serverBaseUrl: &serverBaseUrl http://local-dsp.virtru.com:8080
customAudMapper: &customAudMapper
name: audience-mapper
protocol: openid-connect
@@ -78,7 +78,7 @@ realms:
serviceAccountsEnabled: false
publicClient: true
redirectUris:
- - 'http://localhost:9000/*' # otdfctl CLI tool
+ - 'http://local-dsp.virtru.com:9000/*' # otdfctl CLI tool
protocolMappers:
- *customAudMapper
users:
diff --git a/service/entityresolution/keycloak/keycloak_entity_resolution.go b/service/entityresolution/keycloak/keycloak_entity_resolution.go
index ae271e9c9..4dc1620fe 100644
--- a/service/entityresolution/keycloak/keycloak_entity_resolution.go
+++ b/service/entityresolution/keycloak/keycloak_entity_resolution.go
@@ -92,186 +92,103 @@ func CreateEntityChainFromJwt(
func EntityResolution(ctx context.Context,
req *entityresolution.ResolveEntitiesRequest, kcConfig KeycloakConfig, logger *logger.Logger,
) (entityresolution.ResolveEntitiesResponse, error) {
+
+ logger.InfoContext(ctx, "Starting EntityResolution", slog.Any("Request", req), slog.Any("KeycloakConfig", kcConfig))
+
connector, err := getKCClient(ctx, kcConfig, logger)
if err != nil {
+ logger.Error("Failed to get KC client", slog.String("error", err.Error()))
return entityresolution.ResolveEntitiesResponse{},
status.Error(codes.Internal, ErrTextCreationFailed)
}
+
payload := req.GetEntities()
+ logger.InfoContext(ctx, "Entity Payload", slog.Any("entities", payload))
var resolvedEntities []*entityresolution.EntityRepresentation
for idx, ident := range payload {
- logger.Debug("lookup", "entity", ident.GetEntityType())
+ logger.InfoContext(ctx, "Processing entity", slog.Int("index", idx), slog.Any("entity", ident.GetEntityType()))
+
var keycloakEntities []*gocloak.User
var getUserParams gocloak.GetUsersParams
exactMatch := true
+ var jsonEntities []*structpb.Struct
+
switch ident.GetEntityType().(type) {
case *authorization.Entity_ClientId:
- logger.Debug("looking up", slog.Any("type", ident.GetEntityType()), slog.String("client_id", ident.GetClientId()))
clientID := ident.GetClientId()
+ logger.InfoContext(ctx, "Looking up client", slog.String("client_id", clientID))
+
clients, err := connector.client.GetClients(ctx, connector.token.AccessToken, kcConfig.Realm, gocloak.GetClientsParams{
ClientID: &clientID,
})
if err != nil {
- logger.Error("error getting client info", slog.String("error", err.Error()))
+ logger.Error("Error getting client info", slog.String("error", err.Error()))
return entityresolution.ResolveEntitiesResponse{},
status.Error(codes.Internal, ErrTextGetRetrievalFailed)
}
- var jsonEntities []*structpb.Struct
- for _, client := range clients {
- json, err := typeToGenericJSONMap(client, logger)
- if err != nil {
- logger.Error("error serializing entity representation!", slog.String("error", err.Error()))
- return entityresolution.ResolveEntitiesResponse{},
- status.Error(codes.Internal, ErrTextCreationFailed)
- }
- mystruct, structErr := structpb.NewStruct(json)
- if structErr != nil {
- logger.Error("error making struct!", slog.String("error", structErr.Error()))
- return entityresolution.ResolveEntitiesResponse{},
- status.Error(codes.Internal, ErrTextCreationFailed)
- }
- jsonEntities = append(jsonEntities, mystruct)
- }
- if len(clients) == 0 && kcConfig.InferID.From.ClientID {
- // convert entity to json
- entityStruct, err := entityToStructPb(ident)
- if err != nil {
- logger.Error("unable to make entity struct", slog.String("error", err.Error()))
- return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed)
- }
- jsonEntities = append(jsonEntities, entityStruct)
- }
- // make sure the id field is populated
- originialID := ident.GetId()
- if originialID == "" {
- originialID = auth.EntityIDPrefix + fmt.Sprint(idx)
- }
- resolvedEntities = append(
- resolvedEntities,
- &entityresolution.EntityRepresentation{
- OriginalId: originialID,
- AdditionalProps: jsonEntities,
- },
- )
- continue
+
+ logger.InfoContext(ctx, "Clients found", slog.Any("clients", clients))
+
case *authorization.Entity_EmailAddress:
getUserParams = gocloak.GetUsersParams{Email: func() *string { t := ident.GetEmailAddress(); return &t }(), Exact: &exactMatch}
+ logger.InfoContext(ctx, "Looking up by email", slog.String("email", ident.GetEmailAddress()))
+
case *authorization.Entity_UserName:
getUserParams = gocloak.GetUsersParams{Username: func() *string { t := ident.GetUserName(); return &t }(), Exact: &exactMatch}
+ logger.InfoContext(ctx, "Looking up by username", slog.String("username", ident.GetUserName()))
}
- var jsonEntities []*structpb.Struct
users, err := connector.client.GetUsers(ctx, connector.token.AccessToken, kcConfig.Realm, getUserParams)
+ logger.InfoContext(ctx, "Users found", slog.Any("users", users), slog.Any("error", err))
+
switch {
case err != nil:
- logger.Error(err.Error())
+ logger.Error("Error retrieving users", slog.String("error", err.Error()))
return entityresolution.ResolveEntitiesResponse{},
status.Error(codes.Internal, ErrTextGetRetrievalFailed)
case len(users) == 1:
user := users[0]
- logger.Debug("user found", slog.String("user", *user.ID), slog.String("entity", ident.String()))
- logger.Debug("user", slog.Any("details", user))
- logger.Debug("user", slog.Any("attributes", user.Attributes))
+ logger.InfoContext(ctx, "User found", slog.String("user_id", *user.ID), slog.String("user", ident.String()))
keycloakEntities = append(keycloakEntities, user)
+
default:
- logger.Error("no user found for", slog.Any("entity", ident))
- if ident.GetEmailAddress() != "" { //nolint:nestif // this case has many possible outcomes to handle
- // try by group
- groups, groupErr := connector.client.GetGroups(
- ctx,
- connector.token.AccessToken,
- kcConfig.Realm,
- gocloak.GetGroupsParams{Search: func() *string { t := ident.GetEmailAddress(); return &t }()},
- )
- switch {
- case groupErr != nil:
- logger.Error("error getting group", slog.String("group", groupErr.Error()))
- return entityresolution.ResolveEntitiesResponse{},
- status.Error(codes.Internal, ErrTextGetRetrievalFailed)
- case len(groups) == 1:
- logger.Info("group found for", slog.String("entity", ident.String()))
- group := groups[0]
- expandedRepresentations, exErr := expandGroup(ctx, *group.ID, connector, &kcConfig, logger)
- if exErr != nil {
- return entityresolution.ResolveEntitiesResponse{},
- status.Error(codes.Internal, ErrTextNotFound)
- } else {
- keycloakEntities = expandedRepresentations
- }
- default:
- logger.Error("no group found for", slog.String("entity", ident.String()))
- var entityNotFoundErr entityresolution.EntityNotFoundError
- switch ident.GetEntityType().(type) {
- case *authorization.Entity_EmailAddress:
- entityNotFoundErr = entityresolution.EntityNotFoundError{Code: int32(codes.NotFound), Message: ErrTextGetRetrievalFailed, Entity: ident.GetEmailAddress()}
- case *authorization.Entity_UserName:
- entityNotFoundErr = entityresolution.EntityNotFoundError{Code: int32(codes.NotFound), Message: ErrTextGetRetrievalFailed, Entity: ident.GetUserName()}
- // case "":
- // return &entityresolution.IdpPluginResponse{},
- // status.Error(codes.InvalidArgument, db.ErrTextNotFound)
- default:
- logger.Error("unsupported/unknown type for", slog.String("entity", ident.String()))
- entityNotFoundErr = entityresolution.EntityNotFoundError{Code: int32(codes.NotFound), Message: ErrTextGetRetrievalFailed, Entity: ident.String()}
- }
- logger.Error(entityNotFoundErr.String())
- if kcConfig.InferID.From.Email || kcConfig.InferID.From.Username {
- // user not found -- add json entity to resp instead
- entityStruct, err := entityToStructPb(ident)
- if err != nil {
- logger.Error("unable to make entity struct from email or username", slog.String("error", err.Error()))
- return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed)
- }
- jsonEntities = append(jsonEntities, entityStruct)
- } else {
- return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Code(entityNotFoundErr.GetCode()), entityNotFoundErr.GetMessage())
- }
- }
- } else if ident.GetUserName() != "" {
- if kcConfig.InferID.From.Username {
- // user not found -- add json entity to resp instead
- entityStruct, err := entityToStructPb(ident)
- if err != nil {
- logger.Error("unable to make entity struct from username", slog.String("error", err.Error()))
- return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Internal, ErrTextCreationFailed)
- }
- jsonEntities = append(jsonEntities, entityStruct)
- } else {
- entityNotFoundErr := entityresolution.EntityNotFoundError{Code: int32(codes.NotFound), Message: ErrTextGetRetrievalFailed, Entity: ident.GetUserName()}
- return entityresolution.ResolveEntitiesResponse{}, status.Error(codes.Code(entityNotFoundErr.GetCode()), entityNotFoundErr.GetMessage())
- }
- }
+ logger.WarnContext(ctx, "No user found for entity", slog.Any("entity", ident))
}
for _, er := range keycloakEntities {
json, err := typeToGenericJSONMap(er, logger)
if err != nil {
- logger.Error("error serializing entity representation!", slog.String("error", err.Error()))
+ logger.Error("Error serializing entity representation", slog.String("error", err.Error()))
return entityresolution.ResolveEntitiesResponse{},
status.Error(codes.Internal, ErrTextCreationFailed)
}
+
mystruct, structErr := structpb.NewStruct(json)
if structErr != nil {
- logger.Error("error making struct!", slog.String("error", structErr.Error()))
+ logger.Error("Error creating struct", slog.String("error", structErr.Error()))
return entityresolution.ResolveEntitiesResponse{},
status.Error(codes.Internal, ErrTextCreationFailed)
}
+
jsonEntities = append(jsonEntities, mystruct)
}
- // make sure the id field is populated
- originialID := ident.GetId()
- if originialID == "" {
- originialID = auth.EntityIDPrefix + fmt.Sprint(idx)
+
+ // Ensure ID is populated
+ originalID := ident.GetId()
+ if originalID == "" {
+ originalID = auth.EntityIDPrefix + fmt.Sprint(idx)
}
+
resolvedEntities = append(
resolvedEntities,
&entityresolution.EntityRepresentation{
- OriginalId: originialID,
+ OriginalId: originalID,
AdditionalProps: jsonEntities,
},
)
- logger.Debug("entities", slog.Any("resolved", resolvedEntities))
+ logger.InfoContext(ctx, "Resolved entity", slog.Any("resolvedEntity", resolvedEntities))
}
return entityresolution.ResolveEntitiesResponse{
diff --git a/service/internal/auth/casbin.go b/service/internal/auth/casbin.go
index 7e3cf518c..5f0f5536e 100644
--- a/service/internal/auth/casbin.go
+++ b/service/internal/auth/casbin.go
@@ -257,6 +257,8 @@ func (e *Enforcer) Enforce(token jwt.Token, resource, action string) (bool, erro
// extract the role claim from the token
s := e.buildSubjectFromToken(token)
+ e.logger.Debug("roles extracted from token", slog.Any("roles", s.Roles))
+
if len(s.Roles) == 0 {
sub := rolePrefix + defaultRole
e.logger.Debug("enforcing policy", slog.Any("subject", sub), slog.String("resource", resource), slog.String("action", action))
@@ -266,6 +268,8 @@ func (e *Enforcer) Enforce(token jwt.Token, resource, action string) (bool, erro
allowed := false
for _, role := range s.Roles {
sub := rolePrefix + role
+ e.logger.Debug("attempting to enforce policy", slog.String("subject", sub), slog.String("resource", resource), slog.String("action", action))
+
allowed, err = e.Enforcer.Enforce(sub, resource, action)
if err != nil {
e.logger.Error("enforce by role error", slog.String("subject", sub), slog.String("resource", resource), slog.String("action", action), slog.String("error", err.Error()))
@@ -274,6 +278,8 @@ func (e *Enforcer) Enforce(token jwt.Token, resource, action string) (bool, erro
if allowed {
e.logger.Debug("allowed by policy", slog.String("subject", sub), slog.String("resource", resource), slog.String("action", action))
break
+ } else {
+ e.logger.Debug("policy denied", slog.String("subject", sub), slog.String("resource", resource), slog.String("action", action))
}
}
if !allowed {
diff --git a/service/internal/server/server.go b/service/internal/server/server.go
index 59058ce14..841c8e1e2 100644
--- a/service/internal/server/server.go
+++ b/service/internal/server/server.go
@@ -210,21 +210,46 @@ func newHTTPServer(c Config, h http.Handler, a *auth.Authentication, g *grpc.Ser
// CORS
if c.CORS.Enabled {
+ l.Info("CORS is enabled",
+ "AllowedOrigins", c.CORS.AllowedOrigins,
+ "AllowedMethods", c.CORS.AllowedMethods,
+ "AllowedHeaders", c.CORS.AllowedHeaders,
+ "ExposedHeaders", c.CORS.ExposedHeaders,
+ "AllowCredentials", c.CORS.AllowCredentials,
+ "MaxAge", c.CORS.MaxAge,
+ )
+
h = cors.New(cors.Options{
- AllowOriginFunc: func(_ *http.Request, origin string) bool {
- for _, allowedOrigin := range c.CORS.AllowedOrigins {
+ AllowOriginFunc: func(req *http.Request, origin string) bool {
+ l.Info("CORS request received", "Origin", origin, "RequestURI", req.RequestURI)
+
+ for _, allowedOrigin := range c.CORS.AllowedOrigins {
+ l.Info("Checking allowed origin", "AllowedOrigin", allowedOrigin)
+
if allowedOrigin == "*" {
+ l.Info("CORS origin allowed", "Origin", origin)
return true
}
if strings.EqualFold(origin, allowedOrigin) {
+ l.Info("CORS origin matched", "Origin", origin, "AllowedOrigin", allowedOrigin)
return true
}
}
+ l.Warn("CORS origin denied", "Origin", origin)
return false
},
- AllowedMethods: c.CORS.AllowedMethods,
- AllowedHeaders: c.CORS.AllowedHeaders,
- ExposedHeaders: c.CORS.ExposedHeaders,
+ AllowedMethods: func() []string {
+ l.Info("Allowed methods for CORS", "Methods", c.CORS.AllowedMethods)
+ return c.CORS.AllowedMethods
+ }(),
+ AllowedHeaders: func() []string {
+ l.Info("Allowed headers for CORS", "Headers", c.CORS.AllowedHeaders)
+ return c.CORS.AllowedHeaders
+ }(),
+ ExposedHeaders: func() []string {
+ l.Info("Exposed headers for CORS", "Headers", c.CORS.ExposedHeaders)
+ return c.CORS.ExposedHeaders
+ }(),
AllowCredentials: c.CORS.AllowCredentials,
MaxAge: c.CORS.MaxAge,
}).Handler(h)
diff --git a/service/kas/access/accessPdp.go b/service/kas/access/accessPdp.go
index 257a55909..0b68dce4e 100644
--- a/service/kas/access/accessPdp.go
+++ b/service/kas/access/accessPdp.go
@@ -3,6 +3,9 @@ package access
import (
"context"
"errors"
+ "fmt"
+ "strings"
+ "time"
"github.com/opentdf/platform/protocol/go/authorization"
"github.com/opentdf/platform/protocol/go/policy"
@@ -32,13 +35,176 @@ func canAccess(ctx context.Context, token *authorization.Token, policy Policy, s
return true, nil
}
+func parseTemporalAttribute(attribute string) (string, []string, error) {
+ // e.g. "temporal/value/after::2024-11-05T12:00:00Z"
+ const minParts = 2
+ parts := strings.Split(attribute, "::")
+ if len(parts) < minParts {
+ return "", nil, fmt.Errorf("invalid temporal attribute format")
+ }
+
+ operatorParts := strings.Split(parts[0], "/value/")
+ if len(operatorParts) < 2 {
+ return "", nil, fmt.Errorf("invalid temporal operator format in attribute")
+ }
+
+ operator := operatorParts[1]
+ operands := parts[1:]
+ return operator, operands, nil
+}
+
+/*
+Temporal Attribute:
+The access pdp validates the temporal operator and their provided operands.
+Each operand is a RFC 3339 formatted datetime string, such as "2024-11-05T12:00:00Z", or a duration in seconds.
+
+Expected temporal attribute format: `/temporal/value/::::<...operand>`
+
+ - 'after': Checks that the current time is after the provided datetime.
+ ex: temporal/value/after::2024-11-05T12:00:00Z
+
+ - 'before': Checks that the current time is before the provided datetime.
+ ex: temporal/value/before::2024-11-05T12:00:00Z
+
+ - 'duration': Checks that the current time is within the provided duration, starting at the provided datetime.
+ ex: temporal/value/duration::2024-11-05T12:00:00Z::1h
+
+ - 'between': Checks that the current time is between the provided start datetime and end datetime.
+ ex: temporal/value/between::2024-11-04T12:00:00Z::2024-11-05T12:00:00Z
+*/
+func checkTemporalConditions(ctx context.Context, attributes []string, logger logger.Logger) (bool, error) {
+ layout := time.RFC3339
+ currentTime := time.Now().UTC()
+
+ const (
+ oneOperand int = 1
+ twoOperands int = 2
+ )
+
+ for _, attr := range attributes {
+ operator, operands, err := parseTemporalAttribute(attr)
+ if err != nil {
+ logger.ErrorContext(ctx, "Failed to parse temporal attribute", "attribute", attr, "err", err)
+ return false, err
+ }
+
+ switch operator {
+ case "after": // temporal/value/after::2024-11-05T12:00:00Z
+ if len(operands) != oneOperand {
+ return false, fmt.Errorf("temporal/after: invalid number of operands; operator expects one operand, %d received", len(operands))
+ }
+ afterTime, err := time.Parse(layout, operands[0])
+ if err != nil {
+ logger.ErrorContext(ctx, "temporal/after: invalid RFC3339 datetime format", "value", operands[0])
+ return false, err
+ }
+ if currentTime.Compare(afterTime) >= 0 {
+ logger.DebugContext(ctx, "temporal/after: access denied; current time is before 'after' time", "afterTime", afterTime)
+ return false, nil // Access denied
+ }
+
+ case "before": // temporal/value/before::2024-11-05T12:00:00Z
+ if len(operands) != oneOperand {
+ return false, fmt.Errorf("temporal/before: invalid number of operands; operator expects one operand, %d received", len(operands))
+ }
+ beforeTime, err := time.Parse(layout, operands[0])
+ if err != nil {
+ logger.DebugContext(ctx, "temporal/before: invalid RFC3339 datetime format", "value", operands[0])
+ return false, err
+ }
+ if currentTime.Compare(beforeTime) < 0 {
+ logger.DebugContext(ctx, "temporal/before: access denied; current time is after 'before' time", "beforeTime", beforeTime)
+ return false, nil // Access denied
+ }
+
+ case "duration": // temporal/value/duration::2024-11-05T12:00:00Z::1h
+ if len(operands) != twoOperands {
+ return false, fmt.Errorf("temporal/duration: invalid number of operands; operator expects two operands, %d received", len(operands))
+ }
+ startTime, err := time.Parse(layout, operands[0])
+ if err != nil {
+ logger.ErrorContext(ctx, "temporal/duration: invalid RFC3339 datetime format", "value", operands[0])
+ return false, err
+ }
+ duration, err := time.ParseDuration(operands[1])
+ if err != nil {
+ logger.ErrorContext(ctx, "temporal/duration: invalid duration format", "value", operands[1])
+ return false, err
+ }
+ endTime := startTime.Add(duration)
+ if currentTime.Compare(startTime) >= 0 && currentTime.Compare(endTime) < 0 {
+ logger.DebugContext(ctx, "temporal/duration: access denied; current time is not within the time window", "start", startTime, "end", endTime)
+ return false, nil // Access denied
+ }
+
+ case "between": // temporal/value/between::2024-11-04T12:00:00Z::2024-11-05T12:00:00Z
+ if len(operands) != twoOperands {
+ return false, fmt.Errorf("temporal/between: invalid number of operands; operator expects two operands, %d received", len(operands))
+ }
+ startTime, err := time.Parse(layout, operands[0])
+ if err != nil {
+ logger.ErrorContext(ctx, "temporal/between: invalid RFC3339 datetime format", "startTime", operands[0])
+ return false, err
+ }
+ endTime, err := time.Parse(layout, operands[1])
+ if err != nil {
+ logger.ErrorContext(ctx, "temporal/between: invalid RFC3339 datetime format", "endTime", operands[1])
+ return false, err
+ }
+ if currentTime.Compare(startTime) >= 0 && currentTime.Compare(endTime) < 0 {
+ logger.DebugContext(ctx, "temporal/between: access denied; current time is not within the time window", "start", startTime, "end", endTime)
+ return false, nil
+ }
+
+ default:
+ return false, fmt.Errorf("unknown temporal operator: %s", operator)
+ }
+ }
+ // Conditions satisfied, access granted
+ logger.DebugContext(ctx, "Access granted: all temporal conditions met")
+ return true, nil
+}
+
+func isTemporalAttribute(uri string) bool {
+ return strings.Contains(uri, "temporal/value/")
+}
+
func checkAttributes(ctx context.Context, dataAttrs []Attribute, ent *authorization.Token, sdk *otdf.SDK, logger logger.Logger) (bool, error) {
+ var temporalAttributes []string
ras := []*authorization.ResourceAttribute{{
AttributeValueFqns: make([]string, 0),
}}
+
+ // Iterate over data attributes and classify them as temporal or not
for _, attr := range dataAttrs {
- ras[0].AttributeValueFqns = append(ras[0].GetAttributeValueFqns(), attr.URI)
+ // Check for /temporal attribute and validate
+ if isTemporalAttribute(attr.URI) {
+ temporalAttributes = append(temporalAttributes, attr.URI)
+ logger.DebugContext(ctx, "Found temporal attribute", "attribute", attr.URI)
+ } else {
+ ras[0].AttributeValueFqns = append(ras[0].GetAttributeValueFqns(), attr.URI)
+ logger.DebugContext(ctx, "Added non-temporal attribute to resource attributes", "attribute", attr.URI)
+ }
}
+
+ // Check if there are temporal conditions and validate them
+ if len(temporalAttributes) > 0 {
+ logger.DebugContext(ctx, "Checking temporal conditions", "attributes", temporalAttributes)
+ isValid, err := checkTemporalConditions(ctx, temporalAttributes, logger)
+ if err != nil {
+ logger.ErrorContext(ctx, "Error validating temporal conditions", "err", err)
+ return false, err
+ }
+ if !isValid {
+ logger.DebugContext(ctx, "Temporal conditions not met", "attributes", temporalAttributes)
+ return false, nil
+ }
+ }
+
+ // Log the constructed resource attributes before making the decisions request
+ logger.DebugContext(ctx, "Constructed resource attributes", "attributes", ras[0].GetAttributeValueFqns())
+
+ // Construct the decisions request
in := authorization.GetDecisionsByTokenRequest{
DecisionRequests: []*authorization.TokenDecisionRequest{
{
@@ -50,17 +216,31 @@ func checkAttributes(ctx context.Context, dataAttrs []Attribute, ent *authorizat
},
},
}
+
+ // Call the SDK to get the decisions by token
dr, err := sdk.Authorization.GetDecisionsByToken(ctx, &in)
if err != nil {
logger.ErrorContext(ctx, "Error received from GetDecisionsByToken", "err", err)
return false, errors.Join(ErrDecisionUnexpected, err)
}
+
+ // Check if we got exactly one decision response
if len(dr.GetDecisionResponses()) != 1 {
logger.ErrorContext(ctx, ErrDecisionCountUnexpected.Error(), "count", len(dr.GetDecisionResponses()))
return false, ErrDecisionCountUnexpected
}
+
+ // Log the decision response
+ logger.DebugContext(ctx, "Received decision response", "decision", dr.GetDecisionResponses()[0].GetDecision())
+
+ // Check if the decision is PERMIT
if dr.GetDecisionResponses()[0].GetDecision() == authorization.DecisionResponse_DECISION_PERMIT {
+ logger.DebugContext(ctx, "Access granted")
return true, nil
}
+
+ // Log if the decision is not PERMIT
+ logger.DebugContext(ctx, "Access denied")
return false, nil
}
+
diff --git a/service/policy/db/attribute_fqn.go b/service/policy/db/attribute_fqn.go
index 3ba3dbfae..3cf421f26 100644
--- a/service/policy/db/attribute_fqn.go
+++ b/service/policy/db/attribute_fqn.go
@@ -4,7 +4,7 @@ import (
"context"
"errors"
"fmt"
- "log/slog"
+ "log/slog"
"strings"
"github.com/Masterminds/squirrel"
@@ -155,10 +155,18 @@ func (c *PolicyDBClient) GetAttributesByValueFqns(ctx context.Context, r *attrib
if r.Fqns == nil || r.GetWithValue() == nil {
return nil, errors.Join(db.ErrMissingValue, errors.New("error: one or more FQNs and a WithValue selector must be provided"))
}
+
list := make(map[string]*attributes.GetAttributeValuesByFqnsResponse_AttributeAndValue, len(r.GetFqns()))
for _, fqn := range r.GetFqns() {
// normalize to lower case
fqn = strings.ToLower(fqn)
+
+ // Skip temporal FQNs
+ if strings.Contains(fqn,"temporal/") {
+ c.logger.Debug("skipping temporal FQN", slog.String("fqn", fqn))
+ continue
+ }
+
// ensure the FQN corresponds to an attribute value and not a definition or namespace alone
if !strings.Contains(fqn, "/value/") {
return nil, db.ErrFqnMissingValue