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