From 6c06ade0dcfc8530d85f9949a604acf77fe1e6be Mon Sep 17 00:00:00 2001 From: Richard Marin <34529290+rmarinn@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:27:55 +0800 Subject: [PATCH 1/2] refactor(jans-cedarling): enhance schema parser and entity creation implementation (#10549) * feat(jans-cedarling): implement new CedarRecordAttr struct Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): implement struct for CedarEntityType Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): remove `Cedar` prefix in struct names Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): implement Action type struct Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): rename RecordAttr to AttributeKind Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): change type aliases to newtype definitions Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): implement new CedarSchemaJson struct Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): implement EntityShape newtype for AttributeKind::Record Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): remove newtypes for string types Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): initial entity builder implementation - implement an entity builder can can make workload entities Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): add user entity creation to the EntityBuilder Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): improve entity builder code readability Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): implement EntityBuilder token entity creation Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): implement EntityBuilder resource entity creation Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): replace infallible unwrap with expect Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): implement EntityBuilder role entity creation Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): implement EntityBuilder::build_entities - implement EntityBuilder::build_entities which builds all the cedarling-specific entities Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): replace old implementation - start using the new CedarJsonSchema - start using EntityBuilder to build entities Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): loading unknown attribute variant - make the default type "EntityOrCommon" for unknown variants instead of failing desrialization. Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): multiple access_token entities being created Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): using access_token to create all token entities - fix the bug where the access_token is being used to create all token entities Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): handle common type context - fix CommonType contexts not being handled properly Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): entity references not being fully qualified - fix entity references within entities not being qualified; i.e. the namespace is not included in the reference... which causes problems down the line Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): role entities creation - refactor role entities creation to not fail if no role entities were created but just return an empty Vec Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): broken test: test_failed_workload_mapping Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): broken test: test_failed_id_token_mapping Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): remove outdated test errors_on_invalid_type Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): silently fail non-required attr creation errors - silently fail non-required attr creation errors since it was making an existing test fail: "check_mapping_tokens_data" Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): move build_context to it's own file Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): remove outdated commens Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): move EntityBuilder into the Authz struct - move EntityBuilder into the Authz struct from AuthzConfig Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * docs(jans-cedarling): update auto adding references to context docs - remove the now obsolete naming convention needed to automatically map entity references to the context from the docs Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): check resource schema before creating the entity Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): improve error message for entity resouce Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): resolve clippy issues Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): remove panics in build_context func Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): remove testing println! Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * test(jans-cedarling): cover building resource entity with record attr Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): non-required record attr getting required - fix non required record attributes getting required if the record is also required. Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): improve code readability Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): return str instead of String for value_type_name Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): replace unwrap_or with unwrap_or_else - replace unwrap_or with unwrap_or_else for laziness Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): improve code readability Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): map infallible error to BuildEntityError Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): reduce lookups in merge_json_values - reduce the amount of lookups done in merge_json_values to improve performance Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): delete unused file Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): remove outdated comment Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): rename try_join_namespace to join_namespace Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * docs(jans-cedarling): update PYTHON_TYPES.md Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): swap params when calling merge_json_values - when calling merge_json_values, use the original context as the first param Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): implement ClaimAliasMap struct - implement a struct to describe claim aliases for better readability Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): return an error instead of panicking - return an error instead of panicking in try_build_role_entities Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): remove extra impl block Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): do not create duplicate role entities Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * test(jans-cedarling): add test for creating roles from different tokens Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): return an error when given a wrong token kind - return an error when given a wrong kind of token when building token entities Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * test(jans-cedarling): add test for creating token entities - add test for creating id_token entity - add test for creating userinfo_token entity Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * test(jans-cedarling): added more comprehensive tests for entity builder Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * chore(jans-cedarling): resolve clippy issues Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * feat(jans-cedarling): unify cases in build_role_entities Signed-off-by: John Anderson * fix(jans-cedarling): misspelling in error message Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): test intermittently failing Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): remove unwrap and improve error messages Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * fix(jans-cedarling): python test error msg assert Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): remove deserialize_to_string fn Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> * refactor(jans-cedarling): replace if-else with then_some Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> --------- Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com> Signed-off-by: John Anderson Co-authored-by: John Anderson Co-authored-by: Oleh --- docs/cedarling/cedarling-authz.md | 16 +- .../bindings/cedarling_python/PYTHON_TYPES.md | 28 +- .../cedarling_python/src/authorize/errors.rs | 63 +- .../cedarling_python/tests/test_authorize.py | 6 +- jans-cedarling/cedarling/Cargo.toml | 2 +- .../cedarling/src/authz/build_ctx.rs | 191 ++++++ .../cedarling/src/authz/entities/create.rs | 440 ------------- .../cedarling/src/authz/entities/mod.rs | 278 --------- .../src/authz/entities/test_create.rs | 587 ------------------ .../successful_scenario.schema | 26 - .../successful_scenario_schema.json | 87 --- .../test_create_data/type_error_schema.json | 36 -- .../src/authz/entities/trait_as_expression.rs | 29 - .../cedarling/src/authz/entities/user.rs | 257 -------- .../cedarling/src/authz/entities/workload.rs | 249 -------- .../cedarling/src/authz/entity_builder.rs | 471 ++++++++++++++ .../src/authz/entity_builder/build_attrs.rs | 318 ++++++++++ .../src/authz/entity_builder/build_expr.rs | 586 +++++++++++++++++ .../entity_builder/build_resource_entity.rs | 331 ++++++++++ .../authz/entity_builder/build_role_entity.rs | 286 +++++++++ .../entity_builder/build_token_entities.rs | 368 +++++++++++ .../authz/entity_builder/build_user_entity.rs | 291 +++++++++ .../entity_builder/build_workload_entity.rs | 463 ++++++++++++++ .../src/authz/entity_builder/mapping.rs | 61 ++ .../cedarling/src/authz/merge_json.rs | 56 -- jans-cedarling/cedarling/src/authz/mod.rs | 228 ++----- jans-cedarling/cedarling/src/authz/request.rs | 12 - .../src/common/cedar_schema/cedar_json.rs | 442 ++++--------- .../common/cedar_schema/cedar_json/action.rs | 571 +++++------------ .../cedar_schema/cedar_json/attribute.rs | 283 +++++++++ .../cedar_schema/cedar_json/deserialize.rs | 36 ++ .../cedar_schema/cedar_json/entity_type.rs | 171 +++++ .../cedar_schema/cedar_json/entity_types.rs | 226 ------- .../cedarling/src/common/cedar_schema/mod.rs | 2 +- .../cedarling/src/common/policy_store.rs | 2 +- .../src/common/policy_store/claim_mapping.rs | 24 +- .../cedarling/src/init/service_factory.rs | 10 +- jans-cedarling/cedarling/src/jwt/mod.rs | 2 +- jans-cedarling/cedarling/src/jwt/token.rs | 58 +- jans-cedarling/cedarling/src/lib.rs | 6 +- .../cedarling/src/tests/mapping_entities.rs | 105 ++-- .../src/tests/schema_type_mapping.rs | 2 +- 42 files changed, 4293 insertions(+), 3413 deletions(-) create mode 100644 jans-cedarling/cedarling/src/authz/build_ctx.rs delete mode 100644 jans-cedarling/cedarling/src/authz/entities/create.rs delete mode 100644 jans-cedarling/cedarling/src/authz/entities/mod.rs delete mode 100644 jans-cedarling/cedarling/src/authz/entities/test_create.rs delete mode 100644 jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema delete mode 100644 jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json delete mode 100644 jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json delete mode 100644 jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs delete mode 100644 jans-cedarling/cedarling/src/authz/entities/user.rs delete mode 100644 jans-cedarling/cedarling/src/authz/entities/workload.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs create mode 100644 jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs delete mode 100644 jans-cedarling/cedarling/src/authz/merge_json.rs create mode 100644 jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs create mode 100644 jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs create mode 100644 jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs delete mode 100644 jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs diff --git a/docs/cedarling/cedarling-authz.md b/docs/cedarling/cedarling-authz.md index 89f3a5c7619..70445076942 100644 --- a/docs/cedarling/cedarling-authz.md +++ b/docs/cedarling/cedarling-authz.md @@ -83,14 +83,14 @@ decision_result = await cedarling(input) ## Automatically Adding Entity References to the Context -Cedarling simplifies context creation by automatically including certain entities. This means you don't need to manually pass their references when using them in your policies. The following entities are automatically added to the context, along with their naming conventions in `lower_snake_case` format: - -- **Workload Entity**: `workload` -- **User Entity**: `user` -- **Resource Entity**: `resource` -- **Access Token Entity**: `access_token` -- **ID Token Entity**: `id_token` -- **Userinfo Token Entity**: `userinfo_token` +Cedarling simplifies context creation by automatically including certain entities. This means you don't need to manually pass their references when using them in your policies. The following entities are automatically added to the context. + +- Workload Entity +- User Entity +- Resource Entity +- Access Token Entity +- ID Token Entity +- Userinfo Token Entity ### Example Policy diff --git a/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md b/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md index f0875a6f06d..436c8f5d727 100644 --- a/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md +++ b/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md @@ -209,30 +209,14 @@ ___ Exception raised by authorize_errors ___ -# authorize_errors.CreateAccessTokenEntityError -Error encountered while creating access_token entity +# authorize_errors.BuildEntitiesError +Error encountered while building entities into context ___ # authorize_errors.CreateContextError Error encountered while validating context according to the schema ___ -# authorize_errors.CreateIdTokenEntityError -Error encountered while creating id token entities -___ - -# authorize_errors.CreateUserEntityError -Error encountered while creating User entity -___ - -# authorize_errors.CreateUserinfoTokenEntityError -Error encountered while creating Userinfo_token entity -___ - -# authorize_errors.CreateWorkloadEntityError -Error encountered while creating workload entity -___ - # authorize_errors.EntitiesError Error encountered while collecting all entities ___ @@ -245,14 +229,6 @@ ___ Error encountered while processing JWT token data ___ -# authorize_errors.ResourceEntityError -Error encountered while creating resource entity -___ - -# authorize_errors.RoleEntityError -Error encountered while creating role entity -___ - # authorize_errors.UserRequestValidationError Error encountered while creating cedar_policy::Request for user entity principal ___ diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs index 89ad49ba9a9..feda288a4ad 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs @@ -30,54 +30,6 @@ create_exception!( "Error encountered while processing JWT token data" ); -create_exception!( - authorize_errors, - CreateIdTokenEntityError, - AuthorizeError, - "Error encountered while creating id token entities" -); - -create_exception!( - authorize_errors, - CreateUserinfoTokenEntityError, - AuthorizeError, - "Error encountered while creating Userinfo_token entity" -); -create_exception!( - authorize_errors, - CreateAccessTokenEntityError, - AuthorizeError, - "Error encountered while creating access_token entity" -); - -create_exception!( - authorize_errors, - CreateUserEntityError, - AuthorizeError, - "Error encountered while creating User entity" -); - -create_exception!( - authorize_errors, - CreateWorkloadEntityError, - AuthorizeError, - "Error encountered while creating workload entity" -); - -create_exception!( - authorize_errors, - ResourceEntityError, - AuthorizeError, - "Error encountered while creating resource entity" -); - -create_exception!( - authorize_errors, - RoleEntityError, - AuthorizeError, - "Error encountered while creating role entity" -); - create_exception!( authorize_errors, ActionError, @@ -120,6 +72,13 @@ create_exception!( "Error encountered while parsing all entities to json for logging" ); +create_exception!( + authorize_errors, + BuildEntitiesError, + AuthorizeError, + "Error encountered while building entities into context" +); + create_exception!( authorize_errors, AddEntitiesIntoContextError, @@ -166,17 +125,11 @@ macro_rules! errors_functions { // For each possible case of `AuthorizeError`, we have created a corresponding Python exception that inherits from `cedarling::AuthorizeError`. errors_functions! { ProcessTokens => ProcessTokens, - CreateIdTokenEntity => CreateIdTokenEntityError, - CreateUserinfoTokenEntity => CreateUserinfoTokenEntityError, - CreateAccessTokenEntity => CreateAccessTokenEntityError, - CreateUserEntity => CreateUserEntityError, - CreateWorkloadEntity => CreateWorkloadEntityError, - ResourceEntity => ResourceEntityError, - RoleEntity => RoleEntityError, Action => ActionError, CreateContext => CreateContextError, WorkloadRequestValidation => WorkloadRequestValidationError, UserRequestValidation => UserRequestValidationError, + BuildEntity => BuildEntitiesError, BuildContext => AddEntitiesIntoContextError, Entities => EntitiesError, EntitiesToJson => EntitiesToJsonError diff --git a/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py index 92926ccfccd..b1e8971f1ce 100644 --- a/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py +++ b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py @@ -186,8 +186,8 @@ def test_resource_entity_error(): ''' try: raise_authorize_error(load_bootstrap_config()) - except authorize_errors.ResourceEntityError as e: - assert str(e) == "could not create resource entity: could not get attribute value from payload: type mismatch for key 'org_id'. expected: 'String', but found: 'number'" + except authorize_errors.BuildEntitiesError as e: + assert str(e) == "failed to build resource entity: failed to build `org_id` attribute: failed to build restricted expression: type mismatch for key 'org_id'. expected: 'string', but found: 'number'" def test_authorize_error(): @@ -199,4 +199,4 @@ def test_authorize_error(): try: raise_authorize_error(load_bootstrap_config()) except authorize_errors.AuthorizeError as e: - assert str(e) == "could not create resource entity: could not get attribute value from payload: type mismatch for key 'org_id'. expected: 'String', but found: 'number'" + assert str(e) == "failed to build resource entity: failed to build `org_id` attribute: failed to build restricted expression: type mismatch for key 'org_id'. expected: 'string', but found: 'number'" diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 689991b1919..2e924cad531 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -16,7 +16,7 @@ serde_yml = "0.0.12" thiserror = { workspace = true } sparkv = { workspace = true } uuid7 = { version = "1.1.0", features = ["serde", "uuid"] } -cedar-policy = "4.2" +cedar-policy = { version = "4.2", features = ["partial-eval"] } base64 = "0.22.1" url = "2.5.2" lazy_static = "1.5.0" diff --git a/jans-cedarling/cedarling/src/authz/build_ctx.rs b/jans-cedarling/cedarling/src/authz/build_ctx.rs new file mode 100644 index 00000000000..265a4f7e7aa --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/build_ctx.rs @@ -0,0 +1,191 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use std::collections::HashMap; + +use super::{AuthorizeEntitiesData, AuthzConfig}; +use crate::common::cedar_schema::cedar_json::attribute::Attribute; +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::cedar_schema::CEDAR_NAMESPACE_SEPARATOR; +use cedar_policy::ContextJsonError; +use serde_json::{json, map::Entry, Value}; + +/// Constructs the authorization context by adding the built entities from the tokens +pub fn build_context( + config: &AuthzConfig, + request_context: Value, + entities_data: &AuthorizeEntitiesData, + schema: &cedar_policy::Schema, + action: &cedar_policy::EntityUid, +) -> Result { + let namespace = config.policy_store.namespace(); + let action_name = &action.id().escaped(); + let json_schema = &config.policy_store.schema.json; + let action_schema = json_schema + .get_action(namespace, action_name) + .ok_or(BuildContextError::UnknownAction(action_name.to_string()))?; + + // Get the entities required for the context + let mut ctx_entity_refs = json!({}); + let type_ids = entities_data.type_ids(); + if let Some(ctx) = action_schema.applies_to.context.as_ref() { + match ctx { + Attribute::Record { attrs, .. } => { + for (key, attr) in attrs.iter() { + if let Some(entity_ref) = + build_entity_refs_from_attr(namespace, attr, &type_ids, json_schema)? + { + ctx_entity_refs[key] = entity_ref; + } + } + }, + Attribute::EntityOrCommon { name, .. } => { + // TODO: handle potential namespace collisions when Cedarling starts + // supporting multiple namespaces + if let Some((_namespace, attr)) = json_schema.get_common_type(name) { + match attr { + Attribute::Record { attrs, .. } => { + for (key, attr) in attrs.iter() { + if let Some(entity_ref) = build_entity_refs_from_attr( + namespace, + attr, + &type_ids, + json_schema, + )? { + ctx_entity_refs[key] = entity_ref; + } + } + }, + attr => { + return Err(BuildContextError::InvalidKind( + attr.kind_str().to_string(), + "record".to_string(), + )) + }, + } + } + }, + attr => { + return Err(BuildContextError::InvalidKind( + attr.kind_str().to_string(), + "record or common".to_string(), + )) + }, + } + } + + let context = merge_json_values(request_context, ctx_entity_refs)?; + let context: cedar_policy::Context = + cedar_policy::Context::from_json_value(context, Some((schema, action)))?; + + Ok(context) +} + +/// Builds the JSON entity references from a given attribute. +/// +/// Returns `Ok(None)` if the attr is not an Entity Reference +fn build_entity_refs_from_attr( + namespace: &str, + attr: &Attribute, + type_ids: &HashMap, + schema: &CedarSchemaJson, +) -> Result, BuildContextError> { + match attr { + Attribute::Entity { name, .. } => map_entity_id(namespace, name, type_ids), + Attribute::EntityOrCommon { name, .. } => { + if let Some((entity_namespace, _)) = schema.get_entity_from_base_name(name) { + if namespace == entity_namespace { + return map_entity_id(namespace, name, type_ids); + } + } + Ok(None) + }, + _ => Ok(None), + } +} + +/// Maps a known entity ID to the entity reference +fn map_entity_id( + namespace: &str, + name: &str, + type_ids: &HashMap, +) -> Result, BuildContextError> { + if let Some(type_id) = type_ids.get(name).as_ref() { + let name = join_namespace(namespace, name); + Ok(Some(json!({"type": name, "id": type_id}))) + } else { + Err(BuildContextError::MissingEntityId(name.to_string())) + } +} + +/// Joins the given type name with the given namespace if it's not an empty string. +fn join_namespace(namespace: &str, type_name: &str) -> String { + if namespace.is_empty() { + return type_name.to_string(); + } + [namespace, type_name].join(CEDAR_NAMESPACE_SEPARATOR) +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildContextError { + /// Error encountered while validating context according to the schema + #[error("failed to merge JSON objects due to conflicting keys: {0}")] + KeyConflict(String), + /// Error encountered while deserializing the Context from JSON + #[error(transparent)] + DeserializeFromJson(#[from] ContextJsonError), + /// Error encountered if the action being used as the reference to build the Context + /// is not in the schema + #[error("failed to find the action `{0}` in the schema")] + UnknownAction(String), + /// Error encountered while building entity references in the Context + #[error("failed to build entity reference for `{0}` since an entity id was not provided")] + MissingEntityId(String), + #[error("invalid action context type: {0}. expected: {1}")] + InvalidKind(String, String), +} + +pub fn merge_json_values(mut base: Value, other: Value) -> Result { + if let (Some(base_map), Some(additional_map)) = (base.as_object_mut(), other.as_object()) { + for (key, value) in additional_map { + if let Entry::Vacant(entry) = base_map.entry(key) { + entry.insert(value.clone()); + } else { + return Err(BuildContextError::KeyConflict(key.clone())); + } + } + } + Ok(base) +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + + #[test] + fn can_merge_json_objects() { + let obj1 = json!({ "a": 1, "b": 2 }); + let obj2 = json!({ "c": 3, "d": 4 }); + let expected = json!({"a": 1, "b": 2, "c": 3, "d": 4}); + + let result = merge_json_values(obj1, obj2).expect("Should merge JSON objects"); + + assert_eq!(result, expected); + } + + #[test] + fn errors_on_same_keys() { + // Test for only two objects + let obj1 = json!({ "a": 1, "b": 2 }); + let obj2 = json!({ "b": 3, "c": 4 }); + let result = merge_json_values(obj1, obj2); + + assert!( + matches!(result, Err(BuildContextError::KeyConflict(key)) if key.as_str() == "b"), + "Expected an error due to conflicting keys" + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entities/create.rs b/jans-cedarling/cedarling/src/authz/entities/create.rs deleted file mode 100644 index 517cae03e69..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/create.rs +++ /dev/null @@ -1,440 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; - -use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression}; - -use super::trait_as_expression::AsExpression; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::common::cedar_schema::cedar_json::{ - CedarSchemaEntityShape, CedarSchemaRecord, CedarType, GetCedarTypeError, SchemaDefinedType, -}; -use crate::common::policy_store::ClaimMappings; -use crate::jwt::{Token, TokenClaim, TokenClaimTypeError, TokenClaims}; - -pub const CEDAR_POLICY_SEPARATOR: &str = "::"; - -/// Meta information about an entity type. -/// Is used to store in `static` variable. -#[derive(Debug)] -pub(crate) struct EntityMetadata<'a> { - pub entity_type: EntityParsedTypeName<'a>, - pub entity_id_data_key: &'a str, -} - -impl<'a> EntityMetadata<'a> { - /// create new instance of EntityMetadata. - pub fn new(entity_type: EntityParsedTypeName<'a>, entity_id_data_key: &'a str) -> Self { - Self { - entity_type, - entity_id_data_key, - } - } - - /// Create entity from token data. - // we also can create entity using the ['create_entity'] function. - pub fn create_entity( - &'a self, - schema: &'a CedarSchemaJson, - token: &Token, - parents: HashSet, - claim_mapping: &ClaimMappings, - ) -> Result { - let entity_uid = build_entity_uid( - self.entity_type.full_type_name().as_str(), - token - .get_claim(self.entity_id_data_key) - .ok_or(CreateCedarEntityError::MissingClaim( - self.entity_id_data_key.to_string(), - ))? - .as_str()?, - )?; - - create_entity( - entity_uid, - &self.entity_type, - schema, - token.claims(), - parents, - claim_mapping, - ) - } -} - -/// build [`EntityUid`] based on input parameters -pub(crate) fn build_entity_uid( - entity_type: &str, - entity_id: &str, -) -> Result { - let entity_uid = EntityUid::from_type_name_and_id( - EntityTypeName::from_str(entity_type) - .map_err(|err| CreateCedarEntityError::EntityTypeName(entity_type.to_string(), err))?, - EntityId::new(entity_id), - ); - - Ok(entity_uid) -} - -/// Parsed result of entity type name and namespace. -/// Analog to the internal cedar_policy type `InternalName` -#[derive(Debug)] -pub(crate) struct EntityParsedTypeName<'a> { - pub type_name: &'a str, - pub namespace: &'a str, -} -impl<'a> EntityParsedTypeName<'a> { - pub fn new(typename: &'a str, namespace: &'a str) -> Self { - EntityParsedTypeName { - type_name: typename, - namespace, - } - } - - pub fn full_type_name(&self) -> String { - if self.namespace.is_empty() { - self.type_name.to_string() - } else { - [self.namespace, self.type_name].join(CEDAR_POLICY_SEPARATOR) - } - } -} - -/// Parse entity type name and namespace from entity type string. -/// return (typename, namespace) -pub fn parse_namespace_and_typename(raw_entity_type: &str) -> (&str, String) { - let mut raw_path: Vec<&str> = raw_entity_type.split(CEDAR_POLICY_SEPARATOR).collect(); - let typename = raw_path.pop().unwrap_or_default(); - let namespace = raw_path.join(CEDAR_POLICY_SEPARATOR); - (typename, namespace) -} - -/// fetch the schema record for a given entity type from the cedar schema json -fn fetch_schema_record<'a>( - entity_info: &EntityParsedTypeName, - schema: &'a CedarSchemaJson, -) -> Result<&'a CedarSchemaEntityShape, CreateCedarEntityError> { - let entity_shape = schema - .entity_schema(entity_info.namespace, entity_info.type_name) - .ok_or(CreateCedarEntityError::CouldNotFindEntity( - entity_info.type_name.to_string(), - ))?; - - if let Some(entity_record) = &entity_shape.shape { - if !entity_record.is_record() { - return Err(CreateCedarEntityError::NotRecord( - entity_info.type_name.to_string(), - )); - }; - } - - Ok(entity_shape) -} - -/// get mapping of the entity attributes -fn entity_meta_attributes( - schema_record: &CedarSchemaRecord, -) -> Result, GetCedarTypeError> { - schema_record - .attributes - .iter() - .map(|(attribute_name, attribute)| { - attribute - .get_type() - .map(|attr_type| EntityAttributeMetadata { - attribute_name: attribute_name.as_str(), - cedar_policy_type: attr_type, - is_required: attribute.is_required(), - }) - }) - .collect::, _>>() -} - -/// Build attributes for the entity -fn build_entity_attributes( - schema: &CedarSchemaJson, - parsed_typename: &EntityParsedTypeName, - tkn_data: &TokenClaims, - claim_mapping: &ClaimMappings, -) -> Result, CreateCedarEntityError> { - // fetch the schema entity shape from the json-schema. - let schema_shape = fetch_schema_record(parsed_typename, schema)?; - - if let Some(schema_record) = &schema_shape.shape { - let attr_vec = entity_meta_attributes(schema_record)? - .into_iter() - .filter_map(|attr: EntityAttributeMetadata| { - let attr_name = attr.attribute_name; - let cedar_exp_result = token_attribute_to_cedar_exp( - &attr, - tkn_data, - parsed_typename, - schema, - claim_mapping, - ); - match (cedar_exp_result, attr.is_required) { - (Ok(cedar_exp), _) => Some(Ok((attr_name.to_string(), cedar_exp))), - ( - Err(CreateCedarEntityError::MissingClaim(_)), - false, - // when the attribute is not required and not found in token data we skip it - ) => None, - (Err(err), _) => Some(Err(err)), - } - }) - .collect::, CreateCedarEntityError>>()?; - Ok(HashMap::from_iter(attr_vec)) - } else { - Ok(HashMap::new()) - } -} - -/// Create entity from token payload data. -pub fn create_entity( - entity_uid: EntityUid, - parsed_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - tkn_data: &TokenClaims, - parents: HashSet, - claim_mapping: &ClaimMappings, -) -> Result { - let attrs = build_entity_attributes(schema, parsed_typename, tkn_data, claim_mapping)?; - - let entity_uid_string = entity_uid.to_string(); - cedar_policy::Entity::new(entity_uid, attrs, parents) - .map_err(|err| CreateCedarEntityError::CreateEntity(entity_uid_string, err)) -} - -/// Meta information about an attribute for cedar policy. -pub struct EntityAttributeMetadata<'a> { - // The name of the attribute in the cedar policy - // mapped one-to-one with the attribute in the token data. - pub attribute_name: &'a str, - // The type of the cedar policy attribute. - pub cedar_policy_type: CedarType, - // if this attribute is required - pub is_required: bool, -} - -/// Get the cedar policy expression value for a given type. -fn token_attribute_to_cedar_exp( - attribute_metadata: &EntityAttributeMetadata, - tkn_data: &TokenClaims, - entity_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - let token_claim_key = attribute_metadata.attribute_name; - - let token_claim_value = - tkn_data - .get_claim(token_claim_key) - .ok_or(CreateCedarEntityError::MissingClaim( - token_claim_key.to_string(), - ))?; - - get_expression( - &attribute_metadata.cedar_policy_type, - &token_claim_value, - entity_typename, - schema, - claim_mapping, - ) -} - -/// Build [`RestrictedExpression`] based on input parameters. -fn get_expression( - cedar_type: &CedarType, - claim: &TokenClaim, - base_entity_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - match cedar_type { - CedarType::String => Ok(claim.as_str()?.to_string().to_expression()), - CedarType::Long => Ok(claim.as_i64()?.to_expression()), - CedarType::Boolean => Ok(claim.as_bool()?.to_expression()), - CedarType::TypeName(cedar_typename) => { - match schema.find_type(cedar_typename, base_entity_typename.namespace) { - Some(SchemaDefinedType::Entity(_)) => { - get_entity_expression(cedar_typename, base_entity_typename, claim) - }, - Some(SchemaDefinedType::CommonType(record)) => { - let record_typename = - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace); - - get_record_expression(record, &record_typename, claim, schema, claim_mapping) - .map_err(|err| { - CreateCedarEntityError::CreateRecord( - record_typename.full_type_name(), - Box::new(err), - ) - }) - }, - None => Err(CreateCedarEntityError::FindType( - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace) - .full_type_name(), - )), - } - }, - CedarType::Set(cedar_type) => { - let vec_of_expression = claim - .as_array()? - .into_iter() - .map(|payload| { - get_expression( - cedar_type, - &payload, - base_entity_typename, - schema, - claim_mapping, - ) - }) - .collect::, _>>()?; - - Ok(RestrictedExpression::new_set(vec_of_expression)) - }, - } -} - -/// Create [`RestrictedExpression`] with entity UID as token_claim_value -fn get_entity_expression( - cedar_typename: &str, - base_entity_typename: &EntityParsedTypeName<'_>, - token_claim: &TokenClaim, -) -> Result { - let restricted_expression = { - let entity_full_type_name = - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace) - .full_type_name(); - - let uid = EntityUid::from_type_name_and_id( - EntityTypeName::from_str(entity_full_type_name.as_str()).map_err(|err| { - CreateCedarEntityError::EntityTypeName(entity_full_type_name.to_string(), err) - })?, - EntityId::new(token_claim.as_str()?), - ); - RestrictedExpression::new_entity_uid(uid) - }; - Ok(restricted_expression) -} - -/// Build [`RestrictedExpression`] based on token_claim_value. -/// It tries to find mapping and apply it to `token_claim` json value. -fn get_record_expression( - record: &CedarSchemaRecord, - cedar_record_type: &EntityParsedTypeName<'_>, - token_claim: &TokenClaim, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - // map json value of `token_claim` to TokenPayload object (HashMap) - let mapped_claim: TokenClaims = - match claim_mapping.get_mapping(token_claim.key(), &cedar_record_type.full_type_name()) { - Some(m) => m.apply_mapping(token_claim.value()).into(), - // if we do not have mapping, and value is json object, return TokenPayload based on it. - // if value is not json object, return empty value - None => { - if let Some(map) = token_claim.value().as_object() { - TokenClaims::from_json_map(map.to_owned()) - } else { - TokenClaims::default() - } - }, - }; - - let mut record_restricted_exps = Vec::new(); - - for (attribute_key, entity_attribute) in record.attributes.iter() { - let attribute_type = entity_attribute.get_type()?; - - let mapped_claim_value = - mapped_claim - .get_claim(attribute_key) - .ok_or(CreateCedarEntityError::MissingClaim( - attribute_key.to_string(), - ))?; - - let exp = get_expression( - &attribute_type, - &mapped_claim_value, - cedar_record_type, - schema, - claim_mapping, - ) - .map_err(|err| { - CreateCedarEntityError::BuildAttribute( - cedar_record_type.full_type_name(), - attribute_key.to_string(), - Box::new(err), - ) - })?; - - record_restricted_exps.push((attribute_key.to_string(), exp)); - } - - let restricted_expression = - RestrictedExpression::new_record(record_restricted_exps.into_iter()) - .map_err(CreateCedarEntityError::CreateRecordFromIter)?; - Ok(restricted_expression) -} - -/// Describe errors on creating entity -#[derive(thiserror::Error, Debug)] -pub enum CreateCedarEntityError { - /// Could not parse entity type - #[error("could not parse entity type name: {0}, error: {1}")] - EntityTypeName(String, cedar_policy::ParseErrors), - - /// Could find entity type in the `cedar-policy` schema - #[error("could find entity type: {0} in the schema")] - CouldNotFindEntity(String), - - /// Type in the schema is not record - #[error("type: {0} in the schema is not record")] - NotRecord(String), - - /// Could create entity - #[error("could create entity with uid: {0}, error: {1}")] - CreateEntity(String, cedar_policy::EntityAttrEvaluationError), - - /// Could not get attribute value from payload - #[error("could not get attribute value from payload: {0}")] - GetTokenClaim(#[from] TokenClaimTypeError), - - /// Could not retrieve attribute from cedar-policy schema - #[error("could not retrieve attribute from cedar-policy schema: {0}")] - GetCedarType(#[from] GetCedarTypeError), - - /// Error on cedar-policy type attribute - #[error("err build cedar-policy type: {0}, mapped JWT attribute `{1}`: {2}")] - BuildAttribute(String, String, Box), - - /// Error on creating `cedar-policy` record, in schema it is named as type - #[error("could not create `cedar-policy` record/type {0} : {1}")] - CreateRecord(String, Box), - - /// Wrapped error on [`RestrictedExpression::new_record`] - // this error probably newer happen - #[error("could not build expression from list of expressions: {0}")] - CreateRecordFromIter(cedar_policy::ExpressionConstructionError), - - /// Cause when cannot find record/type in json schema. - #[error("could find record/type: {0}")] - FindType(String), - - /// Error when using the transaction token. Its usage is currently not implemented. - #[error("transaction token not implemented")] - TransactionToken, - - /// Indicates that the creation of an Entity failed due to the absence of available tokens. - #[error("no available token to build the entity from")] - UnavailableToken, - - /// Missing claim - #[error("missing claim: {0}")] - MissingClaim(String), -} diff --git a/jans-cedarling/cedarling/src/authz/entities/mod.rs b/jans-cedarling/cedarling/src/authz/entities/mod.rs deleted file mode 100644 index 6d9310e791d..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/mod.rs +++ /dev/null @@ -1,278 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -//! Module for creating cedar-policy entities - -mod create; -mod trait_as_expression; -mod user; -mod workload; - -#[cfg(test)] -mod test_create; - -use std::collections::HashSet; - -use cedar_policy::{Entity, EntityUid}; -pub use create::{CEDAR_POLICY_SEPARATOR, CreateCedarEntityError}; -use create::{ - EntityMetadata, EntityParsedTypeName, build_entity_uid, create_entity, - parse_namespace_and_typename, -}; -pub use user::*; -pub use workload::*; - -use super::AuthorizeError; -use super::request::ResourceData; -use crate::AuthorizationConfig; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::common::policy_store::{ClaimMappings, PolicyStore, TokenKind}; -use crate::jwt::Token; - -const DEFAULT_ACCESS_TKN_ENTITY_TYPE_NAME: &str = "Access_token"; -const DEFAULT_ID_TKN_ENTITY_TYPE_NAME: &str = "id_token"; -const DEFAULT_USERINFO_TKN_ENTITY_TYPE_NAME: &str = "Userinfo_token"; -const DEFAULT_TKN_PRINCIPAL_IDENTIFIER: &str = "jti"; - -pub struct DecodedTokens<'a> { - pub access_token: Option>, - pub id_token: Option>, - pub userinfo_token: Option>, -} - -impl DecodedTokens<'_> { - pub fn iter(&self) -> impl Iterator { - [ - self.access_token.as_ref(), - self.id_token.as_ref(), - self.userinfo_token.as_ref(), - ] - .into_iter() - .flatten() - } -} - -pub struct TokenEntities { - pub access: Option, - pub id: Option, - pub userinfo: Option, -} - -pub fn create_token_entities( - conf: &AuthorizationConfig, - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result { - let schema = &policy_store.schema.json; - let namespace = policy_store.namespace(); - - // create access token entity - let access = if let Some(token) = tokens.access_token.as_ref() { - let type_name = conf - .mapping_access_token - .as_deref() - .unwrap_or(DEFAULT_ACCESS_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateAccessTokenEntity)?, - ) - } else { - None - }; - - // create id token entity - let id = if let Some(token) = tokens.id_token.as_ref() { - let type_name = conf - .mapping_id_token - .as_deref() - .unwrap_or(DEFAULT_ID_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateIdTokenEntity)?, - ) - } else { - None - }; - - // create userinfo token entity - let userinfo = if let Some(token) = tokens.userinfo_token.as_ref() { - let type_name = conf - .mapping_userinfo_token - .as_deref() - .unwrap_or(DEFAULT_USERINFO_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateUserinfoTokenEntity)?, - ) - } else { - None - }; - - Ok(TokenEntities { - access, - id, - userinfo, - }) -} - -fn create_token_entity( - token: &Token, - schema: &CedarSchemaJson, - namespace: &str, - type_name: &str, -) -> Result { - let claim_mapping = token.claim_mapping(); - let tkn_metadata = EntityMetadata::new( - EntityParsedTypeName { - type_name, - namespace, - }, - token - .metadata() - .principal_identifier - .as_deref() - .unwrap_or(DEFAULT_TKN_PRINCIPAL_IDENTIFIER), - ); - tkn_metadata.create_entity(schema, token, HashSet::new(), claim_mapping) -} - -/// Describe errors on creating resource entity -#[derive(thiserror::Error, Debug)] -pub enum ResourceEntityError { - #[error("could not create resource entity: {0}")] - Create(#[from] CreateCedarEntityError), -} - -/// Create entity from [`ResourceData`] -pub fn create_resource_entity( - resource: ResourceData, - schema: &CedarSchemaJson, -) -> Result { - let entity_uid = resource.entity_uid().map_err(|err| { - CreateCedarEntityError::EntityTypeName(resource.resource_type.clone(), err) - })?; - - let (typename, namespace) = parse_namespace_and_typename(&resource.resource_type); - - Ok(create_entity( - entity_uid, - &EntityParsedTypeName::new(typename, namespace.as_str()), - schema, - &resource.payload.into(), - HashSet::new(), - // we no need mapping for resource because user put json structure and it should be correct - &ClaimMappings::default(), - )?) -} - -/// Describe errors on creating role entity -#[derive(thiserror::Error, Debug)] -pub enum RoleEntityError { - #[error("could not create Jans::Role entity from {token_kind} token: {error}")] - Create { - error: CreateCedarEntityError, - token_kind: TokenKind, - }, - - /// Indicates that the creation of the Role Entity failed due to the absence of available tokens. - #[error("Role Entity creation failed: no available token to build the entity from")] - UnavailableToken, -} - -/// Create `Role` entites from based on `TrustedIssuer` role mapping for each token or default value of `RoleMapping` -pub fn create_role_entities( - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result, RoleEntityError> { - let mut role_entities = Vec::new(); - - for token in tokens.iter() { - let mut entities = extract_roles_from_token(policy_store, token)?; - role_entities.append(&mut entities); - } - - Ok(role_entities) -} - -/// Extract `Role` entites based on single `RoleMapping` -fn extract_roles_from_token( - policy_store: &PolicyStore, - token: &Token, -) -> Result, RoleEntityError> { - let parsed_typename = EntityParsedTypeName::new("Role", policy_store.namespace()); - let role_entity_type = parsed_typename.full_type_name(); - - // get payload of role id in JWT token data - let Some(payload) = token.get_claim(token.role_mapping()) else { - // if key not found we return empty vector - return Ok(Vec::new()); - }; - - // it can be 2 scenario when field is array or field is string - let entity_uid_vec: Vec = if let Ok(payload_str) = payload.as_str() { - // case if it string - let entity_uid = - build_entity_uid(role_entity_type.as_str(), payload_str).map_err(|err| { - RoleEntityError::Create { - error: err, - token_kind: token.kind, - } - })?; - vec![entity_uid] - } else { - // case if it array of string - match payload - // get as array - .as_array() - { - Ok(payload_vec) => { - payload_vec - .iter() - .map(|payload_el| { - // get each element of array as `str` - payload_el.as_str().map_err(|err| RoleEntityError::Create { - error: err.into(), - token_kind: token.kind, - }) - // build entity uid - .and_then(|name| build_entity_uid(role_entity_type.as_str(), name) - .map_err(|err| RoleEntityError::Create { - error: err, - token_kind: token.kind, - })) - }) - .collect::, _>>()? - }, - Err(err) => { - // Handle the case where the payload is neither a string nor an array - return Err(RoleEntityError::Create { - error: err.into(), - token_kind: token.kind, - }); - }, - } - }; - - let schema = &policy_store.schema.json; - - // create role entity for each entity uid - entity_uid_vec - .into_iter() - .map(|entity_uid| { - create_entity( - entity_uid, - &parsed_typename, - schema, - token.claims(), - HashSet::new(), - token.claim_mapping(), - ) - .map_err(|err| RoleEntityError::Create { - error: err, - token_kind: token.kind, - }) - }) - .collect::, _>>() -} diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create.rs b/jans-cedarling/cedarling/src/authz/entities/test_create.rs deleted file mode 100644 index 3cc5cc331ff..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create.rs +++ /dev/null @@ -1,587 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -//! Testing the creating entities - -use std::collections::HashSet; - -use test_utils::{SortedJson, assert_eq}; - -use super::create::*; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::jwt::{Token, TokenClaimTypeError, TokenClaims}; - -// test all successful cases -// with empty namespace -#[test] -fn successful_scenario_empty_namespace() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect("entity should be created"); - - let entity_json = entity.to_json_value().expect("should serialize to json"); - - let expected = serde_json::json!({ - "uid": { - "type": "Test", - "id": "test_id" - }, - "attrs": { - "entity_uid_key": { - "__entity": { - "type": "Test2", - "id": "unique_id" - } - }, - "long_key": 12345, - "string_key": "test string value", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }, - "parents": [] - }); - - assert_eq!(expected.sorted(), entity_json.sorted()); -} - -// test all successful cases -// with empty namespace -#[test] -fn successful_scenario_not_empty_namespace() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "Jans", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect("entity should be created"); - - let entity_json = entity.to_json_value().expect("should serialize to json"); - - let expected = serde_json::json!({ - "uid": { - "type": "Jans::Test", - "id": "test_id" - }, - "attrs": { - "entity_uid_key": { - "__entity": { - "type": "Jans::Test2", - "id": "unique_id" - } - }, - "long_key": 12345, - "string_key": "test string value", - "bool_key": true - }, - "parents": [] - }); - - assert_eq!(expected.sorted(), entity_json.sorted()); -} - -/// test wrong string type in token payload -#[test] -fn get_token_claim_type_string_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "string_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - // This will trigger the type error, because it's not a String. - test_key: 123, - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong long type in token payload -#[test] -fn get_token_claim_type_long_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "long_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - // This will trigger the type error, because it's not an i64. - "long_key": "str", - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong entity_uid type in token payload -#[test] -fn get_token_claim_type_entity_uid_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "entity_uid_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - // This will trigger the type error, because it's not a String. - "entity_uid_key": 123, - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong boolean type in token payload -#[test] -fn get_token_claim_type_boolean_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "bool_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - // This will trigger the type error, because it's not a bool. - "bool_key": 123, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong set type in token payload, should be array of string -#[test] -fn get_token_claim_type_set_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "set_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - "bool_key": false, - // This will trigger the type error, because it's not a array of string. - "set_key": 1, - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong set type in token payload, should be array of array of string -#[test] -fn get_token_claim_type_set_of_set_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - "bool_key": false, - "set_key": ["some_string"], - // This will trigger the type error, because it's not a array of array of string. - "set_set_key": ["some_string"] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get("set_set_key").unwrap(); - let origin_type = - TokenClaimTypeError::json_value_type_name(&json_attr_value.as_array().unwrap()[0]); - - // key set_set_key and zero element in array - let test_key = "set_set_key[0]"; - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// create entity with wrong cedar typename -#[test] -fn get_token_claim_cedar_typename_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - // Mistake in entity type name, should be `"Jans::Test"`, it will trigger error - let (typename, namespace) = parse_namespace_and_typename("Jans:::Test"); - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: &namespace, - type_name: typename, - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::EntityTypeName(typename, _) = &entity_creation_error { - assert_eq!("Jans:::Test", typename); - } else { - panic!( - "error should be CedarPolicyCreateTypeError::EntityTypeName, but got {:?}", - entity_creation_error - ); - } -} - -/// create entity with wrong cedar typename in the attribute -// The JSON schema contains an error.r: -// -// "entity_uid_key": { -// "type": "EntityOrCommon", -// "name": ":Test2" -// }, -// -// ":Test2" is not correct type definition, it will trigger error -#[test] -fn get_token_claim_cedar_typename_in_attr_error() { - let schema_json = include_str!("test_create_data/type_error_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "Jans", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::FindType(typename) = &entity_creation_error { - assert_eq!("Jans:::Test2", typename); - } else { - panic!( - "error should be CedarPolicyCreateTypeError::EntityTypeName, but got {:?}", - entity_creation_error - ); - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema b/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema deleted file mode 100644 index ea5cd8c527a..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema +++ /dev/null @@ -1,26 +0,0 @@ -entity Test = { - string_key: String, - long_key: Long, - bool_key: Boolean, - entity_uid_key: Test2, - optional_key?: String, - set_key: Set, - set_set_key: Set>, -}; - -entity Test2 = { -}; - - -namespace Jans{ - entity Test = { - string_key: String, - long_key: Long, - bool_key: Boolean, - entity_uid_key: Test2, - optional_key?: String - }; - - entity Test2 = { - }; -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json b/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json deleted file mode 100644 index 794637d08ee..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "Jans": { - "entityTypes": { - "Test2": {}, - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": "Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - } - }, - "actions": {} - }, - "": { - "entityTypes": { - "Test2": {}, - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": "Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "set_key": { - "type": "Set", - "element": { - "type": "EntityOrCommon", - "name": "String" - } - }, - "set_set_key": { - "type": "Set", - "element": { - "type": "Set", - "element": { - "type": "EntityOrCommon", - "name": "String" - } - } - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - } - }, - "actions": {} - } -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json b/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json deleted file mode 100644 index 7ee0fa03dbe..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "Jans": { - "entityTypes": { - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": ":Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - }, - "Test2": {} - }, - "actions": {} - } -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs b/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs deleted file mode 100644 index cd66476a33a..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs +++ /dev/null @@ -1,29 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use cedar_policy::RestrictedExpression; - -/// Trait to cast type to [`RestrictedExpression`] -pub(crate) trait AsExpression { - fn to_expression(self) -> RestrictedExpression; -} - -impl AsExpression for i64 { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_long(self) - } -} - -impl AsExpression for String { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_string(self) - } -} - -impl AsExpression for bool { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_bool(self) - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/user.rs b/jans-cedarling/cedarling/src/authz/entities/user.rs deleted file mode 100644 index fd50773bad9..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/user.rs +++ /dev/null @@ -1,257 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::HashSet; -use std::fmt; - -use cedar_policy::EntityUid; - -use super::{CreateCedarEntityError, DecodedTokens, EntityMetadata, EntityParsedTypeName}; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::common::policy_store::{PolicyStore, TokenKind}; -use crate::jwt::Token; - -/// Create user entity -pub fn create_user_entity( - entity_mapping: Option<&str>, - policy_store: &PolicyStore, - tokens: &DecodedTokens, - parents: HashSet, -) -> Result { - let schema: &CedarSchemaJson = &policy_store.schema.json; - let namespace = policy_store.namespace(); - let mut errors = Vec::new(); - - // helper closure to attempt entity creation from a token - let try_create_entity = |token_kind: TokenKind, token: Option<&Token>| { - if let Some(token) = token { - let claim_mapping = token.claim_mapping(); - let user_mapping = token.user_mapping(); - let entity_metadata = EntityMetadata::new( - EntityParsedTypeName { - type_name: entity_mapping.unwrap_or("User"), - namespace, - }, - user_mapping, - ); - entity_metadata - .create_entity(schema, token, parents.clone(), claim_mapping) - .map_err(|e| (token_kind, e)) - } else { - Err((token_kind, CreateCedarEntityError::UnavailableToken)) - } - }; - - // attempt entity creation for each token type that contains user info - for (token_kind, token) in [ - (TokenKind::Userinfo, tokens.userinfo_token.as_ref()), - (TokenKind::Id, tokens.id_token.as_ref()), - ] { - match try_create_entity(token_kind, token) { - Ok(entity) => return Ok(entity), - Err(e) => errors.push(e), - } - } - - Err(CreateUserEntityError { errors }) -} - -#[derive(Debug, thiserror::Error)] -pub struct CreateUserEntityError { - pub errors: Vec<(TokenKind, CreateCedarEntityError)>, -} - -impl fmt::Display for CreateUserEntityError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.errors.is_empty() { - writeln!( - f, - "Failed to create User Entity since no tokens were provided" - )?; - } else { - writeln!( - f, - "Failed to create User Entity due to the following errors:" - )?; - for (token_kind, error) in &self.errors { - writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; - use std::path::Path; - - use cedar_policy::{Entity, RestrictedExpression}; - use serde_json::json; - use test_utils::assert_eq; - use tokio::test; - - use super::create_user_entity; - use crate::authz::entities::DecodedTokens; - use crate::common::policy_store::TokenKind; - use crate::init::policy_store::load_policy_store; - use crate::jwt::Token; - use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; - - #[test] - async fn can_create_from_id_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: Some(Token::new_id( - HashMap::from([ - ("sub".to_string(), json!("user-1")), - ("country".to_string(), json!("US")), - ]) - .into(), - None, - )), - userinfo_token: None, - }; - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect("expected to create user entity"); - assert_eq!( - result, - Entity::new( - "Jans::User::\"user-1\"" - .parse() - .expect("expected to create user UID"), - HashMap::from([( - "country".to_string(), - RestrictedExpression::new_string("US".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected user entity") - ) - } - - #[test] - async fn can_create_from_userinfo_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - id_token: None, - access_token: None, - userinfo_token: Some(Token::new_userinfo( - HashMap::from([ - ("sub".to_string(), json!("user-1")), - ("country".to_string(), json!("US")), - ]) - .into(), - None, - )), - }; - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect("expected to create user entity"); - assert_eq!( - result, - Entity::new( - "Jans::User::\"user-1\"" - .parse() - .expect("expected to create user UID"), - HashMap::from([( - "country".to_string(), - RestrictedExpression::new_string("US".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected user entity") - ) - } - - #[test] - async fn errors_when_tokens_have_missing_claims() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access(HashMap::from([]).into(), None)), - id_token: Some(Token::new_id(HashMap::from([]).into(), None)), - userinfo_token: Some(Token::new_userinfo(HashMap::from([]).into(), None)), - }; - - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect_err("expected to error while creating user entity"); - - for (tkn_kind, err) in result.errors.iter() { - match tkn_kind { - TokenKind::Access => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Id => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Userinfo => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Transaction => (), // we don't support these yet - } - } - } - - #[test] - async fn errors_when_tokens_unavailable() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: None, - userinfo_token: None, - }; - - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect_err("expected to error while creating user entity"); - - assert_eq!(result.errors.len(), 2); - for (_tkn_kind, err) in result.errors.iter() { - assert!( - matches!(err, CreateCedarEntityError::UnavailableToken), - "expected error UnavailableToken, got: {:?}", - err - ); - } - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/workload.rs b/jans-cedarling/cedarling/src/authz/entities/workload.rs deleted file mode 100644 index d10509c08d7..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/workload.rs +++ /dev/null @@ -1,249 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::HashSet; -use std::fmt; - -use super::{CreateCedarEntityError, DecodedTokens, EntityMetadata, EntityParsedTypeName}; -use crate::common::policy_store::{PolicyStore, TokenKind}; -use crate::jwt::Token; - -/// Create workload entity -pub fn create_workload_entity( - entity_mapping: Option<&str>, - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result { - let namespace = policy_store.namespace(); - let schema = &policy_store.schema.json; - let mut errors = Vec::new(); - - // helper closure to attempt entity creation from a token - let try_create_entity = |token_kind: TokenKind, token: Option<&Token>, key: &str| { - if let Some(token) = token { - let claim_mapping = token.claim_mapping(); - let entity_metadta = EntityMetadata::new( - EntityParsedTypeName { - type_name: entity_mapping.unwrap_or("Workload"), - namespace, - }, - key, - ); - entity_metadta - .create_entity(schema, token, HashSet::new(), claim_mapping) - .map_err(|e| (token_kind, e)) - } else { - Err((token_kind, CreateCedarEntityError::UnavailableToken)) - } - }; - - // attempt entity creation for each token type - for (token_kind, token, key) in [ - (TokenKind::Access, tokens.access_token.as_ref(), "client_id"), - (TokenKind::Id, tokens.id_token.as_ref(), "aud"), - ] { - match try_create_entity(token_kind, token, key) { - Ok(entity) => return Ok(entity), - Err(e) => errors.push(e), - } - } - - Err(CreateWorkloadEntityError { errors }) -} - -#[derive(Debug, thiserror::Error)] -pub struct CreateWorkloadEntityError { - pub errors: Vec<(TokenKind, CreateCedarEntityError)>, -} - -impl fmt::Display for CreateWorkloadEntityError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.errors.is_empty() { - writeln!( - f, - "Failed to create Workload Entity since no tokens were provided" - )?; - } else { - writeln!( - f, - "Failed to create Workload Entity due to the following errors:" - )?; - for (token_kind, error) in &self.errors { - writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; - use std::path::Path; - - use cedar_policy::{Entity, RestrictedExpression}; - use serde_json::json; - use test_utils::assert_eq; - use tokio::test; - - use super::create_workload_entity; - use crate::authz::entities::DecodedTokens; - use crate::common::policy_store::TokenKind; - use crate::init::policy_store::load_policy_store; - use crate::jwt::Token; - use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; - - #[test] - async fn can_create_from_id_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: Some(Token::new_id( - HashMap::from([ - ("aud".to_string(), json!("workload-1")), - ("org_id".to_string(), json!("some-org-123")), - ]) - .into(), - None, - )), - userinfo_token: None, - }; - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect("expected to create workload entity"); - assert_eq!( - result, - Entity::new( - "Jans::Workload::\"workload-1\"" - .parse() - .expect("expected to create workload UID"), - HashMap::from([( - "org_id".to_string(), - RestrictedExpression::new_string("some-org-123".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected workload entity") - ) - } - - #[test] - async fn can_create_from_access_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access( - HashMap::from([ - ("client_id".to_string(), json!("workload-1")), - ("org_id".to_string(), json!("some-org-123")), - ]) - .into(), - None, - )), - id_token: None, - userinfo_token: None, - }; - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect("expected to create workload entity"); - assert_eq!( - result, - Entity::new( - "Jans::Workload::\"workload-1\"" - .parse() - .expect("expected to create workload UID"), - HashMap::from([( - "org_id".to_string(), - RestrictedExpression::new_string("some-org-123".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected workload entity") - ) - } - - #[test] - async fn errors_when_tokens_have_missing_claims() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access(HashMap::from([]).into(), None)), - id_token: Some(Token::new_id(HashMap::from([]).into(), None)), - userinfo_token: Some(Token::new_userinfo(HashMap::from([]).into(), None)), - }; - - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect_err("expected to error while creating workload entity"); - - for (tkn_kind, err) in result.errors.iter() { - match tkn_kind { - TokenKind::Access => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "client_id"), - "expected error MissingClaim(\"client_id\")" - ), - TokenKind::Id => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "aud"), - "expected error MissingClaim(\"aud\")" - ), - _ => (), // we don't create workload tokens using other tokens - } - } - } - - #[test] - async fn errors_when_tokens_unavailable() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - // we can only create the workload from the access_token and id_token - let tokens = DecodedTokens { - access_token: None, - id_token: None, - userinfo_token: None, - }; - - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect_err("expected to error while creating workload entity"); - - assert_eq!(result.errors.len(), 2); - for (_tkn_kind, err) in result.errors.iter() { - assert!( - matches!(err, CreateCedarEntityError::UnavailableToken), - "expected error UnavailableToken, got: {:?}", - err - ); - } - } -} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder.rs b/jans-cedarling/cedarling/src/authz/entity_builder.rs new file mode 100644 index 00000000000..4e3d948a8a8 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder.rs @@ -0,0 +1,471 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +mod build_attrs; +mod build_expr; +mod build_resource_entity; +mod build_role_entity; +mod build_token_entities; +mod build_user_entity; +mod build_workload_entity; +mod mapping; + +use crate::common::cedar_schema::CEDAR_NAMESPACE_SEPARATOR; +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::policy_store::TokenKind; +use crate::jwt::{Token, TokenClaimTypeError}; +use crate::{AuthorizationConfig, ResourceData}; +use build_attrs::{BuildAttrError, ClaimAliasMap, build_entity_attrs_from_tkn}; +use build_expr::*; +use build_resource_entity::{BuildResourceEntityError, JsonTypeError}; +use build_role_entity::BuildRoleEntityError; +pub use build_token_entities::BuildTokenEntityError; +use build_user_entity::BuildUserEntityError; +use build_workload_entity::BuildWorkloadEntityError; +use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid}; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::convert::Infallible; +use std::fmt; +use std::str::FromStr; + +use super::AuthorizeEntitiesData; + +const DEFAULT_WORKLOAD_ENTITY_NAME: &str = "Workload"; +const DEFAULT_USER_ENTITY_NAME: &str = "User"; +const DEFAULT_ACCESS_TKN_ENTITY_NAME: &str = "Access_token"; +const DEFAULT_ID_TKN_ENTITY_NAME: &str = "id_token"; +const DEFAULT_USERINFO_TKN_ENTITY_NAME: &str = "Userinfo_token"; +const DEFAULT_ROLE_ENTITY_NAME: &str = "Role"; + +pub struct DecodedTokens<'a> { + pub access: Option>, + pub id: Option>, + pub userinfo: Option>, +} + +/// The names of the entities in the schema +pub struct EntityNames { + user: String, + workload: String, + id_token: String, + access_token: String, + userinfo_token: String, + role: String, +} + +impl From<&AuthorizationConfig> for EntityNames { + fn from(config: &AuthorizationConfig) -> Self { + Self { + user: config + .mapping_user + .clone() + .unwrap_or_else(|| DEFAULT_USER_ENTITY_NAME.to_string()), + workload: config + .mapping_workload + .clone() + .unwrap_or_else(|| DEFAULT_WORKLOAD_ENTITY_NAME.to_string()), + id_token: config + .mapping_id_token + .clone() + .unwrap_or_else(|| DEFAULT_ID_TKN_ENTITY_NAME.to_string()), + access_token: config + .mapping_access_token + .clone() + .unwrap_or_else(|| DEFAULT_ACCESS_TKN_ENTITY_NAME.to_string()), + userinfo_token: config + .mapping_userinfo_token + .clone() + .unwrap_or_else(|| DEFAULT_USERINFO_TKN_ENTITY_NAME.to_string()), + // TODO: implement a bootstrap property to set the Role entity name + role: DEFAULT_ROLE_ENTITY_NAME.to_string(), + } + } +} + +impl Default for EntityNames { + fn default() -> Self { + Self { + user: DEFAULT_USER_ENTITY_NAME.to_string(), + workload: DEFAULT_WORKLOAD_ENTITY_NAME.to_string(), + id_token: DEFAULT_ID_TKN_ENTITY_NAME.to_string(), + access_token: DEFAULT_ACCESS_TKN_ENTITY_NAME.to_string(), + userinfo_token: DEFAULT_USERINFO_TKN_ENTITY_NAME.to_string(), + role: DEFAULT_ROLE_ENTITY_NAME.to_string(), + } + } +} + +pub struct EntityBuilder { + schema: CedarSchemaJson, + entity_names: EntityNames, + build_workload: bool, + build_user: bool, +} + +impl EntityBuilder { + pub fn new( + schema: CedarSchemaJson, + entity_names: EntityNames, + build_workload: bool, + build_user: bool, + ) -> Self { + Self { + schema, + entity_names, + build_workload, + build_user, + } + } + + pub fn build_entities( + &self, + tokens: &DecodedTokens, + resource: &ResourceData, + ) -> Result { + let workload = if self.build_workload { + Some(self.build_workload_entity(tokens)?) + } else { + None + }; + + let (user, roles) = if self.build_user { + let roles = self.try_build_role_entities(tokens)?; + let parents = roles + .iter() + .map(|role| role.uid()) + .collect::>(); + (Some(self.build_user_entity(tokens, parents)?), roles) + } else { + (None, vec![]) + }; + + let access_token = if let Some(token) = tokens.access.as_ref() { + Some( + self.build_access_tkn_entity(token) + .map_err(BuildCedarlingEntityError::AccessToken)?, + ) + } else { + None + }; + + let id_token = if let Some(token) = tokens.id.as_ref() { + Some( + self.build_id_tkn_entity(token) + .map_err(BuildCedarlingEntityError::IdToken)?, + ) + } else { + None + }; + + let userinfo_token = if let Some(token) = tokens.userinfo.as_ref() { + Some( + self.build_userinfo_tkn_entity(token) + .map_err(BuildCedarlingEntityError::UserinfoToken)?, + ) + } else { + None + }; + + let resource = self.build_resource_entity(resource)?; + + Ok(AuthorizeEntitiesData { + workload, + user, + access_token, + id_token, + userinfo_token, + resource, + roles, + }) + } +} + +/// Builds a Cedar Entity using a JWT +fn build_entity( + schema: &CedarSchemaJson, + entity_name: &str, + token: &Token, + id_src_claim: &str, + claim_aliases: Vec, + parents: HashSet, +) -> Result { + // Get entity Id from the specified token claim + let entity_id = token + .get_claim(id_src_claim) + .ok_or(BuildEntityError::MissingClaim(id_src_claim.to_string()))? + .as_str()? + .to_owned(); + + // Get entity namespace and type + let mut entity_name = entity_name.to_string(); + let (namespace, entity_type) = schema + .get_entity_from_base_name(&entity_name) + .ok_or(BuildEntityError::EntityNotInSchema(entity_name.to_string()))?; + if !namespace.is_empty() { + entity_name = [namespace.as_str(), &entity_name].join(CEDAR_NAMESPACE_SEPARATOR); + } + + // Build entity attributes + let entity_attrs = build_entity_attrs_from_tkn(schema, entity_type, token, claim_aliases) + .map_err(BuildEntityError::BuildAttribute)?; + + // Build cedar entity + let entity_type_name = + EntityTypeName::from_str(&entity_name).map_err(BuildEntityError::ParseEntityTypeName)?; + let entity_id = EntityId::from_str(&entity_id).map_err(BuildEntityError::ParseEntityId)?; + let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); + Ok(Entity::new(entity_uid, entity_attrs, parents)?) +} + +/// Errors encountered when building a Cedarling-specific entity +#[derive(Debug, thiserror::Error)] +pub enum BuildCedarlingEntityError { + #[error(transparent)] + Workload(#[from] BuildWorkloadEntityError), + #[error(transparent)] + User(#[from] BuildUserEntityError), + #[error(transparent)] + Role(#[from] BuildRoleEntityError), + #[error("failed to build resource entity: {0}")] + Resource(#[from] BuildResourceEntityError), + #[error("error while building Access Token entity: {0}")] + AccessToken(#[source] BuildTokenEntityError), + #[error("error while building Id Token entity: {0}")] + IdToken(#[source] BuildTokenEntityError), + #[error("error while building Userinfo Token entity: {0}")] + UserinfoToken(#[source] BuildTokenEntityError), +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildEntityError { + #[error("failed to parse entity type name: {0}")] + ParseEntityTypeName(#[source] cedar_policy::ParseErrors), + #[error("failed to parse entity id: {0}")] + ParseEntityId(#[source] Infallible), + #[error("failed to evaluate entity or tag: {0}")] + AttrEvaluation(#[from] cedar_policy::EntityAttrEvaluationError), + #[error("failed to build entity since a token was not provided")] + TokenUnavailable, + #[error("the given token is missing a `{0}` claim")] + MissingClaim(String), + #[error(transparent)] + TokenClaimTypeMismatch(#[from] TokenClaimTypeError), + #[error(transparent)] + JsonTypeError(#[from] JsonTypeError), + #[error("the entity `{0}` is not defined in the schema")] + EntityNotInSchema(String), + #[error(transparent)] + BuildAttribute(#[from] BuildAttrError), + #[error("got {0} token, expected: {1}")] + InvalidToken(TokenKind, TokenKind), +} + +impl BuildEntityError { + pub fn json_type_err(expected_type_name: &str, got_value: &Value) -> Self { + Self::JsonTypeError(JsonTypeError::type_mismatch(expected_type_name, got_value)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::{cedar_schema::cedar_json::CedarSchemaJson, policy_store::TrustedIssuer}, + jwt::{Token, TokenClaims}, + }; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_entity_using_jwt() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let entity = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect("should successfully build entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn errors_on_invalid_entity_type_name() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload!": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + + let err = build_entity( + &schema, + "Workload!", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error while parsing entity type name"); + + assert!( + matches!(err, BuildEntityError::ParseEntityTypeName(_)), + "expected ParseEntityTypeName error but got: {:?}", + err + ); + } + + #[test] + fn errors_when_token_is_missing_entity_id_claim() { + let schema = serde_json::from_value::(json!({})) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error while parsing entity type name"); + + assert!( + matches!( + err, + BuildEntityError::MissingClaim(ref claim_name) + if claim_name =="client_id" + ), + "expected MissingClaim error but got: {}", + err + ); + } + + #[test] + fn errors_token_claim_has_unexpected_type() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([("client_id".to_string(), json!(123))])), + Some(&iss), + ); + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error due to unexpected json type"); + + assert!( + matches!( + err, + BuildEntityError::TokenClaimTypeMismatch(ref err) + if err == &TokenClaimTypeError::type_mismatch("client_id", "String", &json!(123)) + ), + "expected TokenClaimTypeMismatch error but got: {:?}", + err + ); + } + + #[test] + fn errors_when_entity_not_in_schema() { + let schema = serde_json::from_value::(json!({})) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("client-123"), + )])), + Some(&iss), + ); + + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error due to entity not being in the schema"); + assert!( + matches!( + err, + BuildEntityError::EntityNotInSchema(ref type_name) + if type_name == "Workload" + ), + "expected EntityNotInSchema error but got: {:?}", + err + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs new file mode 100644 index 00000000000..e56471cdbb8 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs @@ -0,0 +1,318 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use crate::{common::cedar_schema::cedar_json::entity_type::EntityType, jwt::Token}; +use cedar_policy::RestrictedExpression; +use serde_json::Value; +use std::collections::HashMap; + +/// Builds Cedar entity attributes using a JWT. +/// +/// This uses claim mapping metadata to unwrap claims into their respective Cedar types +pub fn build_entity_attrs_from_tkn( + schema: &CedarSchemaJson, + entity_type: &EntityType, + token: &Token, + claim_aliases: Vec, +) -> Result, BuildAttrError> { + let mut entity_attrs = HashMap::new(); + + let shape = match entity_type.shape.as_ref() { + Some(shape) => shape, + None => return Ok(entity_attrs), + }; + + let mut claims = token.claims_value().clone(); + apply_claim_aliases(&mut claims, claim_aliases); + + for (attr_name, attr) in shape.attrs.iter() { + let expression = if let Some(mapping) = token.claim_mapping().get(attr_name) { + let claim = claims.get(attr_name).ok_or_else(|| { + BuildAttrError::new( + attr_name, + BuildAttrErrorKind::MissingSource(attr_name.to_string()), + ) + })?; + let mapped_claim = mapping.apply_mapping(claim); + attr.build_expr(&mapped_claim, attr_name, schema) + .map_err(|err| BuildAttrError::new(attr_name, err.into()))? + } else { + match attr.build_expr(&claims, attr_name, schema) { + Ok(expr) => expr, + Err(err) if attr.is_required() => Err(BuildAttrError::new(attr_name, err.into()))?, + // silently fail when attribute isn't required + Err(_) => continue, + } + }; + + if let Some(expr) = expression { + entity_attrs.insert(attr_name.to_string(), expr); + } + } + + Ok(entity_attrs) +} + +pub fn build_entity_attrs_from_values( + schema: &CedarSchemaJson, + entity_type: &EntityType, + src: &HashMap, +) -> Result, BuildAttrError> { + let mut entity_attrs = HashMap::new(); + + let shape = match entity_type.shape.as_ref() { + Some(shape) => shape, + None => return Ok(entity_attrs), + }; + + for (attr_name, attr) in shape.attrs.iter() { + let val = match src.get(attr_name) { + Some(val) => val, + None if attr.is_required() => { + return Err(BuildAttrError::new( + attr_name, + BuildAttrErrorKind::MissingSource(attr_name.to_string()), + )); + }, + _ => continue, + }; + + let mapped_src = serde_json::from_value::>(val.clone()); + let src = if let Ok(mapped_src) = mapped_src.as_ref() { + mapped_src + } else { + src + }; + + let expression = match attr.build_expr(src, attr_name, schema) { + Ok(expr) => expr, + Err(err) if attr.is_required() => { + return Err(BuildAttrError::new(attr_name, err.into()))?; + }, + // move on to the next attribute if this isn't required + Err(_) => continue, + }; + + if let Some(expr) = expression { + entity_attrs.insert(attr_name.to_string(), expr); + } + } + + Ok(entity_attrs) +} + +/// Describes how to rename a claim named `from` to `to` +pub struct ClaimAliasMap<'a> { + from: &'a str, + to: &'a str, +} + +impl<'a> ClaimAliasMap<'a> { + pub fn new(from: &'a str, to: &'a str) -> Self { + Self { from, to } + } +} + +fn apply_claim_aliases(claims: &mut HashMap, aliases: Vec) { + for map in aliases { + if let Some(claim) = claims.get(map.from) { + claims.insert(map.to.to_string(), claim.clone()); + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to build `{attr_name}` attribute: {source}")] +pub struct BuildAttrError { + attr_name: String, + #[source] + source: BuildAttrErrorKind, +} + +impl BuildAttrError { + fn new(name: impl ToString, src: BuildAttrErrorKind) -> Self { + Self { + attr_name: name.to_string(), + source: src, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildAttrErrorKind { + #[error("missing attribute source: `{0}`")] + MissingSource(String), + #[error("failed to build restricted expression: {0}")] + BuildExpression(#[from] BuildExprError), +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::{ + cedar_schema::cedar_json::{ + attribute::Attribute, + entity_type::{EntityShape, EntityType}, + }, + policy_store::TrustedIssuer, + }, + jwt::TokenClaims, + }; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_entity_attrs_from_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("workload-123"), + )])), + Some(&iss), + ); + + let attrs = build_entity_attrs_from_tkn(&schema, &entity_type, &token, Vec::new()) + .expect("should build entity attrs"); + // RestrictedExpression does not implement PartialEq so the best we can do is check + // if the attribute was created + assert!( + attrs.contains_key("client_id"), + "there should be a `client_id` attribute" + ); + } + + #[test] + fn errors_when_tkn_missing_src() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let iss = TrustedIssuer::default(); + let token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + + let err = build_entity_attrs_from_tkn(&schema, &entity_type, &token, Vec::new()) + .expect_err("should error due to missing source"); + assert!( + matches!( + err, + BuildAttrError { + attr_name: ref name, + source: BuildAttrErrorKind::BuildExpression(BuildExprError::MissingSource(ref src_name))} + if name == "client_id" && + src_name == "client_id" + ), + "expected MissingSource error but got: {:?}", + err, + ); + } + + #[test] + fn can_build_entity_attrs_from_value() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let src_values = HashMap::from([("client_id".to_string(), json!("workload-123"))]); + + let attrs = build_entity_attrs_from_values(&schema, &entity_type, &src_values) + .expect("should build entity attrs"); + // RestrictedExpression does not implement PartialEq so the best we can do is check + // if the attribute was created + assert!( + attrs.contains_key("client_id"), + "there should be a `client_id` attribute" + ); + } + + #[test] + fn errors_when_values_missing_src() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let src_values = HashMap::new(); + + let err = build_entity_attrs_from_values(&schema, &entity_type, &src_values) + .expect_err("should error due to missing source"); + assert!( + matches!( + err, + BuildAttrError{ + attr_name: ref name, + source: BuildAttrErrorKind::MissingSource(ref src_name)} + if name == "client_id" && + src_name == "client_id"), + "expected MissingSource error but got: {:?}", + err, + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs new file mode 100644 index 00000000000..2f8c67049db --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs @@ -0,0 +1,586 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::cedar_schema::cedar_json::attribute::Attribute; +use cedar_policy::{ + EntityId, EntityTypeName, EntityUid, ExpressionConstructionError, ParseErrors, + RestrictedExpression, +}; +use serde_json::Value; +use std::collections::HashMap; +use std::str::FromStr; + +use super::CEDAR_NAMESPACE_SEPARATOR; + +impl Attribute { + pub fn kind_str(&self) -> &str { + match self { + Attribute::String { .. } => "String", + Attribute::Long { .. } => "Long", + Attribute::Boolean { .. } => "Boolean", + Attribute::Record { .. } => "Record", + Attribute::Set { .. } => "Set", + Attribute::Entity { .. } => "Entity", + Attribute::Extension { .. } => "Extension", + Attribute::EntityOrCommon { .. } => "EntityOrCommon", + } + } + + /// Builds a [`RestrictedExpression`] while checking the schema + pub fn build_expr( + &self, + attr_src: &HashMap, + src_key: &str, + schema: &CedarSchemaJson, + ) -> Result, BuildExprError> { + match self { + // Handle String attributes + Attribute::String { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))? + .to_string(); + Ok(Some(RestrictedExpression::new_string(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Long attributes + Attribute::Long { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_i64() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "number", claim))?; + Ok(Some(RestrictedExpression::new_long(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Boolean attributes + Attribute::Boolean { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_bool() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "bool", claim))?; + Ok(Some(RestrictedExpression::new_bool(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Record attributes + Attribute::Record { attrs, required } => { + let mut fields = HashMap::new(); + for (name, kind) in attrs.iter() { + if let Some(expr) = kind.build_expr(attr_src, name, schema)? { + fields.insert(name.to_string(), expr); + } + } + + if fields.is_empty() && !required { + Ok(None) + } else { + Ok(Some(RestrictedExpression::new_record(fields)?)) + } + }, + + // Handle Set attributes + Attribute::Set { required, element } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_array() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "array", claim))?; + + let mut values = Vec::new(); + for (i, val) in claim.iter().enumerate() { + let claim_name = i.to_string(); + if let Some(expr) = element.build_expr( + &HashMap::from([(claim_name.clone(), val.clone())]), + &claim_name, + schema, + )? { + values.push(expr); + } + } + Ok(Some(RestrictedExpression::new_set(values))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Entity attributes + Attribute::Entity { required, name } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))?; + + let mut name = name.to_string(); + if let Some((namespace, _)) = schema.get_entity_from_base_name(&name) { + if !namespace.is_empty() { + name = [namespace, name.as_str()].join(CEDAR_NAMESPACE_SEPARATOR); + } + } else if *required { + return Err(BuildExprError::EntityNotInSchema(name.to_string())); + } else { + return Ok(None); + } + + let type_name = EntityTypeName::from_str(&name) + .map_err(|e| BuildExprError::ParseEntityTypeName(name, e))?; + let type_id = EntityId::new(claim); + let uid = EntityUid::from_type_name_and_id(type_name, type_id); + Ok(Some(RestrictedExpression::new_entity_uid(uid))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Extension attributes + Attribute::Extension { required, name } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))?; + let expr = match name.as_str() { + "ipaddr" => RestrictedExpression::new_ip(claim), + "decimal" => RestrictedExpression::new_decimal(claim), + name => RestrictedExpression::new_unknown(name), + }; + Ok(Some(expr)) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle EntityOrCommon attributes + Attribute::EntityOrCommon { required, name } => { + if let Some((_namespace_name, attr)) = schema.get_common_type(name) { + attr.build_expr(attr_src, src_key, schema) + } else if schema.get_entity_from_base_name(name).is_some() { + let attr = Attribute::Entity { + required: *required, + name: name.to_string(), + }; + attr.build_expr(attr_src, src_key, schema) + } else if let Some(attr) = str_to_primitive_type(*required, name) { + attr.build_expr(attr_src, src_key, schema) + } else if *required { + Err(BuildExprError::UnkownType(name.to_string())) + } else { + Ok(None) + } + }, + } + } +} + +fn str_to_primitive_type(required: bool, name: &str) -> Option { + let primitive_type = match name { + "String" => Attribute::String { required }, + "Long" => Attribute::Long { required }, + "Boolean" => Attribute::Boolean { required }, + _ => return None, + }; + Some(primitive_type) +} + +/// Errors when building a [`RestrictedExpression`] +#[derive(Debug, thiserror::Error)] +pub enum BuildExprError { + #[error("the given attribute source data is missing the key: {0}")] + MissingSource(String), + #[error(transparent)] + TypeMismatch(#[from] KeyedJsonTypeError), + #[error(transparent)] + ConstructionError(#[from] ExpressionConstructionError), + #[error("the type of `{0}` could not be determined")] + UnkownType(String), + #[error("the entity type `{0}` is not in the schema")] + EntityNotInSchema(String), + #[error("failed to parse entity type name \"{0}\": {1}")] + ParseEntityTypeName(String, ParseErrors), +} + +#[derive(Debug, thiserror::Error)] +#[error("type mismatch for key '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] +pub struct KeyedJsonTypeError { + pub key: String, + pub expected_type: String, + pub actual_type: String, +} + +impl KeyedJsonTypeError { + /// Returns the JSON type name of the given value. + pub fn value_type_name(value: &Value) -> &'static str { + match value { + Value::Null => "null", + Value::Bool(_) => "bool", + Value::Number(_) => "number", + Value::String(_) => "string", + Value::Array(_) => "array", + Value::Object(_) => "object", + } + } + + /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. + pub fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { + let got_value_type_name = Self::value_type_name(got_value).to_string(); + + Self { + key: key.to_string(), + expected_type: expected_type_name.to_string(), + actual_type: got_value_type_name, + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + authz::entity_builder::BuildExprError, + common::cedar_schema::cedar_json::{CedarSchemaJson, attribute::Attribute}, + }; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_string_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("attr-val"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_long_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Long" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::long(); + let src = HashMap::from([("src_key".to_string(), json!(123))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_boolean_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Boolean" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::boolean(); + let src = HashMap::from([("src_key".to_string(), json!(true))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_record_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "outer_attr": { + "type": "Record", + "attributes": { + "inner_attr": { "type": "String" } + }, + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::record(HashMap::from([( + "inner_attr".to_string(), + Attribute::string(), + )])); + let src = HashMap::from([("inner_attr".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_set_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Set", + "element": { + "type": "String", + } + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::set(Attribute::string()); + let src = HashMap::from([("src_key".to_string(), json!(["admin", "user"]))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn errors_when_expected_set_has_different_types() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Set", + "element": { + "type": "String", + } + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::set(Attribute::string()); + let src = HashMap::from([("src_key".to_string(), json!(["admin", 123]))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!(err, BuildExprError::TypeMismatch(_)), + "should error due to type mismatch but got: {:?}", + err + ); + } + + #[test] + fn can_build_entity_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { + "OtherEntity": {}, + "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Entity", + "name": "OtherEntity", + }, + }, + } + } + }} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built"); + } + + #[test] + fn can_build_entity_expr_from_entity_or_common() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { + "OtherEntity": {}, + "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "EntityOrCommon", + "name": "OtherEntity", + }, + }, + } + } + }} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built"); + } + + #[test] + fn errors_when_entity_isnt_in_schema() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Entity", + "name": "OtherEntity", + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!( + err, + BuildExprError::EntityNotInSchema(ref entity_name) + if entity_name == "OtherEntity" + ), + "should error due to type mismatch but got: {:?}", + err + ); + } + + #[test] + fn can_build_ip_addr_extension_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Extension", "name": "ipaddr" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("0.0.0.0"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_decimal_extension_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Extension", "name": "decimal" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("1.1"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_skip_non_required_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::String { required: false }; + let src = HashMap::new(); + let expr = attr + .build_expr(&src, "client_id", &schema) + .expect("should not error"); + assert!(expr.is_none(), "a restricted expression shouldn't built") + } + + #[test] + fn errors_on_type_mismatch() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!(123))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!(err, BuildExprError::TypeMismatch(_)), + "should error due to type mismatch but got: {:?}", + err + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs new file mode 100644 index 00000000000..6878749b1c1 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs @@ -0,0 +1,331 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use build_attrs::build_entity_attrs_from_values; +use cedar_policy::{EntityAttrEvaluationError, ExpressionConstructionError, ParseErrors}; +use serde_json::Value; + +use super::*; +use crate::ResourceData; + +impl EntityBuilder { + pub fn build_resource_entity( + &self, + resource: &ResourceData, + ) -> Result { + let entity_type_name = EntityTypeName::from_str(&resource.resource_type)?; + let (_namespace_name, entity_type) = self + .schema + .get_entity_from_base_name(entity_type_name.basename()) + .ok_or(BuildEntityError::EntityNotInSchema( + entity_type_name.to_string(), + ))?; + + let entity_attrs = + build_entity_attrs_from_values(&self.schema, entity_type, &resource.payload)?; + + // Build cedar entity + let entity_id = + EntityId::from_str(&resource.id).map_err(BuildEntityError::ParseEntityId)?; + let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); + Ok(Entity::new(entity_uid, entity_attrs, HashSet::new())?) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildResourceEntityError { + #[error(transparent)] + BuildEntity(#[from] BuildEntityError), + #[error(transparent)] + TypeMismatch(#[from] JsonTypeError), + #[error(transparent)] + ExpressionConstructExpression(#[from] ExpressionConstructionError), + #[error(transparent)] + EntityAttrEvaluationError(#[from] EntityAttrEvaluationError), + #[error(transparent)] + BuildAttr(#[from] BuildAttrError), + #[error("invalid entity name: {0}")] + InvalidEntityName(#[from] ParseErrors), +} + +#[derive(Debug, thiserror::Error, PartialEq)] +#[error("JSON value type mismatch: expected '{expected_type}', but found '{actual_type}'")] +pub struct JsonTypeError { + pub expected_type: String, + pub actual_type: String, +} + +impl JsonTypeError { + /// Returns the JSON type name of the given value. + pub fn value_type_name(value: &Value) -> String { + match value { + Value::Null => "null".to_string(), + Value::Bool(_) => "bool".to_string(), + Value::Number(_) => "number".to_string(), + Value::String(_) => "string".to_string(), + Value::Array(_) => "array".to_string(), + Value::Object(_) => "object".to_string(), + } + } + + /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. + pub fn type_mismatch(expected_type_name: &str, got_value: &Value) -> Self { + let got_value_type_name = Self::value_type_name(got_value); + + Self { + expected_type: expected_type_name.to_string(), + actual_type: got_value_type_name, + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use cedar_policy::EvalResult; + use serde_json::json; + + #[test] + fn can_build_entity() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "header": { + "type": "Record", + "attributes": { + "Accept": { "type": "EntityOrCommon", "name": "String" }, + }, + }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::from([ + ("header".to_string(), json!({"Accept": "test"})), + ( + "url".to_string(), + json!({"host": "protected.host", "protocol": "http", "path": "/protected"}), + ), + ]), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + let url = entity + .attr("url") + .expect("entity must have an `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record + .get("host") + .expect("expected `url` to have a `host` attribute"), + &EvalResult::String("protected.host".to_string()) + ); + assert_eq!( + record + .get("protocol") + .expect("expected `url` to have a `domain` attribute"), + &EvalResult::String("http".to_string()) + ); + assert_eq!( + record + .get("path") + .expect("expected `url` to have a `path` attribute"), + &EvalResult::String("/protected".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + url + ); + } + + let header = entity + .attr("header") + .expect("entity must have an `header` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = header { + assert_eq!(record.len(), 1); + assert_eq!( + record + .get("Accept") + .expect("expected `url` to have an `Accept` attribute"), + &EvalResult::String("test".to_string()) + ); + } else { + panic!( + "expected the attribute `header` to be a record, got: {:?}", + header + ); + } + } + + #[test] + fn can_build_entity_with_optional_attr() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "url": { "type": "EntityOrCommon", "name": "Url", "required": false}, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::new(), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + assert!( + entity.attr("url").is_none(), + "entity should not have a `url` attribute" + ); + } + + #[test] + fn can_build_entity_with_optional_record_attr() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "header": { + "type": "Record", + "attributes": { + "Accept": { "type": "EntityOrCommon", "name": "String", "required": false }, + }, + }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::from([ + ( + "url".to_string(), + json!({"host": "protected.host", "protocol": "http", "path": "/protected"}), + ), + ("header".to_string(), json!({})), + ]), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + let url = entity + .attr("url") + .expect("entity must have an `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record + .get("host") + .expect("expected `url` to have a `host` attribute"), + &EvalResult::String("protected.host".to_string()) + ); + assert_eq!( + record + .get("protocol") + .expect("expected `url` to have a `domain` attribute"), + &EvalResult::String("http".to_string()) + ); + assert_eq!( + record + .get("path") + .expect("expected `url` to have a `path` attribute"), + &EvalResult::String("/protected".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + url + ); + } + + let header = entity + .attr("header") + .expect("entity must have an `header` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = header { + assert_eq!(record.len(), 0, "the header attribute must be empty"); + } else { + panic!( + "expected the attribute `header` to be a record, got: {:?}", + header + ); + } + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs new file mode 100644 index 00000000000..0be6ba7a284 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs @@ -0,0 +1,286 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use cedar_policy::{EntityId, EntityTypeName, EntityUid}; +use serde::Deserialize; + +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +enum UnifyClaims { + Single(String), + Multiple(Vec), +} + +impl UnifyClaims { + fn iter<'a>(&'a self) -> Box + 'a> { + match self { + Self::Single(ref v) => Box::new(std::iter::once(v)), + Self::Multiple(ref vs) => Box::new(vs.iter()), + } + } +} + +impl EntityBuilder { + /// Tries to build role entities using each given token. Will return an empty Vec + /// if no entities were created. + pub fn try_build_role_entities( + &self, + tokens: &DecodedTokens, + ) -> Result, BuildRoleEntityError> { + // Get entity namespace and type + let mut entity_name = self.entity_names.role.to_string(); + if let Some((namespace, _entity_type)) = self.schema.get_entity_from_base_name(&entity_name) + { + if !namespace.is_empty() { + entity_name = [namespace.as_str(), &entity_name].join(CEDAR_NAMESPACE_SEPARATOR); + } + } + + let mut entities = HashMap::new(); + + let token_refs = [ + tokens.userinfo.as_ref(), + tokens.id.as_ref(), + tokens.access.as_ref(), + ]; + for token in token_refs.into_iter().flatten() { + let role_claim = token.role_mapping(); + if let Some(claim) = token.get_claim(role_claim).as_ref() { + let unified_claims = UnifyClaims::deserialize(claim.value()); + let claim_role_name_iter = match unified_claims { + Ok(ref unified_claims) => unified_claims.iter(), + Err(_) => { + return Err(BuildRoleEntityError::map_tkn_err( + token, + BuildEntityError::TokenClaimTypeMismatch( + TokenClaimTypeError::type_mismatch( + role_claim, + "String or Array", + claim.value(), + ), + ), + )) + }, + }; + + for claim_role_name in claim_role_name_iter { + if !entities.contains_key(claim_role_name) { + let entity = build_entity(&entity_name, claim_role_name) + .map_err(|e| BuildRoleEntityError::map_tkn_err(token, e))?; + entities.insert(claim_role_name.clone(), entity); + } + } + } + } + + Ok(entities.into_values().collect()) + } +} + +fn build_entity(name: &str, id: &str) -> Result { + let name = EntityTypeName::from_str(name).map_err(BuildEntityError::ParseEntityTypeName)?; + let id = EntityId::from_str(id).map_err(BuildEntityError::ParseEntityId)?; + let uid = EntityUid::from_type_name_and_id(name, id); + let entity = Entity::new(uid, HashMap::new(), HashSet::new())?; + Ok(entity) +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildRoleEntityError { + #[error("failed to build role entity from access token: {0}")] + Access(#[source] BuildEntityError), + #[error("failed to build role entity from id token: {0}")] + Id(#[source] BuildEntityError), + #[error("failed to build role entity from userinfo token: {0}")] + Userinfo(#[source] BuildEntityError), +} + +impl BuildRoleEntityError { + pub fn map_tkn_err(token: &Token, err: BuildEntityError) -> Self { + match token.kind { + TokenKind::Access => BuildRoleEntityError::Access(err), + TokenKind::Id => BuildRoleEntityError::Id(err), + TokenKind::Userinfo => BuildRoleEntityError::Userinfo(err), + TokenKind::Transaction => unimplemented!("transaction tokens are not yet supported"), + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::TrustedIssuer; + use crate::jwt::{Token, TokenClaims}; + use serde_json::json; + use std::collections::HashMap; + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "entityTypes": { + "Role": {}, + "User": { + "memberOfTypes": ["Role"], + "shape": { + "type": "Record", + "attributes": {}, + } + }}} + })) + .expect("should successfully create test schema") + } + + fn test_build_entity_from_str_claim(tokens: DecodedTokens) { + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entity = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + assert_eq!(entity.len(), 1); + assert_eq!(entity[0].uid().to_string(), "Jans::Role::\"admin\""); + } + + #[test] + fn can_build_using_userinfo_tkn_vec_claim() { + let iss = TrustedIssuer::default(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([( + "role".to_string(), + json!(["admin", "user"]), + )])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entity = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + assert_eq!(entity.len(), 2); + let entity_uids = entity + .iter() + .map(|e| e.uid().to_string()) + .collect::>(); + assert_eq!( + entity_uids, + HashSet::from(["Jans::Role::\"admin\"", "Jans::Role::\"user\""].map(|s| s.to_string())) + ); + } + + #[test] + fn can_build_using_userinfo_tkn_string_claim() { + let iss = TrustedIssuer::default(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_build_using_id_tkn() { + let iss = TrustedIssuer::default(); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_build_using_access_tkn() { + let iss = TrustedIssuer::default(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn ignores_duplicate_roles() { + let iss = TrustedIssuer::default(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_create_multiple_different_roles_from_different_tokens() { + let iss = TrustedIssuer::default(); + let schema = test_schema(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("role1"))])), + Some(&iss), + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("role2"))])), + Some(&iss), + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([( + "role".to_string(), + json!(["role3", "role4"]), + )])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entities = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + let entities = entities + .iter() + .map(|e| e.uid().to_string()) + .collect::>(); + let expected_entities = (1..=4) + .map(|x| format!("Jans::Role::\"role{}\"", x)) + .collect::>(); + assert_eq!(entities, expected_entities); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs new file mode 100644 index 00000000000..38be043fc10 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs @@ -0,0 +1,368 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; + +const DEFAULT_TKN_PRINCIPAL_IDENTIFIER: &str = "jti"; + +impl EntityBuilder { + pub fn build_access_tkn_entity(&self, token: &Token) -> Result { + if token.kind != TokenKind::Access { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Access, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Access), + }); + } + let entity_name = self.entity_names.access_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + pub fn build_id_tkn_entity(&self, token: &Token) -> Result { + if token.kind != TokenKind::Id { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Id, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Id), + }); + } + let entity_name = self.entity_names.id_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + pub fn build_userinfo_tkn_entity( + &self, + token: &Token, + ) -> Result { + if token.kind != TokenKind::Userinfo { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Userinfo, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Userinfo), + }); + } + let entity_name = self.entity_names.userinfo_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + fn build_tkn_entity( + &self, + entity_name: &str, + token: &Token, + ) -> Result { + let id_src_claim = token + .metadata() + .principal_identifier + .as_deref() + .unwrap_or(DEFAULT_TKN_PRINCIPAL_IDENTIFIER); + build_entity( + &self.schema, + entity_name, + token, + id_src_claim, + vec![], + HashSet::new(), + ) + .map_err(|err| BuildTokenEntityError { + token_kind: token.kind, + err, + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to create {token_kind} token entity: {err}")] +pub struct BuildTokenEntityError { + pub token_kind: TokenKind, + pub err: BuildEntityError, +} + +impl BuildTokenEntityError { + pub fn access_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Access, + err: BuildEntityError::TokenUnavailable, + } + } + + pub fn id_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Id, + err: BuildEntityError::TokenUnavailable, + } + } + + pub fn userinfo_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Userinfo, + err: BuildEntityError::TokenUnavailable, + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ClaimMappings, TokenEntityMetadata, TrustedIssuer}; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Access_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "id_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "Userinfo_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "TrustedIssuer": { + "shape": { + "type": "Record", + "attributes": { + "issuer_entity_id": { "type": "EntityOrCommon", "name": "Url" } + }, + } + } + } + }})) + .expect("should deserialize schema") + } + + fn test_issusers() -> HashMap { + let token_entity_metadata = TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(), + ..Default::default() + }; + let iss = TrustedIssuer { + access_tokens: token_entity_metadata.clone(), + id_tokens: token_entity_metadata.clone(), + userinfo_tokens: token_entity_metadata, + ..Default::default() + }; + let issuers = HashMap::from([("test_iss".into(), iss.clone())]); + issuers + } + + fn test_build_entity(tkn_entity_type_name: &str, token: Token, build_tkn_entity_fn: F) + where + F: FnOnce(&Token) -> Result, + { + let entity = + build_tkn_entity_fn(&token).expect("expected to successfully build token entity"); + + assert_eq!( + entity.uid().to_string(), + format!("Jans::{}::\"tkn-123\"", tkn_entity_type_name) + ); + + assert_eq!( + entity + .attr("jti") + .expect("expected entity to have a `jti` attribute") + .unwrap(), + EvalResult::String("tkn-123".to_string()), + ); + + let trusted_iss = entity + .attr("trusted_issuer") + .expect("expected entity to have a `trusted_issuer` attribute") + .unwrap(); + if let EvalResult::EntityUid(ref uid) = trusted_iss { + assert_eq!(uid.type_name().basename(), "TrustedIssuer"); + assert_eq!( + uid.id().escaped(), + "https://some-iss.com/.well-known/openid-configuration" + ); + } else { + panic!( + "expected the attribute `trusted_issuer` to be an EntityUid, got: {:?}", + trusted_iss + ); + } + } + + #[test] + fn can_build_access_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("Access_token", access_token, |tkn| { + builder.build_access_tkn_entity(tkn) + }); + } + + #[test] + fn can_build_id_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("id_token", id_token, |tkn| builder.build_id_tkn_entity(tkn)); + } + + #[test] + fn can_build_userinfo_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("Userinfo_token", userinfo_token, |tkn| { + builder.build_userinfo_tkn_entity(tkn) + }); + } + + #[test] + fn errors_when_given_incorrect_tkn_kind() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let tkn_claims = TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])); + let iss = Some(issuers.get("test_iss").unwrap()); + let access_token = Token::new_access(tkn_claims.clone(), iss); + let id_token = Token::new_id(tkn_claims.clone(), iss); + let userinfo_token = Token::new_userinfo(tkn_claims, iss); + + for tkn in [&id_token, &userinfo_token].iter() { + let err = builder + .build_access_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Access && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Access + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + + for tkn in [&access_token, &userinfo_token].iter() { + let err = builder + .build_id_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Id && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Id + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + + for tkn in [&access_token, &id_token].iter() { + let err = builder + .build_userinfo_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Userinfo && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Userinfo + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs new file mode 100644 index 00000000000..d22e45b87f3 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs @@ -0,0 +1,291 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use cedar_policy::Entity; +use std::collections::HashSet; + +impl EntityBuilder { + pub fn build_user_entity( + &self, + tokens: &DecodedTokens, + parents: HashSet, + ) -> Result { + let entity_name = self.entity_names.user.as_ref(); + let mut errors = vec![]; + + for token in [tokens.userinfo.as_ref(), tokens.id.as_ref()] + .iter() + .flatten() + { + let user_id_claim = token.user_mapping(); + match build_entity( + &self.schema, + entity_name, + token, + user_id_claim, + vec![], + parents.clone(), + ) { + Ok(entity) => return Ok(entity), + Err(err) => errors.push((token.kind, err)), + } + } + + Err(BuildUserEntityError { errors }) + } +} + +#[derive(Debug, thiserror::Error)] +pub struct BuildUserEntityError { + pub errors: Vec<(TokenKind, BuildEntityError)>, +} + +impl fmt::Display for BuildUserEntityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.errors.is_empty() { + writeln!( + f, + "failed to create User Entity since no tokens were provided" + )?; + } else { + writeln!( + f, + "failed to create User Entity due to the following errors:" + )?; + for (token_kind, error) in &self.errors { + writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ClaimMappings, TokenEntityMetadata, TrustedIssuer}; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + fn test_iss() -> TrustedIssuer { + let token_entity_metadata = TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "email": { + "parser": "regex", + "type": "Jans::Email", + "regex_expression" : "^(?P[^@]+)@(?P.+)$", + "UID": {"attr": "uid", "type":"String"}, + "DOMAIN": {"attr": "domain", "type":"String"}, + }, + })) + .unwrap(), + ..Default::default() + }; + TrustedIssuer { + id_tokens: token_entity_metadata.clone(), + userinfo_tokens: token_entity_metadata, + ..Default::default() + } + } + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Email": { + "type": "Record", + "attributes": { + "uid": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "User": { + "memberOf": ["Role"], + "shape": { + "type": "Record", + "attributes": { + "email": { "type": "EntityOrCommon", "name": "Email" }, + "sub": { "type": "String" }, + }, + } + } + } + }})) + .expect("should successfully create test schema") + } + + fn test_successfully_building_user_entity(tokens: DecodedTokens) { + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let entity = builder + .build_user_entity(&tokens, HashSet::new()) + .expect("expected to build user entity"); + + assert_eq!(entity.uid().to_string(), "Jans::User::\"user-123\""); + + assert_eq!( + entity.attr("sub").unwrap().unwrap(), + EvalResult::String("user-123".to_string()), + ); + + let email = entity + .attr("email") + .expect("entity must have an `email` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = email { + assert_eq!(record.len(), 2); + assert_eq!( + record.get("uid").unwrap(), + &EvalResult::String("test".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("email.com".to_string()) + ); + } else { + panic!( + "expected the attribute `email` to be a record, got: {:?}", + email + ); + } + } + + #[test] + fn can_build_using_userinfo_tkn() { + let iss = test_iss(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("email".to_string(), json!("test@email.com")), + ("sub".to_string(), json!("user-123")), + ("role".to_string(), json!(["admin", "user"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + test_successfully_building_user_entity(tokens); + } + + #[test] + fn can_build_using_id_tkn() { + let iss = test_iss(); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("email".to_string(), json!("test@email.com")), + ("sub".to_string(), json!("user-123")), + ("role".to_string(), json!(["admin", "user"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + test_successfully_building_user_entity(tokens); + } + + #[test] + fn errors_when_token_has_missing_claim() { + let iss = test_iss(); + let schema = test_schema(); + + let id_token = Token::new_id(TokenClaims::new(HashMap::new()), Some(&iss)); + let userinfo_token = Token::new_userinfo(TokenClaims::new(HashMap::new()), Some(&iss)); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let err = builder + .build_user_entity(&tokens, HashSet::new()) + .expect_err("expected to error while building the user entity"); + + assert_eq!(err.errors.len(), 2); + for (i, expected_kind) in [TokenKind::Userinfo, TokenKind::Id].iter().enumerate() { + assert!( + matches!( + err.errors[i], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == expected_kind && + claim_name == "sub" + ), + "expected an error due to missing the `sub` claim, got: {:?}", + err.errors[i] + ); + } + } + + #[test] + fn errors_when_tokens_unavailable() { + let schema = test_schema(); + + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: None, + }; + + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let err = builder + .build_user_entity(&tokens, HashSet::new()) + .expect_err("expected to error while building the user entity"); + + assert_eq!(err.errors.len(), 0); + } + + #[test] + fn can_build_entity_with_roles() { + let iss = test_iss(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("sub".to_string(), json!("user-123")), + ("email".to_string(), json!("someone@email.com")), + ("role".to_string(), json!(["role1", "role2", "role3"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let roles = HashSet::from([ + "Role::\"role1\"".parse().unwrap(), + "Role::\"role2\"".parse().unwrap(), + "Role::\"role3\"".parse().unwrap(), + ]); + + let user_entity = builder + .build_user_entity(&tokens, roles.clone()) + .expect("expected to build user entity"); + + let (_, _, parents) = user_entity.into_inner(); + assert_eq!(parents, roles,); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs new file mode 100644 index 00000000000..3f444413a16 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs @@ -0,0 +1,463 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use cedar_policy::Entity; +use std::collections::HashSet; + +// Default claims to use for the Workload Entity's ID. +const DEFAULT_ACCESS_TKN_WORKLOAD_CLAIM: &str = "client_id"; +const DEFAULT_ID_TKN_WORKLOAD_CLAIM: &str = "aud"; + +impl EntityBuilder { + pub fn build_workload_entity( + &self, + tokens: &DecodedTokens, + ) -> Result { + let entity_name = self.entity_names.workload.as_ref(); + let mut errors = vec![]; + + for (workload_id_claim, token_option, claim_aliases) in [ + ( + DEFAULT_ACCESS_TKN_WORKLOAD_CLAIM, + tokens.access.as_ref(), + vec![], + ), + ( + DEFAULT_ID_TKN_WORKLOAD_CLAIM, + tokens.id.as_ref(), + vec![ClaimAliasMap::new("aud", "client_id")], + ), + ] + .into_iter() + { + if let Some(token) = token_option { + match build_entity( + &self.schema, + entity_name, + token, + workload_id_claim, + claim_aliases, + HashSet::new(), + ) { + Ok(entity) => return Ok(entity), + Err(err) => errors.push((token.kind, err)), + } + } + } + + Err(BuildWorkloadEntityError { errors }) + } +} + +#[derive(Debug, thiserror::Error)] +pub struct BuildWorkloadEntityError { + pub errors: Vec<(TokenKind, BuildEntityError)>, +} + +impl fmt::Display for BuildWorkloadEntityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.errors.is_empty() { + writeln!( + f, + "failed to create Workload Entity since no tokens were provided" + )?; + } else { + writeln!( + f, + "failed to create Workload Entity due to the following errors:" + )?; + for (token_kind, error) in &self.errors { + writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::authz::entity_builder::BuildEntityError; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ + ClaimMappings, TokenEntityMetadata, TokenKind, TrustedIssuer, + }; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_using_access_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expeted to successfully build workload entity"); + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn can_build_using_id_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("aud".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn can_build_expression_with_regex_mapping() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Email": { + "type": "Record", + "attributes": { + "uid": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "email": { "type": "EntityOrCommon", "name": "Email" }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + }} + })) + .unwrap(); + let iss = TrustedIssuer { + access_tokens: TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "email": { + "parser": "regex", + "type": "Jans::Email", + "regex_expression" : "^(?P[^@]+)@(?P.+)$", + "UID": {"attr": "uid", "type":"String"}, + "DOMAIN": {"attr": "domain", "type":"String"}, + }, + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(), + ..Default::default() + }, + ..Default::default() + }; + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("email".to_string(), json!("test@example.com")), + ("url".to_string(), json!("https://test.com/example")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + + assert_eq!( + entity + .attr("client_id") + .expect("expected to workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + + let email = entity + .attr("email") + .expect("expected workload entity to have an `email` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = email { + assert_eq!(record.len(), 2); + assert_eq!( + record.get("uid").unwrap(), + &EvalResult::String("test".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("example.com".to_string()) + ); + } else { + panic!( + "expected the attribute `email` to be a record, got: {:?}", + email + ); + } + + let url = entity + .attr("url") + .expect("entity must have a `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record.get("scheme").unwrap(), + &EvalResult::String("https".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("test.com".to_string()) + ); + assert_eq!( + record.get("path").unwrap(), + &EvalResult::String("/example".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + email + ); + } + } + + #[test] + fn can_build_entity_with_entity_ref() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "entityTypes": { + "TrustedIss": {}, + "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "iss": { "type": "EntityOrCommon", "name": "TrustedIss" }, + }, + } + } + }} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ( + "iss".to_string(), + json!("https://test.com/.well-known/openid-configuration"), + ), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + + assert_eq!( + entity + .attr("client_id") + .expect("expected to workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + + let iss = entity + .attr("iss") + .expect("entity must have a `iss` attribute") + .unwrap(); + if let EvalResult::EntityUid(uid) = iss { + assert_eq!(uid.type_name().namespace(), "Jans"); + assert_eq!(uid.type_name().basename(), "TrustedIss"); + assert_eq!( + uid.id().escaped(), + "https://test.com/.well-known/openid-configuration" + ); + } else { + panic!( + "expected the attribute `iss` to be an EntityUid, got: {:?}", + iss + ); + } + } + + #[test] + fn errors_when_token_has_missing_claim() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + let id_token = Token::new_id(TokenClaims::new(HashMap::new()), Some(&iss)); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: None, + }; + let err = builder + .build_workload_entity(&tokens) + .expect_err("expected to error while building the workload entity"); + + assert_eq!(err.errors.len(), 2); + assert!( + matches!( + err.errors[0], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == &TokenKind::Access && + claim_name == "client_id" + ), + "expected an error due to missing the `client_id` claim" + ); + assert!( + matches!( + err.errors[1], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == &TokenKind::Id && + claim_name == "aud" + ), + "expected an error due to missing the `aud` claim" + ); + } + + #[test] + fn errors_when_tokens_unavailable() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: None, + }; + let err = builder.build_workload_entity(&tokens).unwrap_err(); + + assert_eq!(err.errors.len(), 0); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs b/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs new file mode 100644 index 00000000000..0f1edb8d466 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs @@ -0,0 +1,61 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use crate::common::policy_store::ClaimMappings; +use serde_json::Value; +use std::collections::HashMap; + +impl ClaimMappings { + /// Creates new claims and adds it to the HashMap of the given claims + /// if a mapping exists + /// + /// * Note that this will overwrite existing names + pub fn apply_mapping(&self, claims: &HashMap) -> HashMap { + let mut mapped_claims = HashMap::new(); + for (name, claim) in claims.iter() { + if let Some(mapping) = self.get(name) { + let applied_mapping = mapping.apply_mapping(claim); + mapped_claims.extend(applied_mapping); + } + } + mapped_claims + } +} + +#[cfg(test)] +mod test { + use crate::common::policy_store::ClaimMappings; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + #[test] + fn can_apply_mapping() { + let claims = HashMap::from([ + ("email".to_string(), json!("test@test.com")), + ("url".to_string(), json!("https://example.com/test")), + ]); + let claim_mapping = serde_json::from_value::(json!({ + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(); + let mapped_claims = claim_mapping.apply_mapping(&claims); + assert_eq!( + mapped_claims, + HashMap::from([ + ("scheme".to_string(), json!("https")), + ("domain".to_string(), json!("example.com")), + ("path".to_string(), json!("/test")), + ]) + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/merge_json.rs b/jans-cedarling/cedarling/src/authz/merge_json.rs deleted file mode 100644 index c60a2d032a3..00000000000 --- a/jans-cedarling/cedarling/src/authz/merge_json.rs +++ /dev/null @@ -1,56 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use serde_json::Value; - -#[derive(Debug, thiserror::Error)] -pub enum MergeError { - #[error("Failed to merge JSON objects due to conflicting keys: {0}")] - KeyConflict(String), -} - -pub fn merge_json_values(mut base: Value, other: Value) -> Result { - if let (Some(base_map), Some(additional_map)) = (base.as_object_mut(), other.as_object()) { - for (key, value) in additional_map { - if base_map.contains_key(key) { - return Err(MergeError::KeyConflict(key.clone())); - } - base_map.insert(key.clone(), value.clone()); - } - } - Ok(base) -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use super::merge_json_values; - use crate::authz::merge_json::MergeError; - - #[test] - fn can_merge_json_objects() { - let obj1 = json!({ "a": 1, "b": 2 }); - let obj2 = json!({ "c": 3, "d": 4 }); - let expected = json!({"a": 1, "b": 2, "c": 3, "d": 4}); - - let result = merge_json_values(obj1, obj2).expect("Should merge JSON objects"); - - assert_eq!(result, expected); - } - - #[test] - fn errors_on_same_keys() { - // Test for only two objects - let obj1 = json!({ "a": 1, "b": 2 }); - let obj2 = json!({ "b": 3, "c": 4 }); - let result = merge_json_values(obj1, obj2); - - assert!( - matches!(result, Err(MergeError::KeyConflict(key)) if key.as_str() == "b"), - "Expected an error due to conflicting keys" - ); - } -} diff --git a/jans-cedarling/cedarling/src/authz/mod.rs b/jans-cedarling/cedarling/src/authz/mod.rs index c92a1c84f18..7bf65920ad7 100644 --- a/jans-cedarling/cedarling/src/authz/mod.rs +++ b/jans-cedarling/cedarling/src/authz/mod.rs @@ -8,43 +8,34 @@ //! - evaluate if authorization is granted for *user* //! - evaluate if authorization is granted for *client* / *workload * -use std::collections::{HashMap, HashSet}; -use std::io::Cursor; -use std::str::FromStr; -use std::sync::Arc; - use crate::bootstrap_config::AuthorizationConfig; use crate::common::app_types; -use crate::common::cedar_schema::cedar_json::{BuildJsonCtxError, FindActionError}; use crate::common::policy_store::PolicyStoreWithID; use crate::jwt::{self, TokenStr}; - use crate::log::interface::LogWriter; use crate::log::{ AuthorizationLogInfo, BaseLogEntry, DecisionLogEntry, Diagnostics, DiagnosticsRefs, LogEntry, LogLevel, LogTokensInfo, LogType, Logger, PrincipalLogEntry, UserAuthorizeInfo, WorkloadAuthorizeInfo, }; +use build_ctx::*; +use cedar_policy::{Entities, Entity, EntityUid}; +use chrono::Utc; +use entity_builder::*; +use request::Request; +use std::collections::HashMap; +use std::io::Cursor; +use std::str::FromStr; +use std::sync::Arc; + +pub use authorize_result::AuthorizeResult; mod authorize_result; -mod merge_json; +mod build_ctx; -pub(crate) mod entities; +pub(crate) mod entity_builder; pub(crate) mod request; -pub use authorize_result::AuthorizeResult; -use cedar_policy::{ContextJsonError, Entities, Entity, EntityUid}; -use chrono::Utc; -use entities::{ - CEDAR_POLICY_SEPARATOR, CreateCedarEntityError, CreateUserEntityError, - CreateWorkloadEntityError, DecodedTokens, ResourceEntityError, RoleEntityError, - create_resource_entity, create_role_entities, create_token_entities, create_user_entity, - create_workload_entity, -}; -use merge_json::{MergeError, merge_json_values}; -use request::Request; -use serde_json::Value; - /// Configuration to Authz to initialize service without errors pub(crate) struct AuthzConfig { pub log_service: Logger, @@ -61,11 +52,23 @@ pub(crate) struct AuthzConfig { pub struct Authz { config: AuthzConfig, authorizer: cedar_policy::Authorizer, + entity_builder: EntityBuilder, } impl Authz { /// Create a new Authorization Service pub(crate) fn new(config: AuthzConfig) -> Self { + let json_schema = config.policy_store.schema.json.clone(); + let entity_names = EntityNames::from(&config.authorization); + let build_workload = config.authorization.use_workload_principal; + let build_user = config.authorization.use_user_principal; + let entity_builder = entity_builder::EntityBuilder::new( + json_schema, + entity_names, + build_workload, + build_user, + ); + config.log_service.log( LogEntry::new_with_data( config.pdp_id, @@ -80,6 +83,7 @@ impl Authz { Self { config, authorizer: cedar_policy::Authorizer::new(), + entity_builder, } } @@ -88,33 +92,31 @@ impl Authz { &'a self, request: &'a Request, ) -> Result, AuthorizeError> { - let access_token = if let Some(tkn) = request.tokens.access_token.as_ref() { + let access = if let Some(tkn) = request.tokens.access_token.as_ref() { Some( self.config .jwt_service - .process_token(TokenStr::Access(tkn.as_str())) + .process_token(TokenStr::Access(tkn)) .await?, ) } else { None }; - - let id_token = if let Some(tkn) = request.tokens.id_token.as_ref() { + let id = if let Some(tkn) = request.tokens.id_token.as_ref() { Some( self.config .jwt_service - .process_token(TokenStr::Id(tkn.as_str())) + .process_token(TokenStr::Id(tkn)) .await?, ) } else { None }; - - let userinfo_token = if let Some(tkn) = request.tokens.userinfo_token.as_ref() { + let userinfo = if let Some(tkn) = request.tokens.userinfo_token.as_ref() { Some( self.config .jwt_service - .process_token(TokenStr::Userinfo(tkn.as_str())) + .process_token(TokenStr::Userinfo(tkn)) .await?, ) } else { @@ -122,9 +124,9 @@ impl Authz { }; Ok(DecodedTokens { - access_token, - id_token, - userinfo_token, + access, + id, + userinfo, }) } @@ -143,7 +145,9 @@ impl Authz { .map_err(AuthorizeError::Action)?; // Parse [`cedar_policy::Entity`]-s to [`AuthorizeEntitiesData`] that hold all entities (for usability). - let entities_data: AuthorizeEntitiesData = self.build_entities(&request, &tokens).await?; + let entities_data = self + .entity_builder + .build_entities(&tokens, &request.resource)?; // Get entity UIDs what we will be used on authorize check let resource_uid = entities_data.resource.uid(); @@ -276,7 +280,7 @@ impl Authz { .map(|auth_info| &auth_info.diagnostics); let tokens_logging_info = LogTokensInfo { - access: tokens.access_token.as_ref().map(|tkn| { + access: tokens.access.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -284,7 +288,7 @@ impl Authz { .as_str(), ) }), - id_token: tokens.access_token.as_ref().map(|tkn| { + id_token: tokens.id.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -292,7 +296,7 @@ impl Authz { .as_str(), ) }), - userinfo: tokens.userinfo_token.as_ref().map(|tkn| { + userinfo: tokens.userinfo.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -371,106 +375,18 @@ impl Authz { Ok(response) } - /// Build all the Cedar [`Entities`] from a [`Request`] - /// - /// [`Entities`]: Entity - pub async fn build_entities( + #[cfg(test)] + pub fn build_entities( &self, request: &Request, tokens: &DecodedTokens<'_>, ) -> Result { - let policy_store = &self.config.policy_store; - let auth_conf = &self.config.authorization; - - // build workload entity - let workload = if self.config.authorization.use_workload_principal { - Some(create_workload_entity( - auth_conf.mapping_workload.as_deref(), - policy_store, - tokens, - )?) - } else { - None - }; - - // build role entity - let roles = create_role_entities(policy_store, tokens)?; - - // build user entity - let user = if self.config.authorization.use_user_principal { - Some(create_user_entity( - auth_conf.mapping_user.as_deref(), - policy_store, - tokens, - HashSet::from_iter(roles.iter().map(|e| e.uid())), - )?) - } else { - None - }; - - let token_entities = create_token_entities(auth_conf, policy_store, tokens)?; - - // build resource entity - let resource = create_resource_entity( - request.resource.clone(), - &self.config.policy_store.schema.json, - )?; - - Ok(AuthorizeEntitiesData { - workload, - access_token: token_entities.access, - id_token: token_entities.id, - userinfo_token: token_entities.userinfo, - user, - resource, - roles, - }) + Ok(self + .entity_builder + .build_entities(tokens, &request.resource)?) } } -/// Constructs the authorization context by adding the built entities from the tokens -fn build_context( - config: &AuthzConfig, - request_context: Value, - entities_data: &AuthorizeEntitiesData, - schema: &cedar_policy::Schema, - action: &cedar_policy::EntityUid, -) -> Result { - let namespace = config.policy_store.namespace(); - let action_name = action.id().escaped().to_string(); - let action_schema = config - .policy_store - .schema - .json - .find_action(&action_name, namespace) - .map_err(|e| BuildContextError::FindActionSchema(action_name.clone(), e))? - .ok_or(BuildContextError::MissingActionSchema(action_name))?; - - let mut id_mapping = HashMap::new(); - for entity in entities_data.iter() { - // we strip the namespace from the type_name then make it lowercase - // example: 'Jans::Id_token' -> 'id_token' - let type_name = entity.uid().type_name().to_string(); - let type_name = type_name - .strip_prefix(&format!("{}{}", namespace, CEDAR_POLICY_SEPARATOR)) - .unwrap_or(&type_name) - .to_lowercase(); - let type_id = entity.uid().id().escaped(); - id_mapping.insert(type_name, type_id.to_string()); - } - - let entities_context = action_schema - .build_ctx_entity_refs_json(id_mapping) - .unwrap(); - - let context = merge_json_values(entities_context, request_context)?; - - let context: cedar_policy::Context = - cedar_policy::Context::from_json_value(context, Some((schema, action)))?; - - Ok(context) -} - /// Helper struct to hold named parameters for [`Authz::execute_authorize`] method. struct ExecuteAuthorizeParameters<'a> { entities: &'a Entities, @@ -493,6 +409,17 @@ pub struct AuthorizeEntitiesData { } impl AuthorizeEntitiesData { + // NOTE: the type ids created from these does not include the namespace + fn type_ids(&self) -> HashMap { + self.iter() + .map(|entity| { + let type_name = entity.uid().type_name().basename().to_string(); + let type_id = entity.uid().id().escaped().to_string(); + (type_name, type_id) + }) + .collect::>() + } + /// Create iterator to get all entities fn into_iter(self) -> impl Iterator { vec![self.resource].into_iter().chain(self.roles).chain( @@ -541,27 +468,6 @@ pub enum AuthorizeError { /// Error encountered while processing JWT token data #[error(transparent)] ProcessTokens(#[from] jwt::JwtProcessingError), - /// Error encountered while creating id token entity - #[error("could not create id_token entity: {0}")] - CreateIdTokenEntity(CreateCedarEntityError), - /// Error encountered while creating userinfo entity - #[error("could not create userinfo entity: {0}")] - CreateUserinfoTokenEntity(CreateCedarEntityError), - /// Error encountered while creating access_token entity - #[error("could not create access_token entity: {0}")] - CreateAccessTokenEntity(CreateCedarEntityError), - /// Error encountered while creating user entity - #[error("could not create User entity: {0}")] - CreateUserEntity(#[from] CreateUserEntityError), - /// Error encountered while creating workload - #[error(transparent)] - CreateWorkloadEntity(#[from] CreateWorkloadEntityError), - /// Error encountered while creating resource entity - #[error("{0}")] - ResourceEntity(#[from] ResourceEntityError), - /// Error encountered while creating role entity - #[error(transparent)] - RoleEntity(#[from] RoleEntityError), /// Error encountered while parsing Action to EntityUid #[error("could not parse action: {0}")] Action(cedar_policy::ParseErrors), @@ -583,25 +489,9 @@ pub enum AuthorizeError { /// Error encountered while building the context for the request #[error("Failed to build context: {0}")] BuildContext(#[from] BuildContextError), -} - -#[derive(Debug, thiserror::Error)] -pub enum BuildContextError { - /// Error encountered while validating context according to the schema - #[error(transparent)] - Merge(#[from] MergeError), - /// Error encountered while deserializing the Context from JSON - #[error(transparent)] - DeserializeFromJson(#[from] ContextJsonError), - /// Error encountered while deserializing the Context from JSON - #[error("Failed to find the action `{0}` in the schema: {0}")] - FindActionSchema(String, FindActionError), - /// Error encountered while deserializing the Context from JSON - #[error("The action `{0}` was not found in the schema")] - MissingActionSchema(String), - /// Error encountered while deserializing the Context from JSON + /// Error encountered while building Cedar Entities #[error(transparent)] - BuildJson(#[from] BuildJsonCtxError), + BuildEntity(#[from] BuildCedarlingEntityError), } #[derive(Debug, derive_more::Error, derive_more::Display)] diff --git a/jans-cedarling/cedarling/src/authz/request.rs b/jans-cedarling/cedarling/src/authz/request.rs index a5c80de20b9..daa225775f3 100644 --- a/jans-cedarling/cedarling/src/authz/request.rs +++ b/jans-cedarling/cedarling/src/authz/request.rs @@ -4,9 +4,6 @@ // Copyright (c) 2024, Gluu, Inc. use std::collections::HashMap; -use std::str::FromStr; - -use cedar_policy::{EntityId, EntityTypeName, EntityUid, ParseErrors}; /// Box to store authorization data #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -49,12 +46,3 @@ pub struct ResourceData { #[serde(flatten)] pub payload: HashMap, } - -impl ResourceData { - pub(crate) fn entity_uid(&self) -> Result { - Ok(EntityUid::from_type_name_and_id( - EntityTypeName::from_str(&self.resource_type)?, - EntityId::new(&self.id), - )) - } -} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs index 64f69ef1e9d..c1387ed61fb 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs @@ -3,369 +3,137 @@ // // Copyright (c) 2024, Gluu, Inc. -//! Module contains the JSON representation of a [cedar_policy::Schema] -//! Support translated schema from human representation to JSON via CLI version `cedar-policy-cli 4.1`. -//! To translate human redable format to JSON via CLI use next command: -//! `cedar translate-schema --direction cedar-to-json -s .\cedar.schema` -//! [cedar json schema grammar](https://docs.cedarpolicy.com/schema/json-schema-grammar.html) - documentation about json structure of cedar schema. - -mod action; -mod entity_types; - -use std::collections::HashMap; - -use action::ActionSchema; -pub use action::{BuildJsonCtxError, FindActionError}; -use derive_more::derive::Display; -pub use entity_types::{CedarSchemaEntityShape, CedarSchemaRecord}; - -/// Represent `cedar-policy` schema type for external usage. -#[derive(Debug, PartialEq, Hash, Eq, Display)] -pub enum CedarType { - Long, - String, - Boolean, - TypeName(String), - Set(Box), -} - -/// Possible errors that may occur when retrieving a [`CedarType`] from cedar-policy schema. -#[derive(Debug, thiserror::Error)] -pub enum GetCedarTypeError { - /// Error while getting `cedar-policy` schema not implemented type - #[error("could not get cedar-policy type {0}, it is not implemented")] - TypeNotImplemented(String), -} - -/// Enum to get info about type based on name. -/// Is used as a result in [`CedarSchemaJson::find_type`] -pub enum SchemaDefinedType<'a> { - Entity(&'a CedarSchemaEntityShape), - CommonType(&'a CedarSchemaRecord), -} - -/// JSON representation of a [`cedar_policy::Schema`] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] +//! This module is responsible for deserializing the JSON Cedar schema + +use action::*; +use attribute::*; +use entity_type::*; +use serde::Deserialize; +use std::{collections::HashMap, str::FromStr}; + +pub(crate) mod action; +pub(crate) mod attribute; +pub(crate) mod entity_type; + +mod deserialize; + +pub type ActionName = String; +pub type ActionGroupName = String; +pub type AttributeName = String; +pub type CommonTypeName = String; +pub type EntityName = String; +pub type EntityTypeName = String; +pub type EntityOrCommonName = String; +pub type ExtensionName = String; +pub type NamespaceName = String; + +#[derive(Debug, Deserialize, PartialEq, Clone)] pub struct CedarSchemaJson { #[serde(flatten)] - pub namespace: HashMap, + namespaces: HashMap, } impl CedarSchemaJson { - /// Get schema record by namespace name and entity type name - pub fn entity_schema( - &self, - namespace: &str, - typename: &str, - ) -> Option<&CedarSchemaEntityShape> { - let namespace = self.namespace.get(namespace)?; - namespace.entity_types.get(typename) + pub fn get_action(&self, namespace: &str, name: &str) -> Option<&Action> { + self.namespaces + .get(namespace) + .and_then(|nmspce| nmspce.actions.get(name)) } - /// Find the typename if exist in the schema and return it definition - pub fn find_type(&self, type_name: &str, namespace: &str) -> Option { - let namespace = self.namespace.get(namespace)?; - - let schema_type = namespace - .common_types - .get(type_name) - .as_ref() - .map(|common_type| SchemaDefinedType::CommonType(common_type)); - - if schema_type.is_some() { - return schema_type; + pub fn get_common_type(&self, name: &str) -> Option<(&NamespaceName, &Attribute)> { + for (namespace_name, namespace) in self.namespaces.iter() { + if let Some(attr) = namespace.common_types.get(name) { + return Some((namespace_name, attr)); + } } + None + } - let schema_type = namespace - .entity_types - .get(type_name) - .as_ref() - .map(|entity| SchemaDefinedType::Entity(entity)); - if schema_type.is_some() { - return schema_type; + pub fn get_entity_from_base_name( + &self, + base_name: &str, + ) -> Option<(&NamespaceName, &EntityType)> { + for (namespace_name, namespace) in self.namespaces.iter() { + if let Some(entity_type) = namespace.entity_types.get(base_name) { + return Some((namespace_name, entity_type)); + } } + None + } + pub fn get_entity_from_full_name( + &self, + full_name: &str, + ) -> Option<(NamespaceName, &EntityType)> { + let full_name = cedar_policy::EntityTypeName::from_str(full_name).ok()?; + let namespace_name = full_name.namespace(); + if let Some(namespace) = self.namespaces.get(&namespace_name) { + let base_name = full_name.basename(); + if let Some(entity_type) = namespace.entity_types.get(base_name) { + return Some((namespace_name, entity_type)); + } + } None } } -/// CedarSchemaEntities hold all entities and their shapes in the namespace. -// It may contain more fields, but we don't need all of them. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaEntities { +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct Namespace { #[serde(rename = "entityTypes", default)] - pub entity_types: HashMap, + entity_types: HashMap, #[serde(rename = "commonTypes", default)] - pub common_types: HashMap, - pub actions: HashMap, + common_types: HashMap, + #[serde(default)] + actions: HashMap, } #[cfg(test)] -mod tests { - use std::collections::HashSet; - - use action::CtxAttribute; - use serde_json::json; - use test_utils::{SortedJson, assert_eq}; - - use super::entity_types::*; +mod test_deserialize_json_cedar_schema { use super::*; + use serde_json::json; + use std::collections::HashSet; - /// Test to parse the cedar json schema - /// to debug deserialize the schema #[test] - fn parse_correct_example() { - let json_value = include_str!("test_files/test_data_cedar.json"); - - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("failed to parse json"); - - let entity_types = HashMap::from_iter(vec![ - ("Access_token".to_string(), CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter(vec![ - ("aud".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ("exp".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Long".to_string(), - }), - required: true, - }), - ("iat".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Primitive(PrimitiveType { - kind: PrimitiveTypeKind::Long, - }), - required: true, - }), - ("scope".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Set(Box::new(SetEntityType { - element: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - })), - - required: false, - }), - ]), + fn can_deserialize_entity_types() { + let schema = json!({ + "Jans": { + "entityTypes": { + "User": { + "memberOfTypes": [ "UserGroup" ], + "shape": { + "type": "Record", + "attributes": { + "department": { "type": "String" }, + "jobLevel": { "type": "Long" } + } + } + }, + "UserGroup": {}, + }, + } + }); + let schema = serde_json::from_value::(schema).unwrap(); + let namespace = Namespace { + entity_types: HashMap::from([ + ("User".into(), EntityType { + member_of: Some(HashSet::from(["UserGroup".into()])), + shape: Some(EntityShape::required(HashMap::from([ + ("department".into(), Attribute::string()), + ("jobLevel".into(), Attribute::long()), + ]))), + tags: None, }), - }), - ("Role".to_string(), CedarSchemaEntityShape { shape: None }), - ("TrustedIssuer".to_string(), CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([( - "issuer_entity_id".to_string(), - CedarSchemaEntityAttribute { - required: true, - cedar_type: CedarSchemaEntityType::Typed(EntityType { - name: "Url".to_string(), - kind: "EntityOrCommon".to_string(), - }), - }, - )]), - }), - }), - ("Issue".to_string(), CedarSchemaEntityShape { shape: None }), - ]); - - let common_types = HashMap::from_iter([("Url".to_string(), CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([ - ("host".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ("path".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ("protocol".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, + ("UserGroup".into(), EntityType { + member_of: None, + shape: None, + tags: None, }), ]), - })]); - - let actions = HashMap::from([("Update".to_string(), ActionSchema { - resource_types: HashSet::from(["Issue"].map(|x| x.to_string())), - principal_types: HashSet::from(["Access_token", "Role"].map(|x| x.to_string())), - context: None, - })]); - - let schema_to_compare = CedarSchemaJson { - namespace: HashMap::from_iter(vec![("Jans".to_string(), CedarSchemaEntities { - entity_types, - common_types, - actions, - })]), + common_types: HashMap::new(), + actions: HashMap::new(), }; - - assert_eq!( - serde_json::json!(parsed_cedar_schema).sorted(), - serde_json::json!(schema_to_compare).sorted() - ); - } - - /// test to check if we get error on parsing invalid `EntityOrCommon` type - #[test] - fn parse_error_entity_or_common() { - // In this file we skipped field `name` for `EntityOrCommon` - let json_value = include_str!("test_files/test_data_cedar_err_entity_or_common.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: failed to deserialize EntityOrCommon: \ - missing field `name` at line 17 column 1" - ) - } - - /// test to check if we get error on parsing invalid `PrimitiveType` type - #[test] - fn parse_error_primitive_type() { - // In this file we use `"type": 123` but in OK case should be `"type": "Long"` - let json_value = include_str!("test_files/test_data_cedar_err_primitive_type.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: invalid type: integer `123`, expected a \ - string at line 17 column 1" - ) - } - - /// test to check if we get error on parsing invalid nested Sets :`Set>` type - #[test] - fn parse_error_set_entity_or_common() { - // In this file we skipped field `name` for `EntityOrCommon` in the nested set - let json_value = include_str!("test_files/test_data_cedar_err_set.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: failed to deserialize Set: failed to \ - deserialize Set: failed to deserialize EntityOrCommon: missing field `name` at line \ - 24 column 1" - ) - } - - /// test to check if we get error on parsing invalid type in field `is_required` - #[test] - fn parse_error_field_is_required() { - // In this file we use ` "required": 1234` but in OK case should be ` "required": false` or omit - let json_value = include_str!("test_files/test_data_cedar_err_field_is_required.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityAttribute, field 'is_required': invalid type: \ - integer `1234`, expected a boolean at line 22 column 1" - ) - } - - #[test] - fn can_parse_action_with_ctx() { - let expected_principal_entities = - HashSet::from(["Jans::Workload".into(), "Jans::User".into()]); - let expected_resource_entities = HashSet::from(["Jans::Issue".into()]); - let expected_context_entities = Some(HashSet::from([ - CtxAttribute { - namespace: "Jans".into(), - key: "access_token".into(), - kind: CedarType::TypeName("Access_token".to_string()), - }, - CtxAttribute { - namespace: "Jans".into(), - key: "time".into(), - kind: CedarType::Long, - }, - CtxAttribute { - namespace: "Jans".into(), - key: "user".into(), - kind: CedarType::TypeName("User".to_string()), - }, - CtxAttribute { - namespace: "Jans".into(), - key: "workload".into(), - kind: CedarType::TypeName("Workload".to_string()), - }, - ])); - - // Test case where the context is a record: - // action "Update" appliesTo { - // principal: [Workload, User], - // resource: [Issue], - // context: { - // time: Long, - // user: User, - // workload: Workload, - // access_token: Access_token, - // }}; - let json_value = include_str!("./test_files/test_schema.json"); - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("Should parse JSON schema"); - let action = parsed_cedar_schema - .find_action("UpdateWithRecordCtx", "Jans") - .expect("Should not error while finding action") - .expect("Action should not be none"); - assert_eq!(action.principal_entities, expected_principal_entities); - assert_eq!(action.resource_entities, expected_resource_entities); - assert_eq!(action.context_entities, expected_context_entities); - - // Test case where the context is a type: - // action "Update" appliesTo { - // principal: [Workload, User], - // resource: [Issue], - // context: Context - // }; - let json_value = include_str!("./test_files/test_schema.json"); - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("Should parse JSON schema"); - let action = parsed_cedar_schema - .find_action("UpdateWithTypeCtx", "Jans") - .expect("Should not error while finding action") - .expect("Action should not be none"); - assert_eq!(action.principal_entities, expected_principal_entities); - assert_eq!(action.resource_entities, expected_resource_entities); - assert_eq!(action.context_entities, expected_context_entities); - - let id_mapping = HashMap::from([ - ("access_token".into(), "tkn-1".into()), - ("user".into(), "user-123".into()), - ("workload".into(), "workload-321".into()), - ]); - let ctx_json = action - .build_ctx_entity_refs_json(id_mapping) - .expect("Should build JSON context"); - assert_eq!( - ctx_json, - json!({ - "access_token": { "type": "Jans::Access_token", "id": "tkn-1" }, - "user": { "type": "Jans::User", "id": "user-123" }, - "workload": { "type": "Jans::Workload", "id": "workload-321" }, - }) - ) + assert_eq!(schema, CedarSchemaJson { + namespaces: HashMap::from([("Jans".into(), namespace)]) + }); } } diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs index efc98a2ad7e..72859e180ec 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs @@ -3,450 +3,177 @@ // // Copyright (c) 2024, Gluu, Inc. -use std::collections::{HashMap, HashSet}; - -use serde::ser::SerializeMap; -use serde::{Deserialize, Serialize, de}; -use serde_json::{Value, json}; - -use super::entity_types::{ - CedarSchemaEntityAttribute, CedarSchemaEntityType, PrimitiveType, PrimitiveTypeKind, -}; -use super::{ - CedarSchemaEntities, CedarSchemaJson, CedarSchemaRecord, CedarType, GetCedarTypeError, -}; -use crate::authz::entities::CEDAR_POLICY_SEPARATOR; -use crate::common::cedar_schema::cedar_json::SchemaDefinedType; - -type AttrName = String; - -#[derive(Debug, Eq, Hash, PartialEq)] -pub struct CtxAttribute { - pub namespace: String, - pub key: String, - pub kind: CedarType, -} - -pub struct Action<'a> { - pub principal_entities: HashSet, - pub resource_entities: HashSet, - pub context_entities: Option>, - pub schema_entities: &'a CedarSchemaEntities, - pub schema: &'a ActionSchema, +use super::attribute::Attribute; +use super::*; +use serde::Deserialize; +use std::collections::HashSet; + +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct Action { + #[serde(rename = "memberOf", default)] + member_of: Option>, + #[serde(rename = "appliesTo")] + pub applies_to: AppliesTo, } -impl Action<'_> { - /// Builds the JSON representation of context entities for a given action. +#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] +pub struct ActionGroup { + id: EntityName, + /// Specifies membership for an action group in a different namespace. /// - /// This method processes the context attributes of the action and generates a - /// corresponding JSON value. The context may include entity references (with - /// `type` and `id`) and other values, which can be mapped through the provided - /// `id_mapping` and `value_mapping`. - /// - /// The `id_mapping` param is a A `HashMap` that maps context attribute keys - /// (like `"access_token"`) to their corresponding `id`s (like `"acs-tkn-1"`). - /// - /// # Usage Example - /// - /// ```rs - /// let id_mapping = HashMap::from([("access_token".to_string(), "acs-tkn-1".to_string())]); - /// let json = action.build_ctx_entities_json(id_mapping, value_mapping); - /// ``` - pub fn build_ctx_entity_refs_json( - &self, - id_mapping: HashMap, - ) -> Result { - let mut json = json!({}); - - if let Some(ctx_entities) = &self.context_entities { - for attr in ctx_entities.iter() { - if let CedarType::TypeName(type_name) = &attr.kind { - let id = match id_mapping.get(&attr.key) { - Some(val) => val, - None => Err(BuildJsonCtxError::MissingIdMapping(attr.key.clone()))?, - }; - let type_name = - [attr.namespace.as_str(), type_name].join(CEDAR_POLICY_SEPARATOR); - json[attr.key.as_str()] = json!({"type": type_name, "id": id}); - } - } - } - - Ok(json) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum BuildJsonCtxError { - /// If an entity reference is provided but the ID is missing from `id_mapping`. - /// - /// This is usually caused by: - /// - disabling workload AuthZ but having a Workload entity in the context schema - /// - disabling user AuthZ but referencing User entity in the context schema - #[error( - "An entity reference for `{0}` is required by the schema but an ID was not provided via \ - the `id_mapping`" - )] - MissingIdMapping(String), - /// If a non-entity attribute is provided but the value is missing from `value_mapping`. - #[error( - "A non-entity attribute for `{0}` is required by the schema but a value was not provided \ - via the `value_mapping`" - )] - MissingValueMapping(String), -} - -impl CedarSchemaJson { - /// Find the action in the schema - pub fn find_action( - &self, - action_name: &str, - namespace: &str, - ) -> Result, FindActionError> { - let schema_entities = match self.namespace.get(namespace) { - Some(entities) => entities, - None => return Ok(None), - }; - - let action_schema = match schema_entities.actions.get(action_name) { - Some(schema) => schema, - None => return Ok(None), - }; - - let principal_entities = HashSet::from_iter( - action_schema - .principal_types - .iter() - .map(|principal_type| [namespace, principal_type].join(CEDAR_POLICY_SEPARATOR)), - ); - let resource_entities = HashSet::from_iter( - action_schema - .resource_types - .iter() - .map(|resource_type| [namespace, resource_type].join(CEDAR_POLICY_SEPARATOR)), - ); - let context_entities = action_schema - .context - .as_ref() - .map(|ctx| self.process_action_context(ctx, namespace)) - .transpose()?; - - Ok(Some(Action { - principal_entities, - resource_entities, - context_entities, - schema_entities, - schema: action_schema, - })) - } - - fn process_action_context( - &self, - ctx: &RecordOrType, - namespace: &str, - ) -> Result, FindActionError> { - let mut entities = HashSet::::new(); - - match ctx { - // Case: the context is defined as a record in the schema - // for example: - // Jans { - // action View appliesTo { - // principal: [User], - // resource: [File], - // context: { - // "status": String, - // "id_token": Id_token, - // }, - // }; - // } - RecordOrType::Record(record) => { - for (key, attr) in record.attributes.iter() { - entities.insert(CtxAttribute { - namespace: namespace.to_string(), - key: key.to_string(), - kind: attr.get_type()?, - }); - } - }, - // Case: the context is defined as a type in the schema - // for example: - // Jans { - // type Context = { - // "status": String, - // "id_token": Id_token, - // }; - // action View appliesTo { - // principal: [User], - // resource: [File], - // context: Context, - // }; - // } - RecordOrType::Type(entity_type) => match entity_type { - CedarSchemaEntityType::Primitive(primitive_type) => { - if let PrimitiveTypeKind::TypeName(type_name) = &primitive_type.kind { - let cedar_type = self.find_type(type_name, namespace).unwrap(); - match cedar_type { - SchemaDefinedType::CommonType(common) => { - for (key, attr) in common.attributes.iter() { - entities.insert(CtxAttribute { - namespace: namespace.to_string(), - key: key.to_string(), - kind: attr.get_type()?, - }); - } - }, - SchemaDefinedType::Entity(_) => { - Err(FindActionError::EntityContext(entity_type.clone()))? - }, - } - } - }, - CedarSchemaEntityType::Set(_) => { - Err(FindActionError::SetContext(entity_type.clone()))? - }, - CedarSchemaEntityType::Typed(_) => { - Err(FindActionError::TypedContext(entity_type.clone()))? - }, - }, - } - - Ok(entities) - } -} - -/// Represents an action in the Cedar JSON schema -#[derive(Default, Debug, PartialEq, Clone)] -pub struct ActionSchema { - pub resource_types: HashSet, - pub principal_types: HashSet, - pub context: Option, -} - -impl Serialize for ActionSchema { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut state = serializer.serialize_map(Some(1))?; - state.serialize_entry( - "appliesTo", - &json!({ - "resourceTypes": self.resource_types, - "principalTypes": self.principal_types, - "context": self.context, - }), - )?; - state.end() - } -} - -impl<'de> Deserialize<'de> for ActionSchema { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let mut action = HashMap::>::deserialize(deserializer)?; - let mut action = action - .remove("appliesTo") - .ok_or(de::Error::missing_field("appliesTo"))?; - - let resource_types = action - .remove("resourceTypes") - .map(|val| serde_json::from_value::>(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("resourceTypes"))?; - - let principal_types = action - .remove("principalTypes") - .map(|val| serde_json::from_value::>(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("principalTypes"))?; - - let context = action - .remove("context") - .map(|val| serde_json::from_value::(val).map_err(de::Error::custom)) - .transpose()?; - - Ok(Self { - resource_types, - principal_types, - context, - }) - } -} - -#[derive(Debug, PartialEq, Clone, Serialize)] -pub enum RecordOrType { - Record(CedarSchemaRecord), - Type(CedarSchemaEntityType), + /// e.g.: `kind: "My::Namespace::Action"` + #[serde(rename = "type", default)] + kind: Option, } -impl<'de> Deserialize<'de> for RecordOrType { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let mut context = HashMap::::deserialize(deserializer)?; - let context_type = context - .remove("type") - .map(|val| serde_json::from_value::(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("type"))?; - - match context_type.as_str() { - "Record" => { - let attributes = context - .remove("attributes") - .map(|val| { - serde_json::from_value::>(val) - .map_err(de::Error::custom) - }) - .transpose()? - .ok_or(de::Error::missing_field("attributes"))?; - Ok(RecordOrType::Record(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes, - })) - }, - type_name => Ok(RecordOrType::Type(CedarSchemaEntityType::Primitive( - PrimitiveType { - kind: PrimitiveTypeKind::TypeName(type_name.to_string()), - }, - ))), - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum FindActionError { - #[error("Error while collecting entities from action schema: {0}")] - CollectEntities(#[from] GetCedarTypeError), - #[error("Using `Set` as the context type is unsupported: {0:#?}")] - SetContext(CedarSchemaEntityType), - #[error("Using `Entity` as the context type is unsupported: {0:#?}")] - EntityContext(CedarSchemaEntityType), - #[error("Using `Typed` as the context type is unsupported: {0:#?}")] - TypedContext(CedarSchemaEntityType), +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct AppliesTo { + #[serde(rename = "principalTypes", default)] + pub principal_types: HashSet, + #[serde(rename = "resourceTypes", default)] + pub resource_types: HashSet, + #[serde(default)] + pub context: Option, } #[cfg(test)] -mod test { +mod test_deserialize_action { + use super::super::attribute::Attribute; + use super::{Action, ActionGroup, AppliesTo}; + use serde_json::json; use std::collections::{HashMap, HashSet}; + use test_utils::assert_eq; - use serde::Deserialize; - use serde_json::{Value, json}; - - use super::ActionSchema; - use crate::common::cedar_schema::cedar_json::CedarSchemaRecord; - use crate::common::cedar_schema::cedar_json::action::RecordOrType; - use crate::common::cedar_schema::cedar_json::entity_types::{ - CedarSchemaEntityAttribute, CedarSchemaEntityType, EntityType, PrimitiveType, - PrimitiveTypeKind, - }; - - type ActionType = String; - #[derive(Deserialize, Debug, PartialEq)] - struct MockJsonSchema { - actions: HashMap, - } - - fn build_schema(ctx: Option) -> Value { - let mut schema = json!({ - "actions": { - "Update": { - "appliesTo": { - "resourceTypes": ["Issue"], - "principalTypes": ["Workload", "User"] - } - } + #[test] + fn can_deserialize() { + // Case: both principal types and resource types is empty + let action = json!({ + "appliesTo": { + "principalTypes": [], + "resourceTypes": [], } }); - if let Some(ctx) = ctx { - schema["actions"]["Update"]["appliesTo"]["context"] = ctx; - } - schema - } - - fn build_expected(ctx: Option) -> MockJsonSchema { - MockJsonSchema { - actions: HashMap::from([("Update".to_string(), ActionSchema { - resource_types: HashSet::from(["Issue"].map(|s| s.to_string())), - principal_types: HashSet::from(["Workload", "User"].map(|s| s.to_string())), - context: ctx, - })]), - } - } - - #[test] - pub fn can_deserialize_empty_ctx() { - let schema = build_schema(None); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::new(), + resource_types: HashSet::new(), + context: None, + }, + }); - let expected = build_expected(None); + // Case: resource types is empty + let action = json!({ + "appliesTo": { + "principalTypes": ["PrincipalEntityType1"], + "resourceTypes": [], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::from(["PrincipalEntityType1".into()]), + resource_types: HashSet::new(), + context: None, + }, + }); - assert_eq!(result, expected) + // Case: only principal types is empty + let action = json!({ + "appliesTo": { + "principalTypes": [], + "resourceTypes": ["ResourceEntityType1"], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::new(), + resource_types: HashSet::from(["ResourceEntityType1".into()]), + context: None, + }, + }); } #[test] - pub fn can_deserialize_record_ctx() { - let schema = build_schema(Some(json!({ - "type": "Record", - "attributes": { - "token": { - "type": "EntityOrCommon", - "name": "Access_token" - }, - "username": { - "type": "EntityOrCommon", - "name": "String" - } + fn can_deserialize_with_member_of() { + // Case: action group type is not provided + let action = json!({ + "memberOf": [{"id": "read"}], + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Photo"], } - }))); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); - - let expected = build_expected(Some(RecordOrType::Record(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from([ - ("token".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Access_token".to_string(), - }), - required: true, - }), - ("username".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ]), - }))); + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: Some(HashSet::from([ActionGroup { + id: "read".into(), + kind: None + }])), + applies_to: AppliesTo { + principal_types: HashSet::from(["User".into()]), + resource_types: HashSet::from(["Photo".into()]), + context: None, + }, + }); - assert_eq!(result, expected) + // Case: an action group type is provided + let action = json!({ + "memberOf": [{ + "id": "read", + "type": "My::Namespace::Action", + }], + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Photo"], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: Some(HashSet::from([ActionGroup { + id: "read".into(), + kind: Some("My::Namespace::Action".into()), + }])), + applies_to: AppliesTo { + principal_types: HashSet::from(["User".into()]), + resource_types: HashSet::from(["Photo".into()]), + context: None, + }, + }); } #[test] - pub fn can_deserialize_entity_or_common_ctx() { - let schema = build_schema(Some(json!({ - "type": "Context", - }))); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); - - let expected = build_expected(Some(RecordOrType::Type(CedarSchemaEntityType::Primitive( - PrimitiveType { - kind: PrimitiveTypeKind::TypeName("Context".to_string()), + fn can_deserialize_with_context() { + let action = json!({ + "appliesTo": { + "principalTypes": ["PrincipalEntityType1"], + "resourceTypes": ["ResourceEntityType1"], + "context": { + "type": "Record", + "attributes": { + "field1": { "type": "Boolean" }, + "field2": { "type": "Long" }, + "field3": { "type": "String", "required": false }, + } + }, }, - )))); - - assert_eq!(result, expected) + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::from(["PrincipalEntityType1".into()]), + resource_types: HashSet::from(["ResourceEntityType1".into()]), + context: Some(Attribute::record(HashMap::from([ + ("field1".into(), Attribute::boolean()), + ("field2".into(), Attribute::long()), + ("field3".into(), Attribute::String { required: false }) + ]))), + }, + }); } } diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs new file mode 100644 index 00000000000..91f6492f622 --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs @@ -0,0 +1,283 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::deserialize::*; +use super::*; +use serde::{Deserialize, de}; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Clone)] +pub enum Attribute { + String { + required: bool, + }, + Long { + required: bool, + }, + Boolean { + required: bool, + }, + Record { + required: bool, + attrs: HashMap, + }, + Set { + required: bool, + element: Box, + }, + Entity { + required: bool, + name: EntityName, + }, + Extension { + required: bool, + name: ExtensionName, + }, + EntityOrCommon { + required: bool, + name: EntityOrCommonName, + }, +} + +impl<'de> Deserialize<'de> for Attribute { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut attr = HashMap::::deserialize(deserializer)?; + let kind = attr + .remove("type") + .ok_or(de::Error::missing_field("type"))?; + let required = attr + .remove("required") + .map(serde_json::from_value::) + .transpose() + .map_err(|e| { + de::Error::custom(format!("error while deserializing JSON Value to bool: {e}")) + })? + .unwrap_or(true); + let kind = String::deserialize(&kind).map_err(de::Error::custom)?; + let attr = match kind.as_str() { + "String" => Attribute::String { required }, + "Long" => Attribute::Long { required }, + "Boolean" => Attribute::Boolean { required }, + "Record" => { + let attrs = attr + .remove("attributes") + .ok_or(de::Error::missing_field("attributes"))?; + let attrs = deserialize_record_attrs::(attrs)?; + Self::Record { required, attrs } + }, + "Set" => { + let element = attr + .remove("element") + .ok_or(de::Error::missing_field("element"))?; + let element = serde_json::from_value::(element).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar element attribute: {e}" + )) + })?; + + Self::Set { + required, + element: Box::new(element), + } + }, + "Entity" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::Entity { required, name } + }, + "Extension" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::Extension { required, name } + }, + "EntityOrCommon" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::EntityOrCommon { required, name } + }, + name => Self::EntityOrCommon { + required, + name: name.to_string(), + }, + }; + + Ok(attr) + } +} + +impl Attribute { + pub fn is_required(&self) -> bool { + *match self { + Attribute::String { required } => required, + Attribute::Long { required } => required, + Attribute::Boolean { required } => required, + Attribute::Record { required, .. } => required, + Attribute::Set { required, .. } => required, + Attribute::Entity { required, .. } => required, + Attribute::Extension { required, .. } => required, + Attribute::EntityOrCommon { required, .. } => required, + } + } +} + +#[cfg(test)] +/// Helper methods to easily create required attributes +impl Attribute { + pub fn string() -> Self { + Self::String { required: true } + } + + pub fn long() -> Self { + Self::Long { required: true } + } + + pub fn boolean() -> Self { + Self::Boolean { required: true } + } + + pub fn record(attrs: HashMap) -> Self { + Self::Record { + required: true, + attrs, + } + } + + pub fn set(element: Self) -> Self { + Self::Set { + required: true, + + element: Box::new(element), + } + } + + pub fn entity(name: &str) -> Self { + Self::Entity { + required: true, + name: name.into(), + } + } + + pub fn extension(name: &str) -> Self { + Self::Extension { + required: true, + name: name.into(), + } + } + + pub fn entity_or_common(name: &str) -> Self { + Self::EntityOrCommon { + required: true, + name: name.into(), + } + } +} + +#[cfg(test)] +mod test { + use super::Attribute; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_deserialize_string() { + let attr_json = json!({"type": "String"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::string()); + } + + #[test] + fn can_deserialize_long() { + let attr_json = json!({"type": "Long"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::long()); + } + + #[test] + fn can_deserialize_boolean() { + let attr_json = json!({"type": "Boolean"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::boolean()); + } + + #[test] + fn can_deserialize_record() { + let attr_json = json!({ + "type": "Record", + "attributes": { + "primary": { "type": "String" }, + "secondary": { "type": "String" }, + }, + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + let expected = HashMap::from([ + ("primary".into(), Attribute::string()), + ("secondary".into(), Attribute::string()), + ]); + assert_eq!(deserialized, Attribute::record(expected)); + } + + #[test] + fn can_deserialize_set() { + let attr_json = json!({ + "type": "Set", + "element": { + "type": "EntityOrCommon", + "name": "Subscription" + } + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!( + deserialized, + Attribute::set(Attribute::entity_or_common("Subscription")) + ); + } + + #[test] + fn can_deserialize_entity() { + let attr_json = json!({ + "type": "Entity", + "name": "Role", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::entity("Role")); + } + + #[test] + fn can_deserialize_extension() { + let attr_json = json!({ + "type": "Extension", + "name": "decimal", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::extension("decimal"),); + } + + #[test] + fn can_deserialize_entity_or_common() { + let attr_json = json!({ + "type": "EntityOrCommon", + "name": "String", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::entity_or_common("String"),); + } + + #[test] + fn can_deserialize_non_required_attr() { + let attr_json = json!({"type": "String", "required": false}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::String { required: false }); + } +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs new file mode 100644 index 00000000000..55cf8cfd77d --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs @@ -0,0 +1,36 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use serde::de; +use serde_json::Value; +use std::collections::HashMap; + +/// Deserialize a [`Value`] to a to the attrs of a [`AttributeKind::Record`] +pub fn deserialize_record_attrs<'de, D>( + attrs: Value, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let attrs_json = serde_json::from_value::>(attrs).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar record attribute: {e}" + )) + })?; + + // loop through each attr then deserialize into Self + let mut attrs = HashMap::::new(); + for (key, val) in attrs_json.into_iter() { + let val = serde_json::from_value::(val).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar record attribute: {e}" + )) + })?; + attrs.insert(key, val); + } + + Ok(attrs) +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs new file mode 100644 index 00000000000..217d1c32791 --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs @@ -0,0 +1,171 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::attribute::Attribute; +use super::deserialize::*; +use super::*; +use serde::{Deserialize, de}; +use serde_json::Value; +use std::collections::HashSet; + +#[derive(Debug, PartialEq, Clone)] +pub struct EntityShape { + pub required: bool, + pub attrs: HashMap, +} + +#[cfg(test)] +impl EntityShape { + pub fn required(attrs: HashMap) -> Self { + Self { + required: true, + attrs, + } + } +} + +#[derive(Debug, PartialEq, Deserialize, Clone)] +pub struct EntityType { + #[serde(rename = "memberOfTypes")] + pub member_of: Option>, + #[serde(deserialize_with = "deserialize_entity_shape", default)] + pub shape: Option, + #[serde(default)] + pub tags: Option, +} + +// Forces the `shape` field into the [`AttributeKind::Shape`] variant. +fn deserialize_entity_shape<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let mut attr = HashMap::::deserialize(deserializer)?; + let kind = attr + .remove("type") + .ok_or(de::Error::missing_field("type"))?; + let required = attr + .remove("required") + .map(serde_json::from_value::) + .transpose() + .map_err(|e| { + de::Error::custom(format!("error while deserializing JSON Value to bool: {e}")) + })? + .unwrap_or(true); + let kind = String::deserialize(&kind).map_err(de::Error::custom)?; + let attr = match kind.as_str() { + "Record" => { + let attrs = attr + .remove("attributes") + .ok_or(de::Error::missing_field("attributes"))?; + let attrs = deserialize_record_attrs::(attrs)?; + EntityShape { required, attrs } + }, + variant => { + return Err(de::Error::custom(format!( + "invalid type: {}, expected {}", + variant, "Record" + ))); + }, + }; + + Ok(Some(attr)) +} + +#[cfg(test)] +mod test_deserialize_entity_type { + use super::super::attribute::Attribute; + use super::*; + use serde_json::json; + use std::collections::{HashMap, HashSet}; + use test_utils::assert_eq; + + #[test] + fn can_deserialize() { + let entity_type = json!({ + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + }); + let entity_type = serde_json::from_value::(entity_type).unwrap(); + assert_eq!(entity_type, EntityType { + member_of: None, + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: None, + }); + } + + #[test] + fn can_deserialize_with_member_of() { + let with_member_of = json!({ + "memberOfTypes": ["UserGroup"], + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + }); + let with_member_of = serde_json::from_value::(with_member_of).unwrap(); + assert_eq!(with_member_of, EntityType { + member_of: Some(HashSet::from(["UserGroup".into()])), + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: None, + }); + } + + #[test] + fn can_deserialize_with_tags() { + let with_tags = json!({ + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + "tags": { + "type": "Set", + "element": { + "type": "EntityOrCommon", + "name": "String" + } + } + }); + let with_tags = serde_json::from_value::(with_tags).unwrap(); + assert_eq!(with_tags, EntityType { + member_of: None, + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: Some(Attribute::set(Attribute::entity_or_common("String",))) + }); + } + + #[test] + fn errors_on_invalid_shape() { + let entity_type = json!({ + "shape": { + "type": "Set", + }, + }); + let err = serde_json::from_value::(entity_type).unwrap_err(); + assert!( + err.to_string() + .contains("invalid type: Set, expected Record") + ); + } +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs deleted file mode 100644 index 9d855ed6449..00000000000 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs +++ /dev/null @@ -1,226 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::HashMap; - -use super::{CedarType, GetCedarTypeError}; - -/// CedarSchemaEntityShape hold shape of an entity. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaEntityShape { - pub shape: Option, -} - -/// CedarSchemaRecord defines type name and attributes for an entity. -/// Record ::= '"type": "Record", "attributes": {' [ RecordAttr { ',' RecordAttr } ] '}' -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaRecord { - #[serde(rename = "type")] - pub entity_type: String, - // represent RecordAttr - // RecordAttr ::= STR ': {' Type [',' '"required"' ':' ( true | false )] '}' - // attributes as key is used attribute name - pub attributes: HashMap, -} - -impl CedarSchemaRecord { - // if we want to create entity from attributes it should be record - pub fn is_record(&self) -> bool { - self.entity_type == "Record" - } -} - -/// CedarSchemaRecordAttr defines possible type variants of the entity attribute. -/// RecordAttr ::= STR ': {' Type [',' '"required"' ':' ( true | false )] '}' -#[derive(Debug, Clone, PartialEq, serde::Serialize, Hash)] -pub struct CedarSchemaEntityAttribute { - pub cedar_type: CedarSchemaEntityType, - pub required: bool, -} - -impl CedarSchemaEntityAttribute { - pub fn is_required(&self) -> bool { - self.required - } - - pub fn get_type(&self) -> Result { - self.cedar_type.get_type() - } -} - -impl<'de> serde::Deserialize<'de> for CedarSchemaEntityAttribute { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value: serde_json::Value = serde::Deserialize::deserialize(deserializer)?; - - // used only for deserialization - #[derive(serde::Deserialize)] - pub struct IsRequired { - required: Option, - } - - let is_required = IsRequired::deserialize(&value).map_err(|err| { - serde::de::Error::custom(format!( - "could not deserialize CedarSchemaEntityAttribute, field 'is_required': {}", - err - )) - })?; - - let cedar_type = CedarSchemaEntityType::deserialize(value).map_err(|err| { - serde::de::Error::custom(format!( - "could not deserialize CedarSchemaEntityType: {}", - err - )) - })?; - - Ok(CedarSchemaEntityAttribute { - cedar_type, - required: is_required.required.unwrap_or(true), - }) - } -} - -#[derive(Debug, Clone, PartialEq, serde::Serialize, Hash)] -pub enum CedarSchemaEntityType { - Set(Box), - Typed(EntityType), - Primitive(PrimitiveType), -} - -impl CedarSchemaEntityType { - pub fn get_type(&self) -> Result { - match self { - Self::Set(v) => Ok(CedarType::Set(Box::new(v.element.get_type()?))), - Self::Typed(v) => v.get_type(), - Self::Primitive(primitive) => Ok(primitive.kind.get_type()), - } - } -} - -impl<'de> serde::Deserialize<'de> for CedarSchemaEntityType { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - // is used only on deserialization. - #[derive(serde::Deserialize)] - struct TypeStruct { - #[serde(rename = "type")] - type_name: String, - } - - let value: serde_json::Value = serde::Deserialize::deserialize(deserializer)?; - - let entity_type = match TypeStruct::deserialize(&value) - .map_err(serde::de::Error::custom)? - .type_name - .as_str() - { - "Set" => { - CedarSchemaEntityType::Set(Box::new(SetEntityType::deserialize(&value).map_err( - |err| serde::de::Error::custom(format!("failed to deserialize Set: {}", err)), - )?)) - }, - "EntityOrCommon" => { - CedarSchemaEntityType::Typed(EntityType::deserialize(&value).map_err(|err| { - serde::de::Error::custom(format!( - "failed to deserialize EntityOrCommon: {}", - err - )) - })?) - }, - _ => CedarSchemaEntityType::Primitive(PrimitiveType::deserialize(&value).map_err( - |err| { - // will newer happen because we know that field "type" is string - serde::de::Error::custom(format!( - "failed to deserialize PrimitiveType: {}", - err - )) - }, - )?), - }; - - Ok(entity_type) - } -} - -/// The Primitive element describes -/// Primitive ::= '"type":' ('"Long"' | '"String"' | '"Boolean"' | TYPENAME) -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Hash)] -pub struct PrimitiveType { - #[serde(rename = "type")] - pub kind: PrimitiveTypeKind, -} - -/// Variants of primitive type. -/// Primitive ::= '"type":' ('"Long"' | '"String"' | '"Boolean"' | TYPENAME) -#[derive(Debug, Clone, serde::Serialize, PartialEq, Hash)] -pub enum PrimitiveTypeKind { - Long, - String, - Boolean, - TypeName(String), -} - -impl PrimitiveTypeKind { - pub fn get_type(&self) -> CedarType { - match self { - PrimitiveTypeKind::Long => CedarType::Long, - PrimitiveTypeKind::String => CedarType::String, - PrimitiveTypeKind::Boolean => CedarType::Boolean, - PrimitiveTypeKind::TypeName(name) => CedarType::TypeName(name.to_string()), - } - } -} - -/// impement custom deserialization to deserialize it correctly -impl<'de> serde::Deserialize<'de> for PrimitiveTypeKind { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s: String = serde::Deserialize::deserialize(deserializer)?; - match s.as_str() { - "Long" => Ok(PrimitiveTypeKind::Long), - "String" => Ok(PrimitiveTypeKind::String), - "Boolean" => Ok(PrimitiveTypeKind::Boolean), - _ => Ok(PrimitiveTypeKind::TypeName(s)), - } - } -} - -/// This structure can hold `Extension`, `EntityOrCommon`, `EntityRef` -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Hash)] -pub struct EntityType { - // it also can be primitive type - #[serde(rename = "type")] - pub kind: String, - pub name: String, -} - -impl EntityType { - pub fn get_type(&self) -> Result { - if self.kind == "EntityOrCommon" { - match self.name.as_str() { - "Long" => Ok(CedarType::Long), - "String" => Ok(CedarType::String), - "Boolean" => Ok(CedarType::Boolean), - type_name => Ok(CedarType::TypeName(type_name.to_string())), - } - } else { - Err(GetCedarTypeError::TypeNotImplemented(self.kind.to_string())) - } - } -} - -/// Describes the Set element -/// Set ::= '"type": "Set", "element": ' TypeJson -// "type": "Set" checked during deserialization -#[derive(Debug, Clone, serde::Deserialize, PartialEq, serde::Serialize, Hash)] -pub struct SetEntityType { - pub element: CedarSchemaEntityType, -} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs b/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs index dda9cef224f..6d0489ce491 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs @@ -3,8 +3,8 @@ // // Copyright (c) 2024, Gluu, Inc. -pub(crate) use cedar_json::CedarSchemaJson; pub(crate) mod cedar_json; +pub(crate) const CEDAR_NAMESPACE_SEPARATOR: &str = "::"; /// cedar_schema value which specifies both encoding and content_type /// diff --git a/jans-cedarling/cedarling/src/common/policy_store.rs b/jans-cedarling/cedarling/src/common/policy_store.rs index 794a00babd8..cba32133696 100644 --- a/jans-cedarling/cedarling/src/common/policy_store.rs +++ b/jans-cedarling/cedarling/src/common/policy_store.rs @@ -202,7 +202,7 @@ pub struct TokensMetadata<'a> { pub tx_tokens: &'a TokenEntityMetadata, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub enum TokenKind { /// Access token used for granting access to resources. Access, diff --git a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs index d959e724d21..6137dc069ce 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs @@ -17,9 +17,29 @@ use serde_json::Value; pub struct ClaimMappings(HashMap); impl ClaimMappings { - pub fn get_mapping(&self, field: &str, cedar_policy_type: &str) -> Option<&ClaimMapping> { + pub fn get(&self, claim: &str) -> Option<&ClaimMapping> { + self.0.get(claim) + } + + // returns (claim_name, &ClaimMapping) + pub fn get_mapping_for_type(&self, type_name: &str) -> Option<(&String, &ClaimMapping)> { + // PERF: we can probably avoiding iterating through all of this by changing the + // `claim_mapping` in the Token Entity Metadata Schema + self.0 + .iter() + .find_map(|(claim_name, mapping)| match mapping { + ClaimMapping::Regex(regex_mapping) => { + (regex_mapping.cedar_policy_type == type_name).then_some((claim_name, mapping)) + }, + ClaimMapping::Json { r#type } => { + (r#type == type_name).then_some((claim_name, mapping)) + }, + }) + } + + pub fn get_mapping(&self, claim: &str, cedar_policy_type: &str) -> Option<&ClaimMapping> { self.0 - .get(field) + .get(claim) .filter(|claim_mapping| match claim_mapping { ClaimMapping::Regex(regexp_mapping) => { regexp_mapping.cedar_policy_type == cedar_policy_type diff --git a/jans-cedarling/cedarling/src/init/service_factory.rs b/jans-cedarling/cedarling/src/init/service_factory.rs index 9b7ea155cd9..6438248f1a1 100644 --- a/jans-cedarling/cedarling/src/init/service_factory.rs +++ b/jans-cedarling/cedarling/src/init/service_factory.rs @@ -7,16 +7,14 @@ //! Module to lazily initialize internal cedarling services -use std::sync::Arc; - -use crate::bootstrap_config::BootstrapConfig; -use crate::common::policy_store::PolicyStoreWithID; -use crate::jwt::{JwtService, JwtServiceInitError}; - use super::service_config::ServiceConfig; use crate::authz::{Authz, AuthzConfig}; +use crate::bootstrap_config::BootstrapConfig; use crate::common::app_types; +use crate::common::policy_store::PolicyStoreWithID; +use crate::jwt::{JwtService, JwtServiceInitError}; use crate::log; +use std::sync::Arc; #[derive(Clone)] pub(crate) struct ServiceFactory<'a> { diff --git a/jans-cedarling/cedarling/src/jwt/mod.rs b/jans-cedarling/cedarling/src/jwt/mod.rs index 9f671ac871e..98f39586194 100644 --- a/jans-cedarling/cedarling/src/jwt/mod.rs +++ b/jans-cedarling/cedarling/src/jwt/mod.rs @@ -24,7 +24,7 @@ use std::sync::Arc; pub use jsonwebtoken::Algorithm; use key_service::{KeyService, KeyServiceError}; -pub use token::{Token, TokenClaim, TokenClaimTypeError, TokenClaims, TokenStr}; +pub use token::{Token, TokenClaimTypeError, TokenClaims, TokenStr}; use validator::{JwtValidator, JwtValidatorConfig, JwtValidatorError}; use crate::common::policy_store::TrustedIssuer; diff --git a/jans-cedarling/cedarling/src/jwt/token.rs b/jans-cedarling/cedarling/src/jwt/token.rs index 58a28f31b57..47e5ab3b7a0 100644 --- a/jans-cedarling/cedarling/src/jwt/token.rs +++ b/jans-cedarling/cedarling/src/jwt/token.rs @@ -81,13 +81,13 @@ impl<'a> Token<'a> { self.claims.logging_info(claim) } - pub fn claims(&self) -> &TokenClaims { - &self.claims + pub fn claims_value(&self) -> &HashMap { + &self.claims.claims } } /// A struct holding information on a decoded JWT. -#[derive(Debug, PartialEq, Default, Deserialize)] +#[derive(Debug, PartialEq, Default, Deserialize, Clone)] pub struct TokenClaims { #[serde(flatten)] claims: HashMap, @@ -100,14 +100,11 @@ impl From> for TokenClaims { } impl TokenClaims { + #[cfg(test)] pub fn new(claims: HashMap) -> Self { Self { claims } } - pub fn from_json_map(map: serde_json::Map) -> Self { - Self::new(HashMap::from_iter(map)) - } - pub fn get_claim(&self, name: &str) -> Option { self.claims.get(name).map(|value| TokenClaim { key: name.to_string(), @@ -132,22 +129,10 @@ pub struct TokenClaim<'a> { } impl TokenClaim<'_> { - pub fn key(&self) -> &str { - &self.key - } - pub fn value(&self) -> &serde_json::Value { self.value } - pub fn as_i64(&self) -> Result { - self.value - .as_i64() - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "i64", self.value, - )) - } - pub fn as_str(&self) -> Result<&str, TokenClaimTypeError> { self.value .as_str() @@ -155,39 +140,10 @@ impl TokenClaim<'_> { &self.key, "String", self.value, )) } - - pub fn as_bool(&self) -> Result { - self.value - .as_bool() - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "bool", self.value, - )) - } - - pub fn as_array(&self) -> Result, TokenClaimTypeError> { - self.value - .as_array() - .map(|array| { - array - .iter() - .enumerate() - .map(|(i, v)| { - TokenClaim { - // show current key and index in array - key: format!("{}[{}]", self.key, i), - value: v, - } - }) - .collect() - }) - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "Array", self.value, - )) - } } -#[derive(Debug, thiserror::Error)] -#[error("type mismatch for key '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] +#[derive(Debug, thiserror::Error, PartialEq)] +#[error("type mismatch for token claim '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] pub struct TokenClaimTypeError { pub key: String, pub expected_type: String, @@ -208,7 +164,7 @@ impl TokenClaimTypeError { } /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. - fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { + pub fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { let got_value_type_name = Self::json_value_type_name(got_value); Self { diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 7f60bcc1d6c..389b4ab177f 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -45,8 +45,6 @@ use log::interface::LogWriter; use log::{LogEntry, LogType}; pub use log::{LogLevel, LogStorage}; -pub use crate::authz::entities::CreateCedarEntityError; - #[doc(hidden)] pub mod bindings { pub use cedar_policy; @@ -124,12 +122,12 @@ impl Cedarling { /// Get entites derived from `cedar-policy` schema and tokens for `authorize` request. #[doc(hidden)] #[cfg(test)] - pub async fn authorize_entities_data( + pub async fn build_entities( &self, request: &Request, ) -> Result { let tokens = self.authz.decode_tokens(request).await?; - self.authz.build_entities(request, &tokens).await + self.authz.build_entities(request, &tokens) } } diff --git a/jans-cedarling/cedarling/src/tests/mapping_entities.rs b/jans-cedarling/cedarling/src/tests/mapping_entities.rs index 28f11b40192..05dde523b33 100644 --- a/jans-cedarling/cedarling/src/tests/mapping_entities.rs +++ b/jans-cedarling/cedarling/src/tests/mapping_entities.rs @@ -10,16 +10,17 @@ //! CEDARLING_MAPPING_ACCESS_TOKEN //! CEDARLING_MAPPING_USERINFO_TOKEN +use super::utils::*; +use crate::authz::entity_builder::{ + BuildCedarlingEntityError, BuildEntityError, BuildTokenEntityError, +}; +use crate::common::policy_store::TokenKind; +use crate::{AuthorizeError, Cedarling, cmp_decision, cmp_policy}; +use cedarling_util::get_raw_config; use std::collections::HashSet; use std::sync::LazyLock; -use tokio::test; - -use cedarling_util::get_raw_config; use test_utils::assert_eq; - -use super::utils::*; -use crate::common::policy_store::TokenKind; -use crate::{AuthorizeError, Cedarling, CreateCedarEntityError, cmp_decision, cmp_policy}; +use tokio::test; static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_entity_mapping.yaml"); @@ -112,8 +113,6 @@ async fn test_default_mapping() { /// This function validates the mapping of users and workloads using the defined `cedar` schema. /// For other entities, currently, it is not possible to automatically validate the mapping. /// -/// TODO: Add validation for `IdToken`, `Access_token`, and `Userinfo_token` once they are added to the context. -/// /// Note: Verified that the mapped entity types are present in the logs. #[test] async fn test_custom_mapping() { @@ -189,14 +188,14 @@ async fn test_failed_user_mapping() { .expect_err("request should be parsed with mapping error"); match err { - AuthorizeError::CreateUserEntity(error) => { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::User(error)) => { assert_eq!(error.errors.len(), 2, "there should be 2 errors"); let (token_kind, err) = &error.errors[0]; assert_eq!(token_kind, &TokenKind::Userinfo); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), - "expected CouldNotFindEntity({}), got: {:?}", + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), + "expected EntityNotInSchema({}), got: {:?}", &entity_type, err, ); @@ -204,13 +203,13 @@ async fn test_failed_user_mapping() { let (token_kind, err) = &error.errors[1]; assert_eq!(token_kind, &TokenKind::Id); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), - "expected CouldNotFindEntity({}), got: {:?}", + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), + "expected EntityNotInSchema({}), got: {:?}", &entity_type, err, ); }, - _ => panic!("expected error CreateWorkloadEntity"), + _ => panic!("expected error BuildCedarlingEntityError::User"), } } @@ -237,14 +236,14 @@ async fn test_failed_workload_mapping() { .expect_err("request should be parsed with mapping error"); match err { - AuthorizeError::CreateWorkloadEntity(error) => { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::Workload(error)) => { assert_eq!(error.errors.len(), 2, "there should be 2 errors"); // check for access token error let (token_kind, err) = &error.errors[0]; assert_eq!(token_kind, &TokenKind::Access); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), "expected CouldNotFindEntity(\"{}\"), got: {:?}", &entity_type, err, @@ -254,13 +253,16 @@ async fn test_failed_workload_mapping() { let (token_kind, err) = &error.errors[1]; assert_eq!(token_kind, &TokenKind::Id); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), "expected CouldNotFindEntity(\"{}\"), got: {:?}", &entity_type, err, ); }, - _ => panic!("expected error CreateWorkloadEntity"), + _ => panic!( + "expected BuildEntity(BuildCedarlingEntityError::Workload(_))) error, got: {:?}", + err + ), } } @@ -285,13 +287,22 @@ async fn test_failed_id_token_mapping() { .await .expect_err("request should be parsed with mapping error"); - assert!( - matches!( - err, - AuthorizeError::CreateIdTokenEntity(CreateCedarEntityError::CouldNotFindEntity(_)) + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::IdToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Id); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedIdTokenNotExist"), + "expected EntityNotInSchema(\"MappedIdTokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!( + "expected BuildEntity(BuildCedarlingEntityError::IdToken(_)) error, got: {:?}", + err ), - "should be error CouldNotFindEntity, got: {err:?}" - ); + } } /// Check if we get error on mapping access_token to undefined entity @@ -315,13 +326,19 @@ async fn test_failed_access_token_mapping() { .await .expect_err("request should be parsed with mapping error"); - assert!( - matches!( - err, - AuthorizeError::CreateAccessTokenEntity(CreateCedarEntityError::CouldNotFindEntity(_)) - ), - "should be error CouldNotFindEntity" - ); + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::AccessToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Access); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedAccess_tokenNotExist"), + "expected EntityNotInSchema(\"MappedAccess_tokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!("expected BuildEntity error, got: {:?}", err), + } } /// Check if we get error on mapping userinfo_token to undefined entity @@ -345,15 +362,19 @@ async fn test_failed_userinfo_token_mapping() { .await .expect_err("request should be parsed with mapping error"); - assert!( - matches!( - err, - AuthorizeError::CreateUserinfoTokenEntity(CreateCedarEntityError::CouldNotFindEntity( - _ - )) - ), - "should be error CouldNotFindEntity" - ); + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::UserinfoToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Userinfo); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedUserinfo_tokenNotExist"), + "expected EntityNotInSchema(\"MappedUserinfo_tokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!("expected BuildEntity error, got: {:?}", err), + } } /// Check if we get roles mapping from all tokens. @@ -416,7 +437,7 @@ async fn test_role_many_tokens_mapping() { // iterate over roles that created and filter expected roles let roles_left = cedarling - .authorize_entities_data(&request) + .build_entities(&request) .await .expect("should get authorize_entities_data without errors") .roles diff --git a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs index a90f7d2b949..f1bdb7e9be7 100644 --- a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs +++ b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs @@ -110,7 +110,7 @@ async fn check_mapping_tokens_data() { .expect("Request should be deserialized from json"); let entities = cedarling - .authorize_entities_data(&request) + .build_entities(&request) .await // log err to be human readable .inspect_err(|err| println!("Error: {}", err.to_string())) From 9dbcb0dead119e2bf780bda9153b84c8ce379266 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Thu, 16 Jan 2025 17:12:19 +0200 Subject: [PATCH 2/2] fix(jans-auth-server): NPE during client name rendering #10663 Signed-off-by: YuriyZ --- .../as/server/authorize/ws/rs/AuthorizeAction.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java index 35384a724db..9433e274ce3 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java @@ -86,6 +86,7 @@ @Named public class AuthorizeAction { + public static final String UNKNOWN = "Unknown"; @Inject private Logger log; @@ -985,7 +986,7 @@ public String getClientDisplayName() { log.trace("client {}", clientId); if (StringUtils.isBlank(clientId)) { - return "Unknown"; + return UNKNOWN; } final Client client = clientService.getClient(clientId); @@ -994,15 +995,19 @@ public String getClientDisplayName() { public String getClientDisplayName(final Client client) { log.trace("client {}", client); - + if (client == null) { - getClientDisplayName(); + return UNKNOWN; } return getCheckedClientDisplayName(client); } private String getCheckedClientDisplayName(final Client client) { + if (client == null) { + return UNKNOWN; + } + if (StringUtils.isNotBlank(client.getClientName())) { return client.getClientName(); } @@ -1011,7 +1016,7 @@ private String getCheckedClientDisplayName(final Client client) { return client.getClientId(); } - return "Unknown"; + return UNKNOWN; } public String getAuthReqId() {