From 69552fe4b69f352cd1c6eff7acbb739366e1ff0f Mon Sep 17 00:00:00 2001 From: Eric Torreborre Date: Mon, 13 Nov 2023 17:24:30 +0100 Subject: [PATCH] feat(rust): persist application data in a database --- Cargo.lock | 394 ++++++- .../rust/example_projects/no_std/Cargo.toml | 2 +- examples/rust/get_started/Cargo.toml | 2 + .../06-credentials-exchange-issuer.rs | 4 +- .../06-credentials-exchange-server.rs | 2 +- ...bute-based-authentication-control-plane.rs | 29 +- ...tribute-based-authentication-edge-plane.rs | 28 +- .../ockam/ockly/native/ockly/Cargo.lock | 666 +++++++++++- .../ockam/ockly/native/ockly/src/lib.rs | 24 +- implementations/rust/ockam/ockam/Cargo.toml | 6 +- implementations/rust/ockam/ockam/src/lib.rs | 82 +- implementations/rust/ockam/ockam/src/node.rs | 77 +- .../rust/ockam/ockam_abac/Cargo.toml | 8 +- .../src/attribute_access_control.rs | 20 +- .../rust/ockam/ockam_abac/src/lib.rs | 4 +- .../rust/ockam/ockam_abac/src/mem.rs | 134 --- .../rust/ockam/ockam_abac/src/policy.rs | 25 +- .../ockam_abac/src/storage/lmdb_storage.rs | 82 -- .../rust/ockam/ockam_abac/src/storage/mod.rs | 27 +- .../policy_repository.rs} | 6 +- .../src/storage/policy_repository_sql.rs | 149 +++ .../ockam_abac/src/storage/sqlite_storage.rs | 166 --- .../rust/ockam/ockam_api/Cargo.toml | 9 +- .../rust/ockam/ockam_api/README.md | 112 +- .../rust/ockam/ockam_api/src/auth.rs | 20 +- .../src/authenticator/direct/authenticator.rs | 29 +- .../enrollment_tokens/acceptor.rs | 4 +- .../enrollment_tokens/authenticator.rs | 6 +- .../ockam_api/src/authority_node/authority.rs | 93 +- .../src/authority_node/configuration.rs | 12 +- .../src/bootstrapped_identities_store.rs | 93 +- .../ockam_api/src/cli_state/credentials.rs | 233 +++-- .../ockam_api/src/cli_state/enrollment.rs | 330 ++++++ .../ockam_api/src/cli_state/identities.rs | 745 ++++++++----- .../rust/ockam/ockam_api/src/cli_state/mod.rs | 800 ++++---------- .../ockam/ockam_api/src/cli_state/nodes.rs | 986 ++++++------------ .../ockam/ockam_api/src/cli_state/projects.rs | 252 ++--- .../src/cli_state/projects_repository.rs | 14 + .../src/cli_state/projects_repository_sql.rs | 505 +++++++++ .../ockam/ockam_api/src/cli_state/spaces.rs | 161 +-- .../src/cli_state/spaces_repository.rs | 13 + .../src/cli_state/spaces_repository_sql.rs | 246 +++++ .../ockam/ockam_api/src/cli_state/traits.rs | 347 ------ .../ockam_api/src/cli_state/trust_contexts.rs | 332 ++++-- .../cli_state/trust_contexts_repository.rs | 13 + .../trust_contexts_repository_sql.rs | 413 ++++++++ .../ockam_api/src/cli_state/user_info.rs | 74 -- .../ockam/ockam_api/src/cli_state/users.rs | 36 + .../src/cli_state/users_repository.rs | 13 + .../src/cli_state/users_repository_sql.rs | 182 ++++ .../ockam/ockam_api/src/cli_state/vaults.rs | 291 ++---- .../rust/ockam/ockam_api/src/cloud/addon.rs | 38 +- .../rust/ockam/ockam_api/src/cloud/project.rs | 189 +++- .../ockam_api/src/cloud/secure_clients.rs | 2 +- .../ockam/ockam_api/src/cloud/share/create.rs | 35 +- .../rust/ockam/ockam_api/src/cloud/space.rs | 131 ++- .../rust/ockam/ockam_api/src/config/cli.rs | 385 ------- .../rust/ockam/ockam_api/src/config/lookup.rs | 238 +---- .../rust/ockam/ockam_api/src/config/mod.rs | 1 - .../rust/ockam/ockam_api/src/identity.rs | 16 +- .../src/identity/credentials_repository.rs | 19 + .../identity/credentials_repository_sql.rs | 159 +++ .../src/identity/enrollment_ticket.rs | 12 +- .../src/identity/identities_repository.rs | 120 +++ .../src/identity/identities_repository_sql.rs | 344 ++++++ .../src/identity/vaults_repository.rs | 81 ++ .../src/identity/vaults_repository_sql.rs | 191 ++++ .../outlet_service/interceptor_listener.rs | 4 +- .../ockam_api/src/kafka/secure_channel_map.rs | 11 +- .../rust/ockam/ockam_api/src/lib.rs | 114 +- .../rust/ockam/ockam_api/src/nodes/config.rs | 118 --- .../rust/ockam/ockam_api/src/nodes/mod.rs | 4 +- .../src/nodes/models/transport/json.rs | 6 + .../src/nodes/nodes_repository_sql.rs | 368 +++++++ .../rust/ockam/ockam_api/src/nodes/service.rs | 217 ++-- .../src/nodes/service/background_node.rs | 60 +- .../src/nodes/service/credentials.rs | 38 +- .../src/nodes/service/in_memory_node.rs | 76 +- .../ockam_api/src/nodes/service/message.rs | 2 +- .../src/nodes/service/node_identities.rs | 54 - .../src/nodes/service/node_services.rs | 14 +- .../ockam_api/src/nodes/service/policy.rs | 25 +- .../ockam_api/src/nodes/service/portals.rs | 78 +- .../ockam_api/src/nodes/service/projects.rs | 95 ++ .../ockam_api/src/nodes/service/relay.rs | 7 +- .../src/nodes/service/secure_channel.rs | 96 +- .../rust/ockam/ockam_api/src/okta/mod.rs | 16 +- .../rust/ockam/ockam_api/src/trust_context.rs | 111 -- .../rust/ockam/ockam_api/src/util.rs | 105 +- .../rust/ockam/ockam_api/tests/auth_smoke.rs | 4 +- .../rust/ockam/ockam_api/tests/authority.rs | 16 +- .../ockam_api/tests/credential_issuer.rs | 13 +- .../rust/ockam/ockam_app/Cargo.toml | 1 + .../rust/ockam/ockam_app/src/app/state/mod.rs | 78 +- .../ockam/ockam_app/src/app/state/model.rs | 41 + .../ockam_app/src/app/state/repository.rs | 81 +- .../ockam/ockam_app/src/enroll/enroll_user.rs | 32 +- .../ockam_app/src/invitations/commands.rs | 60 +- .../ockam/ockam_app/src/invitations/mod.rs | 9 +- .../ockam/ockam_app/src/projects/commands.rs | 12 +- .../src/shared_service/relay/create.rs | 6 +- .../src/shared_service/tcp_outlet/state.rs | 2 +- .../rust/ockam/ockam_app_lib/Cargo.toml | 4 + .../ockam_app_lib/src/enroll/enroll_user.rs | 30 +- .../src/incoming_services/commands.rs | 20 +- .../src/incoming_services/mod.rs | 2 +- .../src/incoming_services/state.rs | 46 +- .../ockam_app_lib/src/invitations/commands.rs | 8 +- .../ockam_app_lib/src/invitations/mod.rs | 9 +- .../rust/ockam/ockam_app_lib/src/log.rs | 1 - .../ockam_app_lib/src/projects/commands.rs | 29 +- .../src/shared_service/relay/create.rs | 35 +- .../src/shared_service/tcp_outlet/state.rs | 12 +- .../rust/ockam/ockam_app_lib/src/state/mod.rs | 92 +- .../ockam/ockam_app_lib/src/state/model.rs | 6 +- .../ockam_app_lib/src/state/repository.rs | 198 +++- .../rust/ockam/ockam_command/Cargo.toml | 1 + .../ockam/ockam_command/src/authenticated.rs | 6 +- .../ockam_command/src/authority/create.rs | 159 ++- .../ockam_command/src/configuration/get.rs | 22 +- .../src/configuration/get_default_node.rs | 16 +- .../ockam_command/src/configuration/list.rs | 14 +- .../src/configuration/set_default_node.rs | 19 +- .../ockam/ockam_command/src/credential/get.rs | 6 +- .../ockam_command/src/credential/issue.rs | 47 +- .../ockam_command/src/credential/list.rs | 16 +- .../ockam/ockam_command/src/credential/mod.rs | 96 +- .../ockam_command/src/credential/present.rs | 6 +- .../ockam_command/src/credential/show.rs | 48 +- .../ockam_command/src/credential/store.rs | 110 +- .../ockam_command/src/credential/verify.rs | 108 +- .../ockam/ockam_command/src/enroll/command.rs | 290 +++--- .../src/flow_control/add_consumer.rs | 7 +- .../ockam_command/src/identity/create.rs | 78 +- .../ockam_command/src/identity/default.rs | 67 +- .../ockam_command/src/identity/delete.rs | 6 +- .../ockam/ockam_command/src/identity/list.rs | 43 +- .../ockam/ockam_command/src/identity/mod.rs | 77 +- .../ockam/ockam_command/src/identity/show.rs | 70 +- .../src/kafka/consumer/create.rs | 2 - .../src/kafka/consumer/delete.rs | 9 +- .../ockam_command/src/kafka/consumer/list.rs | 20 +- .../ockam_command/src/kafka/direct/create.rs | 2 - .../ockam_command/src/kafka/direct/delete.rs | 9 +- .../ockam_command/src/kafka/direct/list.rs | 20 +- .../ockam_command/src/kafka/direct/rpc.rs | 7 +- .../ockam_command/src/kafka/outlet/create.rs | 4 +- .../src/kafka/producer/create.rs | 2 - .../src/kafka/producer/delete.rs | 9 +- .../ockam_command/src/kafka/producer/list.rs | 20 +- .../ockam/ockam_command/src/kafka/util.rs | 7 +- .../ockam/ockam_command/src/lease/create.rs | 2 - .../ockam/ockam_command/src/lease/list.rs | 2 - .../rust/ockam/ockam_command/src/lease/mod.rs | 96 +- .../ockam/ockam_command/src/lease/revoke.rs | 2 - .../ockam/ockam_command/src/lease/show.rs | 2 - .../rust/ockam/ockam_command/src/lib.rs | 131 ++- .../ockam/ockam_command/src/message/send.rs | 42 +- .../ockam/ockam_command/src/node/create.rs | 184 ++-- .../ockam/ockam_command/src/node/default.rs | 34 +- .../ockam/ockam_command/src/node/delete.rs | 58 +- .../rust/ockam/ockam_command/src/node/list.rs | 82 +- .../rust/ockam/ockam_command/src/node/logs.rs | 29 +- .../rust/ockam/ockam_command/src/node/mod.rs | 93 +- .../rust/ockam/ockam_command/src/node/show.rs | 207 ++-- .../ockam/ockam_command/src/node/start.rs | 71 +- .../rust/ockam/ockam_command/src/node/stop.rs | 29 +- .../rust/ockam/ockam_command/src/node/util.rs | 76 +- .../ockam/ockam_command/src/output/output.rs | 35 +- .../ockam/ockam_command/src/policy/create.rs | 7 +- .../ockam/ockam_command/src/policy/delete.rs | 9 +- .../ockam/ockam_command/src/policy/list.rs | 23 +- .../ockam/ockam_command/src/policy/mod.rs | 14 +- .../ockam/ockam_command/src/policy/show.rs | 4 +- .../src/project/addon/configure_confluent.rs | 8 +- .../src/project/addon/configure_influxdb.rs | 8 +- .../src/project/addon/configure_okta.rs | 8 +- .../src/project/addon/disable.rs | 7 +- .../ockam_command/src/project/addon/list.rs | 3 +- .../ockam_command/src/project/addon/mod.rs | 42 +- .../ockam/ockam_command/src/project/create.rs | 29 +- .../ockam/ockam_command/src/project/delete.rs | 29 +- .../ockam/ockam_command/src/project/enroll.rs | 76 +- .../ockam/ockam_command/src/project/import.rs | 95 ++ .../ockam/ockam_command/src/project/info.rs | 19 +- .../ockam/ockam_command/src/project/list.rs | 19 +- .../ockam/ockam_command/src/project/mod.rs | 38 +- .../ockam/ockam_command/src/project/show.rs | 34 +- .../project/static/import/after_long_help.txt | 10 + .../src/project/static/import/long_about.txt | 1 + .../ockam/ockam_command/src/project/ticket.rs | 136 +-- .../ockam/ockam_command/src/project/util.rs | 75 +- .../ockam_command/src/project/version.rs | 7 +- .../ockam/ockam_command/src/relay/create.rs | 23 +- .../ockam/ockam_command/src/relay/delete.rs | 13 +- .../ockam/ockam_command/src/relay/list.rs | 23 +- .../ockam/ockam_command/src/relay/show.rs | 9 +- .../rust/ockam/ockam_command/src/reset.rs | 24 +- .../ockam/ockam_command/src/run/parser.rs | 18 +- .../src/secure_channel/create.rs | 37 +- .../src/secure_channel/delete.rs | 9 +- .../ockam_command/src/secure_channel/list.rs | 26 +- .../src/secure_channel/listener/create.rs | 19 +- .../src/secure_channel/listener/delete.rs | 18 +- .../src/secure_channel/listener/list.rs | 34 +- .../src/secure_channel/listener/show.rs | 18 +- .../ockam_command/src/secure_channel/show.rs | 16 +- .../ockam/ockam_command/src/service/list.rs | 24 +- .../ockam/ockam_command/src/service/start.rs | 6 +- .../ockam/ockam_command/src/space/create.rs | 38 +- .../ockam/ockam_command/src/space/delete.rs | 8 +- .../ockam/ockam_command/src/space/list.rs | 10 +- .../rust/ockam/ockam_command/src/space/mod.rs | 1 - .../ockam/ockam_command/src/space/show.rs | 38 +- .../ockam/ockam_command/src/space/util.rs | 21 - .../rust/ockam/ockam_command/src/status.rs | 249 ++--- .../src/tcp/connection/create.rs | 63 +- .../src/tcp/connection/delete.rs | 5 +- .../ockam_command/src/tcp/connection/list.rs | 25 +- .../ockam_command/src/tcp/connection/show.rs | 8 +- .../ockam_command/src/tcp/inlet/create.rs | 23 +- .../ockam_command/src/tcp/inlet/delete.rs | 15 +- .../ockam/ockam_command/src/tcp/inlet/list.rs | 22 +- .../ockam/ockam_command/src/tcp/inlet/show.rs | 13 +- .../ockam_command/src/tcp/listener/create.rs | 15 +- .../ockam_command/src/tcp/listener/delete.rs | 15 +- .../ockam_command/src/tcp/listener/list.rs | 26 +- .../ockam_command/src/tcp/listener/show.rs | 8 +- .../ockam_command/src/tcp/outlet/create.rs | 27 +- .../ockam_command/src/tcp/outlet/delete.rs | 17 +- .../ockam_command/src/tcp/outlet/list.rs | 35 +- .../ockam_command/src/tcp/outlet/show.rs | 19 +- .../ockam_command/src/trust_context/create.rs | 99 +- .../src/trust_context/default.rs | 20 +- .../ockam_command/src/trust_context/delete.rs | 28 +- .../ockam_command/src/trust_context/list.rs | 20 +- .../ockam_command/src/trust_context/show.rs | 18 +- .../rust/ockam/ockam_command/src/util/api.rs | 27 +- .../rust/ockam/ockam_command/src/util/mod.rs | 183 +--- .../ockam/ockam_command/src/util/parsers.rs | 40 +- .../ockam_command/src/vault/attach_key.rs | 1 + .../ockam/ockam_command/src/vault/create.rs | 25 +- .../ockam/ockam_command/src/vault/default.rs | 28 +- .../ockam/ockam_command/src/vault/delete.rs | 4 +- .../ockam/ockam_command/src/vault/list.rs | 14 +- .../rust/ockam/ockam_command/src/vault/mod.rs | 15 +- .../ockam/ockam_command/src/vault/show.rs | 38 +- .../ockam/ockam_command/src/vault/util.rs | 39 +- .../ockam/ockam_command/src/worker/list.rs | 28 +- .../ockam_command/tests/bats/authority.bats | 75 +- .../tests/bats/command-reference.bats | 12 +- .../ockam_command/tests/bats/load/base.bash | 2 +- .../tests/bats/load/orchestrator.bash | 29 +- .../ockam/ockam_command/tests/bats/nodes.bats | 2 +- .../tests/bats/portals_orchestrator.bats | 46 +- .../ockam_command/tests/bats/projects.bats | 27 + .../tests/bats/projects_orchestrator.bats | 42 +- .../tests/bats/trust_context.bats | 28 +- .../ockam_command/tests/bats/use_cases.bats | 22 +- .../ockam/ockam_command/tests/bats/vault.bats | 6 +- .../tests/fixtures/user.enrollment.ticket | 2 +- .../rust/ockam/ockam_identity/Cargo.toml | 12 +- .../src/credentials/authority_service.rs | 1 + .../src/credentials/credentials.rs | 38 +- .../src/credentials/credentials_creation.rs | 32 +- .../src/credentials/credentials_issuer.rs | 22 +- .../credentials/credentials_server_worker.rs | 13 +- .../credentials/credentials_verification.rs | 46 +- .../src/credentials/trust_context.rs | 79 +- .../src/identities/identities.rs | 74 +- .../src/identities/identities_builder.rs | 45 +- .../src/identities/identities_creation.rs | 79 +- .../ockam_identity/src/identities/mod.rs | 4 +- .../storage/change_history_repository.rs | 24 + .../storage/change_history_repository_sql.rs | 168 +++ .../storage/identities_repository_impl.rs | 170 --- .../storage/identities_repository_trait.rs | 86 -- .../storage/identity_attributes_repository.rs | 30 + .../identity_attributes_repository_sql.rs | 238 +++++ .../src/identities/storage/mod.rs | 19 +- .../ockam_identity/src/identity/identity.rs | 39 +- .../rust/ockam/ockam_identity/src/lib.rs | 37 +- .../src/models/credential_and_purpose_key.rs | 88 +- .../ockam_identity/src/models/identifiers.rs | 12 +- .../src/models/utils/change_history.rs | 24 +- .../src/models/utils/credentials.rs | 5 + .../src/purpose_keys/purpose_key_creation.rs | 22 +- .../purpose_keys/purpose_key_verification.rs | 20 +- .../src/purpose_keys/purpose_keys.rs | 17 +- .../src/purpose_keys/storage/mod.rs | 11 +- ...ry_trait.rs => purpose_keys_repository.rs} | 0 .../storage/purpose_keys_repository_impl.rs | 91 -- .../storage/purpose_keys_repository_sql.rs | 196 ++++ .../credential_access_control.rs | 10 +- .../src/secure_channel/handshake/handshake.rs | 4 +- .../handshake/handshake_state_machine.rs | 42 +- .../src/secure_channel/listener.rs | 15 +- .../src/secure_channels/secure_channels.rs | 5 +- .../secure_channels_builder.rs | 48 +- .../src/storage/lmdb_storage.rs | 146 --- .../ockam_identity/src/storage/memory.rs | 76 -- .../ockam/ockam_identity/src/storage/mod.rs | 20 - .../src/storage/sqlite_storage.rs | 192 ---- .../ockam_identity/src/storage/storage.rs | 20 - .../rust/ockam/ockam_identity/src/vault.rs | 57 +- .../ockam/ockam_identity/tests/channel.rs | 4 +- .../ockam/ockam_identity/tests/credentials.rs | 16 +- .../ockam_identity/tests/identity_creation.rs | 48 +- .../tests/identity_verification.rs | 4 +- .../tests/plaintext_message_flow_auth.rs | 12 +- .../rust/ockam/ockam_node/Cargo.toml | 5 +- .../20231006100000_create_database.sql | 187 ++++ .../ockam_node/src/storage/database/mod.rs | 5 + .../src/storage/database/sqlx_database.rs | 223 ++++ .../src/storage/database/sqlx_types.rs | 200 ++++ .../rust/ockam/ockam_node/src/storage/mod.rs | 20 +- .../rust/ockam/ockam_vault/Cargo.toml | 4 +- .../rust/ockam/ockam_vault/src/lib.rs | 1 - .../vault_for_secure_channels.rs | 57 +- .../vault_for_signing/vault_for_signing.rs | 38 +- .../rust/ockam/ockam_vault/src/storage/mod.rs | 10 + .../src/storage/secrets_repository.rs | 53 + .../src/storage/secrets_repository_sql.rs | 324 ++++++ 323 files changed, 11959 insertions(+), 9594 deletions(-) delete mode 100644 implementations/rust/ockam/ockam_abac/src/mem.rs delete mode 100644 implementations/rust/ockam/ockam_abac/src/storage/lmdb_storage.rs rename implementations/rust/ockam/ockam_abac/src/{traits.rs => storage/policy_repository.rs} (60%) create mode 100644 implementations/rust/ockam/ockam_abac/src/storage/policy_repository_sql.rs delete mode 100644 implementations/rust/ockam/ockam_abac/src/storage/sqlite_storage.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/enrollment.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs delete mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/user_info.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/users.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs create mode 100644 implementations/rust/ockam/ockam_api/src/identity/credentials_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/identity/credentials_repository_sql.rs create mode 100644 implementations/rust/ockam/ockam_api/src/identity/identities_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/identity/identities_repository_sql.rs create mode 100644 implementations/rust/ockam/ockam_api/src/identity/vaults_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/identity/vaults_repository_sql.rs delete mode 100644 implementations/rust/ockam/ockam_api/src/nodes/config.rs create mode 100644 implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs delete mode 100644 implementations/rust/ockam/ockam_api/src/nodes/service/node_identities.rs create mode 100644 implementations/rust/ockam/ockam_api/src/nodes/service/projects.rs delete mode 100644 implementations/rust/ockam/ockam_api/src/trust_context.rs create mode 100644 implementations/rust/ockam/ockam_command/src/project/import.rs create mode 100644 implementations/rust/ockam/ockam_command/src/project/static/import/after_long_help.txt create mode 100644 implementations/rust/ockam/ockam_command/src/project/static/import/long_about.txt delete mode 100644 implementations/rust/ockam/ockam_command/src/space/util.rs create mode 100644 implementations/rust/ockam/ockam_command/src/vault/attach_key.rs create mode 100644 implementations/rust/ockam/ockam_command/tests/bats/projects.bats create mode 100644 implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository.rs create mode 100644 implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs delete mode 100644 implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_impl.rs delete mode 100644 implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_trait.rs create mode 100644 implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository.rs create mode 100644 implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository_sql.rs rename implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/{purpose_keys_repository_trait.rs => purpose_keys_repository.rs} (100%) delete mode 100644 implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_impl.rs create mode 100644 implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_sql.rs delete mode 100644 implementations/rust/ockam/ockam_identity/src/storage/lmdb_storage.rs delete mode 100644 implementations/rust/ockam/ockam_identity/src/storage/memory.rs delete mode 100644 implementations/rust/ockam/ockam_identity/src/storage/mod.rs delete mode 100644 implementations/rust/ockam/ockam_identity/src/storage/sqlite_storage.rs delete mode 100644 implementations/rust/ockam/ockam_identity/src/storage/storage.rs create mode 100644 implementations/rust/ockam/ockam_node/src/storage/database/migrations/20231006100000_create_database.sql create mode 100644 implementations/rust/ockam/ockam_node/src/storage/database/mod.rs create mode 100644 implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs create mode 100644 implementations/rust/ockam/ockam_node/src/storage/database/sqlx_types.rs create mode 100644 implementations/rust/ockam/ockam_vault/src/storage/secrets_repository.rs create mode 100644 implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs diff --git a/Cargo.lock b/Cargo.lock index deded5c31c0..d8de4178c57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", + "getrandom 0.2.10", "once_cell", "version_check", ] @@ -395,6 +396,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -2276,6 +2286,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast" version = "0.11.0" @@ -2376,6 +2392,9 @@ name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -2571,6 +2590,17 @@ dependencies = [ "rustversion", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2711,6 +2741,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "flate2" version = "1.0.28" @@ -2736,6 +2772,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2886,6 +2933,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.29" @@ -3410,6 +3468,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hello_ockam" @@ -3967,6 +4028,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "leb128" @@ -4051,6 +4115,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -4082,29 +4147,6 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" -[[package]] -name = "lmdb-rkv" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447a296f7aca299cfbb50f4e4f3d49451549af655fb7215d7f8c0c3d64bad42b" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "libc", - "lmdb-rkv-sys", -] - -[[package]] -name = "lmdb-rkv-sys" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "lock_api" version = "0.4.10" @@ -4625,6 +4667,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -4635,6 +4694,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -4779,11 +4849,11 @@ name = "ockam_abac" version = "0.38.0" dependencies = [ "either", - "lmdb-rkv", "minicbor", "ockam_core", "ockam_executor", "ockam_identity", + "ockam_node", "once_cell", "quickcheck", "rand 0.8.5", @@ -4791,6 +4861,7 @@ dependencies = [ "rusqlite", "rustyline", "rustyline-derive", + "sqlx", "str-buf 3.0.2", "tempfile", "tokio", @@ -4810,6 +4881,7 @@ dependencies = [ "either", "fake", "fs2", + "futures 0.3.28", "hex", "home", "indexmap 2.0.2", @@ -4835,6 +4907,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sqlx", "sysinfo", "tempfile", "thiserror", @@ -4867,6 +4940,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", + "sqlx", "tauri", "tauri-build", "tauri-plugin-log", @@ -4899,6 +4973,8 @@ dependencies = [ "ockam_transport_tcp", "serde", "serde_json", + "sqlx", + "tempfile", "thiserror", "tokio", "tokio-retry", @@ -4927,6 +5003,7 @@ dependencies = [ "dialoguer", "duct", "flate2", + "futures 0.3.28", "hex", "home", "indicatif", @@ -5029,11 +5106,11 @@ dependencies = [ "arrayref", "async-trait", "cfg-if", + "chrono", "delegate", "group", "heapless", "hex", - "lmdb-rkv", "minicbor", "ockam_core", "ockam_macros", @@ -5045,12 +5122,12 @@ dependencies = [ "quickcheck_macros", "rand 0.8.5", "rand_xorshift", - "rusqlite", "serde", "serde-big-array", "serde_bare", "serde_json", "sha2", + "sqlx", "subtle", "tempfile", "time", @@ -5113,8 +5190,11 @@ dependencies = [ "serde", "serde_bare", "serde_json", + "sqlx", "tempfile", + "time", "tokio", + "tokio-retry", "tracing", "tracing-error", "tracing-subscriber", @@ -5246,6 +5326,7 @@ dependencies = [ "serde_cbor", "serde_json", "sha2", + "sqlx", "static_assertions", "tempfile", "thiserror", @@ -5653,6 +5734,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -6385,6 +6477,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rusqlite" version = "0.29.0" @@ -7098,6 +7212,211 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools 0.11.0", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes 1.5.0", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.2", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +dependencies = [ + "atoi", + "base64 0.21.4", + "bitflags 2.4.0", + "byteorder", + "bytes 1.5.0", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf", + "hmac", + "itoa 1.0.9", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +dependencies = [ + "atoi", + "base64 0.21.4", + "bitflags 2.4.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa 1.0.9", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -7242,6 +7561,17 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strip-ansi-escapes" version = "0.2.0" @@ -8402,6 +8732,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.4.0" @@ -8839,6 +9175,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.3.9" diff --git a/examples/rust/example_projects/no_std/Cargo.toml b/examples/rust/example_projects/no_std/Cargo.toml index f7821dadfe6..fa7cd24fa6e 100644 --- a/examples/rust/example_projects/no_std/Cargo.toml +++ b/examples/rust/example_projects/no_std/Cargo.toml @@ -52,7 +52,7 @@ log-semihosting = [] log-uart = [] [dependencies] -ockam = { path = "../../../../implementations/rust/ockam/ockam", default_features = false, features = ["software_vault"] } +ockam = { path = "../../../../implementations/rust/ockam/ockam", optional = true, default_features = false, features = ["software_vault"] } alloc-cortex-m = { version = "0.4.1", optional = true } cortex-m = { version = "0.7.2", optional = true } diff --git a/examples/rust/get_started/Cargo.toml b/examples/rust/get_started/Cargo.toml index dc8a8c58f95..17f25b68fbe 100644 --- a/examples/rust/get_started/Cargo.toml +++ b/examples/rust/get_started/Cargo.toml @@ -18,6 +18,7 @@ std = [ "serde_json/default", "ockam_multiaddr/std", "ockam_api/std", + "storage", ] # Feature: "no_std" enables functionality required for platforms @@ -27,6 +28,7 @@ no_std = ["ockam/no_std"] # Feature: "alloc" enables support for heap allocation on "no_std" # platforms, requires nightly. alloc = ["ockam/alloc", "serde_json/alloc"] +storage = ["ockam_api/storage"] [dependencies] anyhow = "1" diff --git a/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs b/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs index 28c2a7d9dc1..ea89a226439 100644 --- a/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs +++ b/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs @@ -48,14 +48,14 @@ async fn main(ctx: Context) -> Result<()> { // For a different application this attested attribute set can be different and // distinct for each identifier, but for this example we'll keep things simple. let credential_issuer = CredentialsIssuer::new( - node.identities().repository(), + node.identities().identity_attributes_repository(), node.credentials(), &issuer, "trust_context".into(), ); for identifier in known_identifiers.iter() { node.identities() - .repository() + .identity_attributes_repository() .put_attribute_value(identifier, b"cluster".to_vec(), b"production".to_vec()) .await?; } diff --git a/examples/rust/get_started/examples/06-credentials-exchange-server.rs b/examples/rust/get_started/examples/06-credentials-exchange-server.rs index 93165375400..404b186f78f 100644 --- a/examples/rust/get_started/examples/06-credentials-exchange-server.rs +++ b/examples/rust/get_started/examples/06-credentials-exchange-server.rs @@ -90,7 +90,7 @@ async fn main(ctx: Context) -> Result<()> { DefaultAddress::ECHO_SERVICE, &sc_listener_options.spawner_flow_control_id(), ); - let allow_production = AbacAccessControl::create(node.identities_repository(), "cluster", "production"); + let allow_production = AbacAccessControl::create(node.identity_attributes_repository(), "cluster", "production"); node.start_worker_with_access_control(DefaultAddress::ECHO_SERVICE, Echoer, allow_production, AllowAll) .await?; diff --git a/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs b/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs index cd7935287e7..901735acee8 100644 --- a/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs +++ b/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use hello_ockam::{create_token, import_project}; use ockam::abac::AbacAccessControl; -use ockam::identity::OneTimeCode; use ockam::identity::{ AuthorityService, RemoteCredentialsRetriever, RemoteCredentialsRetrieverInfo, SecureChannelListenerOptions, SecureChannelOptions, TrustContext, TrustMultiIdentifiersPolicy, }; +use ockam::identity::{CredentialsRetriever, OneTimeCode}; use ockam::remote::RemoteRelayOptions; use ockam::{node, route, Context, Result, TcpOutletOptions}; use ockam_api::authenticator::enrollment_tokens::TokenAcceptor; @@ -12,7 +14,6 @@ use ockam_api::nodes::NodeManager; use ockam_api::{multiaddr_to_route, DefaultAddress}; use ockam_multiaddr::MultiAddr; use ockam_transport_tcp::TcpTransportExtension; -use std::sync::Arc; /// This node supports a "control" server on which several "edge" devices can connect /// @@ -74,26 +75,24 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime let tcp_project_session = multiaddr_to_route(&project.authority_route(), &tcp).await.unwrap(); // FIXME: Handle error // Create a trust context that will be used to authenticate credential exchanges + let credentials_retriever = Arc::new(RemoteCredentialsRetriever::new( + node.secure_channels(), + RemoteCredentialsRetrieverInfo::new( + project.authority_identifier(), + tcp_project_session.route, + DefaultAddress::CREDENTIAL_ISSUER.into(), + ), + )); let trust_context = TrustContext::new( "trust_context_id".to_string(), Some(AuthorityService::new( node.credentials(), project.authority_identifier(), - Some(Arc::new(RemoteCredentialsRetriever::new( - node.secure_channels(), - RemoteCredentialsRetrieverInfo::new( - project.authority_identifier(), - tcp_project_session.route, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ), - ))), + Some(credentials_retriever.clone()), )), ); - let credential = trust_context - .authority()? - .credential(node.context(), &control_plane) - .await?; + let credential = credentials_retriever.retrieve(node.context(), &control_plane).await?; // start a credential exchange worker which will be // later on to exchange credentials with the edge node @@ -108,7 +107,7 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime .await?; // 3. create an access control policy checking the value of the "component" attribute of the caller - let access_control = AbacAccessControl::create(node.identities_repository(), "component", "edge"); + let access_control = AbacAccessControl::create(node.identity_attributes_repository(), "component", "edge"); // 4. create a tcp outlet with the above policy tcp.create_outlet( diff --git a/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs b/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs index c6d8686e8be..88cba601d18 100644 --- a/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs +++ b/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs @@ -1,10 +1,10 @@ use hello_ockam::{create_token, import_project}; use ockam::abac::AbacAccessControl; -use ockam::identity::OneTimeCode; use ockam::identity::{ identities, AuthorityService, RemoteCredentialsRetriever, RemoteCredentialsRetrieverInfo, SecureChannelOptions, TrustContext, TrustMultiIdentifiersPolicy, }; +use ockam::identity::{CredentialsRetriever, OneTimeCode}; use ockam::node; use ockam::{route, Context, Result}; use ockam_api::authenticator::enrollment_tokens::TokenAcceptor; @@ -74,26 +74,25 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime // Create a trust context that will be used to authenticate credential exchanges let tcp_project_session = multiaddr_to_route(&project.route(), &tcp).await.unwrap(); // FIXME: Handle error + // Create a trust context that will be used to authenticate credential exchanges + let credentials_retriever = Arc::new(RemoteCredentialsRetriever::new( + node.secure_channels(), + RemoteCredentialsRetrieverInfo::new( + project.authority_identifier(), + tcp_project_session.route, + DefaultAddress::CREDENTIAL_ISSUER.into(), + ), + )); let trust_context = TrustContext::new( "trust_context_id".to_string(), Some(AuthorityService::new( node.credentials(), project.authority_identifier(), - Some(Arc::new(RemoteCredentialsRetriever::new( - node.secure_channels(), - RemoteCredentialsRetrieverInfo::new( - project.authority_identifier(), - tcp_project_session.route, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ), - ))), + Some(credentials_retriever.clone()), )), ); - let credential = trust_context - .authority()? - .credential(node.context(), &edge_plane) - .await?; + let credential = credentials_retriever.retrieve(node.context(), &edge_plane).await?; // start a credential exchange worker which will be // later on to exchange credentials with the control node @@ -108,7 +107,8 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime .await?; // 3. create an access control policy checking the value of the "component" attribute of the caller - let access_control = AbacAccessControl::create(identities().repository(), "component", "control"); + let access_control = + AbacAccessControl::create(identities().identity_attributes_repository(), "component", "control"); // 4. create a tcp inlet with the above policy diff --git a/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock b/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock index 66d1104953c..1b475190776 100644 --- a/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock +++ b/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock @@ -60,6 +60,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", ] @@ -73,6 +74,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "arrayref" version = "0.3.7" @@ -90,6 +97,15 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-polyfill" version = "0.1.11" @@ -468,6 +484,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -520,6 +545,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "num-traits", +] + [[package]] name = "cipher" version = "0.3.0" @@ -569,6 +603,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "critical-section" version = "1.1.2" @@ -696,6 +745,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "ecdsa" version = "0.16.8" @@ -739,6 +794,9 @@ name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -760,6 +818,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "2.0.1" @@ -782,6 +873,23 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -815,6 +923,7 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -837,6 +946,28 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.29" @@ -945,19 +1076,13 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "hash32" version = "0.2.1" @@ -980,9 +1105,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash", + "allocator-api2", "serde", ] +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.0", +] + [[package]] name = "heapless" version = "0.7.16" @@ -1001,6 +1136,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -1032,6 +1170,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "http" version = "0.2.9" @@ -1106,6 +1253,16 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1116,6 +1273,25 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1136,6 +1312,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" @@ -1144,28 +1323,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] -name = "lmdb-rkv" -version = "0.14.0" +name = "libm" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447a296f7aca299cfbb50f4e4f3d49451549af655fb7215d7f8c0c3d64bad42b" -dependencies = [ - "bitflags", - "byteorder", - "libc", - "lmdb-rkv-sys", -] +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] -name = "lmdb-rkv-sys" -version = "0.11.2" +name = "libsqlite3-sys" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", - "libc", "pkg-config", + "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + [[package]] name = "lock_api" version = "0.4.10" @@ -1191,6 +1370,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.6.3" @@ -1217,6 +1406,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1237,6 +1432,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1247,6 +1452,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1257,6 +1479,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -1264,6 +1497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1328,11 +1562,11 @@ dependencies = [ "arrayref", "async-trait", "cfg-if", + "chrono", "delegate", "group", "heapless", "hex", - "lmdb-rkv", "minicbor", "ockam_core", "ockam_macros", @@ -1343,6 +1577,7 @@ dependencies = [ "serde-big-array", "serde_bare", "sha2", + "sqlx", "subtle", "time", "tokio-retry", @@ -1373,7 +1608,10 @@ dependencies = [ "serde", "serde_bare", "serde_json", + "sqlx", + "time", "tokio", + "tokio-retry", "tracing", "tracing-error", "tracing-subscriber", @@ -1404,8 +1642,8 @@ dependencies = [ "p256", "rand", "serde", - "serde_cbor", "sha2", + "sqlx", "static_assertions", "tracing", "x25519-dalek", @@ -1485,6 +1723,35 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1532,6 +1799,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -1635,6 +1913,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.9.5" @@ -1704,6 +1991,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "rsa" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1719,6 +2026,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustler" version = "0.29.1" @@ -1846,7 +2166,7 @@ version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1896,16 +2216,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.188" @@ -1928,6 +2238,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2018,6 +2339,211 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.1", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2030,6 +2556,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "subtle" version = "2.5.0" @@ -2058,6 +2595,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -2319,12 +2869,39 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.4.0" @@ -2350,6 +2927,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "urlencoding" version = "2.1.3" @@ -2368,6 +2956,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -2465,6 +3059,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.3.9" diff --git a/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs b/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs index bcc4eeb993d..7aa3ebfb129 100644 --- a/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs +++ b/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs @@ -88,17 +88,19 @@ fn identities_ref() -> NifResult> { } fn load_memory_vault() -> bool { - let identity_vault = SoftwareVaultForSigning::create(); - let secure_channel_vault = SoftwareVaultForSecureChannels::create(); - *IDENTITY_MEMORY_VAULT.write().unwrap() = Some(identity_vault.clone()); - *SECURE_CHANNEL_MEMORY_VAULT.write().unwrap() = Some(secure_channel_vault.clone()); - let builder = ockam_identity::Identities::builder().with_vault(Vault::new( - identity_vault, - secure_channel_vault, - Vault::create_credential_vault(), - Vault::create_verifying_vault(), - )); - *IDENTITIES.write().unwrap() = Some(builder.build()); + block_future(async move { + let identity_vault = SoftwareVaultForSigning::create(); + let secure_channel_vault = SoftwareVaultForSecureChannels::create(); + *IDENTITY_MEMORY_VAULT.write().unwrap() = Some(identity_vault.clone()); + *SECURE_CHANNEL_MEMORY_VAULT.write().unwrap() = Some(secure_channel_vault.clone()); + let builder = ockam_identity::Identities::builder().with_vault(Vault::new( + identity_vault, + secure_channel_vault, + Vault::create_credential_vault(), + Vault::create_verifying_vault(), + )); + *IDENTITIES.write().unwrap() = Some(builder.build()); + }); true } diff --git a/implementations/rust/ockam/ockam/Cargo.toml b/implementations/rust/ockam/ockam/Cargo.toml index 2b150a12b7d..bc572c6f749 100644 --- a/implementations/rust/ockam/ockam/Cargo.toml +++ b/implementations/rust/ockam/ockam/Cargo.toml @@ -31,9 +31,9 @@ all-features = false rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["std", "ockam_transport_tcp", "software_vault_storage"] +default = ["std", "ockam_transport_tcp", "storage"] software_vault = ["ockam_identity/software_vault"] -software_vault_storage = ["software_vault", "ockam_vault/storage"] +storage = ["ockam_identity/storage"] OCKAM_XX_25519_AES256_GCM_SHA256 = ["ockam_identity/OCKAM_XX_25519_AES256_GCM_SHA256"] OCKAM_XX_25519_AES128_GCM_SHA256 = ["ockam_identity/OCKAM_XX_25519_AES128_GCM_SHA256"] OCKAM_XX_25519_ChaChaPolyBLAKE2s = ["ockam_identity/OCKAM_XX_25519_ChaChaPolyBLAKE2s"] @@ -87,7 +87,7 @@ hex = { version = "0.4", default-features = false } minicbor = { version = "0.20.0", features = ["alloc", "derive"] } ockam_abac = { path = "../ockam_abac", version = "^0.38.0", default_features = false, optional = true } ockam_core = { path = "../ockam_core", version = "^0.93.0", default-features = false } -ockam_identity = { path = "../ockam_identity", version = "^0.92.0", default_features = false } +ockam_identity = { path = "../ockam_identity", optional = true, version = "^0.92.0", default_features = false } ockam_macros = { path = "../ockam_macros", version = "^0.32.0", default_features = false } ockam_node = { path = "../ockam_node", version = "^0.98.0", default-features = false } ockam_transport_tcp = { path = "../ockam_transport_tcp", version = "^0.96.0", optional = true } diff --git a/implementations/rust/ockam/ockam/src/lib.rs b/implementations/rust/ockam/ockam/src/lib.rs index 90cb3dfd3e9..6578eb7590f 100644 --- a/implementations/rust/ockam/ockam/src/lib.rs +++ b/implementations/rust/ockam/ockam/src/lib.rs @@ -43,26 +43,56 @@ )] #![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] -extern crate core; - #[cfg(feature = "alloc")] #[macro_use] extern crate alloc; - +#[cfg(feature = "std")] +extern crate core; #[macro_use] extern crate tracing; +pub use error::OckamError; +pub use metadata::OckamMessage; +pub use node::*; +#[cfg(feature = "std")] +pub use ockam_abac as abac; +/// Mark an Ockam Processor implementation. +/// +/// This is currently implemented as a re-export of the `async_trait` macro, but +/// may be changed in the future to a [`Processor`](crate::Processor)-specific macro. +pub use ockam_core::processor; +/// Mark an Ockam Worker implementation. +/// +/// This is currently implemented as a re-export of the `async_trait` macro, but +/// may be changed in the future to a [`Worker`](crate::Worker)-specific macro. +pub use ockam_core::worker; +pub use ockam_core::{ + allow, deny, errcode, route, Address, Any, AsyncTryClone, Encoded, Error, LocalMessage, + Mailbox, Mailboxes, Message, Processor, ProtocolId, Result, Route, Routed, TransportMessage, + Worker, +}; +pub use ockam_identity as identity; // --- // Export the ockam macros that aren't coming from ockam_core. pub use ockam_macros::{node, test}; -// --- - // Export node implementation +#[cfg(feature = "std")] +pub use ockam_node::database::*; pub use ockam_node::{ debugger, Context, DelayedEvent, Executor, MessageReceiveOptions, MessageSendReceiveOptions, NodeBuilder, WorkerBuilder, }; +#[cfg(feature = "ockam_transport_tcp")] +pub use ockam_transport_tcp::{ + TcpConnectionOptions, TcpInletOptions, TcpListenerOptions, TcpOutletOptions, TcpTransport, + TcpTransportExtension, +}; +pub use relay_service::{RelayService, RelayServiceOptions}; +pub use system::{SystemBuilder, SystemHandler, WorkerSystem}; +pub use unique::unique_with_prefix; + +// --- + // --- mod delay; @@ -73,12 +103,6 @@ mod relay_service; mod system; mod unique; -pub use error::OckamError; -pub use metadata::OckamMessage; -pub use relay_service::{RelayService, RelayServiceOptions}; -pub use system::{SystemBuilder, SystemHandler, WorkerSystem}; -pub use unique::unique_with_prefix; - pub mod channel; pub mod pipe; pub mod pipe2; @@ -87,18 +111,6 @@ pub mod remote; pub mod stream; pub mod workers; -#[cfg(feature = "std")] -pub use ockam_abac as abac; -pub use ockam_identity as identity; -#[cfg(feature = "std")] -pub use ockam_identity::storage::lmdb_storage::*; - -pub use ockam_core::{ - allow, deny, errcode, route, Address, Any, AsyncTryClone, Encoded, Error, LocalMessage, - Mailbox, Mailboxes, Message, Processor, ProtocolId, Result, Route, Routed, TransportMessage, - Worker, -}; - /// Access Control pub mod access_control { pub use ockam_core::access_control::*; @@ -110,18 +122,6 @@ pub mod flow_control { pub use ockam_core::flow_control::*; } -/// Mark an Ockam Worker implementation. -/// -/// This is currently implemented as a re-export of the `async_trait` macro, but -/// may be changed in the future to a [`Worker`](crate::Worker)-specific macro. -pub use ockam_core::worker; - -/// Mark an Ockam Processor implementation. -/// -/// This is currently implemented as a re-export of the `async_trait` macro, but -/// may be changed in the future to a [`Processor`](crate::Processor)-specific macro. -pub use ockam_core::processor; - // TODO: think about how to handle this more. Probably extract these into an // `ockam_compat` crate. pub mod compat { @@ -138,20 +138,12 @@ pub mod vault { //! Types and traits relating to ockam vaults. pub use ockam_vault::*; - #[cfg(feature = "software_vault_storage")] + #[cfg(feature = "storage")] /// Storage pub mod storage { pub use ockam_vault::storage::*; } } -#[cfg(feature = "ockam_transport_tcp")] -pub use ockam_transport_tcp::{ - TcpConnectionOptions, TcpInletOptions, TcpListenerOptions, TcpOutletOptions, TcpTransport, - TcpTransportExtension, -}; - /// List of all top-level services pub mod node; - -pub use node::*; diff --git a/implementations/rust/ockam/ockam/src/node.rs b/implementations/rust/ockam/ockam/src/node.rs index e119dac9985..d5e4b093efe 100644 --- a/implementations/rust/ockam/ockam/src/node.rs +++ b/implementations/rust/ockam/ockam/src/node.rs @@ -1,11 +1,3 @@ -use crate::identity::models::Identifier; -use crate::identity::storage::Storage; -use crate::identity::{ - secure_channels, Credentials, CredentialsServer, Identities, IdentitiesCreation, - IdentitiesKeys, IdentitiesRepository, SecureChannel, SecureChannelListener, - SecureChannelRegistry, SecureChannels, SecureChannelsBuilder, -}; -use crate::identity::{SecureChannelListenerOptions, SecureChannelOptions}; use ockam_core::compat::string::String; use ockam_core::compat::sync::Arc; use ockam_core::flow_control::FlowControls; @@ -13,10 +5,20 @@ use ockam_core::{ Address, AsyncTryClone, IncomingAccessControl, Message, OutgoingAccessControl, Processor, Result, Route, Routed, Worker, }; -use ockam_identity::{PurposeKeys, Vault, VaultStorage}; +use ockam_identity::{IdentityAttributesRepository, PurposeKeys, Vault}; use ockam_node::{Context, HasContext, MessageReceiveOptions, MessageSendReceiveOptions}; +use ockam_vault::storage::SecretsRepository; use ockam_vault::SigningSecretKeyHandle; +use crate::identity::models::Identifier; +#[cfg(feature = "storage")] +use crate::identity::secure_channels; +use crate::identity::{ + ChangeHistoryRepository, Credentials, CredentialsServer, Identities, IdentitiesCreation, + IdentitiesKeys, SecureChannel, SecureChannelListener, SecureChannelRegistry, SecureChannels, + SecureChannelsBuilder, +}; +use crate::identity::{SecureChannelListenerOptions, SecureChannelOptions}; use crate::remote::{RemoteRelay, RemoteRelayInfo, RemoteRelayOptions}; use crate::stream::Stream; use crate::OckamError; @@ -36,28 +38,16 @@ pub struct Node { /// use std::sync::Arc; /// use ockam::{Node, Result}; /// use ockam_node::Context; -/// use ockam_vault::storage::PersistentStorage; +/// use ockam_vault::storage::SecretsSqlxDatabase; /// /// async fn make_node(ctx: Context) -> Result { -/// let node = Node::builder().with_vault_storage(PersistentStorage::create(Path::new("vault")).await?).build(&ctx).await?; +/// let node = Node::builder().with_secrets_repository(SecretsSqlxDatabase::create()).build(&ctx).await?; /// Ok(node) /// } /// /// /// ``` -/// Here is another example where we specify a local LMDB database to store identity attributes -/// ```rust -/// use std::sync::Arc; -/// use ockam::{Node, Result}; -/// use ockam::LmdbStorage; -/// use ockam_node::Context; -/// -/// async fn make_node(ctx: Context) -> Result { -/// let lmdb_storage = Arc::new(LmdbStorage::new("identities").await?); -/// let node = Node::builder().with_identities_storage(lmdb_storage).build(&ctx).await?; -/// Ok(node) -/// } -/// ``` +#[cfg(feature = "storage")] pub fn node(ctx: Context) -> Node { Node { context: ctx, @@ -309,11 +299,21 @@ impl Node { } /// Return the repository used to store identities data - pub fn identities_repository(&self) -> Arc { - self.secure_channels.identities().repository() + pub fn identities_repository(&self) -> Arc { + self.secure_channels + .identities() + .change_history_repository() + } + + /// Return the repository used to store identities attributes + pub fn identity_attributes_repository(&self) -> Arc { + self.secure_channels + .identities() + .identity_attributes_repository() } /// Return a new builder for top-level services + #[cfg(feature = "storage")] pub fn builder() -> NodeBuilder { NodeBuilder::new() } @@ -335,6 +335,7 @@ pub struct NodeBuilder { } impl NodeBuilder { + #[cfg(feature = "storage")] fn new() -> Self { Self { builder: SecureChannels::builder(), @@ -347,21 +348,27 @@ impl NodeBuilder { self } - /// With Software Vault with given Storage - pub fn with_vault_storage(mut self, storage: VaultStorage) -> Self { - self.builder = self.builder.with_vault_storage(storage); + /// With Software Vault with given secrets repository + pub fn with_secrets_repository(mut self, repository: Arc) -> Self { + self.builder = self.builder.with_secrets_repository(repository); self } - /// Set a specific storage for identities - pub fn with_identities_storage(mut self, storage: Arc) -> Self { - self.builder = self.builder.with_identities_storage(storage); + /// Set a specific change history repository + pub fn with_change_history_repository( + mut self, + repository: Arc, + ) -> Self { + self.builder = self.builder.with_change_history_repository(repository); self } - /// Set a specific identities repository - pub fn with_identities_repository(mut self, repository: Arc) -> Self { - self.builder = self.builder.with_identities_repository(repository); + /// Set a specific identity attributes repository + pub fn with_identity_attributes_repository( + mut self, + repository: Arc, + ) -> Self { + self.builder = self.builder.with_identity_attributes_repository(repository); self } diff --git a/implementations/rust/ockam/ockam_abac/Cargo.toml b/implementations/rust/ockam/ockam_abac/Cargo.toml index 8933711706d..49515a0bd92 100644 --- a/implementations/rust/ockam/ockam_abac/Cargo.toml +++ b/implementations/rust/ockam/ockam_abac/Cargo.toml @@ -20,31 +20,31 @@ repl = ["rustyline", "rustyline-derive", "std"] std = [ "ockam_core/std", "ockam_identity/std", + "ockam_node/std", "minicbor/std", "tracing/std", "either/use_std", - "lmdb", "once_cell/std", + "sqlx", "regex", "tokio", "wast", ] -lmdb = ["tokio", "lmdb-rkv"] -sqlite = ["rusqlite"] [dependencies] either = { version = "1.9.0", default-features = false } -lmdb-rkv = { version = "0.14.0", optional = true } minicbor = { version = "0.20.0", features = ["derive", "alloc"] } ockam_core = { version = "0.93.0", path = "../ockam_core", default-features = false } ockam_executor = { version = "0.61.0", path = "../ockam_executor", default-features = false } ockam_identity = { version = "0.92.0", path = "../ockam_identity", default-features = false } +ockam_node = { version = "0.98.0", path = "../ockam_node", default-features = false } once_cell = { version = "1.18.0", default-features = false, features = ["alloc"] } # optional: regex = { version = "1.10.2", default-features = false, optional = true } rusqlite = { version = "0.29.0", optional = true } rustyline = { version = "12.0.0", optional = true } rustyline-derive = { version = "0.9.0", optional = true } +sqlx = { version = "0.7.2", optional = true } str-buf = "3.0.1" tokio = { version = "1.33", default-features = false, optional = true, features = ["sync", "time", "rt", "rt-multi-thread", "macros"] } tracing = { version = "0.1", default-features = false } diff --git a/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs b/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs index 85dba10c91d..def21de5063 100644 --- a/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs +++ b/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs @@ -16,14 +16,14 @@ use crate::{eval, Env, Expr}; use ockam_core::compat::format; use ockam_core::compat::string::ToString; use ockam_core::compat::sync::Arc; -use ockam_identity::{Identifier, IdentitiesRepository, IdentitySecureChannelLocalInfo}; +use ockam_identity::{Identifier, IdentityAttributesRepository, IdentitySecureChannelLocalInfo}; /// This AccessControl uses a storage for authenticated attributes in order /// to verify if a policy expression is valid /// A similar access control policy is available as [`crate::policy::PolicyAccessControl`] where -/// as [`crate::PolicyStorage`] can be used to retrieve a specific policy for a given resource and action +/// as [`crate::PoliciesRepository`] can be used to retrieve a specific policy for a given resource and action pub struct AbacAccessControl { - repository: Arc, + identity_attributes_repository: Arc, expression: Expr, environment: Env, } @@ -39,12 +39,12 @@ impl Debug for AbacAccessControl { impl AbacAccessControl { /// Create a new AccessControl using a specific policy for checking attributes pub fn new( - repository: Arc, + identity_attributes_repository: Arc, expression: Expr, environment: Env, ) -> Self { Self { - repository, + identity_attributes_repository, expression, environment, } @@ -53,7 +53,7 @@ impl AbacAccessControl { /// Create an AccessControl which will verify that the sender of /// a message has an authenticated attribute with the correct name and value pub fn create( - repository: Arc, + identity_attributes_repository: Arc, attribute_name: &str, attribute_value: &str, ) -> AbacAccessControl @@ -63,7 +63,7 @@ where { Ident(format!("subject.{attribute_name}")), Str(attribute_value.into()), ]); - AbacAccessControl::new(repository, expression, Env::new()) + AbacAccessControl::new(identity_attributes_repository, expression, Env::new()) } } @@ -73,7 +73,11 @@ impl AbacAccessControl { let mut environment = self.environment.clone(); // Get identity attributes and populate the environment: - if let Some(attrs) = self.repository.get_attributes(&id).await? { + if let Some(attrs) = self + .identity_attributes_repository + .get_attributes(&id) + .await? + { for (key, value) in attrs.attrs() { let key = match from_utf8(key) { Ok(key) => key, diff --git a/implementations/rust/ockam/ockam_abac/src/lib.rs b/implementations/rust/ockam/ockam_abac/src/lib.rs index 3894d287c61..6c62a439c1f 100644 --- a/implementations/rust/ockam/ockam_abac/src/lib.rs +++ b/implementations/rust/ockam/ockam_abac/src/lib.rs @@ -10,7 +10,6 @@ mod env; mod error; mod eval; mod policy; -mod traits; mod types; #[cfg(feature = "std")] @@ -18,7 +17,6 @@ mod parser; pub mod attribute_access_control; pub mod expr; -pub mod mem; mod storage; pub use attribute_access_control::AbacAccessControl; @@ -27,7 +25,7 @@ pub use error::{EvalError, ParseError}; pub use eval::eval; pub use expr::Expr; pub use policy::PolicyAccessControl; -pub use traits::PolicyStorage; +pub use storage::*; pub use types::{Action, Resource, Subject}; #[cfg(feature = "std")] diff --git a/implementations/rust/ockam/ockam_abac/src/mem.rs b/implementations/rust/ockam/ockam_abac/src/mem.rs deleted file mode 100644 index 5f5a10f99fe..00000000000 --- a/implementations/rust/ockam/ockam_abac/src/mem.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::expr::Expr; -use crate::traits::PolicyStorage; -use crate::types::{Action, Resource}; -use core::fmt; -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::sync::{Arc, RwLock}; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; - -#[derive(Default)] -pub struct Memory { - pub(crate) inner: Arc>, -} - -impl fmt::Debug for Memory { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Memory") - } -} - -impl Memory { - pub fn new() -> Self { - Self { - inner: Arc::new(RwLock::new(Inner::new())), - } - } -} - -#[derive(Default)] -pub struct Inner { - policies: BTreeMap>, -} - -impl Inner { - fn new() -> Self { - Inner::default() - } - - fn del_policy(&mut self, r: &Resource, a: &Action) { - if let Some(p) = self.policies.get_mut(r) { - p.remove(a); - if p.is_empty() { - self.policies.remove(r); - } - } - } - - fn get_policy(&self, r: &Resource, a: &Action) -> Option { - self.policies.get(r).and_then(|p| p.get(a).cloned()) - } - - fn set_policy(&mut self, r: &Resource, a: &Action, p: &Expr) { - self.policies - .entry(r.clone()) - .or_insert_with(BTreeMap::new) - .insert(a.clone(), p.clone()); - } - - fn policies(&self, r: &Resource) -> Vec<(Action, Expr)> { - if let Some(p) = self.policies.get(r) { - p.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>() - } else { - Vec::new() - } - } -} - -#[async_trait] -impl PolicyStorage for Memory { - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()> { - self.inner.write().unwrap().del_policy(r, a); - Ok(()) - } - - async fn get_policy(&self, r: &Resource, a: &Action) -> Result> { - Ok(self.inner.read().unwrap().get_policy(r, a)) - } - - async fn set_policy(&self, r: &Resource, a: &Action, p: &Expr) -> Result<()> { - self.inner.write().unwrap().set_policy(r, a, p); - Ok(()) - } - - async fn policies(&self, r: &Resource) -> Result> { - Ok(self.inner.write().unwrap().policies(r)) - } -} - -#[cfg(test)] -mod tests { - use crate::env::Env; - use crate::eval::eval; - use crate::expr::{int, seq, str}; - use crate::mem::Memory; - use crate::parser::parse; - use crate::types::{Action, Resource}; - - #[test] - fn example1() { - let condition = r#" - (and (= resource.version "1.0.0") - (= subject.name "John") - (member? "John" resource.admins)) - "#; - - let action = Action::new("r"); - let resource = Resource::new("/foo/bar/baz"); - let store = Memory::new(); - - store.inner.write().unwrap().set_policy( - &resource, - &action, - &parse(condition).unwrap().unwrap(), - ); - - let mut e = Env::new(); - e.put("subject.age", int(25)) - .put("subject.name", str("John")) - .put("resource.version", str("1.0.0")) - .put("resource.admins", seq([str("root"), str("John")])); - - let policy = store - .inner - .write() - .unwrap() - .get_policy(&resource, &action) - .unwrap(); - assert!(eval(&policy, &e).unwrap().is_true()) - } -} diff --git a/implementations/rust/ockam/ockam_abac/src/policy.rs b/implementations/rust/ockam/ockam_abac/src/policy.rs index 90f3f6be3f0..91791047dd8 100644 --- a/implementations/rust/ockam/ockam_abac/src/policy.rs +++ b/implementations/rust/ockam/ockam_abac/src/policy.rs @@ -1,6 +1,5 @@ -use crate::traits::PolicyStorage; use crate::types::{Action, Resource}; -use crate::AbacAccessControl; +use crate::{AbacAccessControl, PoliciesRepository}; use crate::{Env, Expr}; use core::fmt; use core::fmt::{Debug, Formatter}; @@ -9,7 +8,7 @@ use ockam_core::compat::format; use ockam_core::compat::sync::Arc; use ockam_core::{async_trait, RelayMessage}; use ockam_core::{IncomingAccessControl, Result}; -use ockam_identity::IdentitiesRepository; +use ockam_identity::IdentityAttributesRepository; use tracing as log; /// Evaluates a policy expression against an environment of attributes. @@ -19,8 +18,8 @@ use tracing as log; pub struct PolicyAccessControl { resource: Resource, action: Action, - policies: Arc, - repository: Arc, + policies: Arc, + identity_attributes_repository: Arc, environment: Env, } @@ -43,8 +42,8 @@ impl PolicyAccessControl { /// the given authenticated storage, adding them the given environment, /// which may already contain other resource, action or subject attributes. pub fn new( - policies: Arc, - repository: Arc, + policies: Arc, + identity_attributes_repository: Arc, r: Resource, a: Action, env: Env, @@ -53,7 +52,7 @@ impl PolicyAccessControl { resource: r, action: a, policies, - repository, + identity_attributes_repository, environment: env, } } @@ -85,8 +84,12 @@ impl IncomingAccessControl for PolicyAccessControl { return Ok(false); }; - AbacAccessControl::new(self.repository.clone(), expr, self.environment.clone()) - .is_authorized(msg) - .await + AbacAccessControl::new( + self.identity_attributes_repository.clone(), + expr, + self.environment.clone(), + ) + .is_authorized(msg) + .await } } diff --git a/implementations/rust/ockam/ockam_abac/src/storage/lmdb_storage.rs b/implementations/rust/ockam/ockam_abac/src/storage/lmdb_storage.rs deleted file mode 100644 index fc456573519..00000000000 --- a/implementations/rust/ockam/ockam_abac/src/storage/lmdb_storage.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::tokio::task::{spawn_blocking, JoinError}; -use crate::{Action, Expr, PolicyStorage, Resource}; -use core::str; -use lmdb::{Cursor, Transaction}; -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_identity::storage::LmdbStorage; -use std::borrow::Cow; -use tracing as log; - -use super::PolicyEntry; - -#[async_trait] -impl PolicyStorage for LmdbStorage { - async fn get_policy(&self, r: &Resource, a: &Action) -> Result> { - let d = self.clone(); - let k = format!("{r}:{a}"); - let t = move || { - let r = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - match r.get(d.map, &k) { - Ok(value) => { - let e: PolicyEntry = minicbor::decode(value)?; - Ok(Some(e.expr.into_owned())) - } - Err(lmdb::Error::NotFound) => Ok(None), - Err(e) => Err(map_lmdb_err(e)), - } - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()> { - let v = minicbor::to_vec(PolicyEntry { - expr: Cow::Borrowed(c), - })?; - self.write(format!("{r}:{a}"), v).await - } - - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()> { - self.delete(format!("{r}:{a}")).await - } - - async fn policies(&self, r: &Resource) -> Result> { - let d = self.clone(); - let r = r.clone(); - let t = move || { - let tx = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - let mut c = tx.open_ro_cursor(d.map).map_err(map_lmdb_err)?; - let mut xs = Vec::new(); - for entry in c.iter_from(r.as_str()) { - let (k, v) = entry.map_err(map_lmdb_err)?; - let ks = str::from_utf8(k).map_err(from_utf8_err)?; - if let Some((prefix, a)) = ks.split_once(':') { - if prefix != r.as_str() { - break; - } - let x: PolicyEntry = minicbor::decode(v)?; - xs.push((Action::new(a), x.expr.into_owned())) - } else { - log::warn!(key = %ks, "malformed key in policy database") - } - } - Ok(xs) - }; - spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_lmdb_err(err: lmdb::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn from_utf8_err(err: str::Utf8Error) -> Error { - Error::new(Origin::Other, Kind::Invalid, err) -} diff --git a/implementations/rust/ockam/ockam_abac/src/storage/mod.rs b/implementations/rust/ockam/ockam_abac/src/storage/mod.rs index aed266986cd..e732cce2680 100644 --- a/implementations/rust/ockam/ockam_abac/src/storage/mod.rs +++ b/implementations/rust/ockam/ockam_abac/src/storage/mod.rs @@ -1,26 +1,7 @@ +mod policy_repository; #[cfg(feature = "std")] -pub mod lmdb_storage; +pub mod policy_repository_sql; +pub use policy_repository::*; #[cfg(feature = "std")] -pub use lmdb_storage::*; - -#[cfg(feature = "sqlite")] -pub mod sqlite_storage; - -#[cfg(feature = "sqlite")] -pub use sqlite_storage::*; - -use minicbor::{Decode, Encode}; -use ockam_core::compat::borrow::Cow; - -use crate::Expr; - -/// Policy storage entry. -/// -/// Used instead of storing plain `Expr` values to allow for additional -/// metadata, versioning, etc. -#[derive(Debug, Encode, Decode)] -#[rustfmt::skip] -struct PolicyEntry<'a> { - #[b(0)] expr: Cow<'a, Expr>, -} +pub use policy_repository_sql::*; diff --git a/implementations/rust/ockam/ockam_abac/src/traits.rs b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository.rs similarity index 60% rename from implementations/rust/ockam/ockam_abac/src/traits.rs rename to implementations/rust/ockam/ockam_abac/src/storage/policy_repository.rs index 9a9509c7ac8..e9393f4fc9f 100644 --- a/implementations/rust/ockam/ockam_abac/src/traits.rs +++ b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository.rs @@ -5,9 +5,9 @@ use ockam_core::compat::vec::Vec; use ockam_core::Result; #[async_trait] -pub trait PolicyStorage: Send + Sync + 'static { +pub trait PoliciesRepository: Send + Sync + 'static { async fn get_policy(&self, r: &Resource, a: &Action) -> Result>; async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()>; - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()>; - async fn policies(&self, r: &Resource) -> Result>; + async fn delete_policy(&self, r: &Resource, a: &Action) -> Result<()>; + async fn get_policies_by_resource(&self, r: &Resource) -> Result>; } diff --git a/implementations/rust/ockam/ockam_abac/src/storage/policy_repository_sql.rs b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository_sql.rs new file mode 100644 index 00000000000..bab377aec2e --- /dev/null +++ b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository_sql.rs @@ -0,0 +1,149 @@ +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::{Action, Expr, PoliciesRepository, Resource}; + +#[derive(Clone)] +pub struct PolicySqlxDatabase { + database: Arc, +} + +impl PolicySqlxDatabase { + /// Create a new database for policies keys + pub fn new(database: Arc) -> Self { + debug!("create a repository for policies"); + Self { database } + } + + /// Create a new in-memory database for policies + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("policies")))) + } +} + +#[async_trait] +impl PoliciesRepository for PolicySqlxDatabase { + async fn get_policy(&self, resource: &Resource, action: &Action) -> Result> { + let query = query_as("SELECT * FROM policy WHERE resource=$1 and action=$2") + .bind(resource.to_sql()) + .bind(action.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.expression()).transpose()?) + } + + async fn set_policy( + &self, + resource: &Resource, + action: &Action, + expression: &Expr, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO policy VALUES (?, ?, ?)") + .bind(resource.to_sql()) + .bind(action.to_sql()) + .bind(minicbor::to_vec(expression)?.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_policy(&self, resource: &Resource, action: &Action) -> Result<()> { + let query = query("DELETE FROM policy WHERE resource = ? and action = ?") + .bind(resource.to_sql()) + .bind(action.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_policies_by_resource(&self, resource: &Resource) -> Result> { + let query = query_as("SELECT * FROM policy where resource = $1").bind(resource.to_sql()); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.into_iter() + .map(|r| r.expression().map(|e| (r.action(), e))) + .collect::>>() + } +} + +impl ToSqlxType for Resource { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.as_str().to_string()) + } +} + +impl ToSqlxType for Action { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.as_str().to_string()) + } +} + +#[derive(FromRow)] +pub(crate) struct PolicyRow { + resource: String, + action: String, + expression: Vec, +} + +impl PolicyRow { + #[allow(dead_code)] + pub(crate) fn resource(&self) -> Resource { + Resource::from(self.resource.clone()) + } + + pub(crate) fn action(&self) -> Action { + Action::from(self.action.clone()) + } + + pub(crate) fn expression(&self) -> Result { + Ok(minicbor::decode(self.expression.as_slice())?) + } +} + +#[cfg(test)] +mod test { + use core::str::FromStr; + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + + let r = Resource::from("1"); + let a = Action::from("2"); + let e = Expr::from_str("345")?; + repository.set_policy(&r, &a, &e).await?; + assert!( + repository.get_policy(&r, &a).await?.unwrap().equals(&e)?, + "Verify set and get" + ); + + let policies = repository.get_policies_by_resource(&r).await?; + assert_eq!(policies.len(), 1); + + let a = Action::from("3"); + repository.set_policy(&r, &a, &e).await?; + let policies = repository.get_policies_by_resource(&r).await?; + assert_eq!(policies.len(), 2); + + repository.delete_policy(&r, &a).await?; + let policies = repository.get_policies_by_resource(&r).await?; + assert_eq!(policies.len(), 1); + + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(PolicySqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_abac/src/storage/sqlite_storage.rs b/implementations/rust/ockam/ockam_abac/src/storage/sqlite_storage.rs deleted file mode 100644 index b28037d0b9a..00000000000 --- a/implementations/rust/ockam/ockam_abac/src/storage/sqlite_storage.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::tokio::task::{spawn_blocking, JoinError}; -use crate::{Action, Expr, PolicyStorage, Resource}; -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_identity::SqliteStorage; -use rusqlite::{params, ToSql}; -use std::borrow::Cow; - -use super::PolicyEntry; - -impl ToSql for Resource { - fn to_sql(&self) -> rusqlite::Result> { - self.as_str().to_sql() - } -} - -impl ToSql for Action { - fn to_sql(&self) -> rusqlite::Result> { - self.as_str().to_sql() - } -} - -#[async_trait] -impl PolicyStorage for SqliteStorage { - async fn get_policy(&self, r: &Resource, a: &Action) -> Result> { - let conn = self.conn(); - let r = r.clone(); - let a = a.clone(); - let t = move || { - let conn = conn.lock().unwrap(); - let result = conn - .query_row::, _, _>( - "SELECT value FROM policy WHERE resource = ?1 AND action = ?2;", - params![r, a], - |row| { - row.get::<_, Vec>(0).map(|value| { - let e: PolicyEntry = minicbor::decode(&value).unwrap(); - Some(e.expr.into_owned()) - }) - }, - ) - .map_err(map_sqlite_err); - result - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()> { - let conn = self.conn(); - let r = r.clone(); - let a = a.clone(); - let v = minicbor::to_vec(PolicyEntry { - expr: Cow::Borrowed(c), - })?; - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "INSERT OR REPLACE INTO policy (resource, action, value) VALUES (?1, ?2, ?3)", - params![r, a, v], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()> { - let conn = self.conn(); - let r = r.clone(); - let a = a.clone(); - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "DELETE FROM policy WHERE resource = ?1 AND action = ?2;", - params![r, a], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn policies(&self, r: &Resource) -> Result> { - let conn = self.conn(); - let r = r.clone(); - let t = move || { - let conn = conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT action, value FROM policy WHERE resource = ?1;") - .map_err(map_sqlite_err)?; - let result = stmt - .query_map::<(Action, Vec), _, _>(params![r], |row| { - let action: Action = Action::from(row.get::<_, String>(0)?); - let value: Vec = row.get(1)?; - Ok((action, value)) - }) - .map_err(map_sqlite_err)? - .map( - |value: core::result::Result<(Action, Vec), rusqlite::Error>| { - value.map_err(map_sqlite_err) - }, - ) - .collect::)>, Error>>()?; - let decoded_result = result - .iter() - .map(|(action, value)| { - let e: PolicyEntry = minicbor::decode(value).map_err(map_decode_err)?; - Ok((action.to_owned(), e.expr.into_owned())) - }) - .collect(); - decoded_result - }; - spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_sqlite_err(err: rusqlite::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_decode_err(err: minicbor::decode::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -#[cfg(test)] -mod test { - use super::*; - use core::str::FromStr; - use tempfile::NamedTempFile; - - #[tokio::test] - async fn test_basic_functionality() -> Result<()> { - let temp_path = NamedTempFile::new().unwrap().into_temp_path(); - let db = SqliteStorage::new(temp_path.to_path_buf()).await?; - - let r = Resource::from("1"); - let a = Action::from("2"); - let e = Expr::from_str("345")?; - db.set_policy(&r, &a, &e).await?; - assert!( - db.get_policy(&r, &a).await?.unwrap().equals(&e)?, - "Verify set and get" - ); - - let policies = db.policies(&r).await?; - assert_eq!(policies.len(), 1); - - let a = Action::from("3"); - db.set_policy(&r, &a, &e).await?; - let policies = db.policies(&r).await?; - assert_eq!(policies.len(), 2); - - db.del_policy(&r, &a).await?; - let policies = db.policies(&r).await?; - assert_eq!(policies.len(), 1); - - Ok(()) - } -} diff --git a/implementations/rust/ockam/ockam_api/Cargo.toml b/implementations/rust/ockam/ockam_api/Cargo.toml index 2e6e2d6d285..4d9af604cd6 100644 --- a/implementations/rust/ockam/ockam_api/Cargo.toml +++ b/implementations/rust/ockam/ockam_api/Cargo.toml @@ -15,8 +15,8 @@ std = [ "either/use_std", "hex/std", "minicbor/std", - "ockam_core/std", "ockam_abac/std", + "ockam_core/std", "ockam/std", "ockam_multiaddr/std", "ockam_node/std", @@ -24,8 +24,9 @@ std = [ "ockam_vault_aws/std", "tinyvec/std", "tracing/std", + "storage", ] -vault-storage = ["ockam_vault/storage"] +storage = ["ockam/storage"] [dependencies] anyhow = "1" @@ -34,6 +35,7 @@ base64-url = "2.0.0" bytes = { version = "1.5.0", default-features = false, features = ["serde"] } either = { version = "1.9.0", default-features = false } fs2 = { version = "0.4.3" } +futures = { version = "0.3.28" } hex = { version = "0.4.3", default-features = false, features = ["alloc", "serde"] } home = "0.5" kafka-protocol = "0.7.0" @@ -46,6 +48,7 @@ rand = "0.8" reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls-native-roots"] } serde = { version = "1.0.190", features = ["derive"] } serde_json = "1.0.108" +sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } sysinfo = "0.29" tempfile = "3.8.0" thiserror = "1.0" @@ -90,7 +93,7 @@ features = ["std"] version = "^0.104.0" path = "../ockam" default-features = false -features = ["ockam_transport_tcp", "software_vault_storage"] +features = ["ockam_transport_tcp", "storage"] [dependencies.ockam_abac] version = "0.38.0" diff --git a/implementations/rust/ockam/ockam_api/README.md b/implementations/rust/ockam/ockam_api/README.md index a247c1b437c..7da79ed3ffa 100644 --- a/implementations/rust/ockam/ockam_api/README.md +++ b/implementations/rust/ockam/ockam_api/README.md @@ -13,126 +13,18 @@ This crate supports the creation of a fully-featured Ockam Node ## Configuration -A `NodeManager` maintains its configuration as a list of directories and files stored under +A `NodeManager` maintains its database and log files on disk in the `OCKAM_HOME` directory (`~/.ockam`) by default: ```shell root -├─ credentials -│ ├─ c1.json -│ ├─ c2.json -│ └─ ... -├─ defaults -│ ├── credential -> ... -│ ├── identity -> ... -│ ├── node -> ... -│ └── vault -> ... -├─ identities -│ ├─ data -│ │ ├─ authenticated-storage.lmdb -│ │ └─ authenticated-storage.lmdb-lock -│ ├─ identity1.json -│ ├─ identity2.json -│ └─ ... +├─ database.sqlite ├─ nodes │ ├─ node1 -│ │ ├─ default_identity -> ... -│ │ ├─ default_vault -> ... -│ │ ├─ policies-storage.lmdb -│ │ ├─ policies-storage.lmdb-lock -│ │ ├─ setup.json │ │ ├─ stderr.log │ │ ├─ stdout.log -│ │ └─ version.log │ ├─ node2 │ └─ ... -├─ projects -│ └─ default.json -├─ trust_contexts -│ └─ default.json -└─ vaults - ├─ vault1.json - ├─ vault2.json - ├─ ... - └─ data - ├─ vault1.lmdb - ├─ vault1.lmdb-lock - ├─ vault2.lmdb - ├─ vault2.lmdb-lock - └─ ... ``` -## `credentials` - -Each file stored under the `credentials` directory contains the credential for a given identity. -Those files are created with the `ockam credential store` command. They are then read during the creation of -a secure channel to send the credentials to the other party - -## `defaults` - -This directory contains symlinks to other files or directories in order to specify which node, -identity, credential or vault must be considered as a default when running a command expecting those -inputs - -## `identities` - -This directory contains one file per identity and a data directory. An identity file is created -with the `ockam identity create` command or created by default for some commands (in that case the -`defaults/identity` symlink points to that identity). The identity file contains: - -- the identity identifier -- the enrollment status for that identity - -The `data` directory contains a LMDB database with other information about identities: - - the credential attributes that have been verified for this identity. Those attributes are - generally used in ABAC rules that are specified on secure channels. For example when sending messages - via a secure channel and using the Orchestrator the `project` attribute will be checked and the LMDB database accessed - - - the list of key changes for each identity. These key changes are created (or updated) when an identity - is created either by using the command line or by using the identity service. - The key changes are accessed in order to get the latest public key associated to a given identity - when checking its signature during the creation of a secure channel. - They are also accessed to retrieve the key id associated to that key and then use a Vault to create a signature - for an identity - -Note: for each `.lmdb` file there is a corresponding `lmdb-lock` file which is used to control -the exclusive access to the LMDB database even if several OS processes are trying to modify it. -For example when several nodes are started using the same `NodeManager`. - -## `nodes` - -This directory contains: - - - symlinks to default values for the node: identity and vault - - a database for ABAC policies - - a setup file containing some configuration information for the node (is it an authority node?, what is the TCP listener address?,...). - That file is created when a node is created and read again if the node is restarted - - log files: for system errors and system outputs. The stdout.log file is where almost all the node logs are written - - a version number for the configuration - -## `projects` - -This directory contains a list of files, one per project that was created, either the default project -or via the `ockam project create` command. A project file contains: - - - the project identifier and the space it belongs to - - the authority used by that project (identity, route) - - the configuration for the project plugins - -## `trust_context` - -This directory contains a list of files, one per trust context. A trust context can created with -the `ockam trust_context create` command. It can then be referred to during the creation of a -secure channel as a way to specify which authority can attest to the validity of which attributes - -## `vaults` - -This directory contains one file per vault that is either created by default or with the `ockam vault create` -command. That file contains the configuration for the vault, which for now consists only in -declaring if the vault is backed by an AWS KMS or not. - -The rest of the vault data is stored in an LMDB database under the `data` directory with one `.lmdb` -file per vault. A vault contains secrets which are generally used during the creation of secure -channels to sign or encrypt data involved in the handshake. - ## Usage diff --git a/implementations/rust/ockam/ockam_api/src/auth.rs b/implementations/rust/ockam/ockam_api/src/auth.rs index b1eb1e37828..c2bd9f9381a 100644 --- a/implementations/rust/ockam/ockam_api/src/auth.rs +++ b/implementations/rust/ockam/ockam_api/src/auth.rs @@ -4,7 +4,7 @@ use minicbor::Decoder; use tracing::trace; use crate::nodes::BackgroundNode; -use ockam::identity::{AttributesEntry, Identifier, IdentityAttributesReader}; +use ockam::identity::{AttributesEntry, Identifier, IdentityAttributesRepository}; use ockam_core::api::{Method, RequestHeader}; use ockam_core::api::{Request, Response}; use ockam_core::compat::sync::Arc; @@ -16,7 +16,7 @@ pub mod types; /// Auth API server. pub struct Server { - store: Arc, + identity_attributes_repository: Arc, } #[ockam_core::worker] @@ -35,8 +35,10 @@ impl Worker for Server { } impl Server { - pub fn new(s: Arc) -> Self { - Server { store: s } + pub fn new(identity_attributes_repository: Arc) -> Self { + Server { + identity_attributes_repository, + } } async fn on_request(&mut self, data: &[u8]) -> Result> { @@ -54,10 +56,16 @@ impl Server { let res = match req.method() { Some(Method::Get) => match req.path_segments::<2>().as_slice() { - [""] => Response::ok(&req).body(self.store.list().await?).to_vec()?, + [""] => Response::ok(&req) + .body(self.identity_attributes_repository.list().await?) + .to_vec()?, [id] => { let identifier = Identifier::try_from(id.to_string())?; - if let Some(a) = self.store.get_attributes(&identifier).await? { + if let Some(a) = self + .identity_attributes_repository + .get_attributes(&identifier) + .await? + { Response::ok(&req).body(a).to_vec()? } else { Response::not_found(&req, &format!("identity {} not found", id)).to_vec()? diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs b/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs index d3e815efbb2..e96fee1e00c 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs @@ -1,33 +1,32 @@ +use std::collections::HashMap; + use minicbor::Decoder; +use tracing::trace; + use ockam::identity::utils::now; -use ockam::identity::{secure_channel_required, TRUST_CONTEXT_ID}; -use ockam::identity::{AttributesEntry, IdentityAttributesReader, IdentityAttributesWriter}; +use ockam::identity::AttributesEntry; +use ockam::identity::{secure_channel_required, IdentityAttributesRepository, TRUST_CONTEXT_ID}; use ockam::identity::{Identifier, IdentitySecureChannelLocalInfo}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; use ockam_core::{CowStr, Result, Routed, Worker}; use ockam_node::Context; -use std::collections::HashMap; -use tracing::trace; use crate::authenticator::direct::types::AddMember; pub struct DirectAuthenticator { trust_context: String, - attributes_writer: Arc, - attributes_reader: Arc, + identity_attributes_repository: Arc, } impl DirectAuthenticator { pub async fn new( trust_context: String, - attributes_writer: Arc, - attributes_reader: Arc, + identity_attributes_repository: Arc, ) -> Result { Ok(Self { trust_context, - attributes_writer, - attributes_reader, + identity_attributes_repository, }) } @@ -49,11 +48,13 @@ impl DirectAuthenticator { ) .collect(); let entry = AttributesEntry::new(auth_attrs, now()?, None, Some(enroller.clone())); - self.attributes_writer.put_attributes(id, entry).await + self.identity_attributes_repository + .put_attributes(id, entry) + .await } async fn list_members(&self) -> Result> { - let all_attributes = self.attributes_reader.list().await?; + let all_attributes = self.identity_attributes_repository.list().await?; let attested_by_me = all_attributes.into_iter().collect(); Ok(attested_by_me) } @@ -98,7 +99,9 @@ impl Worker for DirectAuthenticator { } (Some(Method::Delete), [id]) | (Some(Method::Delete), ["members", id]) => { let identifier = Identifier::try_from(id.to_string())?; - self.attributes_writer.delete(&identifier).await?; + self.identity_attributes_repository + .delete(&identifier) + .await?; Response::ok(&req).to_vec()? } diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs index a4c756e7435..1caf3c6c93f 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs @@ -2,7 +2,7 @@ use minicbor::Decoder; use ockam::identity::utils::now; use ockam::identity::OneTimeCode; use ockam::identity::{secure_channel_required, TRUST_CONTEXT_ID}; -use ockam::identity::{AttributesEntry, IdentityAttributesWriter}; +use ockam::identity::{AttributesEntry, IdentityAttributesRepository}; use ockam::identity::{Identifier, IdentitySecureChannelLocalInfo}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; @@ -14,7 +14,7 @@ use crate::authenticator::enrollment_tokens::EnrollmentTokenAuthenticator; pub struct EnrollmentTokenAcceptor( pub(super) EnrollmentTokenAuthenticator, - pub(super) Arc, + pub(super) Arc, ); impl EnrollmentTokenAcceptor { diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs index ba3418f2f32..1693f07d1b3 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs @@ -1,4 +1,4 @@ -use ockam::identity::IdentityAttributesWriter; +use ockam::identity::IdentityAttributesRepository; use ockam_core::compat::collections::HashMap; use ockam_core::compat::sync::{Arc, RwLock}; use std::time::Duration; @@ -18,7 +18,7 @@ pub struct EnrollmentTokenAuthenticator { impl EnrollmentTokenAuthenticator { pub fn new_worker_pair( trust_context: String, - attributes_writer: Arc, + identity_attributes_repository: Arc, ) -> (EnrollmentTokenIssuer, EnrollmentTokenAcceptor) { let base = Self { trust_context, @@ -26,7 +26,7 @@ impl EnrollmentTokenAuthenticator { }; ( EnrollmentTokenIssuer(base.clone()), - EnrollmentTokenAcceptor(base, attributes_writer), + EnrollmentTokenAcceptor(base, identity_attributes_repository), ) } } diff --git a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs index 79a0d941189..e0a05d27a97 100644 --- a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs +++ b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs @@ -2,12 +2,12 @@ use std::path::Path; use tracing::info; -use ockam::identity::storage::LmdbStorage; -use ockam::identity::Vault; +use ockam::identity::storage::PurposeKeysSqlxDatabase; +use ockam::identity::{ChangeHistorySqlxDatabase, Vault}; use ockam::identity::{ - CredentialsIssuer, Identifier, Identities, IdentitiesRepository, IdentitiesStorage, - IdentityAttributesReader, IdentityAttributesWriter, SecureChannelListenerOptions, - SecureChannels, TrustEveryonePolicy, + CredentialsIssuer, Identifier, Identities, IdentityAttributesRepository, + IdentityAttributesSqlxDatabase, SecureChannelListenerOptions, SecureChannels, + TrustEveryonePolicy, }; use ockam_abac::expr::{and, eq, ident, str}; use ockam_abac::{AbacAccessControl, Env}; @@ -15,13 +15,14 @@ use ockam_core::compat::sync::Arc; use ockam_core::errcode::{Kind, Origin}; use ockam_core::flow_control::FlowControlId; use ockam_core::{Error, Result, Worker}; +use ockam_node::database::SqlxDatabase; use ockam_node::{Context, WorkerBuilder}; use ockam_transport_tcp::{TcpListenerOptions, TcpTransport}; use crate::authenticator::enrollment_tokens::EnrollmentTokenAuthenticator; use crate::authority_node::authority::EnrollerCheck::{AnyMember, EnrollerOnly}; use crate::authority_node::Configuration; -use crate::bootstrapped_identities_store::BootstrapedIdentityStore; +use crate::bootstrapped_identities_store::BootstrapedIdentityAttributesStore; use crate::echoer::Echoer; use crate::{actions, DefaultAddress}; @@ -56,11 +57,26 @@ impl Authority { /// In practice it contains the list of identities with the ockam-role attribute set as 'enroller' pub async fn create(configuration: &Configuration) -> Result { debug!(?configuration, "creating the authority"); - let vault = Self::create_secure_channels_vault(configuration).await?; - let repository = Self::create_identities_repository(configuration).await?; + + // create the database + let database_path = &configuration.database_path; + Self::create_ockam_directory_if_necessary(database_path)?; + let database = Arc::new(SqlxDatabase::create(database_path).await?); + + // create the repositories + let vault = Vault::create_with_persistent_storage_path(database_path).await?; + let identity_attributes_repository = + Arc::new(IdentityAttributesSqlxDatabase::new(database.clone())); + let identity_attributes_repository = + Self::bootstrap_repository(identity_attributes_repository, configuration); + let change_history_repository = Arc::new(ChangeHistorySqlxDatabase::new(database.clone())); + let purpose_keys_repository = Arc::new(PurposeKeysSqlxDatabase::new(database)); + let secure_channels = SecureChannels::builder() .with_vault(vault) - .with_identities_repository(repository) + .with_identity_attributes_repository(identity_attributes_repository) + .with_change_history_repository(change_history_repository) + .with_purpose_keys_repository(purpose_keys_repository) .build(); let identifier = configuration.identifier(); @@ -100,7 +116,10 @@ impl Authority { let tcp = TcpTransport::create(ctx).await?; let listener = tcp - .listen(configuration.tcp_listener_address(), tcp_listener_options) + .listen( + configuration.tcp_listener_address().to_string(), + tcp_listener_options, + ) .await?; info!("started a TCP listener at {listener:?}"); @@ -120,8 +139,7 @@ impl Authority { let direct = crate::authenticator::direct::DirectAuthenticator::new( configuration.project_identifier(), - self.attributes_writer(), - self.attributes_reader(), + self.identity_attributes_repository(), ) .await?; @@ -149,7 +167,7 @@ impl Authority { let (issuer, acceptor) = EnrollmentTokenAuthenticator::new_worker_pair( configuration.project_identifier(), - self.attributes_writer(), + self.identity_attributes_repository(), ); // start an enrollment token issuer with an abac policy checking that @@ -192,7 +210,9 @@ impl Authority { ) -> Result<()> { // create and start a credential issuer worker let issuer = CredentialsIssuer::new( - self.secure_channels.identities().repository(), + self.secure_channels + .identities() + .identity_attributes_repository(), self.secure_channels.identities().credentials(), &self.identifier, configuration.project_identifier(), @@ -218,7 +238,7 @@ impl Authority { ) -> Result<()> { if let Some(okta) = &configuration.okta { let okta_worker = crate::okta::Server::new( - self.attributes_writer(), + self.identity_attributes_repository(), configuration.project_identifier(), okta.tenant_base_url(), okta.certificate(), @@ -255,38 +275,9 @@ impl Authority { self.secure_channels.identities() } - /// Return the identities repository used by the authority - fn identities_repository(&self) -> Arc { - self.identities().repository().clone() - } - - /// Return the identities repository as writer used by the authority - fn attributes_writer(&self) -> Arc { - self.identities_repository().as_attributes_writer().clone() - } - - /// Return the identities repository as reader used by the authority - fn attributes_reader(&self) -> Arc { - self.identities_repository().as_attributes_reader().clone() - } - - /// Create an identity vault backed by a FileStorage - async fn create_secure_channels_vault(configuration: &Configuration) -> Result { - let vault_path = &configuration.vault_path; - Self::create_ockam_directory_if_necessary(vault_path)?; - let vault = Vault::create_with_persistent_storage_path(vault_path).await?; - Ok(vault) - } - - /// Create an authenticated storage backed by a Lmdb database - async fn create_identities_repository( - configuration: &Configuration, - ) -> Result> { - let storage_path = &configuration.storage_path; - Self::create_ockam_directory_if_necessary(storage_path)?; - let storage = Arc::new(LmdbStorage::new(&storage_path).await?); - let repository = Arc::new(IdentitiesStorage::new(storage)); - Ok(Self::bootstrap_repository(repository, configuration)) + /// Return the identity attributes repository used by the authority + fn identity_attributes_repository(&self) -> Arc { + self.identities().identity_attributes_repository().clone() } /// Create a directory to save storage files if they haven't been created before @@ -302,11 +293,11 @@ impl Authority { /// identities. The values either come from the command line or are read directly from a file /// every time we try to retrieve some attributes fn bootstrap_repository( - repository: Arc, + repository: Arc, configuration: &Configuration, - ) -> Arc { + ) -> Arc { let trusted_identities = &configuration.trusted_identities; - Arc::new(BootstrapedIdentityStore::new( + Arc::new(BootstrapedIdentityAttributesStore::new( Arc::new(trusted_identities.clone()), repository.clone(), )) @@ -369,7 +360,7 @@ impl Authority { str(configuration.project_identifier.clone()), ); let abac = Arc::new(AbacAccessControl::new( - self.identities_repository(), + self.identity_attributes_repository(), rule, env, )); diff --git a/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs b/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs index 16f8487864a..9bcf4c3deb6 100644 --- a/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs +++ b/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs @@ -1,6 +1,7 @@ use crate::bootstrapped_identities_store::PreTrustedIdentities; use crate::DefaultAddress; +use crate::config::lookup::InternetAddress; use ockam::identity::utils::now; use ockam::identity::{AttributesEntry, Identifier, TRUST_CONTEXT_ID}; use ockam_core::compat::collections::HashMap; @@ -16,17 +17,14 @@ pub struct Configuration { /// Authority identity or identity associated with the newly created node pub identifier: Identifier, - /// path where the storage for identity attributes should be persisted - pub storage_path: PathBuf, - - /// path where secrets should be persisted - pub vault_path: PathBuf, + /// path where the database should be stored + pub database_path: PathBuf, /// Project identifier on the Orchestrator node pub project_identifier: String, /// listener address for the TCP listener, for example "127.0.0.1:4000" - pub tcp_listener_address: String, + pub tcp_listener_address: InternetAddress, /// service name for the secure channel listener, for example "secure" /// The default is DefaultAddress::SECURE_CHANNEL_LISTENER @@ -62,7 +60,7 @@ impl Configuration { } /// Return the address for the TCP listener - pub(crate) fn tcp_listener_address(&self) -> String { + pub(crate) fn tcp_listener_address(&self) -> InternetAddress { self.tcp_listener_address.clone() } diff --git a/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs b/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs index 47de22c2a77..54a009ff0e5 100644 --- a/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs +++ b/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs @@ -1,29 +1,27 @@ -use ockam::identity::models::ChangeHistory; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use serde_json as json; +use tracing::trace; + use ockam::identity::utils::now; -use ockam::identity::{ - AttributesEntry, Identifier, IdentitiesReader, IdentitiesRepository, IdentitiesWriter, - IdentityAttributesReader, IdentityAttributesWriter, -}; +use ockam::identity::{AttributesEntry, Identifier, IdentityAttributesRepository}; use ockam_core::async_trait; use ockam_core::compat::sync::Arc; use ockam_core::compat::{collections::HashMap, string::String, vec::Vec}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::Result; -use serde::{Deserialize, Serialize}; -use serde_json as json; -use std::path::PathBuf; -use tracing::trace; #[derive(Clone)] -pub struct BootstrapedIdentityStore { - bootstrapped: Arc, - repository: Arc, +pub struct BootstrapedIdentityAttributesStore { + bootstrapped: Arc, + repository: Arc, } -impl BootstrapedIdentityStore { +impl BootstrapedIdentityAttributesStore { pub fn new( - bootstrapped: Arc, - repository: Arc, + bootstrapped: Arc, + repository: Arc, ) -> Self { Self { bootstrapped, @@ -33,7 +31,7 @@ impl BootstrapedIdentityStore { } #[async_trait] -impl IdentityAttributesReader for BootstrapedIdentityStore { +impl IdentityAttributesRepository for BootstrapedIdentityAttributesStore { async fn get_attributes(&self, identity_id: &Identifier) -> Result> { trace! { target: "ockam_api::bootstrapped_identities_store", @@ -52,10 +50,7 @@ impl IdentityAttributesReader for BootstrapedIdentityStore { l.append(&mut l2); Ok(l) } -} -#[async_trait] -impl IdentityAttributesWriter for BootstrapedIdentityStore { async fn put_attributes(&self, sender: &Identifier, entry: AttributesEntry) -> Result<()> { trace! { target: "ockam_api::bootstrapped_identities_store", @@ -89,47 +84,6 @@ impl IdentityAttributesWriter for BootstrapedIdentityStore { } } -#[async_trait] -impl IdentitiesReader for BootstrapedIdentityStore { - async fn retrieve_identity(&self, identifier: &Identifier) -> Result> { - self.repository.retrieve_identity(identifier).await - } - async fn get_identity(&self, identifier: &Identifier) -> Result { - self.repository.get_identity(identifier).await - } -} - -#[async_trait] -impl IdentitiesWriter for BootstrapedIdentityStore { - async fn update_identity( - &self, - identifier: &Identifier, - change_history: &ChangeHistory, - ) -> Result<()> { - self.repository - .update_identity(identifier, change_history) - .await - } -} - -impl IdentitiesRepository for BootstrapedIdentityStore { - fn as_attributes_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_attributes_writer(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_writer(&self) -> Arc { - Arc::new(self.clone()) - } -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub enum PreTrustedIdentities { Fixed(HashMap), @@ -183,7 +137,7 @@ impl From> for PreTrustedIdentities { } #[async_trait] -impl IdentityAttributesReader for PreTrustedIdentities { +impl IdentityAttributesRepository for PreTrustedIdentities { async fn get_attributes(&self, identity_id: &Identifier) -> Result> { match self { PreTrustedIdentities::Fixed(trusted) => Ok(trusted.get(identity_id).cloned()), @@ -205,4 +159,21 @@ impl IdentityAttributesReader for PreTrustedIdentities { .collect()), } } + + async fn put_attributes(&self, _identity: &Identifier, _entry: AttributesEntry) -> Result<()> { + Ok(()) + } + + async fn put_attribute_value( + &self, + _subject: &Identifier, + _attribute_name: Vec, + _attribute_value: Vec, + ) -> Result<()> { + Ok(()) + } + + async fn delete(&self, _identity: &Identifier) -> Result<()> { + Ok(()) + } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs b/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs index 1ec52a829e4..b999d0f7b71 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs @@ -1,107 +1,174 @@ +use ockam::identity::models::{ChangeHistory, CredentialAndPurposeKey}; +use ockam::identity::{AttributesEntry, Identifier, Identity}; + +use crate::cli_state::{CliState, CliStateError}; + use super::Result; -use crate::cli_state::CliStateError; -use ockam::identity::models::CredentialAndPurposeKey; -use ockam::identity::Identifier; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CredentialsState { - dir: PathBuf, -} -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct CredentialState { - name: String, - path: PathBuf, - config: CredentialConfig, -} +impl CliState { + /// Store a credential inside the local database + /// This function stores both the credential as a named entity + /// and the identity attributes in another table. + /// TODO: normalize the storage so that the data is only represented once + pub async fn store_credential( + &self, + name: &str, + issuer: &Identity, + credential: CredentialAndPurposeKey, + ) -> Result<()> { + // store the subject attributes + let credential_data = credential.get_credential_data()?; + let identity_attributes_repository = self.identity_attributes_repository().await?; + if let Some(subject) = credential_data.subject { + let attributes_entry = AttributesEntry::new( + credential_data + .subject_attributes + .map + .into_iter() + .map(|(k, v)| (k.to_vec(), v.to_vec())) + .collect(), + credential_data.created_at, + Some(credential_data.expires_at), + Some(issuer.identifier().clone()), + ); + identity_attributes_repository + .put_attributes(&subject, attributes_entry) + .await?; + } + + // store the credential itself + let credentials_repository = self.credentials_repository().await?; + credentials_repository + .store_credential(name, issuer, credential) + .await?; + Ok(()) + } -impl CredentialState { - pub fn name(&self) -> &str { - &self.name + pub async fn get_credential_by_name(&self, name: &str) -> Result { + match self + .credentials_repository() + .await? + .get_credential(name) + .await? + { + Some(credential) => Ok(credential), + None => Err(CliStateError::ResourceNotFound { + name: name.to_string(), + resource: "credential".into(), + }), + } + } + + pub async fn get_credentials(&self) -> Result> { + Ok(self + .credentials_repository() + .await? + .get_credentials() + .await?) } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CredentialConfig { - pub issuer_identifier: Identifier, - // FIXME: Appear as array of number in JSON - pub encoded_issuer_change_history: Vec, - // FIXME: Appear as array of number in JSON - pub encoded_credential: Vec, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NamedCredential { + name: String, + issuer_identifier: Identifier, + issuer_change_history: ChangeHistory, + credential: CredentialAndPurposeKey, } -impl CredentialConfig { - pub fn new( +impl NamedCredential { + pub fn new(name: &str, issuer: &Identity, credential: CredentialAndPurposeKey) -> Self { + Self::make( + name, + issuer.identifier().clone(), + issuer.change_history().clone(), + credential, + ) + } + + pub fn make( + name: &str, issuer_identifier: Identifier, - encoded_issuer_change_history: Vec, - encoded_credential: Vec, - ) -> Result { - Ok(Self { + issuer_change_history: ChangeHistory, + credential: CredentialAndPurposeKey, + ) -> Self { + Self { + name: name.to_string(), issuer_identifier, - encoded_issuer_change_history, - encoded_credential, - }) + issuer_change_history, + credential, + } + } +} + +impl NamedCredential { + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn issuer_identifier(&self) -> Identifier { + self.issuer_identifier.clone() + } + + pub async fn issuer_identity(&self) -> Result { + Ok(Identity::create_from_change_history(&self.issuer_change_history).await?) } - pub fn credential(&self) -> Result { - minicbor::decode(&self.encoded_credential).map_err(|e| { - error!(%e, "Unable to decode credential"); - CliStateError::InvalidOperation("Unable to decode credential".to_string()) - }) + pub fn issuer_change_history(&self) -> ChangeHistory { + self.issuer_change_history.clone() + } + + pub fn credential_and_purpose_key(&self) -> CredentialAndPurposeKey { + self.credential.clone() } } -mod traits { +#[cfg(test)] +mod test { + use std::sync::Arc; + use std::time::Duration; + + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identities}; + use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for CredentialsState { - type Item = CredentialState; - const DEFAULT_FILENAME: &'static str = "credential"; - const DIR_NAME: &'static str = "credentials"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } - fn dir(&self) -> &PathBuf { - &self.dir - } - } + #[tokio::test] + async fn test_cli_spaces() -> Result<()> { + let cli = CliState::test().await?; + let identities = identities(); + let issuer_identifier = identities.identities_creation().create_identity().await?; + let issuer = identities.get_identity(&issuer_identifier).await?; + let credential = create_credential(identities, &issuer_identifier).await?; - #[async_trait] - impl StateItemTrait for CredentialState { - type Config = CredentialConfig; + // a credential can be stored and retrieved by name + cli.store_credential("name1", &issuer, credential.clone()) + .await?; + let result = cli.get_credential_by_name("name1").await?; + assert_eq!(result.name(), "name1".to_string()); + assert_eq!(result.issuer_identifier(), issuer_identifier); + assert_eq!(result.issuer_change_history(), *issuer.change_history()); + assert_eq!(result.credential_and_purpose_key(), credential); - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) - } + Ok(()) + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) - } + /// HELPERS + async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; - fn path(&self) -> &PathBuf { - &self.path - } + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); - fn config(&self) -> &Self::Config { - &self.config - } + Ok(identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await?) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/enrollment.rs b/implementations/rust/ockam/ockam_api/src/cli_state/enrollment.rs new file mode 100644 index 00000000000..740404b916c --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/enrollment.rs @@ -0,0 +1,330 @@ +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::FromRow; +use sqlx::*; +use time::OffsetDateTime; + +use ockam::identity::Identifier; +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_core::async_trait; + +use crate::cli_state::CliState; +use crate::cli_state::Result; + +impl CliState { + pub async fn is_identity_enrolled(&self, name: &Option) -> Result { + let repository = self.enrollment_repository().await?; + + match name { + Some(name) => repository.is_identity_enrolled(name).await, + None => repository.is_default_identity_enrolled().await, + } + } + + pub async fn is_default_identity_enrolled(&self) -> Result { + self.enrollment_repository() + .await? + .is_default_identity_enrolled() + .await + } + + pub async fn set_identifier_as_enrolled(&self, identifier: &Identifier) -> Result<()> { + self.enrollment_repository() + .await? + .set_as_enrolled(identifier) + .await + } + + pub async fn set_node_as_enrolled(&self, node_name: &str) -> Result<()> { + let node = self.get_node(node_name).await?; + self.set_identifier_as_enrolled(&node.identifier()).await + } + + pub async fn get_identity_enrollments( + &self, + enrollment_status: EnrollmentStatus, + ) -> Result> { + let repository = self.enrollment_repository().await?; + match enrollment_status { + EnrollmentStatus::Enrolled => repository.get_enrolled_identities().await, + EnrollmentStatus::Any => repository.get_all_identities_enrollments().await, + } + } +} + +#[async_trait] +pub trait EnrollmentsRepository: Send + Sync + 'static { + async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()>; + async fn get_enrolled_identities(&self) -> Result>; + async fn get_all_identities_enrollments(&self) -> Result>; + async fn is_default_identity_enrolled(&self) -> Result; + async fn is_identity_enrolled(&self, name: &str) -> Result; +} + +pub struct EnrollmentsSqlxDatabase { + database: Arc, +} + +impl EnrollmentsSqlxDatabase { + pub fn new(database: Arc) -> Self { + debug!("create a repository for enrollments"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("enrollments")))) + } +} + +#[async_trait] +impl EnrollmentsRepository for EnrollmentsSqlxDatabase { + async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity_enrollment VALUES (?, ?)") + .bind(identifier.to_sql()) + .bind(OffsetDateTime::now_utc().to_sql()); + Ok(query.execute(&self.database.pool).await.void()?) + } + + async fn get_enrolled_identities(&self) -> Result> { + let query = query_as( + r#" + SELECT + identity.identifier, named_identity.name, named_identity.is_default, + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + "#, + ) + .bind(None as Option); + let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identity_enrollment()) + .collect::>>() + } + + async fn get_all_identities_enrollments(&self) -> Result> { + let query = query_as( + r#" + SELECT + identity.identifier, named_identity.name, named_identity.is_default, + identity_enrollment.enrolled_at + FROM identity + LEFT JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + "#, + ); + let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identity_enrollment()) + .collect::>>() + } + + async fn is_default_identity_enrolled(&self) -> Result { + let query = query( + r#" + SELECT + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + WHERE + named_identity.is_default = ? + "#, + ) + .bind(true.to_sql()); + let result: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(result.map(|_| true).unwrap_or(false)) + } + + async fn is_identity_enrolled(&self, name: &str) -> Result { + let query = query( + r#" + SELECT + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + WHERE + named_identity.name = ? + "#, + ) + .bind(name.to_sql()); + let result: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(result.map(|_| true).unwrap_or(false)) + } +} + +pub enum EnrollmentStatus { + Enrolled, + Any, +} + +pub struct IdentityEnrollment { + identifier: Identifier, + name: Option, + is_default: bool, + enrolled_at: Option, +} + +impl IdentityEnrollment { + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } + + #[allow(dead_code)] + pub fn name(&self) -> Option { + self.name.clone() + } + + #[allow(dead_code)] + pub fn is_enrolled(&self) -> bool { + self.enrolled_at.is_some() + } + + #[allow(dead_code)] + pub fn is_default(&self) -> bool { + self.is_default + } + + #[allow(dead_code)] + pub fn enrolled_at(&self) -> Option { + self.enrolled_at + } +} + +#[derive(FromRow)] +pub struct EnrollmentRow { + identifier: String, + name: Option, + is_default: bool, + enrolled_at: Option, +} + +impl EnrollmentRow { + fn identity_enrollment(&self) -> Result { + let identifier = Identifier::from_str(self.identifier.as_str())?; + Ok(IdentityEnrollment { + identifier, + name: self.name.clone(), + is_default: self.is_default, + enrolled_at: self.enrolled_at(), + }) + } + + fn enrolled_at(&self) -> Option { + self.enrolled_at + .map(|at| OffsetDateTime::from_unix_timestamp(at).unwrap_or(OffsetDateTime::now_utc())) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use tempfile::NamedTempFile; + + use ockam::identity::{ChangeHistoryRepository, ChangeHistorySqlxDatabase, Identity}; + + use crate::identity::{IdentitiesRepository, IdentitiesSqlxDatabase}; + + use super::*; + + #[tokio::test] + async fn test_identities_enrollment_repository() -> Result<()> { + let db_file = NamedTempFile::new().unwrap(); + let identity1 = create_identity1(db_file.path(), "identity1").await?; + create_identity2(db_file.path(), "identity2").await?; + let repository = create_repository(db_file.path()).await?; + + // an identity can be enrolled + repository.set_as_enrolled(identity1.identifier()).await?; + + // retrieve the identities and their enrollment status + let result = repository.get_all_identities_enrollments().await?; + assert_eq!(result.len(), 2); + + // retrieve only the enrolled identities + let result = repository.get_enrolled_identities().await?; + assert_eq!(result.len(), 1); + + // the first identity has been set as the default one + let result = repository.is_default_identity_enrolled().await?; + assert!(result); + + Ok(()) + } + + /// HELPERS + async fn create_identity1(path: &Path, name: &str) -> Result { + let identity = Identity::create( + "81a201583ba20101025835a4028201815820530d1c2e9822433b679a66a60b9c2ed47c370cd0ce51cbe1a7ad847b5835a96303f4041a64dd4060051a77a94360028201815840042fff8f6c80603fb1cec4a3cf1ff169ee36889d3ed76184fe1dfbd4b692b02892df9525c61c2f1286b829586d13d5abf7d18973141f734d71c1840520d40a0e", + ) + .await?; + store_identity(path, name, identity).await + } + + async fn create_identity2(path: &Path, name: &str) -> Result { + let identity = Identity::create( + "81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805", + ) + .await?; + store_identity(path, name, identity).await + } + + async fn store_identity(path: &Path, name: &str, identity: Identity) -> Result { + let change_history_repository = create_change_history_repository(path).await?; + let identities_repository = create_identities_repository(path).await?; + change_history_repository + .store_change_history(&identity) + .await?; + + identities_repository + .store_named_identity(identity.identifier(), name, "vault") + .await?; + if name == "identity1" { + identities_repository + .set_as_default(identity.identifier()) + .await?; + } + Ok(identity) + } + + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(EnrollmentsSqlxDatabase::new(Arc::new(db)))) + } + + async fn create_change_history_repository( + path: &Path, + ) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(ChangeHistorySqlxDatabase::new(Arc::new(db)))) + } + + async fn create_identities_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(IdentitiesSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs index 828e2c35825..819e8ecaaaf 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs @@ -1,347 +1,530 @@ -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::SystemTime; - -use serde::{Deserialize, Serialize}; -use time::format_description::well_known::Iso8601; -use time::OffsetDateTime; - -use ockam::identity::storage::LmdbStorage; -use ockam::identity::{Identifier, IdentitiesRepository, IdentitiesStorage}; - -use crate::cli_state::traits::{StateDirTrait, StateItemTrait}; -use crate::cli_state::{CliStateError, DATA_DIR_NAME}; - -use super::Result; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct IdentitiesState { - dir: PathBuf, -} - -impl IdentitiesState { - pub fn get_or_default(&self, name: Option<&str>) -> Result { - if let Some(identity_name) = name { - self.get(identity_name) - } else { - self.default() +use ockam::identity::models::ChangeHistory; +use ockam::identity::{Identifier, Identity}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_vault::{HandleToSecret, SigningSecretKeyHandle}; + +use crate::cli_state::{random_name, CliState, Result}; +use crate::identity::{NamedIdentity, NamedVault}; +use crate::nodes::NodeInfo; + +/// The methods below help with the management of identities +impl CliState { + /// Create an identity associated with a name and a specific vault name + /// Don't do anything if the identity has already been created with the same name + pub async fn create_identity_with_name_and_vault( + &self, + name: &str, + vault_name: &str, + ) -> Result { + match self.get_named_identity(name).await.ok() { + Some(identity) => Ok(identity.identifier()), + None => { + let vault = self.get_named_vault(vault_name).await?; + let identity = self.create_identity_with_vault(vault).await?; + Ok(self + .store_named_identity(&identity, name, vault_name) + .await? + .identifier()) + } } } - pub fn get_by_identifier(&self, identifier: &Identifier) -> Result { - self.list()? - .into_iter() - .find(|ident_state| &ident_state.config.identifier() == identifier) - .ok_or(CliStateError::ResourceNotFound { - resource: Self::default_filename().to_string(), - name: identifier.to_string(), - }) + /// Create an identity associated with an optional name and an optional vault name + /// If the vault name is not specified then the default vault is used + pub async fn create_identity_with_optional_name_and_optional_vault( + &self, + name: &Option, + vault_name: &Option, + ) -> Result { + // don't recreate an identity if it already exists with that name + let name = name.clone().unwrap_or_else(|| "default".to_string()); + if let Ok(identity) = self.get_named_identity(&name).await { + return Ok(identity); + }; + + let vault = self.get_named_vault_or_default(vault_name).await?; + let identity = self.create_identity_with_vault(vault.clone()).await?; + self.store_named_identity(&identity, &name, &vault.name()) + .await } - pub async fn identities_repository(&self) -> Result> { - let lmdb_path = self.identities_repository_path()?; - Ok(Arc::new(IdentitiesStorage::new(Arc::new( - LmdbStorage::new(lmdb_path).await?, - )))) + /// Create an identity associated with a name, using the default vault + pub async fn create_identity_with_name(&self, name: &str) -> Result { + info!("creating an identity with name {name}"); + let vault = self.get_default_vault().await?; + let identity = self.create_identity_with_vault(vault.clone()).await?; + self.identities_repository() + .await? + .store_named_identity(&identity, name, &vault.name()) + .await?; + Ok(identity) } - pub fn identities_repository_path(&self) -> Result { - let lmdb_path = self - .dir - .join(DATA_DIR_NAME) - .join("authenticated_storage.lmdb"); - Ok(lmdb_path) + /// Create an identity associated with no name, using the default vault + pub async fn create_identity(&self) -> Result { + let vault = self.get_default_vault().await?; + self.create_identity_with_vault(vault).await } -} -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct IdentityState { - name: String, - path: PathBuf, - /// The path to the directory containing the authenticated storage files, shared amongst all identities - data_path: PathBuf, - config: IdentityConfig, -} - -impl IdentityState { - pub fn identifier(&self) -> Identifier { - self.config.identifier() + pub async fn create_identity_with_random_name(&self) -> Result { + let vault_name = self.get_default_vault_name().await?; + self.create_identity_with_name_and_vault(&random_name(), &vault_name) + .await } - pub fn set_enrollment_status(&mut self) -> Result<()> { - self.config.enrollment_status = Some(EnrollmentStatus::enrolled()); - self.persist() + /// Create an identity with specific key id + /// The given vault needs to be a KMS vault + pub async fn create_identity_with_key_id( + &self, + name: &str, + vault_name: &str, + key_id: &str, + ) -> Result { + let vault = self.get_named_vault(vault_name).await?; + + // Check that the vault is an KMS vault + if !vault.is_kms() { + return Err(Error::new( + Origin::Api, + Kind::Misuse, + format!("Vault {vault_name} is not a KMS vault"), + ) + .into()); + }; + + let handle = SigningSecretKeyHandle::ECDSASHA256CurveP256(HandleToSecret::new( + key_id.as_bytes().to_vec(), + )); + + // create the identity + let identifier = self + .get_identities_for_vault(vault) + .await? + .identities_creation() + .identity_builder() + .with_existing_key(handle) + .build() + .await? + .clone(); + + Ok(self + .store_named_identity(&identifier, name, vault_name) + .await? + .identifier()) } - fn build_data_path(path: &Path) -> PathBuf { - path.parent() - .expect("Should have parent") - .join(DATA_DIR_NAME) + pub async fn get_identifier_by_name(&self, name: &str) -> Result { + match self + .identities_repository() + .await? + .get_identifier_by_name(name) + .await? + { + Some(identifier) => Ok(identifier), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no identity with name {name}"), + ) + .into()), + } } - pub fn name(&self) -> &str { - &self.name + pub async fn get_named_identities(&self) -> Result> { + Ok(self + .identities_repository() + .await? + .get_named_identities() + .await?) } - pub fn is_enrolled(&self) -> bool { - self.config - .enrollment_status - .as_ref() - .map(|s| s.is_enrolled) - .unwrap_or(false) + pub async fn get_named_identity_or_default( + &self, + name: &Option, + ) -> Result { + let repository = self.identities_repository().await?; + let result = match name { + Some(name) => repository.get_named_identity(name).await?, + None => repository.get_default_named_identity().await?, + }; + match result { + Some(identity) => Ok(identity), + None => Err(Error::new(Origin::Api, Kind::NotFound, "no identity found").into()), + } } -} -impl Display for IdentityState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!( - f, - "Name: {}", - self.path.as_path().file_stem().unwrap().to_str().unwrap() - )?; - writeln!(f, "State Path: {}", self.path.clone().to_str().unwrap())?; - writeln!(f, "Config Identifier: {}", self.config.identifier())?; - match &self.config.enrollment_status { - Some(enrollment) => { - writeln!(f, "Enrollment Status:")?; - for line in enrollment.to_string().lines() { - writeln!(f, "{:2}{}", "", line)?; - } - } - None => (), + pub async fn get_named_identity(&self, name: &str) -> Result { + let repository = self.identities_repository().await?; + match repository.get_named_identity(name).await? { + Some(identity) => Ok(identity), + None => Err(Error::new(Origin::Api, Kind::NotFound, "no identity found").into()), } - Ok(()) } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct IdentityConfig { - pub identifier: Identifier, - pub enrollment_status: Option, -} -impl PartialEq for IdentityConfig { - fn eq(&self, other: &Self) -> bool { - self.identifier == other.identifier + pub async fn get_identifier_by_optional_name( + &self, + name: &Option, + ) -> Result { + let repository = self.identities_repository().await?; + let result = match name { + Some(name) => repository.get_identifier_by_name(name).await?, + None => repository.get_default_identifier().await?, + }; + + result.ok_or_else(|| Self::missing_identifier(name).into()) } -} -impl Eq for IdentityConfig {} + pub async fn get_identifier_by_optional_name_or_create_identity( + &self, + name: &Option, + ) -> Result { + let identifier = match name { + Some(name) => { + self.identities_repository() + .await? + .get_identifier_by_name(name) + .await? + } -impl IdentityConfig { - pub async fn new(identifier: &Identifier) -> Self { - Self { - identifier: identifier.clone(), - enrollment_status: None, + None => { + self.identities_repository() + .await? + .get_default_identifier() + .await? + } + }; + + match identifier { + Some(identifier) => Ok(identifier), + None => { + let identity = match name { + Some(name) => self.create_identity_with_name(name).await?, + None => self.create_identity().await?, + }; + Ok(identity.clone()) + } } } - pub fn identifier(&self) -> Identifier { - self.identifier.clone() + pub async fn get_identity_by_optional_name(&self, name: &Option) -> Result { + let named_identity = match name { + Some(name) => { + self.identities_repository() + .await? + .get_named_identity(name) + .await? + } + + None => { + self.identities_repository() + .await? + .get_default_named_identity() + .await? + } + }; + match named_identity { + Some(identity) => { + let change_history = self.get_change_history(&identity.identifier()).await?; + Ok(Identity::import_from_change_history( + Some(&identity.identifier()), + change_history, + self.get_default_vault() + .await? + .vault() + .await? + .verifying_vault, + ) + .await?) + } + None => Err(Self::missing_identifier(name).into()), + } } -} -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EnrollmentStatus { - pub is_enrolled: bool, - pub created_at: SystemTime, -} + /// Return the name of the default identity (create one if necessary) + pub async fn get_default_identity_name(&self) -> Result { + Ok(self.get_default_named_identity().await?.name()) + } -impl EnrollmentStatus { - pub fn enrolled() -> EnrollmentStatus { - EnrollmentStatus { - is_enrolled: true, - created_at: SystemTime::now(), + /// Return the default named identity (create one if necessary) + pub async fn get_default_named_identity(&self) -> Result { + match self + .identities_repository() + .await? + .get_default_named_identity() + .await? + { + Some(named_identity) => Ok(named_identity), + None => { + let identifier = self.create_identity_with_random_name().await?; + self.get_named_identity_by_identifier(&identifier).await + } } } -} -impl Display for EnrollmentStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.is_enrolled { - writeln!(f, "Enrolled: yes")?; - } else { - writeln!(f, "Enrolled: no")?; + /// Return the named identity with the given identifier + pub async fn get_named_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result { + match self + .identities_repository() + .await? + .get_named_identity_by_identifier(identifier) + .await? + { + Some(named_identity) => Ok(named_identity), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no named identity found for identifier {identifier}"), + ) + .into()), } + } - match OffsetDateTime::from(self.created_at).format(&Iso8601::DEFAULT) { - Ok(time_str) => writeln!(f, "Timestamp: {}", time_str)?, - Err(err) => writeln!( - f, - "Error formatting OffsetDateTime as Iso8601 String: {}", - err - )?, + /// Return the identity with the given identifier + pub async fn get_identity(&self, identifier: &Identifier) -> Result { + match self + .change_history_repository() + .await? + .get_change_history(identifier) + .await? + { + Some(change_history) => { + Ok(Identity::create_from_change_history(&change_history).await?) + } + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no identity found for identifier {identifier}"), + ) + .into()), } - - Ok(()) } -} -// TODO: No longer supported: consider deleting -#[derive(Deserialize, Debug, Clone)] -struct IdentityConfigV1 { - // Easiest way to fail deserialization - _non_existent_field: bool, -} - -// TODO: No longer supported: consider deleting -#[derive(Deserialize, Debug, Clone)] -struct IdentityConfigV2 { - // Easiest way to fail deserialization - _non_existent_field: bool, -} - -// TODO: No longer supported: consider deleting -#[derive(Deserialize, Debug, Clone)] -struct IdentityConfigV3 { - // Easiest way to fail deserialization - _non_existent_field: bool, -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(untagged)] -enum IdentityConfigs { - V1(IdentityConfigV1), - V2(IdentityConfigV2), - V3(IdentityConfigV3), - V4(IdentityConfig), -} - -mod traits { - use ockam_core::async_trait; - - use crate::cli_state::traits::*; - use crate::cli_state::{file_stem, CliStateError}; + /// Return: + /// - the given name if defined + /// - or the name of the default identity if it exists + /// - or a random name to be used to create a default identity + pub async fn get_identity_name_or_default(&self, name: &Option) -> Result { + match name { + Some(name) => Ok(name.clone()), + None => Ok(match self.get_default_identity_name().await.ok() { + Some(name) => name, + None => random_name(), + }), + } + } - use super::*; + /// Return true if there is an identity with that name and it is the default one + pub async fn is_default_identity_by_name(&self, name: &str) -> Result { + Ok(self + .identities_repository() + .await? + .is_default_identity_by_name(name) + .await?) + } - #[async_trait] - impl StateDirTrait for IdentitiesState { - type Item = IdentityState; - const DEFAULT_FILENAME: &'static str = "identity"; - const DIR_NAME: &'static str = "identities"; - const HAS_DATA_DIR: bool = true; + /// Return the name of the default identity + pub async fn set_as_default_identity(&self, name: &str) -> Result<()> { + Ok(self + .identities_repository() + .await? + .set_as_default_by_name(name) + .await?) + } - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), + /// Delete an identity by name: + /// + /// - check that the identity is not used by a node first + /// - then remove the the name association to the identity + /// - and the identity change history + pub async fn delete_identity_by_name(&self, name: &str) -> Result<()> { + match self.get_node_by_identity_name(name).await? { + Some(node) => Err(Error::new( + Origin::Api, + Kind::Invalid, + format!( + "The identity named {name} cannot be deleted because it is used by the node {}", + node.name() + ), + ) + .into()), + None => { + if let Some(identifier) = self + .identities_repository() + .await? + .delete_identity_by_name(name) + .await? + { + self.change_history_repository() + .await? + .delete_change_history(&identifier) + .await?; + }; + Ok(()) } } + } - fn dir(&self) -> &PathBuf { - &self.dir - } - - fn delete(&self, name: impl AsRef) -> Result<()> { - // Retrieve identity. If doesn't exist do nothing. - let identity = match self.get(&name) { - Ok(i) => i, - Err(CliStateError::ResourceNotFound { .. }) => return Ok(()), - Err(e) => return Err(e), - }; - - // If it's the default, remove link - if let Ok(default) = self.default() { - if default.path == identity.path { - let _ = std::fs::remove_file(self.default_path()?); - } + /// Delete an identity by identifier: + /// + /// - check that the identity is not used by a node first + /// - then remove the the name association to the identity + /// - and the identity change history + pub async fn delete_identity_by_identifier(&self, identifier: &Identifier) -> Result<()> { + match self.get_node_by_identifier(identifier).await? { + Some(node) => Err(Error::new( + Origin::Api, + Kind::Invalid, + format!( + "The identity {identifier} cannot be deleted because it is used by the node {}", + node.name() + ), + ) + .into()), + None => { + self.identities_repository() + .await? + .delete_identity_by_identifier(identifier) + .await?; + self.change_history_repository() + .await? + .delete_change_history(identifier) + .await?; + Ok(()) } - // Remove identity file - identity.delete()?; - Ok(()) } + } - async fn migrate(&self, path: &Path) -> Result<()> { - let contents = std::fs::read_to_string(path)?; - - // read the configuration and migrate to the most recent format if an old format is found - // the most recent configuration only contains an identity identifier, so if we find an - // old format we store the full identity in the shared identities repository before - // writing the most recent configuration format - match serde_json::from_str(&contents)? { - IdentityConfigs::V1(_) | IdentityConfigs::V2(_) | IdentityConfigs::V3(_) => { - return Err(CliStateError::InvalidVersion( - "Migration not supported for old Identities".to_string(), - )) - } - IdentityConfigs::V4(_) => {} - } - Ok(()) - } + pub async fn get_node_by_identity_name(&self, identity_name: &str) -> Result> { + let identifier = self.get_identifier_by_name(identity_name).await?; + self.get_node_by_identifier(&identifier).await } - #[async_trait] - impl StateItemTrait for IdentityState { - type Config = IdentityConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - let data_path = IdentityState::build_data_path(&path); - Ok(Self { - name, - path, - data_path, - config, - }) - } + pub async fn get_node_by_identifier( + &self, + identifier: &Identifier, + ) -> Result> { + Ok(self + .nodes_repository() + .await? + .get_node_by_identifier(identifier) + .await?) + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - let data_path = IdentityState::build_data_path(&path); - Ok(Self { - name, - path, - data_path, - config, - }) + pub async fn get_identifier_vault(&self, identifier: &Identifier) -> Result { + if let Some(vault_name) = self + .identities_repository() + .await? + .get_identifier_vault_name(identifier) + .await? + { + self.get_named_vault(&vault_name).await + } else { + Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no vault found for identifier {identifier}"), + ) + .into()) } + } +} + +/// Support methods +impl CliState { + /// Create an identity using the given vault to generate keys and + /// store the identity change history + async fn create_identity_with_vault(&self, vault: NamedVault) -> Result { + Ok(self + .get_identities_for_vault(vault) + .await? + .identities_creation() + .create_identity() + .await? + .clone()) + } - fn path(&self) -> &PathBuf { - &self.path + /// Once a identity has been created, store it. + /// If there is no previous default identity we set it as the default identity + async fn store_named_identity( + &self, + identifier: &Identifier, + name: &str, + vault_name: &str, + ) -> Result { + let repository = self.identities_repository().await?; + + // If there is no previously created identity we set this identity as the default one + let is_default_identity = repository.get_default_named_identity().await?.is_none(); + let named_identity = repository + .store_named_identity(identifier, name, vault_name) + .await?; + if is_default_identity { + repository + .set_as_default(&named_identity.identifier()) + .await?; } + Ok(named_identity) + } - fn config(&self) -> &Self::Config { - &self.config + /// Return the change history of a persisted identity + async fn get_change_history(&self, identifier: &Identifier) -> Result { + match self + .change_history_repository() + .await? + .get_change_history(identifier) + .await? + { + Some(change_history) => Ok(change_history), + None => Err(Error::new( + Origin::Core, + Kind::NotFound, + format!("identity not found for identifier {}", identifier), + ) + .into()), } } + + fn missing_identifier(name: &Option) -> Error { + let message = name + .clone() + .map_or("no default identifier found".to_string(), |n| { + format!("no identifier found with name {}", n) + }); + Error::new(Origin::Api, Kind::NotFound, message) + } } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_serialize() { - let identity_config = create_identity_config(); - let expected = create_identity_config_json(); - assert_eq!(serde_json::to_string(&identity_config).unwrap(), expected) - } + #[tokio::test] + async fn test_create_identity() -> Result<()> { + let cli = CliState::test().await?; + + // create a vault first + let vault_name = "vault-name"; + let _ = cli.create_vault(vault_name).await?; + + // then create an identity + let identity_name = "identity-name"; + let identifier = cli + .create_identity_with_name_and_vault(identity_name, vault_name) + .await?; + let identity = cli.get_named_identity(identity_name).await?; + assert_eq!(identifier, identity.identifier()); + + // don't recreate the identity if it already exists with that name + let _ = cli + .create_identity_with_name_and_vault(identity_name, vault_name) + .await?; + let identities = cli.get_named_identities().await?; + assert_eq!(identities.len(), 1); - #[test] - fn test_deserialize() { - let json = create_identity_config_json(); - let actual: IdentityConfig = serde_json::from_str(json.as_str()).unwrap(); - let expected = create_identity_config(); - assert_eq!(actual, expected) - } - - fn create_identity_config() -> IdentityConfig { - let identifier = Identifier::try_from("Ifa804b7fca12a19eed206ae180b5b576860ae651").unwrap(); - IdentityConfig { - identifier, - enrollment_status: Some(EnrollmentStatus { - is_enrolled: true, - created_at: SystemTime::from(OffsetDateTime::from_unix_timestamp(0).unwrap()), - }), - } - } - - fn create_identity_config_json() -> String { - r#"{"identifier":"Ifa804b7fca12a19eed206ae180b5b576860ae651","enrollment_status":{"is_enrolled":true,"created_at":{"secs_since_epoch":0,"nanos_since_epoch":0}}}"#.into() + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs b/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs index 5ebf43d5311..de7381f11a2 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs @@ -1,33 +1,60 @@ -pub mod credentials; -pub mod identities; -pub mod nodes; -pub mod projects; -pub mod spaces; -pub mod traits; -pub mod trust_contexts; -pub mod user_info; -pub mod vaults; +use std::path::{Path, PathBuf}; + +use miette::Diagnostic; +use rand::random; +use thiserror::Error; + +use ockam::identity::storage::{PurposeKeysRepository, PurposeKeysSqlxDatabase}; +use ockam::identity::{ + ChangeHistoryRepository, ChangeHistorySqlxDatabase, Identities, IdentityAttributesRepository, + IdentityAttributesSqlxDatabase, +}; +use ockam::SqlxDatabase; +use ockam_abac::{PoliciesRepository, PolicySqlxDatabase}; +use ockam_core::compat::sync::Arc; +use ockam_core::env::get_env_with_default; +use ockam_node::Executor; +use ockam_vault::storage::{SecretsRepository, SecretsSqlxDatabase}; +pub use projects_repository::*; +pub use projects_repository_sql::*; +pub use spaces_repository::*; +pub use spaces_repository_sql::*; +pub use users::*; pub use crate::cli_state::credentials::*; -pub use crate::cli_state::identities::*; +use crate::cli_state::enrollment::{EnrollmentsRepository, EnrollmentsSqlxDatabase}; pub use crate::cli_state::nodes::*; pub use crate::cli_state::projects::*; pub use crate::cli_state::spaces::*; -pub use crate::cli_state::traits::*; pub use crate::cli_state::trust_contexts::*; -use crate::cli_state::user_info::UsersInfoState; +use crate::cli_state::trust_contexts_repository::TrustContextsRepository; +use crate::cli_state::trust_contexts_repository_sql::TrustContextsSqlxDatabase; +use crate::cli_state::users_repository::UsersRepository; +use crate::cli_state::users_repository_sql::UsersSqlxDatabase; pub use crate::cli_state::vaults::*; -use crate::config::cli::LegacyCliConfig; -use miette::Diagnostic; -use ockam::identity::Identifier; -use ockam::identity::Identities; -use ockam::identity::Vault; -use ockam_core::compat::sync::Arc; -use ockam_core::env::get_env_with_default; -use ockam_node::Executor; -use rand::random; -use std::path::{Path, PathBuf}; -use thiserror::Error; +use crate::identity::{ + CredentialsRepository, CredentialsSqlxDatabase, IdentitiesRepository, IdentitiesSqlxDatabase, + NamedVault, VaultsRepository, VaultsSqlxDatabase, +}; +use crate::nodes::{NodesRepository, NodesSqlxDatabase}; + +pub mod credentials; +pub mod enrollment; +pub mod identities; +pub mod nodes; +pub mod projects; +pub mod projects_repository; +pub mod projects_repository_sql; +pub mod spaces; +pub mod spaces_repository; +pub mod spaces_repository_sql; +pub mod trust_contexts; +pub mod trust_contexts_repository; +pub mod trust_contexts_repository_sql; +pub mod users; +pub mod users_repository; +pub mod users_repository_sql; +pub mod vaults; type Result = std::result::Result; @@ -99,54 +126,134 @@ impl From for ockam_core::Error { } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone)] pub struct CliState { - pub vaults: VaultsState, - pub identities: IdentitiesState, - pub nodes: NodesState, - pub spaces: SpacesState, - pub projects: ProjectsState, - pub credentials: CredentialsState, - pub trust_contexts: TrustContextsState, - pub users_info: UsersInfoState, - pub dir: PathBuf, + dir: PathBuf, + database: Arc, } impl CliState { - /// Return an initialized CliState - /// There should only be one call to this function since it also performs a migration - /// of configuration files if necessary - pub fn initialize() -> Result { - let dir = Self::default_dir()?; - std::fs::create_dir_all(dir.join("defaults"))?; - Executor::execute_future(Self::initialize_cli_state())? - } - - /// Create a new CliState by initializing all of its components - /// The calls to 'init(dir)' are loading each piece of configuration and possibly doing some - /// configuration migration if necessary - async fn initialize_cli_state() -> Result { - let default = Self::default_dir()?; - let dir = default.as_path(); - let state = Self { - vaults: VaultsState::init(dir).await?, - identities: IdentitiesState::init(dir).await?, - nodes: NodesState::init(dir).await?, - spaces: SpacesState::init(dir).await?, - projects: ProjectsState::init(dir).await?, - credentials: CredentialsState::init(dir).await?, - trust_contexts: TrustContextsState::init(dir).await?, - users_info: UsersInfoState::init(dir).await?, - dir: dir.to_path_buf(), - }; - state.migrate()?; + /// Return a new CliState using a default directory to store its data + pub fn with_default_dir() -> Result { + Self::new(Self::default_dir()?.as_path()) + } + + /// Create a new CliState in a given directory + pub fn new(dir: &Path) -> Result { + Executor::execute_future(Self::create(dir.into()))? + } + + /// Create a new CliState + async fn create(dir: PathBuf) -> Result { + std::fs::create_dir_all(&dir)?; + let database = Arc::new(SqlxDatabase::create(Self::make_database_path(&dir)).await?); + debug!("Opened the database with options {:?}", database); + let state = Self { dir, database }; Ok(state) } + pub fn dir(&self) -> PathBuf { + self.dir.clone() + } + + pub async fn change_history_repository(&self) -> Result> { + Ok(Arc::new(ChangeHistorySqlxDatabase::new(self.database()))) + } + + pub async fn secrets_repository(&self) -> Result> { + Ok(Arc::new(SecretsSqlxDatabase::new(self.database()))) + } + + pub async fn identity_attributes_repository( + &self, + ) -> Result> { + Ok(Arc::new(IdentityAttributesSqlxDatabase::new( + self.database(), + ))) + } + + pub async fn identities_repository(&self) -> Result> { + Ok(Arc::new(IdentitiesSqlxDatabase::new(self.database()))) + } + + pub async fn purpose_keys_repository(&self) -> Result> { + Ok(Arc::new(PurposeKeysSqlxDatabase::new(self.database()))) + } + + async fn vaults_repository(&self) -> Result> { + Ok(Arc::new(VaultsSqlxDatabase::new(self.database()))) + } + + async fn enrollment_repository(&self) -> Result> { + Ok(Arc::new(EnrollmentsSqlxDatabase::new(self.database()))) + } + + async fn nodes_repository(&self) -> Result> { + Ok(Arc::new(NodesSqlxDatabase::new(self.database()))) + } + + pub async fn policies_repository(&self) -> Result> { + Ok(Arc::new(PolicySqlxDatabase::new(self.database()))) + } + + pub async fn projects_repository(&self) -> Result> { + Ok(Arc::new(ProjectsSqlxDatabase::new(self.database()))) + } + + pub async fn spaces_repository(&self) -> Result> { + Ok(Arc::new(SpacesSqlxDatabase::new(self.database()))) + } + + pub async fn users_repository(&self) -> Result> { + Ok(Arc::new(UsersSqlxDatabase::new(self.database()))) + } + + pub async fn credentials_repository(&self) -> Result> { + Ok(Arc::new(CredentialsSqlxDatabase::new(self.database()))) + } + + pub async fn trust_contexts_repository(&self) -> Result> { + Ok(Arc::new(TrustContextsSqlxDatabase::new(self.database()))) + } + + pub fn database(&self) -> Arc { + self.database.clone() + } + + pub fn database_path(&self) -> PathBuf { + Self::make_database_path(&self.dir) + } + + pub async fn get_identities_for_vault(&self, vault: NamedVault) -> Result> { + Ok(Identities::builder() + .with_vault(vault.vault().await?) + .with_change_history_repository(self.change_history_repository().await?) + .with_identity_attributes_repository(self.identity_attributes_repository().await?) + .with_purpose_keys_repository(self.purpose_keys_repository().await?) + .build()) + } + + pub async fn get_identities_with_vault(&self, vault_name: &str) -> Result> { + let vault = self.get_named_vault(vault_name).await?; + self.get_identities_for_vault(vault).await + } + + pub async fn get_identities_with_optional_vault_name( + &self, + vault_name: &Option, + ) -> Result> { + let vault_name = self.get_vault_name_or_default(vault_name).await?; + self.get_identities_with_vault(&vault_name).await + } + + pub async fn get_identities(&self) -> Result> { + self.get_identities_with_optional_vault_name(&None).await + } + /// Reset all directories and return a new CliState pub async fn reset(&self) -> Result { - Self::delete_at(&self.dir)?; - Self::initialize_cli_state().await + self.delete()?; + Self::create(self.dir.clone()).await } pub fn backup_and_reset() -> Result { @@ -169,92 +276,42 @@ impl CliState { // Reset state Self::delete_at(&dir)?; - Self::initialize() - } - - fn migrate(&self) -> Result<()> { - // If there is a `config.json` file, migrate its contents to the spaces and project states. - let legacy_config_path = self.dir.join("config.json"); - if legacy_config_path.exists() { - let contents = std::fs::read_to_string(&legacy_config_path)?; - let legacy_config: LegacyCliConfig = serde_json::from_str(&contents)?; - let spaces = self.spaces.list()?; - for (name, lookup) in legacy_config.lookup.spaces() { - if !spaces.iter().any(|s| s.name() == name) { - let config = SpaceConfig::from_lookup(&name, lookup); - self.spaces.create(name, config)?; - } - } - let projects = self.projects.list()?; - for (name, lookup) in legacy_config.lookup.projects() { - if !projects.iter().any(|p| p.name() == name) { - self.projects.create(name, lookup.into())?; - } - } - std::fs::remove_file(legacy_config_path)?; - } - Ok(()) - } + let state = Self::new(&dir)?; - pub fn delete_at(root_path: &PathBuf) -> Result<()> { - // Delete nodes' state and processes, if possible - let nodes_state = NodesState::new(root_path); - let _ = nodes_state.list().map(|nodes| { - nodes.iter().for_each(|n| { - let _ = n.delete_sigkill(true); - }); - }); - - // Delete all other state directories - for dir in &[ - nodes_state.dir(), - IdentitiesState::new(root_path).dir(), - VaultsState::new(root_path).dir(), - SpacesState::new(root_path).dir(), - ProjectsState::new(root_path).dir(), - CredentialsState::new(root_path).dir(), - TrustContextsState::new(root_path).dir(), - UsersInfoState::new(root_path).dir(), - &root_path.join("defaults"), - ] { - let _ = std::fs::remove_dir_all(dir); - } + let dir = &state.dir; + let backup_dir = CliState::backup_default_dir().unwrap(); + eprintln!("The {dir:?} directory has been reset and has been backed up to {backup_dir:?}"); + Ok(state) + } - // Delete config files located at the root of the state directory - let config_file = root_path.join("config.json"); - let _ = std::fs::remove_file(config_file); + fn make_database_path(root_path: &Path) -> PathBuf { + root_path.join("database.sqlite3") + } - // If the state directory is now empty, delete it - let is_empty = std::fs::read_dir(root_path) - .map(|mut d| d.next().is_none()) - .unwrap_or(false); - if is_empty { - let _ = std::fs::remove_dir(root_path); - } + fn make_node_dir_path(root_path: &Path, node_name: &str) -> PathBuf { + Self::make_nodes_dir_path(root_path).join(node_name) + } - Ok(()) + fn make_nodes_dir_path(root_path: &Path) -> PathBuf { + root_path.join("nodes") } - pub fn delete() -> Result<()> { - Self::delete_at(&Self::default_dir()?) + pub fn delete_at(root_path: &Path) -> Result<()> { + // Delete nodes logs + let _ = std::fs::remove_dir_all(Self::make_nodes_dir_path(root_path)); + // Delete the database + let _ = std::fs::remove_file(Self::make_database_path(root_path)); + // If the state directory is now empty, delete it + let _ = std::fs::remove_dir(root_path); + Ok(()) } - pub fn delete_identity(&self, identity_state: IdentityState) -> Result<()> { - // Abort if identity is being used by some running node. - for node in self.nodes.list()? { - if node.config().identity_config()?.identifier() == identity_state.identifier() { - return Err(CliStateError::InvalidOperation(format!( - "Can't delete identity '{}' as it's being used by node '{}'", - &identity_state.name(), - &node.name() - ))); - } - } - identity_state.delete() + pub fn delete(&self) -> Result<()> { + Self::delete_at(&self.dir) } /// Returns the default directory for the CLI state. - pub fn default_dir() -> Result { + fn default_dir() -> Result { Ok(get_env_with_default::( "OCKAM_HOME", home::home_dir() @@ -278,88 +335,28 @@ impl CliState { Ok(parent.join(format!("{dir_name}.bak"))) } - /// Returns the directory where the default objects are stored. - fn defaults_dir(dir: &Path) -> Result { - Ok(dir.join("defaults")) - } - - pub async fn create_vault_state(&self, vault_name: Option<&str>) -> Result { - // Try to get the vault with the given name - let vault_state = if let Some(v) = vault_name { - self.vaults.get(v)? - } - // Or get the default - else if let Ok(v) = self.vaults.default() { - v - } - // Or create a new one with a random name - else { - let n = random_name(); - let c = VaultConfig::default(); - self.vaults.create_async(&n, c).await? - }; - Ok(vault_state) - } - - pub async fn create_identity_state( - &self, - identifier: &Identifier, - identity_name: Option<&str>, - ) -> Result { - if let Ok(identity) = self.identities.get_or_default(identity_name) { - Ok(identity) - } else { - self.make_identity_state(identifier, identity_name).await - } - } - - async fn make_identity_state( - &self, - identifier: &Identifier, - name: Option<&str>, - ) -> Result { - let identity_config = IdentityConfig::new(identifier).await; - let identity_name = name.map(|x| x.to_string()).unwrap_or_else(random_name); - self.identities.create(identity_name, identity_config) - } - - pub async fn get_identities(&self, vault: Vault) -> Result> { - Ok(Identities::builder() - .with_vault(vault) - .with_identities_repository(self.identities.identities_repository().await?) - .build()) - } - - pub async fn default_identities(&self) -> Result> { - Ok(Identities::builder() - .with_vault(self.vaults.default()?.vault().await?) - .with_identities_repository(self.identities.identities_repository().await?) - .build()) - } - /// Return true if the user is enrolled. /// At the moment this check only verifies that there is a default project. /// This project should be the project that is created at the end of the enrollment procedure - pub fn is_enrolled(&self) -> Result { - let identity_state = self.identities.default()?; - if !identity_state.is_enrolled() { + pub async fn is_enrolled(&self) -> miette::Result { + if !self.is_default_identity_enrolled().await? { return Ok(false); } - let default_space_exists = self.spaces.default().is_ok(); + let default_space_exists = self.get_default_space().await.is_ok(); if !default_space_exists { let message = "There should be a default space set for the current user. Please re-enroll"; error!("{}", message); - return Err(message.into()); + return Err(CliStateError::from(message).into()); } - let default_project_exists = self.projects.default().is_ok(); + let default_project_exists = self.get_default_project().await.is_ok(); if !default_project_exists { let message = "There should be a default project set for the current user. Please re-enroll"; error!("{}", message); - return Err(message.into()); + return Err(CliStateError::from(message).into()); } Ok(true) @@ -368,44 +365,9 @@ impl CliState { /// Test support impl CliState { - #[cfg(test)] - /// Initialize CliState at the given directory - async fn initialize_at(dir: &Path) -> Result { - std::fs::create_dir_all(dir.join("defaults"))?; - let state = Self { - vaults: VaultsState::init(dir).await?, - identities: IdentitiesState::init(dir).await?, - nodes: NodesState::init(dir).await?, - spaces: SpacesState::init(dir).await?, - projects: ProjectsState::init(dir).await?, - credentials: CredentialsState::init(dir).await?, - trust_contexts: TrustContextsState::init(dir).await?, - users_info: UsersInfoState::init(dir).await?, - dir: dir.to_path_buf(), - }; - state.migrate()?; - Ok(state) - } - - /// Create a new CliState (but do not run migrations) - fn new(dir: &Path) -> Result { - std::fs::create_dir_all(dir.join("defaults"))?; - Ok(Self { - vaults: VaultsState::load(dir)?, - identities: IdentitiesState::load(dir)?, - nodes: NodesState::load(dir)?, - spaces: SpacesState::load(dir)?, - projects: ProjectsState::load(dir)?, - credentials: CredentialsState::load(dir)?, - trust_contexts: TrustContextsState::load(dir)?, - users_info: UsersInfoState::load(dir)?, - dir: dir.to_path_buf(), - }) - } - /// Return a test CliState with a random root directory - pub fn test() -> Result { - Self::new(&Self::test_dir()?) + pub async fn test() -> Result { + Self::create(Self::test_dir()?).await } /// Return a random root directory @@ -421,361 +383,3 @@ impl CliState { pub fn random_name() -> String { petname::petname(2, "-").unwrap_or(hex::encode(random::<[u8; 4]>())) } - -fn file_stem(path: &Path) -> Result { - let path_str = path.to_str().ok_or(CliStateError::EmptyPath)?; - path.file_stem() - .ok_or(CliStateError::InvalidPath(path_str.to_string()))? - .to_str() - .map(|name| name.to_string()) - .ok_or(CliStateError::InvalidPath(path_str.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cloud::enroll::auth0::UserInfo; - use crate::config::cli::TrustContextConfig; - use crate::config::lookup::{ConfigLookup, LookupValue, ProjectLookup, SpaceLookup}; - use ockam_core::compat::rand::random_string; - use ockam_multiaddr::MultiAddr; - use std::str::FromStr; - - #[tokio::test] - async fn test_create_default_identity_state() { - let state = CliState::test().unwrap(); - let identifier = "Ie92f183eb4c324804ef4d62962dea94cf095a265" - .try_into() - .unwrap(); - let identity1 = state - .create_identity_state(&identifier, None) - .await - .unwrap(); - let identity2 = state - .create_identity_state(&identifier, None) - .await - .unwrap(); - - let default_identity = state.identities.default().unwrap(); - assert_eq!(identity1, default_identity); - - // make sure that a default identity is not recreated twice - assert_eq!(identity1.name(), identity2.name()); - assert_eq!(identity1.path(), identity2.path()); - } - - #[tokio::test] - async fn test_create_named_identity_state() { - let state = CliState::test().unwrap(); - let alice = "Ie92f183eb4c324804ef4d62962dea94cf095a265" - .try_into() - .unwrap(); - let identity1 = state - .create_identity_state(&alice, Some("alice")) - .await - .unwrap(); - let identity2 = state - .create_identity_state(&alice, Some("alice")) - .await - .unwrap(); - - assert_eq!(identity1.name(), "alice"); - assert!(identity1 - .path() - .to_string_lossy() - .to_string() - .contains("alice.json")); - - // make sure that a named identity is not recreated twice - assert_eq!(identity1.name(), identity2.name()); - assert_eq!(identity1.path(), identity2.path()); - } - - #[tokio::test] - async fn migrate_legacy_cli_config() { - // Before this migration, there was a `config.json` file in the root $OCKAM_HOME directory - // that contained a map of space names to space and project lookups. This test ensures that - // the migration correctly moves the space and project lookups into the new `spaces` and - // `projects` directories, respectively. - let space_name = "sname"; - let space_lookup = SpaceLookup { - id: "sid".to_string(), - }; - let project_lookup = ProjectLookup { - node_route: Some(MultiAddr::from_str("/node/p").unwrap()), - id: "pid".to_string(), - name: "pname".to_string(), - identity_id: Some( - Identifier::from_str("Ibb37445cacb3ca7a20040a9b36469e321a57d2cd").unwrap(), - ), - authority: None, - okta: None, - }; - let test_dir = CliState::test_dir().unwrap(); - let legacy_config = { - let map = vec![ - (space_name.to_string(), LookupValue::Space(space_lookup)), - ( - project_lookup.name.clone(), - LookupValue::Project(project_lookup.clone()), - ), - ]; - let lookup = ConfigLookup { - map: map.into_iter().collect(), - }; - LegacyCliConfig { - dir: Some(test_dir.clone()), - lookup, - } - }; - std::fs::create_dir_all(&test_dir).unwrap(); - std::fs::write( - test_dir.join("config.json"), - serde_json::to_string(&legacy_config).unwrap(), - ) - .unwrap(); - let state = CliState::initialize_at(&test_dir).await.unwrap(); - let space = state.spaces.get(space_name).unwrap(); - assert_eq!(space.config().id, "sid"); - let project = state.projects.get(&project_lookup.name).unwrap(); - assert_eq!(project.config().id, project_lookup.id); - assert_eq!( - project.config().access_route, - project_lookup.node_route.unwrap().to_string() - ); - assert!(!test_dir.join("config.json").exists()); - } - - #[ockam_macros::test(crate = "ockam")] - async fn integration(ctx: &mut ockam::Context) -> ockam::Result<()> { - let sut = CliState::test()?; - - // Vaults - let vault_name = { - let name = random_name(); - let config = VaultConfig::default(); - - let state = sut.vaults.create_async(&name, config).await.unwrap(); - let got = sut.vaults.get(&name).unwrap(); - assert_eq!(got, state); - - let got = sut.vaults.default().unwrap(); - assert_eq!(got, state); - - name - }; - - // Identities - let identity_name = { - let name = random_name(); - let vault_state = sut.vaults.get(&vault_name).unwrap(); - let vault: Vault = vault_state.get().await.unwrap(); - let identities = Identities::builder() - .with_vault(vault) - .with_identities_repository(sut.identities.identities_repository().await?) - .build(); - let identifier = identities - .identities_creation() - .create_identity() - .await - .unwrap(); - let config = IdentityConfig::new(&identifier).await; - - let state = sut.identities.create(&name, config).unwrap(); - let got = sut.identities.get(&name).unwrap(); - assert_eq!(got, state); - - let got = sut.identities.default().unwrap(); - assert_eq!(got, state); - - name - }; - - // Nodes - let node_name = { - let name = random_name(); - let config = NodeConfig::try_from(&sut).unwrap(); - - let state = sut.nodes.create(&name, config).unwrap(); - let got = sut.nodes.get(&name).unwrap(); - assert_eq!(got, state); - - let got = sut.nodes.default().unwrap(); - assert_eq!(got, state); - - name - }; - - // Spaces - let space_name = { - let name = random_name(); - let id = random_string(); - let config = SpaceConfig { - name: name.clone(), - id, - }; - - let state = sut.spaces.create(&name, config).unwrap(); - let got = sut.spaces.get(&name).unwrap(); - assert_eq!(got, state); - - name - }; - - // Projects - let project_name = { - let name = random_name(); - let config = ProjectConfig::default(); - - let state = sut.projects.create(&name, config).unwrap(); - let got = sut.projects.get(&name).unwrap(); - assert_eq!(got, state); - - name - }; - - // Trust Contexts - let trust_context_name = { - let name = random_name(); - let config = TrustContextConfig::new(name.to_string(), None); - - let state = sut.trust_contexts.create(&name, config).unwrap(); - let got = sut.trust_contexts.get(&name).unwrap(); - assert_eq!(got, state); - - name - }; - - // Users Info - let user_info_email = { - let email = random_name(); - let config = UserInfo { - email: email.clone(), - ..Default::default() - }; - - let state = sut.users_info.create(&email, config).unwrap(); - let got = sut.users_info.get(&email).unwrap(); - assert_eq!(got, state); - - email - }; - - // Check structure - let mut expected_entries = vec![ - "vaults".to_string(), - format!("vaults/{vault_name}.json"), - "vaults/data".to_string(), - format!("vaults/data/{vault_name}-storage.json"), - "identities".to_string(), - format!("identities/{identity_name}.json"), - "identities/data/authenticated_storage.lmdb".to_string(), - "nodes".to_string(), - format!("nodes/{node_name}"), - "spaces".to_string(), - format!("spaces/{space_name}.json"), - "projects".to_string(), - format!("projects/{project_name}.json"), - "trust_contexts".to_string(), - format!("trust_contexts/{trust_context_name}.json"), - "users_info".to_string(), - format!("users_info/{user_info_email}.json"), - "credentials".to_string(), - "defaults".to_string(), - "defaults/vault".to_string(), - "defaults/identity".to_string(), - "defaults/node".to_string(), - "defaults/space".to_string(), - "defaults/project".to_string(), - "defaults/trust_context".to_string(), - "defaults/user_info".to_string(), - ]; - expected_entries.sort(); - let mut found_entries = vec![]; - sut.dir.read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let dir_name = entry.file_name().into_string().unwrap(); - match dir_name.as_str() { - "vaults" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let entry_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{entry_name}")); - if entry.path().is_dir() { - assert_eq!(entry_name, DATA_DIR_NAME); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let file_name = entry.file_name().into_string().unwrap(); - if !file_name.ends_with(".lock") { - found_entries - .push(format!("{dir_name}/{entry_name}/{file_name}")); - assert_eq!(file_name, format!("{vault_name}-storage.json")); - } - }); - } else { - assert_eq!(entry_name, format!("{vault_name}.json")); - } - }); - } - "identities" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let entry_name = entry.file_name().into_string().unwrap(); - if entry.path().is_dir() { - assert_eq!(entry_name, DATA_DIR_NAME); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let file_name = entry.file_name().into_string().unwrap(); - if !file_name.ends_with("-lock") { - found_entries - .push(format!("{dir_name}/{entry_name}/{file_name}")); - assert_eq!(file_name, format!("authenticated_storage.lmdb")); - } - }) - } else { - assert!(entry.path().is_file()); - let file_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{file_name}")); - } - }); - } - "nodes" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - assert!(entry.path().is_dir()); - let file_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{file_name}")); - }); - } - "defaults" | "spaces" | "projects" | "credentials" | "trust_contexts" - | "users_info" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let entry_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{entry_name}")); - }); - } - _ => panic!("unexpected file"), - } - }); - found_entries.sort(); - assert_eq!(expected_entries, found_entries); - - sut.spaces.delete(&space_name).unwrap(); - sut.projects.delete(&project_name).unwrap(); - sut.nodes.delete(&node_name).unwrap(); - sut.identities.delete(&identity_name).unwrap(); - sut.vaults.delete(&vault_name).unwrap(); - - ctx.stop().await?; - Ok(()) - } -} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs index d8f2d97254d..da79e4c4a0b 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs @@ -1,740 +1,430 @@ -use super::Result; -use crate::cli_state::{ - CliState, CliStateError, IdentityConfig, IdentityState, ProjectConfig, ProjectConfigCompact, - StateDirTrait, StateItemTrait, VaultState, -}; -use crate::config::lookup::ProjectLookup; -use crate::nodes::models::transport::CreateTransportJson; -use backwards_compatibility::*; -use miette::{IntoDiagnostic, WrapErr}; +use std::path::PathBuf; +use std::process; + use nix::errno::Errno; -use ockam::identity::Identifier; -use ockam::identity::Vault; -use ockam::LmdbStorage; -use ockam_core::compat::collections::HashSet; -use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use sysinfo::{Pid, ProcessExt, ProcessStatus, System, SystemExt}; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NodesState { - dir: PathBuf, -} -impl NodesState { - pub fn stdout_logs(&self, name: &str) -> Result { - let dir = self.path(name); - std::fs::create_dir_all(&dir)?; - Ok(NodePaths::new(&dir).stdout()) +use ockam::identity::{Identifier, Vault}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; + +use crate::cli_state::{random_name, Result}; +use crate::cli_state::{CliState, CliStateError}; +use crate::cloud::project::Project; +use crate::nodes::NodeInfo; + +impl CliState { + /// Create a node with an identity (possibly associated with a name) and an optionally specified vault + pub async fn create_node_with_optional_name_and_optional_vault_and_optional_project( + &self, + node_name: &Option, + identity_name: &Option, + vault_name: &Option, + project_name: &Option, + ) -> Result { + let node_name = self.get_node_name_or_default(node_name).await?; + let vault_name = self.get_vault_name_or_default(vault_name).await?; + let identity_name = self.get_identity_name_or_default(identity_name).await?; + // note that the identity is created only if it has not been created before + let identifier = self + .create_identity_with_name_and_vault(&identity_name, &vault_name) + .await?; + let node = self + .create_node_with_identifier(&node_name, &identifier) + .await?; + self.set_node_project(&node_name, project_name).await?; + Ok(node) + } + + /// This method creates a node with an associated identity + /// The vault used to create the identity is the default vault + pub async fn create_node(&self, node_name: &str) -> Result { + let identifier = self.create_identity_with_random_name().await?; + self.create_node_with_identifier(node_name, &identifier) + .await + } + + pub async fn create_node_with_identifier( + &self, + node_name: &str, + identifier: &Identifier, + ) -> Result { + let repository = self.nodes_repository().await?; + let is_default = repository.is_default_node(node_name).await? + || repository.get_nodes().await?.is_empty(); + let tcp_listener_address = repository.get_tcp_listener_address(node_name).await?; + let node_info = NodeInfo::new( + node_name.to_string(), + identifier.clone(), + 0, + is_default, + false, + tcp_listener_address, + Some(process::id()), + ); + repository.store_node(&node_info).await?; + Ok(node_info) } - pub fn delete_sigkill(&self, name: &str, sigkill: bool) -> Result<()> { - self._delete(name, sigkill) + pub async fn store_node(&self, node_info: &NodeInfo) -> Result<()> { + Ok(self.nodes_repository().await?.store_node(node_info).await?) } - fn _delete(&self, name: impl AsRef, sigkill: bool) -> Result<()> { - // If doesn't exist do nothing - if !self.exists(&name) { - return Ok(()); - } - let node = self.get(&name)?; - // Set default to another node if it's the default - if self.is_default(&name)? { - // Remove link if it exists - let _ = std::fs::remove_file(self.default_path()?); - for node in self.list()? { - if node.name() != name.as_ref() && self.set_default(node.name()).is_ok() { - debug!(name=%node.name(), "set default node"); - break; - } - } + pub async fn get_node(&self, node_name: &str) -> Result { + if let Some(node) = self.nodes_repository().await?.get_node(node_name).await? { + Ok(node) + } else { + Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("There is no node with name {node_name}"), + ) + .into()) } - // Remove node directory - node.delete_sigkill(sigkill)?; - Ok(()) } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NodeState { - name: String, - path: PathBuf, - paths: NodePaths, - config: NodeConfig, -} -impl NodeState { - fn _delete(&self, sikgill: bool) -> Result<()> { - self.kill_process(sikgill)?; - std::fs::remove_dir_all(&self.path)?; - let _ = std::fs::remove_file(self.path.with_extension("lock")); - let _ = std::fs::remove_dir(&self.path); // Make sure the dir is gone - info!(name=%self.name, "node deleted"); - Ok(()) + /// Return all the registered nodes + pub async fn get_nodes(&self) -> Result> { + Ok(self.nodes_repository().await?.get_nodes().await?) } - pub fn delete_sigkill(&self, sigkill: bool) -> Result<()> { - self._delete(sigkill) + /// Return the identifier associated to a node + pub async fn get_node_identifier(&self, node_name: &str) -> Result { + Ok(self.get_node(node_name).await?.identifier()) } - pub fn kill_process(&self, sigkill: bool) -> Result<()> { - if let Some(pid) = self.pid()? { - nix::sys::signal::kill( - nix::unistd::Pid::from_raw(pid), - if sigkill { - nix::sys::signal::Signal::SIGKILL - } else { - nix::sys::signal::Signal::SIGTERM - }, - ) - .or_else(|e| { - if e == Errno::ESRCH { - tracing::warn!(node = %self.name(), %pid, "No such process"); - Ok(()) - } else { - Err(e) - } - }) - .map_err(|e| { - CliStateError::Io(std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to stop PID `{pid}` with error `{e}`"), - )) - })?; - std::fs::remove_file(self.paths.pid())?; - } - info!(name = %self.name(), "node process killed"); - Ok(()) + /// Return true if that node is the default one + pub async fn is_default_node(&self, node_name: &str) -> Result { + Ok(self.get_node(node_name).await?.is_default()) } - pub fn set_setup(&self, setup: &NodeSetupConfig) -> Result<()> { - let contents = serde_json::to_string(setup)?; - std::fs::write(self.paths.setup(), contents)?; - info!(name = %self.name(), "setup config updated"); - Ok(()) + /// Return true if that node is currently running + pub async fn is_node_running(&self, node_name: &str) -> Result { + Ok(self + .get_node(node_name) + .await + .ok() + .map(|n| n.is_running()) + .unwrap_or(false)) } - pub fn pid(&self) -> Result> { - let path = self.paths.pid(); - if path.exists() { - let pid = std::fs::read_to_string(path)? - .parse::() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - Ok(Some(pid)) - } else { - Ok(None) - } + /// Return true if that node is an authority node + pub async fn is_authority_node(&self, node_name: &str) -> Result { + Ok(self + .get_node(node_name) + .await + .ok() + .map(|n| n.is_authority_node()) + .unwrap_or(false)) } - pub fn set_pid(&self, pid: i32) -> Result<()> { - std::fs::write(self.paths.pid(), pid.to_string())?; - Ok(()) + /// Return the name of the identifier associated to a node + pub async fn get_node_identifier_name(&self, node_name: &str) -> Result> { + let identifier = self.get_node_identifier(node_name).await?; + Ok(self + .identities_repository() + .await? + .get_identity_name_by_identifier(&identifier) + .await?) } - pub fn is_running(&self) -> bool { - if let Ok(Some(pid)) = self.pid() { - let mut sys = System::new(); - sys.refresh_processes(); - if let Some(p) = sys.process(Pid::from(pid as usize)) { - // Under certain circumstances the process can be in a state where it's not running - // and we are unable to kill it. For example, `kill -9` a process created by - // `node create` in a Docker environment will result in a zombie process. - !matches!(p.status(), ProcessStatus::Dead | ProcessStatus::Zombie) - } else { - false - } + /// Return information about the default node (if there is one) + pub async fn get_default_node(&self) -> Result { + if let Some(node) = self.nodes_repository().await?.get_default_node().await? { + Ok(node) } else { - false + let identity = self.get_default_named_identity().await?; + let node = self + .create_node_with_identifier(&random_name(), &identity.identifier()) + .await?; + Ok(node) } } - pub fn stdout_log(&self) -> PathBuf { - self.paths.stdout() + pub async fn set_default_node(&self, node_name: &str) -> Result<()> { + Ok(self + .nodes_repository() + .await? + .set_default_node(node_name) + .await?) } - pub fn stderr_log(&self) -> PathBuf { - self.paths.stderr() + pub async fn set_tcp_listener_address(&self, node_name: &str, address: String) -> Result<()> { + Ok(self + .nodes_repository() + .await? + .set_tcp_listener_address(node_name, address.as_str()) + .await?) } - pub async fn policies_storage(&self) -> Result { - Ok(LmdbStorage::new(self.paths.policies_storage()).await?) + pub async fn set_node_pid(&self, node_name: &str, pid: u32) -> Result<()> { + Ok(self + .nodes_repository() + .await? + .set_node_pid(node_name, pid) + .await?) } - pub fn name(&self) -> &str { - &self.name + /// Return the node_name if Some otherwise return the default node name (if there is one) + pub async fn get_node_name_or_default(&self, node_name: &Option) -> Result { + match node_name { + Some(name) => Ok(name.clone()), + None => self.get_default_node_name().await, + } } -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct NodeConfig { - #[serde(flatten)] - setup: NodeSetupConfig, - #[serde(skip)] - version: ConfigVersion, - #[serde(skip)] - default_vault: PathBuf, - #[serde(skip)] - default_identity: PathBuf, -} -impl NodeConfig { - pub fn new(cli_state: &CliState) -> Result { - Self::try_from(cli_state) + /// Return the node information for the given node name, otherwise for the default node + pub async fn get_node_or_default(&self, node_name: &Option) -> Result { + match node_name { + Some(name) => self.get_node(name).await, + None => self.get_default_node().await, + } } - pub fn setup(&self) -> &NodeSetupConfig { - &self.setup + /// Return the default node name. + /// If there is no existing default node, return a constant name to use as the default + pub async fn get_default_node_name(&self) -> Result { + self.get_default_node().await.map(|n| n.name()) } - pub fn setup_mut(&self) -> NodeSetupConfig { - self.setup.clone() + /// Return the vault which was used to create the identity associated to a node + pub async fn get_node_vault(&self, node_name: &str) -> Result { + let identifier = self.get_node_identifier(node_name).await?; + let named_vault = self.get_identifier_vault(&identifier).await?; + Ok(named_vault.vault().await?) } - pub fn vault_path(&self) -> Result { - Ok(std::fs::canonicalize(&self.default_vault)?) + pub fn stdout_logs(&self, node_name: &str) -> Result { + Ok(self.create_node_dir(node_name)?.join("stdout.log")) } - pub async fn vault(&self) -> Result { - let state = VaultState::load(self.vault_path()?)?; - state.get().await + pub fn stderr_logs(&self, node_name: &str) -> Result { + Ok(self.create_node_dir(node_name)?.join("stderr.log")) } - pub fn identity_config(&self) -> Result { - let path = std::fs::canonicalize(&self.default_identity)?; - Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) + pub fn create_node_dir(&self, node_name: &str) -> Result { + let path = self.make_node_dir(node_name); + std::fs::create_dir_all(&path)?; + Ok(path) } - pub fn identifier(&self) -> Result { - let state_path = std::fs::canonicalize(&self.default_identity)?; - let state = IdentityState::load(state_path)?; - Ok(state.identifier()) + pub fn make_node_dir(&self, node_name: &str) -> PathBuf { + Self::make_node_dir_path(&self.dir, node_name) } -} -impl TryFrom<&CliState> for NodeConfig { - type Error = CliStateError; - - fn try_from(cli_state: &CliState) -> std::result::Result { - let default_vault = cli_state.vaults.default_path()?; - assert!(default_vault.exists(), "default vault does not exist"); - let default_identity = cli_state.identities.default_path()?; - assert!(default_identity.exists(), "default identity does not exist"); - Ok(Self { - version: ConfigVersion::latest(), - default_vault, - default_identity, - setup: NodeSetupConfig::default(), - }) + /// Delete all registered nodes + pub async fn delete_all_nodes(&self, force: bool) -> Result<()> { + let nodes = self.nodes_repository().await?.get_nodes().await?; + for node in nodes { + self.delete_node(&node.name(), force).await?; + } + Ok(()) } -} -#[derive(Debug, Clone, Default)] -pub struct NodeConfigBuilder { - vault: Option, - identity: Option, -} - -impl NodeConfigBuilder { - pub fn vault(mut self, path: PathBuf) -> Self { - self.vault = Some(path); - self + /// Delete the default node if there is one + pub async fn delete_default_node(&self, force: bool) -> Result<()> { + let node_name = self.get_default_node().await?.name(); + self.delete_node(&node_name, force).await } - pub fn identity(mut self, path: PathBuf) -> Self { - self.identity = Some(path); - self + pub async fn delete_node(&self, node_name: &str, force: bool) -> Result<()> { + self.stop_node(node_name, force).await?; + self.remove_node(node_name).await?; + Ok(()) } - pub fn build(self, cli_state: &CliState) -> Result { - let vault = match self.vault { - Some(path) => path, - None => cli_state.vaults.default_path()?, - }; - let identity = match self.identity { - Some(path) => path, - None => cli_state.identities.default_path()?, + /// Remove a node + pub async fn remove_node(&self, node_name: &str) -> Result<()> { + // don't try to remove a node on a non-existent database + if !self.database_path().exists() { + return Ok(()); }; - Ok(NodeConfig { - default_vault: vault, - default_identity: identity, - ..NodeConfig::new(cli_state)? - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum ConfigVersion { - V1, -} - -impl ConfigVersion { - fn latest() -> Self { - Self::V1 - } -} -impl Display for ConfigVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - ConfigVersion::V1 => "1", - }) + self.nodes_repository() + .await? + .delete_node(node_name) + .await?; + let _ = std::fs::remove_dir_all(self.make_node_dir(node_name)); + debug!(name=%node_name, "node deleted"); + Ok(()) } -} -impl FromStr for ConfigVersion { - type Err = CliStateError; + pub async fn stop_node(&self, node_name: &str, force: bool) -> Result<()> { + let node = self.get_node(node_name).await?; + self.nodes_repository() + .await? + .set_no_node_pid(node_name) + .await?; - fn from_str(s: &str) -> std::result::Result { - match s { - "1" => Ok(Self::V1), - _ => Err(CliStateError::InvalidVersion(s.to_string())), + if let Some(pid) = node.pid() { + nix::sys::signal::kill( + nix::unistd::Pid::from_raw(pid as i32), + if force { + nix::sys::signal::Signal::SIGKILL + } else { + nix::sys::signal::Signal::SIGTERM + }, + ) + .or_else(|e| { + if e == Errno::ESRCH { + tracing::warn!(node = %node.name(), %pid, "No such process"); + Ok(()) + } else { + Err(e) + } + }) + .map_err(|e| { + CliStateError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!("failed to stop PID `{pid}` with error `{e}`"), + )) + })?; } - } -} - -impl Default for ConfigVersion { - fn default() -> Self { - Self::latest() - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] -pub struct NodeSetupConfig { - pub verbose: u8, - - /// This flag is used to determine how the node status should be - /// displayed in print_query_status. - /// The field might be missing in previous configuration files, hence it is an Option - pub authority_node: Option, - pub project: Option, - pub api_transport: Option, -} - -impl NodeSetupConfig { - pub fn set_verbose(mut self, verbose: u8) -> Self { - self.verbose = verbose; - self - } - - pub fn set_authority_node(mut self) -> Self { - self.authority_node = Some(true); - self - } - - pub fn set_project(&mut self, project: ProjectLookup) -> &mut Self { - self.project = Some(project); - self - } - - pub fn set_api_transport(mut self, transport: CreateTransportJson) -> Self { - self.api_transport = Some(transport); - self + info!(name = %node.name(), "node process killed"); + Ok(()) } - pub fn api_transport(&self) -> Result<&CreateTransportJson> { - self.api_transport.as_ref().ok_or_else(|| { - CliStateError::InvalidOperation( - "The api transport was not set for the node".to_string(), + pub async fn get_node_project(&self, node_name: &str) -> Result { + match self + .nodes_repository() + .await? + .get_node_project_name(node_name) + .await? + { + Some(project_name) => self.get_project_by_name(&project_name).await, + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no project associated to node {node_name}"), ) - }) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -struct NodePaths { - path: PathBuf, -} - -impl NodePaths { - fn new(path: &Path) -> Self { - Self { - path: path.to_path_buf(), + .into()), } } - fn setup(&self) -> PathBuf { - self.path.join("setup.json") - } - - fn vault(&self) -> PathBuf { - self.path.join("default_vault") - } - - fn identity(&self) -> PathBuf { - self.path.join("default_identity") - } - - fn pid(&self) -> PathBuf { - self.path.join("pid") - } - - fn version(&self) -> PathBuf { - self.path.join("version") - } - - fn stdout(&self) -> PathBuf { - self.path.join("stdout.log") - } - - fn stderr(&self) -> PathBuf { - self.path.join("stderr.log") - } + pub async fn set_node_project( + &self, + node_name: &str, + project_name: &Option, + ) -> Result<()> { + let project = match project_name { + Some(name) => Some(self.get_project_by_name(name).await?), + None => self.get_default_project().await.ok(), + }; - fn policies_storage(&self) -> PathBuf { - self.path.join("policies_storage.lmdb") + if let Some(project) = project { + self.nodes_repository() + .await? + .set_node_project_name(node_name, &project.name()) + .await? + }; + Ok(()) } } -mod backwards_compatibility { - use super::*; - - #[derive(Deserialize, Debug, Clone)] - #[serde(untagged)] - pub(super) enum NodeConfigs { - V1(NodeConfigV1), - V2(NodeConfig), - } - - #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] - pub(super) struct NodeConfigV1 { - #[serde(flatten)] - pub setup: NodeSetupConfigV1, - #[serde(skip)] - pub version: ConfigVersion, - #[serde(skip)] - pub default_vault: PathBuf, - #[serde(skip)] - pub default_identity: PathBuf, - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(untagged)] - pub(super) enum NodeSetupConfigs { - V1(NodeSetupConfigV1), - V2(NodeSetupConfig), - } - - // The change was replacing the `transports` field with `api_transport` - #[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] - pub(super) struct NodeSetupConfigV1 { - pub verbose: u8, - - /// This flag is used to determine how the node status should be - /// displayed in print_query_status. - /// The field might be missing in previous configuration files, hence it is an Option - pub authority_node: Option, - pub project: Option, - pub transports: HashSet, - } - - #[cfg(test)] - impl NodeSetupConfigV1 { - pub fn add_transport(mut self, transport: CreateTransportJson) -> Self { - self.transports.insert(transport); - self - } - } -} +#[cfg(test)] +mod tests { + use crate::config::lookup::InternetAddress; -mod traits { use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use crate::nodes::models::transport::{TransportMode, TransportType}; - use ockam_core::async_trait; - - #[async_trait] - impl StateDirTrait for NodesState { - type Item = NodeState; - const DEFAULT_FILENAME: &'static str = "node"; - const DIR_NAME: &'static str = "nodes"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } - fn dir(&self) -> &PathBuf { - &self.dir - } + #[tokio::test] + async fn test_create_node() -> Result<()> { + let cli = CliState::test().await?; - fn path(&self, name: impl AsRef) -> PathBuf { - self.dir().join(name.as_ref()) - } + // a node can be created with just a name + let node_name = "node-1"; + let result = cli.create_node(node_name).await?; + assert_eq!(result.name(), node_name.to_string()); - /// A node contains several files, and the existence of the main directory is not not enough - /// to determine if a node exists as it could be created but empty. - fn exists(&self, name: impl AsRef) -> bool { - let paths = NodePaths::new(&self.path(&name)); - paths.setup().exists() - } - - fn delete(&self, name: impl AsRef) -> Result<()> { - self._delete(&name, false) - } + // the first node is the default one + let result = cli.get_default_node_name().await?; + assert_eq!(result, node_name.to_string()); - async fn migrate(&self, node_path: &Path) -> Result<()> { - if node_path.is_file() { - // If path is a file, it is probably a non supported file (e.g. .DS_Store) - return Ok(()); - } - let paths = NodePaths::new(node_path); - let contents = std::fs::read_to_string(paths.setup())?; - match serde_json::from_str(&contents)? { - NodeSetupConfigs::V1(setup) => { - // Get the first tcp-listener from the transports hashmap and - // use it as the api transport - let mut new_setup = NodeSetupConfig { - verbose: setup.verbose, - authority_node: setup.authority_node, - project: setup.project, - api_transport: None, - }; - if let Some(t) = setup - .transports - .into_iter() - .find(|t| t.tt == TransportType::Tcp && t.tm == TransportMode::Listen) - { - new_setup.api_transport = Some(t); - } - std::fs::write(paths.setup(), serde_json::to_string(&new_setup)?)?; - } - NodeSetupConfigs::V2(_) => (), - } - Ok(()) - } - } + // as a consequence, a default identity must have been created + let result = cli.get_default_vault().await.ok(); + assert!(result.is_some()); - #[async_trait] - impl StateItemTrait for NodeState { - type Config = NodeConfig; - - fn new(path: PathBuf, mut config: Self::Config) -> Result { - std::fs::create_dir_all(&path)?; - let paths = NodePaths::new(&path); - let name = file_stem(&path)?; - std::fs::write(paths.setup(), serde_json::to_string(config.setup())?)?; - std::fs::write(paths.version(), config.version.to_string())?; - let _ = std::fs::remove_file(paths.vault()); - std::os::unix::fs::symlink(&config.default_vault, paths.vault())?; - config.default_vault = paths.vault(); - let _ = std::fs::remove_file(paths.identity()); - std::os::unix::fs::symlink(&config.default_identity, paths.identity())?; - config.default_identity = paths.identity(); - Ok(Self { - name, - path, - paths, - config, - }) - } + let result = cli.get_default_named_identity().await.ok(); + assert!(result.is_some()); - fn load(path: PathBuf) -> Result { - let paths = NodePaths::new(&path); - let name = file_stem(&path)?; - let setup = { - let contents = std::fs::read_to_string(paths.setup())?; - serde_json::from_str(&contents)? - }; - let version = { - let contents = std::fs::read_to_string(paths.version())?; - contents.parse::()? - }; - let config = NodeConfig { - setup, - version, - default_vault: paths.vault(), - default_identity: paths.identity(), - }; - Ok(Self { - name, - path, - paths, - config, - }) - } + // that identity is associated to the node + let identifier = result.unwrap().identifier(); + let result = cli.get_node_identifier(node_name).await?; + assert_eq!(result, identifier); + Ok(()) + } - fn delete(&self) -> Result<()> { - self._delete(false) - } + #[tokio::test] + async fn test_update_node() -> Result<()> { + let cli = CliState::test().await?; - fn path(&self) -> &PathBuf { - &self.path - } + // create a node + let node_name = "node-1"; + let _ = cli.create_node(node_name).await?; + cli.set_tcp_listener_address(node_name, "127.0.0.1:0".to_string()) + .await?; - fn config(&self) -> &Self::Config { - &self.config - } - } -} + // recreate the node with the same name + let _ = cli.create_node(node_name).await?; -pub async fn init_node_state( - cli_state: &CliState, - node_name: &str, - vault_name: Option<&str>, - identity_name: Option<&str>, -) -> miette::Result<()> { - debug!(name=%node_name, "initializing node state"); - // Get vault specified in the argument, or get the default - let vault_state = cli_state.create_vault_state(vault_name).await?; - - // create an identity for the node - let identifier = cli_state - .get_identities(vault_state.get().await?) - .await? - .identities_creation() - .create_identity() - .await - .into_diagnostic() - .wrap_err("Failed to create identity")?; - - let identity_state = cli_state - .create_identity_state(&identifier, identity_name) - .await?; - - // Create the node with the given vault and identity - let node_config = NodeConfigBuilder::default() - .vault(vault_state.path().clone()) - .identity(identity_state.path().clone()) - .build(cli_state)?; - cli_state.nodes.overwrite(node_name, node_config)?; - - info!(name=%node_name, "node state initialized"); - Ok(()) -} + // the node must still be the default node + let result = cli.get_default_node().await?; + assert_eq!(result.name(), node_name.to_string()); + assert!(result.is_default()); -pub async fn add_project_info_to_node_state( - node_name: &str, - cli_state: &CliState, - project_path: Option<&PathBuf>, -) -> Result> { - debug!(name=%node_name, "Adding project info to state"); - let proj_path = if let Some(path) = project_path { - Some(path.clone()) - } else if let Ok(proj) = cli_state.projects.default() { - Some(proj.path().clone()) - } else { - None - }; - - match proj_path { - Some(path) => { - debug!(path=%path.display(), "Reading project info from path"); - let s = std::fs::read_to_string(path)?; - let proj_info: ProjectConfigCompact = serde_json::from_str(&s)?; - let proj_lookup = ProjectLookup::from_project(&(&proj_info).into()) - .await - .map_err(|e| { - CliStateError::InvalidData(format!("Failed to read project: {}", e)) - })?; - let proj_config = ProjectConfig::from(&proj_info); - let state = cli_state.nodes.get(node_name)?; - state.set_setup(state.config().setup_mut().set_project(proj_lookup.clone()))?; - cli_state - .projects - .overwrite(proj_lookup.name, proj_config)?; - Ok(Some(proj_lookup.id)) - } - None => { - debug!("No project info used"); - Ok(None) - } + // the original tcp listener address has been kept + assert_eq!( + result.tcp_listener_address(), + InternetAddress::new("127.0.0.1:0") + ); + Ok(()) } -} -pub async fn update_enrolled_identity(cli_state: &CliState, node_name: &str) -> Result { - let identities = cli_state.identities.list()?; + #[tokio::test] + async fn test_remove_node() -> Result<()> { + let cli = CliState::test().await?; - let node_state = cli_state.nodes.get(node_name)?; - let node_identifier = node_state.config().identifier()?; + // a node can be created with just a name + let node_name = "node-1"; + let _ = cli.create_node(node_name).await?; + cli.remove_node(node_name).await?; - for mut identity in identities { - if node_identifier == identity.config().identifier() { - identity.set_enrollment_status()?; - } + let result = cli.get_node(node_name).await.ok(); + assert_eq!( + result, None, + "the node information is not available anymore" + ); + assert!( + !cli.make_node_dir(node_name).exists(), + "the node directory must be deleted" + ); + Ok(()) } - Ok(node_identifier) -} + #[tokio::test] + async fn test_create_node_with_optional_name_and_optional_vault() -> Result<()> { + let cli = CliState::test().await?; -#[cfg(test)] -mod tests { - use super::*; - use crate::config::lookup::InternetAddress; - use crate::nodes::models::transport::{TransportMode, TransportType}; - - #[test] - fn node_config_setup_transports_no_duplicates() { - let mut config = NodeSetupConfigV1 { - verbose: 0, - authority_node: None, - project: None, - transports: HashSet::new(), - }; - let transport = CreateTransportJson { - tt: TransportType::Tcp, - tm: TransportMode::Listen, - addr: InternetAddress::V4("127.0.0.1:1020".parse().unwrap()), - }; - config = config.add_transport(transport.clone()); - assert_eq!(config.transports.len(), 1); - assert_eq!(config.transports.iter().next(), Some(&transport)); - - config = config.add_transport(transport); - assert_eq!(config.transports.len(), 1); - } - - #[test] - fn node_config_setup_transports_parses_a_json_with_duplicate_entries() { - // This test is to ensure backwards compatibility, for versions where transports where stored as a Vec<> - let config_json = r#"{ - "verbose": 0, - "authority_node": null, - "project": null, - "transports": [ - {"tt":"Tcp","tm":"Listen","addr":{"V4":"127.0.0.1:1020"}}, - {"tt":"Tcp","tm":"Listen","addr":{"V4":"127.0.0.1:1020"}} - ] - }"#; - let config = serde_json::from_str::(config_json).unwrap(); - assert_eq!(config.transports.len(), 1); - } + // a node can be created with no name + let node = cli + .create_node_with_optional_name_and_optional_vault_and_optional_project( + &None, &None, &None, &None, + ) + .await?; + let result = cli.get_default_node().await?; + assert_eq!(result.name(), node.name()); + + // a node can be created with just a name + let node = cli + .create_node_with_optional_name_and_optional_vault_and_optional_project( + &Some("node-1".to_string()), + &None, + &None, + &None, + ) + .await?; + let result = cli.get_node("node-1").await?; + assert_eq!(result.name(), node.name()); - #[tokio::test] - async fn migrate_node_config_from_v1_to_v2() { - // Create a v1 setup.json file - let v1_json_json = r#"{ - "verbose": 0, - "authority_node": null, - "project": null, - "transports": [ - {"tt":"Tcp","tm":"Listen","addr":{"V4":"127.0.0.1:1020"}} - ] - }"#; - let tmp_dir = tempfile::tempdir().unwrap(); - let node_dir = tmp_dir.path().join("n"); - std::fs::create_dir(&node_dir).unwrap(); - let tmp_file = node_dir.join("setup.json"); - std::fs::write(&tmp_file, v1_json_json).unwrap(); - - // Run migration - let nodes_state = NodesState::new(tmp_dir.path()); - nodes_state.migrate(&node_dir).await.unwrap(); - - // Check migration was done correctly - let contents = std::fs::read_to_string(&tmp_file).unwrap(); - let v2_setup: NodeSetupConfig = serde_json::from_str(&contents).unwrap(); - assert_eq!( - v2_setup.api_transport, - Some(CreateTransportJson { - tt: TransportType::Tcp, - tm: TransportMode::Listen, - addr: InternetAddress::V4("127.0.0.1:1020".parse().unwrap()) - }) - ); + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs index 00a81869f00..cebdc17996c 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs @@ -1,171 +1,145 @@ -use super::Result; -use crate::cloud::project::{OktaConfig, Project}; -use crate::config::lookup::ProjectLookup; -use crate::error::ApiError; -use ockam::identity::Identifier; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ProjectsState { - dir: PathBuf, -} +use std::collections::HashMap; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ProjectState { - name: String, - path: PathBuf, - config: ProjectConfig, -} +use ockam::identity::{Identifier, Identity}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_multiaddr::MultiAddr; -impl ProjectState { - pub fn id(&self) -> &str { - &self.config.id - } - pub fn name(&self) -> &str { - &self.name - } -} +use crate::cli_state::CliState; +use crate::cloud::project::Project; -pub type ProjectConfig = Project; +use super::Result; -impl From for Project { - fn from(lookup: ProjectLookup) -> Self { - Self { - id: lookup.id, - name: lookup.name, +impl CliState { + pub async fn import_project( + &self, + project_id: &str, + project_name: &str, + project_identifier: &Option, + project_access_route: &MultiAddr, + authority_identity: &Option, + authority_access_route: &Option, + ) -> Result<()> { + let authority_identity = match authority_identity { + Some(identity) => Some(identity.change_history().export_as_string()?), + None => None, + }; + let project = Project { + id: project_id.to_string(), + name: project_name.to_string(), space_name: "".to_string(), - access_route: lookup - .node_route - .map(|r| r.to_string()) - .unwrap_or("".to_string()), + access_route: project_access_route.to_string(), users: vec![], space_id: "".to_string(), - identity: lookup.identity_id, - authority_access_route: lookup.authority.as_ref().map(|a| a.address().to_string()), - authority_identity: lookup.authority.as_ref().map(|a| hex::encode(a.identity())), - okta_config: lookup.okta.map(|o| o.into()), + identity: project_identifier.clone(), + authority_access_route: authority_access_route.clone().map(|r| r.to_string()), + authority_identity, + okta_config: None, confluent_config: None, version: None, running: None, operation_id: None, user_roles: vec![], - } + }; + self.store_project(project).await } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -#[non_exhaustive] -pub struct ProjectConfigCompact { - pub id: String, - pub name: String, - pub identity: Option, - pub access_route: String, - pub authority_access_route: Option, - pub authority_identity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub okta_config: Option, -} - -impl TryFrom for ProjectConfigCompact { - type Error = ApiError; - fn try_from(p: ProjectLookup) -> core::result::Result { - Ok(Self { - id: p.id, - name: p.name, - identity: p.identity_id, - access_route: p - .node_route - .map_or( - Err(ApiError::message("Project access route is missing")), - Ok, - )? - .to_string(), - authority_access_route: p.authority.as_ref().map(|a| a.address().to_string()), - authority_identity: p.authority.as_ref().map(|a| hex::encode(a.identity())), - okta_config: p.okta.map(|o| o.into()), - }) + pub async fn store_project(&self, project: Project) -> Result<()> { + let repository = self.projects_repository().await?; + repository.store_project(&project).await?; + // If there is no previous default project set this project as the default + let default_project = repository.get_default_project().await?; + if default_project.is_none() { + repository.set_default_project(&project.id).await? + }; + + // create a corresponding trust context + self.create_trust_context( + Some(project.name()), + Some(project.id()), + None, + Some(project.authority_identity().await?), + Some(project.authority_access_route()?), + ) + .await?; + Ok(()) } -} -impl From for ProjectConfigCompact { - fn from(p: Project) -> Self { - Self { - id: p.id, - name: p.name, - identity: p.identity, - access_route: p.access_route, - authority_access_route: p.authority_access_route, - authority_identity: p.authority_identity, - okta_config: p.okta_config, - } + pub async fn delete_project(&self, project_id: &str) -> Result<()> { + Ok(self + .projects_repository() + .await? + .delete_project(project_id) + .await?) } -} - -impl From<&ProjectConfigCompact> for Project { - fn from(p: &ProjectConfigCompact) -> Self { - Project { - id: p.id.to_string(), - name: p.name.to_string(), - identity: p.identity.to_owned(), - access_route: p.access_route.to_string(), - authority_access_route: p.authority_access_route.as_ref().map(|a| a.to_string()), - authority_identity: p.authority_identity.as_ref().map(|a| a.to_string()), - okta_config: p.okta_config.clone(), - ..Default::default() - } - } -} - -mod traits { - use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for ProjectsState { - type Item = ProjectState; - const DEFAULT_FILENAME: &'static str = "project"; - const DIR_NAME: &'static str = "projects"; - const HAS_DATA_DIR: bool = false; - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), + pub async fn get_default_project(&self) -> Result { + match self + .projects_repository() + .await? + .get_default_project() + .await? + { + Some(project) => Ok(project), + None => { + Err(Error::new(Origin::Api, Kind::NotFound, "there is no default project").into()) } } + } - fn dir(&self) -> &PathBuf { - &self.dir + pub async fn get_project_by_name(&self, name: &str) -> Result { + match self + .projects_repository() + .await? + .get_project_by_name(name) + .await? + { + Some(project) => Ok(project), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no project named {name}"), + ) + .into()), } } - #[async_trait] - impl StateItemTrait for ProjectState { - type Config = ProjectConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) + pub async fn get_project(&self, project_id: &str) -> Result { + match self + .projects_repository() + .await? + .get_project(project_id) + .await? + { + Some(project) => Ok(project), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no space project with id {project_id}"), + ) + .into()), } + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) + pub async fn get_project_by_name_or_default( + &self, + project_name: &Option, + ) -> Result { + match project_name { + Some(project_name) => self.get_project_by_name(project_name.as_str()).await, + None => self.get_default_project().await, } + } - fn path(&self) -> &PathBuf { - &self.path - } + pub async fn get_projects(&self) -> Result> { + Ok(self.projects_repository().await?.get_projects().await?) + } - fn config(&self) -> &Self::Config { - &self.config + pub async fn get_projects_grouped_by_name(&self) -> Result> { + let mut projects = HashMap::new(); + for project in self.get_projects().await? { + projects.insert(project.name.clone(), project); } + Ok(projects) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs new file mode 100644 index 00000000000..b7e6d5fef88 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs @@ -0,0 +1,14 @@ +use crate::cloud::project::Project; +use ockam_core::async_trait; +use ockam_core::Result; + +#[async_trait] +pub trait ProjectsRepository: Send + Sync + 'static { + async fn store_project(&self, project: &Project) -> Result<()>; + async fn get_project(&self, project_id: &str) -> Result>; + async fn get_project_by_name(&self, name: &str) -> Result>; + async fn get_projects(&self) -> Result>; + async fn get_default_project(&self) -> Result>; + async fn set_default_project(&self, project_id: &str) -> Result<()>; + async fn delete_project(&self, project_id: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs new file mode 100644 index 00000000000..6135de75ce2 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs @@ -0,0 +1,505 @@ +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::env::FromString; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::cloud::addon::ConfluentConfig; +use crate::cloud::project::{OktaConfig, Project, ProjectUserRole}; +use crate::cloud::share::{RoleInShare, ShareScope}; +use crate::minicbor_url::Url; + +use super::ProjectsRepository; + +#[derive(Clone)] +pub struct ProjectsSqlxDatabase { + database: Arc, +} + +impl ProjectsSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for projects"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("projects")))) + } +} + +#[async_trait] +impl ProjectsRepository for ProjectsSqlxDatabase { + async fn store_project(&self, project: &Project) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_project() + .await? + .map(|p| p.id == project.id) + .unwrap_or(false); + + // store the project data + let query1 = query( + "INSERT OR REPLACE INTO project VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + ) + .bind(project.id.to_sql()) + .bind(project.name.to_sql()) + .bind(is_already_default.to_sql()) + .bind(project.space_id.to_sql()) + .bind(project.space_name.to_sql()) + .bind(project.identity.as_ref().map(|r| r.to_sql())) + .bind(project.access_route.to_sql()) + .bind(project.authority_identity.as_ref().map(|r| r.to_sql())) + .bind(project.authority_access_route.as_ref().map(|r| r.to_sql())) + .bind(project.version.as_ref().map(|r| r.to_sql())) + .bind(project.running.as_ref().map(|r| r.to_sql())) + .bind(project.operation_id.as_ref().map(|r| r.to_sql())); + query1.execute(&self.database.pool).await.void()?; + + // remove any existing users related to that project if any + let query2 = + query("DELETE FROM user_project WHERE project_id=$1").bind(project.id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + // store the users associated to that project + for user_email in &project.users { + let query3 = query("INSERT OR REPLACE INTO user_project VALUES (?, ?)") + .bind(user_email.to_sql()) + .bind(project.id.to_sql()); + query3.execute(&self.database.pool).await.void()?; + } + + // remove any existing user roles related to that project if any + let query2 = query("DELETE FROM user_role WHERE project_id=$1").bind(project.id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + // store the user roles associated to that project + for user_role in &project.user_roles { + let query4 = query("INSERT OR REPLACE INTO user_role VALUES (?, ?, ?, ?, ?)") + .bind(user_role.id.to_sql()) + .bind(project.id.to_sql()) + .bind(user_role.email.to_sql()) + .bind(user_role.role.to_string().to_sql()) + .bind(user_role.scope.to_string().to_sql()); + query4.execute(&self.database.pool).await.void()?; + } + + // store the okta configuration if any + for okta_config in &project.okta_config { + let query5 = query("INSERT OR REPLACE INTO okta_config VALUES (?, ?, ?, ?, ?)") + .bind(project.id.to_sql()) + .bind(okta_config.tenant_base_url.to_string().to_sql()) + .bind(okta_config.client_id.to_sql()) + .bind(okta_config.certificate.to_string().to_sql()) + .bind(okta_config.attributes.join(",").to_string().to_sql()); + query5.execute(&self.database.pool).await.void()?; + } + + // store the confluent configuration if any + for confluent_config in &project.confluent_config { + let query6 = query("INSERT OR REPLACE INTO confluent_config VALUES (?, ?)") + .bind(project.id.to_sql()) + .bind(confluent_config.bootstrap_server.to_sql()); + query6.execute(&self.database.pool).await.void()?; + } + + transaction.commit().await.void() + } + + async fn get_project(&self, project_id: &str) -> Result> { + let query = + query("SELECT project_name FROM project WHERE project_id=$1").bind(project_id.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + match row { + Some(r) => { + let project_name: String = r.get(0); + self.get_project_by_name(&project_name).await + } + None => Ok(None), + } + } + + async fn get_project_by_name(&self, name: &str) -> Result> { + let transaction = self.database.begin().await.into_core()?; + + let query = query_as("SELECT * FROM project WHERE project_name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let project = match row.map(|r| r.project()).transpose()? { + Some(mut project) => { + // get the project users emails + let query2 = query_as("SELECT * FROM user_project WHERE project_id=$1") + .bind(project.id.to_sql()); + let rows: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let users = rows.into_iter().map(|r| r.user_email).collect(); + project.users = users; + + // get the project users roles + let query3 = query_as("SELECT * FROM user_role WHERE project_id=$1") + .bind(project.id.to_sql()); + let rows: Vec = + query3.fetch_all(&self.database.pool).await.into_core()?; + let user_roles: Vec = rows + .into_iter() + .map(|r| r.project_user_role()) + .collect::>>()?; + project.user_roles = user_roles; + + // get the project okta configuration + let query4 = query_as("SELECT * FROM okta_config WHERE project_id=$1") + .bind(project.id.to_sql()); + let row: Option = query4 + .fetch_optional(&self.database.pool) + .await + .into_core()?; + project.okta_config = row.map(|r| r.okta_config()).transpose()?; + + // get the project confluent configuration + let query5 = query_as("SELECT * FROM confluent_config WHERE project_id=$1") + .bind(project.id.to_sql()); + let row: Option = query5 + .fetch_optional(&self.database.pool) + .await + .into_core()?; + project.confluent_config = row.map(|r| r.confluent_config()); + + Some(project) + } + + None => None, + }; + transaction.commit().await.void()?; + Ok(project) + } + + async fn get_projects(&self) -> Result> { + let query = query("SELECT project_name FROM project"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + let project_names: Vec = rows.iter().map(|r| r.get(0)).collect(); + let mut projects = vec![]; + for project_name in project_names { + let project = self.get_project_by_name(&project_name).await?; + if let Some(project) = project { + projects.push(project); + }; + } + Ok(projects) + } + + async fn get_default_project(&self) -> Result> { + let query = + query("SELECT project_name FROM project WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + match row { + Some(r) => { + let project_name: String = r.get(0); + self.get_project_by_name(&project_name).await + } + None => Ok(None), + } + } + + async fn set_default_project(&self, project_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the project as the default one + let query1 = query("UPDATE project SET is_default = ? WHERE project_id = ?") + .bind(true.to_sql()) + .bind(project_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE project SET is_default = ? WHERE project_id <> ?") + .bind(false.to_sql()) + .bind(project_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn delete_project(&self, project_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + + let query1 = query("DELETE FROM project WHERE project_id=?").bind(project_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + let query2 = query("DELETE FROM user_project WHERE project_id=?").bind(project_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + let query3 = query("DELETE FROM user_role WHERE project_id=?").bind(project_id.to_sql()); + query3.execute(&self.database.pool).await.void()?; + + let query4 = query("DELETE FROM okta_config WHERE project_id=?").bind(project_id.to_sql()); + query4.execute(&self.database.pool).await.void()?; + + let query5 = + query("DELETE FROM confluent_config WHERE project_id=?").bind(project_id.to_sql()); + query5.execute(&self.database.pool).await.void()?; + + transaction.commit().await.void() + } +} + +#[derive(sqlx::FromRow)] +struct ProjectRow { + project_id: String, + project_name: String, + #[allow(unused)] + is_default: bool, + space_id: String, + space_name: String, + identifier: Option, + access_route: String, + authority_identity: Option, + authority_access_route: Option, + version: Option, + running: Option, + operation_id: Option, +} + +impl ProjectRow { + pub(crate) fn project(&self) -> Result { + self.complete_project(vec![], vec![], None, None) + } + + pub(crate) fn complete_project( + &self, + user_emails: Vec, + user_roles: Vec, + okta_config: Option, + confluent_config: Option, + ) -> Result { + let identifier = self + .identifier + .as_ref() + .map(|i| Identifier::from_string(i)) + .transpose()?; + Ok(Project { + id: self.project_id.clone(), + name: self.project_name.clone(), + space_id: self.space_id.clone(), + space_name: self.space_name.clone(), + identity: identifier, + access_route: self.access_route.clone(), + authority_access_route: self.authority_access_route.clone(), + authority_identity: self.authority_identity.clone(), + version: self.version.clone(), + running: self.running, + operation_id: self.operation_id.clone(), + users: user_emails, + user_roles, + okta_config, + confluent_config, + }) + } +} + +#[derive(sqlx::FromRow)] +struct UserProjectRow { + #[allow(unused)] + project_id: String, + user_email: String, +} + +#[derive(sqlx::FromRow)] +struct UserRoleRow { + user_id: i64, + #[allow(unused)] + project_id: String, + user_email: String, + role: String, + scope: String, +} + +impl UserRoleRow { + fn project_user_role(&self) -> Result { + let role = RoleInShare::from_str(&self.role) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + let scope = ShareScope::from_str(&self.scope) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + Ok(ProjectUserRole { + id: self.user_id as usize, + email: self.user_email.clone(), + role, + scope, + }) + } +} + +#[derive(sqlx::FromRow)] +struct OktaConfigRow { + #[allow(unused)] + project_id: String, + tenant_base_url: String, + client_id: String, + certificate: String, + attributes: String, +} + +impl OktaConfigRow { + fn okta_config(&self) -> Result { + let tenant_base_url = Url::parse(&self.tenant_base_url.clone()) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + Ok(OktaConfig { + tenant_base_url, + certificate: self.certificate.clone(), + client_id: self.client_id.clone(), + attributes: self.attributes.split(',').map(|a| a.to_string()).collect(), + }) + } +} + +#[derive(sqlx::FromRow)] +struct ConfluentConfigRow { + #[allow(unused)] + project_id: String, + bootstrap_server: String, +} + +impl ConfluentConfigRow { + fn confluent_config(&self) -> ConfluentConfig { + ConfluentConfig { + bootstrap_server: self.bootstrap_server.clone(), + } + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + + // create and store 2 projects + let project1 = create_project( + "1", + "name1", + vec!["me@ockam.io", "you@ockam.io"], + vec![ + create_project_user_role(1, RoleInShare::Admin), + create_project_user_role(2, RoleInShare::Guest), + ], + ); + let mut project2 = create_project( + "2", + "name2", + vec!["me@ockam.io", "him@ockam.io", "her@ockam.io"], + vec![ + create_project_user_role(1, RoleInShare::Admin), + create_project_user_role(2, RoleInShare::Guest), + ], + ); + repository.store_project(&project1).await?; + repository.store_project(&project2).await?; + + // retrieve them as a vector or by name + let result = repository.get_projects().await?; + assert_eq!(result, vec![project1.clone(), project2.clone()]); + + let result = repository.get_project_by_name("name1").await?; + assert_eq!(result, Some(project1.clone())); + + // a project can be marked as the default project + repository.set_default_project("1").await?; + let result = repository.get_default_project().await?; + assert_eq!(result, Some(project1.clone())); + + repository.set_default_project("2").await?; + let result = repository.get_default_project().await?; + assert_eq!(result, Some(project2.clone())); + + // updating a project which was already the default should keep it the default + project2.users = vec!["someone@ockam.io".into()]; + repository.store_project(&project2).await?; + let result = repository.get_default_project().await?; + assert_eq!(result, Some(project2.clone())); + + // a project can be deleted + repository.delete_project("2").await?; + let result = repository.get_default_project().await?; + assert_eq!(result, None); + + let result = repository.get_projects().await?; + assert_eq!(result, vec![project1.clone()]); + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(ProjectsSqlxDatabase::new(Arc::new(db)))) + } + + fn create_project( + id: &str, + name: &str, + user_emails: Vec<&str>, + user_roles: Vec, + ) -> Project { + Project { + id: id.into(), + name: name.into(), + space_id: "space-id".into(), + space_name: "space-name".into(), + access_route: "route".into(), + users: user_emails.iter().map(|u| u.to_string()).collect(), + identity: Some( + Identifier::from_str("I124ed0b2e5a2be82e267ead6b3279f683616b66d").unwrap(), + ), + authority_access_route: Some("authority-route".into()), + authority_identity: Some("authority-identity".into()), + okta_config: Some(create_okta_config()), + confluent_config: Some(create_confluent_config()), + version: Some("1.0".into()), + running: Some(true), + operation_id: Some("abc".into()), + user_roles, + } + } + + fn create_project_user_role(user_id: usize, role: RoleInShare) -> ProjectUserRole { + ProjectUserRole { + email: "user_email".into(), + id: user_id, + role, + scope: ShareScope::Project, + } + } + + fn create_okta_config() -> OktaConfig { + OktaConfig { + tenant_base_url: Url::parse("http://ockam.io").unwrap(), + certificate: "certificate".to_string(), + client_id: "client-id".to_string(), + attributes: vec!["attribute1".into(), "attribute2".into()], + } + } + + fn create_confluent_config() -> ConfluentConfig { + ConfluentConfig { + bootstrap_server: "bootstrap_server".to_string(), + } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs index 8d3d8d6ce44..c143b6be255 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs @@ -1,100 +1,103 @@ -use super::Result; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; + +use crate::cli_state::CliState; use crate::cloud::space::Space; -use crate::config::lookup::SpaceLookup; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SpacesState { - dir: PathBuf, -} +use super::Result; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SpaceState { - name: String, - path: PathBuf, - config: SpaceConfig, -} +impl CliState { + pub async fn store_space( + &self, + space_id: &str, + space_name: &str, + users: Vec<&str>, + ) -> Result { + let repository = self.spaces_repository().await?; + let space = Space { + id: space_id.to_string(), + name: space_name.to_string(), + users: users.iter().map(|u| u.to_string()).collect(), + }; -impl SpaceState { - pub fn name(&self) -> &str { - &self.name - } -} + repository.store_space(&space).await?; -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct SpaceConfig { - pub name: String, - pub id: String, -} + // If there is no previous default space set this space as the default + let default_space = repository.get_default_space().await?; + if default_space.is_none() { + repository.set_default_space(&space.id).await? + }; -impl SpaceConfig { - pub fn from_lookup(name: &str, lookup: SpaceLookup) -> Self { - Self { - name: name.to_string(), - id: lookup.id, - } + Ok(space) } -} -impl From<&Space> for SpaceConfig { - fn from(s: &Space) -> Self { - Self { - name: s.name.to_string(), - id: s.id.to_string(), + pub async fn get_default_space(&self) -> Result { + match self.spaces_repository().await?.get_default_space().await? { + Some(space) => Ok(space), + None => { + Err(Error::new(Origin::Api, Kind::NotFound, "there is no default space").into()) + } } } -} -mod traits { - use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for SpacesState { - type Item = SpaceState; - const DEFAULT_FILENAME: &'static str = "space"; - const DIR_NAME: &'static str = "spaces"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } + pub async fn get_space_by_name(&self, name: &str) -> Result { + match self + .spaces_repository() + .await? + .get_space_by_name(name) + .await? + { + Some(space) => Ok(space), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no space with name {name}"), + ) + .into()), } + } - fn dir(&self) -> &PathBuf { - &self.dir - } + pub async fn get_spaces(&self) -> Result> { + Ok(self.spaces_repository().await?.get_spaces().await?) } - #[async_trait] - impl StateItemTrait for SpaceState { - type Config = SpaceConfig; + pub async fn delete_space(&self, space_id: &str) -> Result<()> { + Ok(self + .spaces_repository() + .await? + .delete_space(space_id) + .await?) + } - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) - } + pub async fn set_space_as_default(&self, space_id: &str) -> Result<()> { + Ok(self + .spaces_repository() + .await? + .set_default_space(space_id) + .await?) + } +} - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) - } +#[cfg(test)] +mod test { + use super::*; - fn path(&self) -> &PathBuf { - &self.path - } + #[tokio::test] + async fn test_cli_spaces() -> Result<()> { + let cli = CliState::test().await?; - fn config(&self) -> &Self::Config { - &self.config - } + // the first created space becomes the default + let space1 = cli + .store_space("1", "name1", vec!["me@ockam.io", "you@ockam.io"]) + .await?; + let result = cli.get_default_space().await?; + assert_eq!(result, space1); + + // the store method can be used to update a space + let updated_space1 = cli.store_space("1", "name1", vec!["them@ockam.io"]).await?; + let result = cli.get_default_space().await?; + assert_eq!(result, updated_space1); + + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs new file mode 100644 index 00000000000..92752cb2ec1 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs @@ -0,0 +1,13 @@ +use crate::cloud::space::Space; +use ockam_core::async_trait; +use ockam_core::Result; + +#[async_trait] +pub trait SpacesRepository: Send + Sync + 'static { + async fn store_space(&self, space: &Space) -> Result<()>; + async fn get_space_by_name(&self, name: &str) -> Result>; + async fn get_spaces(&self) -> Result>; + async fn get_default_space(&self) -> Result>; + async fn set_default_space(&self, space_id: &str) -> Result<()>; + async fn delete_space(&self, space_id: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs new file mode 100644 index 00000000000..d3af1f73b52 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs @@ -0,0 +1,246 @@ +use sqlx::sqlite::SqliteRow; + +use sqlx::*; +use std::sync::Arc; + +use ockam_core::async_trait; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::cloud::space::Space; + +use super::SpacesRepository; + +#[derive(Clone)] +pub struct SpacesSqlxDatabase { + database: Arc, +} + +impl SpacesSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for spaces"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("spaces")))) + } +} + +#[async_trait] +impl SpacesRepository for SpacesSqlxDatabase { + async fn store_space(&self, space: &Space) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_space() + .await? + .map(|s| s.id == space.id) + .unwrap_or(false); + + let query1 = query("INSERT OR REPLACE INTO space VALUES (?, ?, ?)") + .bind(space.id.to_sql()) + .bind(space.name.to_sql()) + .bind(is_already_default.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // remove any existing users related to that space if any + let query2 = query("DELETE FROM user_space WHERE space_id=$1").bind(space.id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + // store the users associated to that space + for user_email in &space.users { + let query3 = query("INSERT OR REPLACE INTO user_space VALUES (?, ?)") + .bind(user_email.to_sql()) + .bind(space.id.to_sql()); + query3.execute(&self.database.pool).await.void()?; + } + + transaction.commit().await.void() + } + + async fn get_space_by_name(&self, name: &str) -> Result> { + let transaction = self.database.begin().await.into_core()?; + + let query1 = query_as("SELECT * FROM space WHERE space_name=$1").bind(name.to_sql()); + let row: Option = query1 + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let space = match row.map(|r| r.space()) { + Some(mut space) => { + let query2 = + query_as("SELECT * FROM user_space WHERE space_id=$1").bind(space.id.to_sql()); + let rows: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let users = rows.into_iter().map(|r| r.user_email).collect(); + space.users = users; + Some(space) + } + None => None, + }; + transaction.commit().await.void()?; + Ok(space) + } + + async fn get_spaces(&self) -> Result> { + let transaction = self.database.begin().await.into_core()?; + + let query = query_as("SELECT * FROM space"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + + let mut spaces = vec![]; + for space_row in row { + let query2 = query_as("SELECT * FROM user_space WHERE space_id=$1") + .bind(space_row.space_id.to_sql()); + let rows: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let users = rows.into_iter().map(|r| r.user_email).collect(); + spaces.push(space_row.space_with_user_emails(users)) + } + + transaction.commit().await.void()?; + + Ok(spaces) + } + + async fn get_default_space(&self) -> Result> { + let query = query("SELECT space_name FROM space WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let name: Option = row.map(|r| r.get(0)); + match name { + Some(name) => self.get_space_by_name(&name).await, + None => Ok(None), + } + } + + async fn set_default_space(&self, space_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the space as the default one + let query1 = query("UPDATE space SET is_default = ? WHERE space_id = ?") + .bind(true.to_sql()) + .bind(space_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE space SET is_default = ? WHERE space_id <> ?") + .bind(false.to_sql()) + .bind(space_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn delete_space(&self, space_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + + let query1 = query("DELETE FROM space WHERE space_id=?").bind(space_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + let query2 = query("DELETE FROM user_space WHERE space_id=?").bind(space_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + transaction.commit().await.void() + } +} + +#[derive(sqlx::FromRow)] +struct SpaceRow { + space_id: String, + space_name: String, +} + +impl SpaceRow { + pub(crate) fn space(&self) -> Space { + self.space_with_user_emails(vec![]) + } + + pub(crate) fn space_with_user_emails(&self, user_emails: Vec) -> Space { + Space { + id: self.space_id.clone(), + name: self.space_name.clone(), + users: user_emails, + } + } +} + +#[derive(sqlx::FromRow)] +struct UserSpaceRow { + #[allow(unused)] + space_id: String, + user_email: String, +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + + // create and store 2 spaces + let space1 = Space { + id: "1".to_string(), + name: "name1".to_string(), + users: vec!["me@ockam.io".to_string(), "you@ockam.io".to_string()], + }; + let mut space2 = Space { + id: "2".to_string(), + name: "name2".to_string(), + users: vec![ + "me@ockam.io".to_string(), + "him@ockam.io".to_string(), + "her@ockam.io".to_string(), + ], + }; + + repository.store_space(&space1).await?; + repository.store_space(&space2).await?; + + // retrieve them as a vector or by name + let result = repository.get_spaces().await?; + assert_eq!(result, vec![space1.clone(), space2.clone()]); + + let result = repository.get_space_by_name("name1").await?; + assert_eq!(result, Some(space1.clone())); + + // a space can be marked as the default space + repository.set_default_space("1").await?; + let result = repository.get_default_space().await?; + assert_eq!(result, Some(space1.clone())); + + repository.set_default_space("2").await?; + let result = repository.get_default_space().await?; + assert_eq!(result, Some(space2.clone())); + + // updating a space which was already the default should keep it the default + space2.users = vec!["someone@ockam.io".to_string()]; + repository.store_space(&space2).await?; + let result = repository.get_default_space().await?; + assert_eq!(result, Some(space2.clone())); + + // a space can be deleted + repository.delete_space("2").await?; + let result = repository.get_default_space().await?; + assert_eq!(result, None); + + let result = repository.get_spaces().await?; + assert_eq!(result, vec![space1.clone()]); + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(SpacesSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/traits.rs b/implementations/rust/ockam/ockam_api/src/cli_state/traits.rs index 3019e9fec9e..8b137891791 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/traits.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/traits.rs @@ -1,348 +1 @@ -use crate::cli_state::{file_stem, CliState, CliStateError}; -use fs2::FileExt; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{async_trait, Error}; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; -use super::Result; -pub const DATA_DIR_NAME: &str = "data"; - -/// Represents the directory of a type of state. This directory contains a list of items, uniquely -/// identified by a name, and represented by the same `Item` type. -/// -/// One item can be set as the "default" item, which is used in some CLI commands when no -/// argument is provided for that type of `Item`. -#[async_trait] -pub trait StateDirTrait: Sized + Send + Sync { - type Item: StateItemTrait; - const DEFAULT_FILENAME: &'static str; - const DIR_NAME: &'static str; - const HAS_DATA_DIR: bool; - - fn new(root_path: &Path) -> Self; - - fn default_filename() -> &'static str { - Self::DEFAULT_FILENAME - } - fn build_dir(root_path: &Path) -> PathBuf { - root_path.join(Self::DIR_NAME) - } - fn has_data_dir() -> bool { - Self::HAS_DATA_DIR - } - - /// Load the root configuration - /// and migrate each entry if necessary - async fn init(root_path: &Path) -> Result { - let root = Self::load(root_path)?; - for path in root.list_items_paths()? { - root.migrate(path.as_path()).await?; - } - Ok(root) - } - - /// Do not run any migration by default - async fn migrate(&self, _path: &Path) -> Result<()> { - Ok(()) - } - - fn load(root_path: &Path) -> Result { - Self::create_dirs(root_path)?; - Ok(Self::new(root_path)) - } - - /// Recreate all the state directories - fn reset(&self, root_path: &Path) -> Result { - Self::create_dirs(root_path) - } - - /// Create all the state directories - fn create_dirs(root_path: &Path) -> Result { - let dir = Self::build_dir(root_path); - if Self::has_data_dir() { - std::fs::create_dir_all(dir.join(DATA_DIR_NAME))?; - } else { - std::fs::create_dir_all(&dir)?; - }; - Ok(dir) - } - - fn dir(&self) -> &PathBuf; - fn dir_as_string(&self) -> String { - self.dir().to_string_lossy().to_string() - } - - fn path(&self, name: impl AsRef) -> PathBuf { - self.dir().join(format!("{}.json", name.as_ref())) - } - - fn overwrite( - &self, - name: impl AsRef, - config: <::Item as StateItemTrait>::Config, - ) -> Result { - let path = self.path(&name); - let state = with_lock(&path, || Self::Item::new(path.clone(), config))?; - if !self.default_path()?.exists() { - self.set_default(&name)?; - } - Ok(state) - } - - fn create( - &self, - name: impl AsRef, - config: <::Item as StateItemTrait>::Config, - ) -> Result { - debug!(name = %name.as_ref(), "Creating new config resource"); - if self.exists(&name) { - return Err(CliStateError::AlreadyExists { - resource: Self::default_filename().to_string(), - name: name.as_ref().to_string(), - }); - } - trace!(name = %name.as_ref(), "Creating config resource instance"); - let state = Self::Item::new(self.path(&name), config)?; - if !self.default_path()?.exists() { - self.set_default(&name)?; - } - info!(name = %name.as_ref(), "Created new config resource"); - Ok(state) - } - - fn get(&self, name: impl AsRef) -> Result { - if !self.exists(&name) { - return Err(CliStateError::ResourceNotFound { - resource: Self::default_filename().to_string(), - name: name.as_ref().to_string(), - }); - } - Self::Item::load(self.path(&name)) - } - - fn list(&self) -> Result> { - let mut items = Vec::default(); - for name in self.list_items_names()? { - if let Ok(item) = self.get(name) { - items.push(item); - } - } - Ok(items) - } - - fn list_items_names(&self) -> Result> { - let mut items = Vec::default(); - let iter = std::fs::read_dir(self.dir()).map_err(|e| { - let dir = self.dir().as_path().to_string_lossy(); - error!(%dir, %e, "Unable to read state directory"); - CliStateError::InvalidOperation(format!("Unable to read state from directory {dir}")) - })?; - for entry in iter { - let entry_path = entry?.path(); - if self.is_item_path(&entry_path)? { - items.push(file_stem(&entry_path)?); - } - } - Ok(items) - } - - // If a path has been created with the self.path function - // then we know that the current name is an item name - fn is_item_path(&self, path: &PathBuf) -> Result { - let name = file_stem(path)?; - Ok(path.eq(&self.path(name))) - } - - fn list_items_paths(&self) -> Result> { - let mut items = Vec::default(); - for name in self.list_items_names()? { - let path = self.path(name); - items.push(path); - } - Ok(items) - } - - // TODO: move to StateItemTrait - fn delete(&self, name: impl AsRef) -> Result<()> { - // Retrieve state. If doesn't exist do nothing. - let s = match self.get(&name) { - Ok(project) => project, - Err(CliStateError::ResourceNotFound { .. }) => return Ok(()), - Err(e) => return Err(e), - }; - // If it's the default, remove link - if let Ok(default) = self.default() { - if default.path() == s.path() { - let _ = std::fs::remove_file(self.default_path()?); - } - } - // Remove state data - s.delete() - } - - fn default_path(&self) -> Result { - let root_path = self.dir().parent().expect("Should have parent"); - Ok(CliState::defaults_dir(root_path)?.join(Self::default_filename())) - } - - fn default(&self) -> Result { - let path = std::fs::canonicalize(self.default_path()?)?; - Self::Item::load(path) - } - - fn set_default(&self, name: impl AsRef) -> Result<()> { - debug!(name = %name.as_ref(), "Setting default item"); - if !self.exists(&name) { - return Err(CliStateError::ResourceNotFound { - resource: Self::default_filename().to_string(), - name: name.as_ref().to_string(), - }); - } - let original = self.path(&name); - let link = self.default_path()?; - info!("removing link {:?}", link); - // Remove link if it exists - let _ = std::fs::remove_file(&link); - info!("symlink to {:?}", original); - // Create link to the default item - std::fs::create_dir_all(link.parent().unwrap()) - .map_err(|e| Error::new(Origin::Node, Kind::Io, e))?; - std::os::unix::fs::symlink(original, link)?; - info!(name = %name.as_ref(), "Set default item"); - Ok(()) - } - - fn is_default(&self, name: impl AsRef) -> Result { - if !self.exists(&name) { - return Ok(false); - } - let default_name = { - let path = std::fs::canonicalize(self.default_path()?)?; - file_stem(&path)? - }; - Ok(default_name.eq(name.as_ref())) - } - - fn is_empty(&self) -> Result { - for entry in std::fs::read_dir(self.dir())? { - let name = file_stem(&entry?.path())?; - if self.get(name).is_ok() { - return Ok(false); - } - } - Ok(true) - } - - fn exists(&self, name: impl AsRef) -> bool { - self.path(&name).exists() - } -} - -/// This trait defines the methods to retrieve an item from a state directory. -/// The details of the item are defined in the `Config` type. -#[async_trait] -pub trait StateItemTrait: Sized + Send { - type Config: Serialize + for<'a> Deserialize<'a> + Send; - - /// Create a new item with the given config. - fn new(path: PathBuf, config: Self::Config) -> Result; - - /// Load an item from the given path. - fn load(path: PathBuf) -> Result; - - /// Persist the item to disk after updating the config. - fn persist(&self) -> Result<()> { - with_lock(self.path(), || { - let contents = serde_json::to_string(self.config())?; - std::fs::write(self.path(), contents)?; - Ok(()) - }) - } - - fn delete(&self) -> Result<()> { - with_lock(self.path(), || { - std::fs::remove_file(self.path())?; - Ok(()) - })?; - let _ = std::fs::remove_file(self.path().with_extension("lock")); - Ok(()) - } - - fn path(&self) -> &PathBuf; - fn config(&self) -> &Self::Config; -} - -fn with_lock(path: &Path, f: impl FnOnce() -> Result) -> Result { - let lock_file = std::fs::OpenOptions::new() - .write(true) - .read(true) - .create(true) - .open(path.with_extension("lock"))?; - lock_file.lock_exclusive()?; - let res = f(); - lock_file.unlock()?; - res -} - -#[cfg(test)] -mod tests { - use crate::cli_state::{StateDirTrait, StateItemTrait}; - use std::path::{Path, PathBuf}; - - #[test] - fn test_is_item_path() { - let config = TestConfig::new(Path::new("dir")); - let path = config.path("name"); - assert!(config.is_item_path(&path).unwrap()) - } - - /// Dummy configuration - struct TestConfig { - dir: PathBuf, - } - impl StateDirTrait for TestConfig { - type Item = TestConfigItem; - const DEFAULT_FILENAME: &'static str = "test"; - const DIR_NAME: &'static str = "test"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - let dir = Self::build_dir(root_path); - std::fs::create_dir_all(&dir).unwrap(); - Self { dir } - } - - fn dir(&self) -> &PathBuf { - &self.dir - } - } - - struct TestConfigItem { - path: PathBuf, - config: u32, - } - impl StateItemTrait for TestConfigItem { - type Config = u32; - - fn new(path: PathBuf, config: Self::Config) -> crate::cli_state::Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - Ok(Self { path, config }) - } - - fn load(path: PathBuf) -> crate::cli_state::Result { - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(TestConfigItem { path, config }) - } - - fn path(&self) -> &PathBuf { - &self.path - } - - fn config(&self) -> &Self::Config { - &self.config - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs index 67be9d0669f..63d9f35821c 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs @@ -1,101 +1,271 @@ -use super::Result; -use crate::config::cli::TrustContextConfig; -use std::fmt::{Display, Formatter}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct TrustContextsState { - dir: PathBuf, -} +use ockam::identity::Identity; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_multiaddr::MultiAddr; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct TrustContextState { - name: String, - path: PathBuf, - config: TrustContextConfig, -} +use crate::cli_state::trust_contexts_repository_sql::NamedTrustContext; +use crate::cli_state::CliState; -impl TrustContextState { - pub fn name(&self) -> &str { - &self.name - } -} +use super::Result; -impl Display for TrustContextState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Name: {}", self.name)?; - Ok(()) +impl CliState { + pub async fn get_trust_context(&self, name: &str) -> Result { + match self + .trust_contexts_repository() + .await? + .get_trust_context(name) + .await? + { + Some(trust_context) => Ok(trust_context), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no trust context with name {name}"), + ) + .into()), + } } -} -mod traits { - use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for TrustContextsState { - type Item = TrustContextState; - const DEFAULT_FILENAME: &'static str = "trust_context"; - const DIR_NAME: &'static str = "trust_contexts"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } + pub async fn get_default_trust_context(&self) -> Result { + match self + .trust_contexts_repository() + .await? + .get_default_trust_context() + .await? + { + Some(trust_context) => Ok(trust_context), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + "there is no default trust context", + ) + .into()), } + } - fn dir(&self) -> &PathBuf { - &self.dir + pub async fn get_trust_context_or_default( + &self, + name: &Option, + ) -> Result { + match name { + Some(name) => self.get_trust_context(name).await, + None => self.get_default_trust_context().await, } } - impl TrustContextsState { - pub fn read_config_from_path(&self, path: &str) -> Result { - let tcc = match std::fs::read_to_string(path) { - Ok(contents) => { - let mut tc = serde_json::from_str::(&contents)?; - tc.set_path(PathBuf::from(path)); - tc - } - Err(_) => { - let state = self.get(path)?; - let mut tcc = state.config().clone(); - tcc.set_path(state.path().clone()); - tcc + pub async fn retrieve_trust_context( + &self, + trust_context_name: &Option, + project_name: &Option, + authority_identity: &Option, + credential_name: &Option, + ) -> Result> { + match trust_context_name { + Some(name) => Ok(Some(self.get_trust_context(name).await?)), + None => { + let project = match project_name { + Some(name) => self.get_project_by_name(name).await.ok(), + None => self.get_default_project().await.ok(), + }; + match project { + Some(project) => Ok(self.get_trust_context(&project.name).await.ok()), + None => match credential_name { + Some(credential_name) => Ok(Some( + self.create_trust_context( + None, + None, + Some(credential_name.clone()), + authority_identity.clone(), + None, + ) + .await?, + )), + None => Ok(None), + }, } - }; - Ok(tcc) + } } } - #[async_trait] - impl StateItemTrait for TrustContextState { - type Config = TrustContextConfig; + pub async fn get_trust_contexts(&self) -> Result> { + Ok(self + .trust_contexts_repository() + .await? + .get_trust_contexts() + .await?) + } - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) - } + pub async fn delete_trust_context(&self, name: &str) -> Result<()> { + Ok(self + .trust_contexts_repository() + .await? + .delete_trust_context(name) + .await?) + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) - } + pub async fn set_default_trust_context(&self, name: &str) -> Result<()> { + Ok(self + .trust_contexts_repository() + .await? + .set_default_trust_context(name) + .await?) + } - fn path(&self) -> &PathBuf { - &self.path - } + pub async fn create_trust_context( + &self, + name: Option, + trust_context_id: Option, + credential_name: Option, + authority_identity: Option, + authority_route: Option, + ) -> Result { + let credential = match credential_name { + Some(name) => self.get_credential_by_name(&name).await.ok(), + None => None, + }; + let name = name.unwrap_or("default".to_string()); - fn config(&self) -> &Self::Config { - &self.config - } + // if the authority identity is not defined use the + // authority identity defined on the credential + let authority_identity = match authority_identity.clone() { + Some(identity) => Some(identity), + None => match credential.clone() { + None => None, + Some(credential) => Some(credential.issuer_identity().await?), + }, + }; + + let trust_context_id = trust_context_id + .or_else(|| { + authority_identity + .clone() + .map(|i| i.identifier().to_string()) + }) + .unwrap_or("default".to_string()); + + let trust_context = NamedTrustContext::new( + &name, + &trust_context_id, + credential.map(|c| c.credential_and_purpose_key()), + authority_identity.map(|i| i.change_history().clone()), + authority_route, + ); + + let repository = self.trust_contexts_repository().await?; + repository.store_trust_context(&trust_context).await?; + + // If there is no previous default trust_context set this trust_context as the default + let default_trust_context = repository.get_default_trust_context().await?; + if default_trust_context.is_none() { + repository + .set_default_trust_context(&trust_context.name()) + .await? + }; + + Ok(trust_context) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Duration; + + use ockam::identity::models::CredentialAndPurposeKey; + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identifier, Identities}; + use ockam_core::env::FromString; + + use super::*; + + // There are 3 ways to create a trust context + // - with only an id + // - with a credential + // - with an authority identity + route + #[tokio::test] + async fn test_create_trust_context() -> Result<()> { + let cli = CliState::test().await?; + + // 1. with only an id + let result = cli + .create_trust_context( + Some("trust-context".into()), + Some("1".into()), + None, + None, + None, + ) + .await?; + let expected = NamedTrustContext::new("trust-context", "1", None, None, None); + assert_eq!(result, expected); + + // that trust context is the default one because it is the first created trust context + let result = cli.get_default_trust_context().await?; + assert_eq!(result, expected); + + // 2. with a credential + let identities = identities(); + let authority_identifier = identities.identities_creation().create_identity().await?; + let authority = identities.get_identity(&authority_identifier).await?; + let credential = create_credential(identities, &authority_identifier).await?; + cli.store_credential("credential-name", &authority, credential.clone()) + .await?; + let result = cli + .create_trust_context( + Some("trust-context".into()), + None, + Some("credential-name".into()), + None, + None, + ) + .await?; + let expected = NamedTrustContext::new( + "trust-context", + authority.identifier().to_string().as_str(), + Some(credential), + Some(authority.change_history().clone()), + None, + ); + assert_eq!(result, expected); + + // 3. with an authority + let authority_route = MultiAddr::from_string("/dnsaddr/127.0.0.1/tcp/5000/service/api")?; + let result = cli + .create_trust_context( + Some("trust-context".into()), + None, + None, + Some(authority.clone()), + Some(authority_route.clone()), + ) + .await?; + let expected = NamedTrustContext::new( + "trust-context", + authority.identifier().to_string().as_str(), + None, + Some(authority.change_history().clone()), + Some(authority_route), + ); + assert_eq!(result, expected); + Ok(()) + } + + /// HELPERS + pub async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + Ok(identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await?) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs new file mode 100644 index 00000000000..4e9a5750a93 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs @@ -0,0 +1,13 @@ +use crate::cli_state::trust_contexts_repository_sql::NamedTrustContext; +use ockam_core::async_trait; +use ockam_core::Result; + +#[async_trait] +pub trait TrustContextsRepository: Send + Sync + 'static { + async fn store_trust_context(&self, trust_context: &NamedTrustContext) -> Result<()>; + async fn get_default_trust_context(&self) -> Result>; + async fn set_default_trust_context(&self, name: &str) -> Result<()>; + async fn get_trust_context(&self, name: &str) -> Result>; + async fn get_trust_contexts(&self) -> Result>; + async fn delete_trust_context(&self, name: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs new file mode 100644 index 00000000000..45d169a2098 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs @@ -0,0 +1,413 @@ +use std::fmt::{Display, Formatter}; +use std::sync::Arc; + +use sqlx::*; + +use ockam::identity::models::{ChangeHistory, CredentialAndPurposeKey}; +use ockam::identity::SecureChannels; +use ockam::identity::{ + AuthorityService, CredentialsMemoryRetriever, Identifier, Identity, RemoteCredentialsRetriever, + RemoteCredentialsRetrieverInfo, TrustContext, +}; +use ockam_core::async_trait; +use ockam_core::env::FromString; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_core::Result; +use ockam_multiaddr::MultiAddr; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_transport_tcp::TcpTransport; + +use crate::identity::CredentialAndPurposeKeySql; +use crate::{multiaddr_to_route, DefaultAddress}; + +use super::trust_contexts_repository::TrustContextsRepository; + +#[derive(Clone)] +pub struct TrustContextsSqlxDatabase { + database: Arc, +} + +impl TrustContextsSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for trust contexts"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory( + "trust contexts", + )))) + } +} + +#[async_trait] +impl TrustContextsRepository for TrustContextsSqlxDatabase { + async fn store_trust_context(&self, trust_context: &NamedTrustContext) -> Result<()> { + let is_already_default = self + .get_default_trust_context() + .await? + .map(|tc| tc.name() == trust_context.name()) + .unwrap_or(false); + + let query = query("INSERT OR REPLACE INTO trust_context VALUES ($1, $2, $3, $4, $5, $6)") + .bind(trust_context.name().to_sql()) + .bind(trust_context.trust_context_id().to_sql()) + .bind(is_already_default.to_sql()) + .bind( + trust_context + .credential() + .as_ref() + .map(|c| CredentialAndPurposeKeySql(c.clone()).to_sql()), + ) + .bind( + trust_context + .authority_identity + .as_ref() + .map(|c| c.to_sql()), + ) + .bind( + trust_context + .authority_route + .as_ref() + .map(|r| r.to_string().to_sql()), + ); + query.execute(&self.database.pool).await.void() + } + + async fn get_default_trust_context(&self) -> Result> { + let query = query_as("SELECT * FROM trust_context WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_trust_context()).transpose() + } + + async fn set_default_trust_context(&self, name: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the trust context as the default one + let query1 = query("UPDATE trust_context SET is_default = ? WHERE name = ?") + .bind(true.to_sql()) + .bind(name.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE trust_context SET is_default = ? WHERE name <> ?") + .bind(false.to_sql()) + .bind(name.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn get_trust_context(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM trust_context WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|u| u.named_trust_context()).transpose() + } + + async fn get_trust_contexts(&self) -> Result> { + let query = query_as("SELECT * FROM trust_context"); + let rows: Vec = + query.fetch_all(&self.database.pool).await.into_core()?; + rows.iter().map(|u| u.named_trust_context()).collect() + } + + async fn delete_trust_context(&self, name: &str) -> Result<()> { + let query1 = query("DELETE FROM trust_context WHERE name=?").bind(name.to_sql()); + query1.execute(&self.database.pool).await.void() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NamedTrustContext { + name: String, + trust_context_id: String, + credential: Option, + authority_identity: Option, + authority_route: Option, +} + +impl NamedTrustContext { + pub fn new( + name: &str, + trust_context_id: &str, + credential: Option, + authority_identity: Option, + authority_route: Option, + ) -> Self { + Self { + name: name.to_string(), + trust_context_id: trust_context_id.to_string(), + credential, + authority_identity, + authority_route, + } + } +} + +impl Display for NamedTrustContext { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Name: {}", self.name())?; + Ok(()) + } +} + +impl NamedTrustContext { + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn trust_context_id(&self) -> String { + self.trust_context_id.to_string() + } + + pub fn credential(&self) -> Option { + self.credential.clone() + } + + pub async fn authority_identity(&self) -> Result> { + match &self.authority_identity { + Some(change_history) => Ok(Some( + Identity::create_from_change_history(change_history).await?, + )), + None => Ok(None), + } + } + + pub async fn authority_identifier(&self) -> Result> { + Ok(self + .authority_identity() + .await? + .map(|i| i.identifier().clone())) + } + + pub async fn trust_context( + &self, + tcp_transport: &TcpTransport, + secure_channels: Arc, + ) -> Result { + let authority_identifier = self.authority_identifier().await?; + let authority_service = match ( + self.credential.clone(), + authority_identifier, + self.authority_route.clone(), + ) { + (Some(credential), Some(identifier), _) => { + let credential_retriever = CredentialsMemoryRetriever::new(credential); + Some(AuthorityService::new( + secure_channels.identities().credentials(), + identifier, + Some(Arc::new(credential_retriever)), + )) + } + (None, Some(identifier), Some(route)) => { + let credential_retriever = RemoteCredentialsRetriever::new( + secure_channels.clone(), + RemoteCredentialsRetrieverInfo::new( + identifier.clone(), + multiaddr_to_route(&route, tcp_transport) + .await + .ok_or_else(|| { + Error::new( + Origin::Api, + Kind::Internal, + format!("cannot create a route from the address {route}"), + ) + })? + .route, + DefaultAddress::CREDENTIAL_ISSUER.into(), + ), + ); + Some(AuthorityService::new( + secure_channels.identities().credentials(), + identifier, + Some(Arc::new(credential_retriever)), + )) + } + (None, Some(identifier), None) => Some(AuthorityService::new( + secure_channels.identities().credentials(), + identifier.clone(), + None, + )), + _ => None, + }; + Ok(TrustContext::new( + self.trust_context_id.clone(), + authority_service, + )) + } + + pub async fn authority(&self) -> Result> { + match ( + self.authority_identifier().await?, + self.authority_route.clone(), + ) { + (Some(identifier), Some(route)) => Ok(Some(Authority::new(identifier, route))), + _ => Ok(None), + } + } +} + +#[derive(sqlx::FromRow)] +struct NamedTrustContextRow { + name: String, + trust_context_id: String, + #[allow(unused)] + is_default: bool, + credential: Option, + authority_change_history: Option, + authority_route: Option, +} + +impl NamedTrustContextRow { + fn named_trust_context(&self) -> Result { + let credential: Option = self + .credential + .as_ref() + .map(|c| CredentialAndPurposeKey::decode_from_string(c)) + .transpose()?; + let authority_change_history = self + .authority_change_history + .as_ref() + .map(|i| ChangeHistory::import_from_string(i)) + .transpose()?; + let authority_route = self + .authority_route + .as_ref() + .map(|r| MultiAddr::from_string(r)) + .transpose()?; + Ok(NamedTrustContext { + name: self.name.clone(), + trust_context_id: self.trust_context_id.clone(), + credential, + authority_identity: authority_change_history, + authority_route, + }) + } + + #[allow(unused)] + async fn trust_context( + &self, + tcp_transport: &TcpTransport, + secure_channels: Arc, + ) -> Result { + self.named_trust_context()? + .trust_context(tcp_transport, secure_channels) + .await + } +} + +#[derive(Clone)] +pub struct Authority { + identifier: Identifier, + route: MultiAddr, +} + +impl Authority { + pub fn new(identifier: Identifier, route: MultiAddr) -> Self { + Self { identifier, route } + } + + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } + + pub fn route(&self) -> MultiAddr { + self.route.clone() + } +} + +#[cfg(test)] +mod test { + use core::time::Duration; + use std::path::Path; + + use tempfile::NamedTempFile; + + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identities}; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + let identities = identities(); + let issuer_identifier = identities.identities_creation().create_identity().await?; + let issuer = identities.get_identity(&issuer_identifier).await?; + + let trust_context1 = + create_trust_context("trust-context-1", identities.clone(), &issuer).await?; + let trust_context2 = create_trust_context("trust-context-2", identities, &issuer).await?; + repository.store_trust_context(&trust_context1).await?; + repository.store_trust_context(&trust_context2).await?; + + let result = repository.get_trust_context("trust-context-1").await?; + assert_eq!(result, Some(trust_context1.clone())); + + let result = repository.get_trust_contexts().await?; + assert_eq!(result, vec![trust_context1.clone(), trust_context2.clone()]); + + repository + .set_default_trust_context("trust-context-1") + .await?; + let result = repository.get_default_trust_context().await?; + assert_eq!(result, Some(trust_context1)); + + repository + .set_default_trust_context("trust-context-2") + .await?; + let result = repository.get_default_trust_context().await?; + assert_eq!(result, Some(trust_context2.clone())); + + repository.delete_trust_context("trust-context-1").await?; + let result = repository.get_trust_contexts().await?; + assert_eq!(result, vec![trust_context2]); + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(TrustContextsSqlxDatabase::new(Arc::new(db)))) + } + + async fn create_trust_context( + name: &str, + identities: Arc, + issuer: &Identity, + ) -> Result { + Ok(NamedTrustContext { + name: name.into(), + trust_context_id: name.into(), + credential: Some(create_credential(identities, issuer.identifier()).await?), + authority_identity: Some(issuer.change_history().clone()), + authority_route: Some(MultiAddr::from_string("/dnsaddr/k8s-hubdev-hubconso-85b649e0fe-0c482fd0e8117e9d.elb.us-west-1.amazonaws.com/tcp/6252/service/api").unwrap()), + }) + } + + async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/user_info.rs b/implementations/rust/ockam/ockam_api/src/cli_state/user_info.rs deleted file mode 100644 index 01b923446ef..00000000000 --- a/implementations/rust/ockam/ockam_api/src/cli_state/user_info.rs +++ /dev/null @@ -1,74 +0,0 @@ -use super::Result; -use crate::cloud::enroll::auth0::UserInfo; -use std::fmt::{Display, Formatter}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct UsersInfoState { - dir: PathBuf, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct UserInfoState { - path: PathBuf, - config: UserInfoConfig, -} - -impl Display for UserInfoState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Email: {}", self.config.email)?; - Ok(()) - } -} - -type UserInfoConfig = UserInfo; - -mod traits { - use super::*; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for UsersInfoState { - type Item = UserInfoState; - const DEFAULT_FILENAME: &'static str = "user_info"; - const DIR_NAME: &'static str = "users_info"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } - - fn dir(&self) -> &PathBuf { - &self.dir - } - } - - #[async_trait] - impl StateItemTrait for UserInfoState { - type Config = UserInfoConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - Ok(Self { path, config }) - } - - fn load(path: PathBuf) -> Result { - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { path, config }) - } - - fn path(&self) -> &PathBuf { - &self.path - } - - fn config(&self) -> &Self::Config { - &self.config - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/users.rs b/implementations/rust/ockam/ockam_api/src/cli_state/users.rs new file mode 100644 index 00000000000..db373f1affc --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/users.rs @@ -0,0 +1,36 @@ +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; + +use crate::cli_state::CliState; +use crate::cli_state::Result; +use crate::cloud::enroll::auth0::UserInfo; + +impl CliState { + pub async fn store_user(&self, user: &UserInfo) -> Result<()> { + let repository = self.users_repository().await?; + let default_user_exists = repository.get_default_user().await?.is_none(); + repository.store_user(user).await?; + + // if this is the first user we store we mark it as the default user + if !default_user_exists { + self.set_default_user(&user.email).await? + } + Ok(()) + } + + pub async fn set_default_user(&self, email: &str) -> Result<()> { + self.users_repository() + .await? + .set_default_user(email) + .await?; + Ok(()) + } + + pub async fn get_default_user(&self) -> Result { + let repository = self.users_repository().await?; + match repository.get_default_user().await? { + Some(user) => Ok(user), + None => Err(Error::new(Origin::Api, Kind::NotFound, "there is no default user").into()), + } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs new file mode 100644 index 00000000000..4d22a914523 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs @@ -0,0 +1,13 @@ +use crate::cloud::enroll::auth0::UserInfo; +use ockam_core::async_trait; +use ockam_core::Result; + +#[async_trait] +pub trait UsersRepository: Send + Sync + 'static { + async fn store_user(&self, user: &UserInfo) -> Result<()>; + async fn get_default_user(&self) -> Result>; + async fn set_default_user(&self, email: &str) -> Result<()>; + async fn get_user(&self, email: &str) -> Result>; + async fn get_users(&self) -> Result>; + async fn delete_user(&self, email: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs new file mode 100644 index 00000000000..4bace33e989 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs @@ -0,0 +1,182 @@ +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use std::sync::Arc; + +use super::UsersRepository; +use crate::cloud::enroll::auth0::UserInfo; +use ockam_core::async_trait; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +#[derive(Clone)] +pub struct UsersSqlxDatabase { + database: Arc, +} + +impl UsersSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for users"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("users")))) + } +} + +#[async_trait] +impl UsersRepository for UsersSqlxDatabase { + async fn store_user(&self, user: &UserInfo) -> Result<()> { + let is_already_default = self + .get_default_user() + .await? + .map(|u| u.email == user.email) + .unwrap_or(false); + + let query = query("INSERT OR REPLACE INTO user VALUES ($1, $2, $3, $4, $5, $6, $7, $8)") + .bind(user.email.to_sql()) + .bind(user.sub.to_sql()) + .bind(user.nickname.to_sql()) + .bind(user.name.to_sql()) + .bind(user.picture.to_sql()) + .bind(user.updated_at.to_sql()) + .bind(user.email_verified.to_sql()) + .bind(is_already_default.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_default_user(&self) -> Result> { + let query = query("SELECT email FROM user WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let email: Option = row.map(|r| r.get(0)); + match email { + Some(email) => self.get_user(&email).await, + None => Ok(None), + } + } + + async fn set_default_user(&self, email: &str) -> Result<()> { + let query = query("UPDATE user SET is_default = ? WHERE email = ?") + .bind(true.to_sql()) + .bind(email.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_user(&self, email: &str) -> Result> { + let query = query_as("SELECT * FROM user WHERE email=$1").bind(email.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|u| u.user())) + } + + async fn get_users(&self) -> Result> { + let query = query_as("SELECT * FROM user"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + Ok(rows.iter().map(|u| u.user()).collect()) + } + + async fn delete_user(&self, email: &str) -> Result<()> { + let query1 = query("DELETE FROM user WHERE email=?").bind(email.to_sql()); + query1.execute(&self.database.pool).await.void() + } +} + +#[derive(sqlx::FromRow)] +struct UserRow { + email: String, + sub: String, + nickname: String, + name: String, + picture: String, + updated_at: String, + email_verified: bool, + #[allow(unused)] + is_default: bool, +} + +impl UserRow { + fn user(&self) -> UserInfo { + UserInfo { + email: self.email.clone(), + sub: self.sub.clone(), + nickname: self.nickname.clone(), + name: self.name.clone(), + picture: self.picture.clone(), + updated_at: self.updated_at.clone(), + email_verified: self.email_verified, + } + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + + // create and store 2 users + let user1 = UserInfo { + sub: "sub".into(), + nickname: "me".to_string(), + name: "me".to_string(), + picture: "me".to_string(), + updated_at: "today".to_string(), + email: "me@ockam.io".into(), + email_verified: false, + }; + let user2 = UserInfo { + sub: "sub".into(), + nickname: "you".to_string(), + name: "you".to_string(), + picture: "you".to_string(), + updated_at: "today".to_string(), + email: "you@ockam.io".into(), + email_verified: false, + }; + + repository.store_user(&user1).await?; + repository.store_user(&user2).await?; + + // retrieve them as a vector or by name + let result = repository.get_users().await?; + assert_eq!(result, vec![user1.clone(), user2.clone()]); + + let result = repository.get_user("me@ockam.io").await?; + assert_eq!(result, Some(user1.clone())); + + // a user can be set created as the default user + repository.set_default_user("me@ockam.io").await?; + let result = repository.get_default_user().await?; + assert_eq!(result, Some(user1.clone())); + + // a user can be deleted + repository.delete_user("you@ockam.io").await?; + let result = repository.get_user("you@ockam.io").await?; + assert_eq!(result, None); + + let result = repository.get_users().await?; + assert_eq!(result, vec![user1.clone()]); + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(UsersSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs index 3851c704296..20290577649 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs @@ -1,217 +1,148 @@ -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use serde::{Deserialize, Serialize}; - use ockam::identity::Vault; -use ockam_vault_aws::AwsSigningVault; +use ockam_core::errcode::{Kind, Origin}; -use crate::cli_state::traits::StateItemTrait; -use crate::cli_state::{CliStateError, StateDirTrait, DATA_DIR_NAME}; +use crate::cli_state::{random_name, CliState}; +use crate::identity::NamedVault; use super::Result; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct VaultsState { - dir: PathBuf, -} - -impl VaultsState { - pub async fn create_async(&self, name: &str, config: VaultConfig) -> Result { - if self.exists(name) { - return Err(CliStateError::AlreadyExists { - resource: Self::default_filename().to_string(), - name: name.to_string(), - }); - } - let state = VaultState::new(self.path(name), config)?; - state.get().await?; - if !self.default_path()?.exists() { - self.set_default(name)?; +impl CliState { + /// Create a vault with the given name if it was not created before + /// If no name is given use a random name as the default vault name + /// If the vault was not created before return Ok(vault) + /// Otherwise return Err(name of the vault) + pub async fn create_named_vault( + &self, + vault_name: &Option, + ) -> Result> { + match vault_name { + Some(vault_name) => { + if self.get_named_vault(vault_name).await.is_ok() { + Ok(Err(vault_name.clone())) + } else { + let vault = self.create_vault(vault_name).await?; + Ok(Ok(vault)) + } + } + None => match self.get_default_vault().await.ok() { + Some(vault) => Ok(Err(vault.name())), + None => Ok(Ok(self.create_vault(&random_name()).await?)), + }, } - Ok(state) } -} -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct VaultState { - name: String, - path: PathBuf, - /// The path to the vault's storage config file, contained in the data directory - data_path: PathBuf, - config: VaultConfig, -} - -impl VaultState { - pub async fn get(&self) -> Result { - if self.config.aws_kms { - let mut vault = Vault::create(); - let aws_vault = Arc::new(AwsSigningVault::create().await?); - vault.identity_vault = aws_vault.clone(); - vault.credential_vault = aws_vault; - - Ok(vault) - } else { - let vault = - Vault::create_with_persistent_storage_path(self.vault_file_path().as_path()) - .await?; - Ok(vault) - } + pub async fn create_vault(&self, vault_name: &str) -> Result { + self.create_a_vault(vault_name, false).await } - fn build_data_path(name: &str, path: &Path) -> PathBuf { - path.parent() - .expect("Should have parent") - .join(DATA_DIR_NAME) - .join(format!("{name}-storage.json")) + pub async fn create_kms_vault(&self, vault_name: &str) -> Result { + self.create_a_vault(vault_name, true).await } - pub fn vault_file_path(&self) -> &PathBuf { - &self.data_path - } + async fn create_a_vault(&self, vault_name: &str, is_kms: bool) -> Result { + let vaults_repository = self.vaults_repository().await?; + // use the database to store secrets + // if we are creating an AWS KMS vault we just store the key ids + let path = self.database_path(); + + // the first created vault is the default one + let is_default_vault = vaults_repository.get_default_vault_name().await?.is_none(); - pub async fn vault(&self) -> Result { - let path = self.vault_file_path().clone(); - let vault = Vault::create_with_persistent_storage_path(path.as_path()).await?; + let vault = vaults_repository + .store_vault(vault_name, path, is_kms) + .await?; + if is_default_vault { + vaults_repository.set_as_default(vault_name).await?; + } Ok(vault) } - pub fn name(&self) -> &str { - &self.name + pub async fn is_default_vault(&self, vault_name: &str) -> Result { + Ok(self + .vaults_repository() + .await? + .is_default(vault_name) + .await?) } - pub fn is_aws(&self) -> bool { - self.config.is_aws() + pub async fn set_default_vault(&self, vault_name: &str) -> Result<()> { + Ok(self + .vaults_repository() + .await? + .set_as_default(vault_name) + .await?) } -} -impl Display for VaultState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Name: {}", self.name)?; - writeln!( - f, - "Type: {}", - match self.config.is_aws() { - true => "AWS KMS", - false => "OCKAM", - } - )?; - Ok(()) + pub async fn get_vault_names(&self) -> Result> { + let named_vaults = self.vaults_repository().await?.get_named_vaults().await?; + Ok(named_vaults.iter().map(|v| v.name()).collect()) } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default)] -pub struct VaultConfig { - #[serde(default)] - aws_kms: bool, -} -impl VaultConfig { - pub fn new(aws_kms: bool) -> Result { - Ok(Self { aws_kms }) + pub async fn get_named_vaults(&self) -> Result> { + Ok(self.vaults_repository().await?.get_named_vaults().await?) } - pub fn is_aws(&self) -> bool { - self.aws_kms + /// Return either the default vault or a vault with the given name + pub async fn get_vault_or_default(&self, vault_name: &Option) -> Result { + let vault_name = self.get_vault_name_or_default(vault_name).await?; + Ok(self.get_named_vault(&vault_name).await?.vault().await?) } -} - -mod traits { - use ockam_core::async_trait; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - - use super::*; - - #[async_trait] - impl StateDirTrait for VaultsState { - type Item = VaultState; - const DEFAULT_FILENAME: &'static str = "vault"; - const DIR_NAME: &'static str = "vaults"; - const HAS_DATA_DIR: bool = true; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } - - fn dir(&self) -> &PathBuf { - &self.dir - } - - fn create( - &self, - _name: impl AsRef, - _config: <::Item as StateItemTrait>::Config, - ) -> Result { - unreachable!() - } + /// Return either the default vault or a vault with the given name + pub async fn get_named_vault_or_default( + &self, + vault_name: &Option, + ) -> Result { + let vault_name = self.get_vault_name_or_default(vault_name).await?; + self.get_named_vault(&vault_name).await + } - fn delete(&self, name: impl AsRef) -> Result<()> { - // If doesn't exist do nothing. - if !self.exists(&name) { - return Ok(()); - } - let vault = self.get(&name)?; - // If it's the default, remove link - if let Ok(default) = self.default() { - if default.path == vault.path { - let _ = std::fs::remove_file(self.default_path()?); - } - } - // Remove vault files - vault.delete()?; - Ok(()) - } + /// Return the vault with the given name + pub async fn get_vault_by_name(&self, vault_name: &str) -> Result { + Ok(self.get_named_vault(vault_name).await?.vault().await?) } - #[async_trait] - impl StateItemTrait for VaultState { - type Config = VaultConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::create_dir_all(path.parent().unwrap())?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - let data_path = VaultState::build_data_path(&name, &path); - Ok(Self { - name, - path, - data_path, - config, - }) + pub async fn get_vault_name_or_default(&self, vault_name: &Option) -> Result { + match vault_name { + Some(name) => Ok(name.clone()), + None => self.get_default_vault_name().await, } + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - let data_path = VaultState::build_data_path(&name, &path); - Ok(Self { - name, - path, - data_path, - config, - }) - } + pub async fn get_named_vault(&self, vault_name: &str) -> Result { + let result = self + .vaults_repository() + .await? + .get_vault_by_name(vault_name) + .await?; + result.ok_or_else(|| { + ockam_core::Error::new( + Origin::Api, + Kind::NotFound, + format!("no vault found with name {vault_name}"), + ) + .into() + }) + } - fn delete(&self) -> Result<()> { - std::fs::remove_file(&self.path)?; - std::fs::remove_file(&self.data_path)?; - std::fs::remove_file(self.data_path.with_extension("json.lock"))?; - Ok(()) + pub(crate) async fn get_default_vault(&self) -> Result { + let result = self.vaults_repository().await?.get_default_vault().await?; + match result { + Some(vault) => Ok(vault), + None => self.create_vault(&random_name()).await, } + } - fn path(&self) -> &PathBuf { - &self.path - } + pub async fn get_default_vault_name(&self) -> Result { + Ok(self.get_default_vault().await?.name()) + } - fn config(&self) -> &Self::Config { - &self.config - } + /// Return the vault with the given name + pub async fn delete_vault(&self, vault_name: &str) -> Result<()> { + Ok(self + .vaults_repository() + .await? + .delete_vault(vault_name) + .await?) } } diff --git a/implementations/rust/ockam/ockam_api/src/cloud/addon.rs b/implementations/rust/ockam/ockam_api/src/cloud/addon.rs index f8286530590..efd3cc46f8d 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/addon.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/addon.rs @@ -1,12 +1,14 @@ -use crate::cloud::operation::CreateOperationResponse; -use crate::cloud::project::{InfluxDBTokenLeaseManagerConfig, OktaConfig}; -use crate::cloud::Controller; use miette::IntoDiagnostic; use minicbor::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + use ockam_core::api::Request; use ockam_core::async_trait; use ockam_node::Context; -use serde::{Deserialize, Serialize}; + +use crate::cloud::operation::CreateOperationResponse; +use crate::cloud::project::{InfluxDBTokenLeaseManagerConfig, OktaConfig}; +use crate::cloud::Controller; const TARGET: &str = "ockam_api::cloud::addon"; const API_SERVICE: &str = "projects"; @@ -23,7 +25,7 @@ pub struct Addon { pub enabled: bool, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[rustfmt::skip] #[cbor(map)] pub struct ConfluentConfig { @@ -54,7 +56,7 @@ impl ConfluentConfigResponse { } #[cfg(test)] -impl quickcheck::Arbitrary for ConfluentConfigResponse { +impl quickcheck::Arbitrary for ConfluentConfig { fn arbitrary(g: &mut quickcheck::Gen) -> Self { Self { bootstrap_server: String::arbitrary(g), @@ -79,40 +81,40 @@ impl DisableAddon { #[async_trait] pub trait Addons { - async fn list_addons(&self, ctx: &Context, project_id: String) -> miette::Result>; + async fn list_addons(&self, ctx: &Context, project_id: &str) -> miette::Result>; async fn configure_confluent_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: ConfluentConfig, ) -> miette::Result; async fn configure_okta_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: OktaConfig, ) -> miette::Result; async fn configure_influxdb_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: InfluxDBTokenLeaseManagerConfig, ) -> miette::Result; async fn disable_addon( &self, ctx: &Context, - project_id: String, - addon_id: String, + project_id: &str, + addon_id: &str, ) -> miette::Result; } #[async_trait] impl Addons for Controller { - async fn list_addons(&self, ctx: &Context, project_id: String) -> miette::Result> { + async fn list_addons(&self, ctx: &Context, project_id: &str) -> miette::Result> { trace!(target: TARGET, project_id, "listing addons"); let req = Request::get(format!("/v0/{project_id}/addons")); self.secure_client @@ -126,7 +128,7 @@ impl Addons for Controller { async fn configure_confluent_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: ConfluentConfig, ) -> miette::Result { trace!(target: TARGET, project_id, "configuring confluent addon"); @@ -145,7 +147,7 @@ impl Addons for Controller { async fn configure_okta_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: OktaConfig, ) -> miette::Result { trace!(target: TARGET, project_id, "configuring okta addon"); @@ -162,7 +164,7 @@ impl Addons for Controller { async fn configure_influxdb_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: InfluxDBTokenLeaseManagerConfig, ) -> miette::Result { // @@ -182,8 +184,8 @@ impl Addons for Controller { async fn disable_addon( &self, ctx: &Context, - project_id: String, - addon_id: String, + project_id: &str, + addon_id: &str, ) -> miette::Result { trace!(target: TARGET, project_id, "disabling addon"); let req = Request::post(format!("/v1/projects/{project_id}/disable_addon")) diff --git a/implementations/rust/ockam/ockam_api/src/cloud/project.rs b/implementations/rust/ockam/ockam_api/src/cloud/project.rs index 8e966fa763f..25416de9bbd 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/project.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/project.rs @@ -6,17 +6,18 @@ use serde::{Deserialize, Serialize}; use tokio_retry::strategy::FixedInterval; use tokio_retry::Retry; -use ockam::identity::Identifier; +use ockam::identity::models::ChangeHistory; +use ockam::identity::{identities, Identifier, Identity}; use ockam_core::api::Request; -use ockam_core::{async_trait, Result}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{async_trait, Error, Result}; use ockam_multiaddr::MultiAddr; use ockam_node::{tokio, Context}; -use crate::cloud::addon::ConfluentConfigResponse; +use crate::cloud::addon::ConfluentConfig; use crate::cloud::operation::Operations; use crate::cloud::share::ShareScope; use crate::cloud::{Controller, ORCHESTRATOR_AWAIT_TIMEOUT}; -use crate::config::lookup::ProjectAuthority; use crate::error::ApiError; use crate::minicbor_url::Url; @@ -60,7 +61,7 @@ pub struct Project { #[cbor(n(12))] #[serde(skip_serializing_if = "Option::is_none")] - pub confluent_config: Option, + pub confluent_config: Option, #[cbor(n(13))] pub version: Option, @@ -86,10 +87,97 @@ pub struct ProjectUserRole { } impl Project { + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn id(&self) -> String { + self.id.clone() + } + + pub fn identifier(&self) -> Result { + match &self.identity.clone() { + Some(identifier) => Ok(identifier.clone()), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no identity has been created for the project {}", self.name), + )), + } + } + + pub fn project_name(&self) -> String { + self.name.clone() + } + pub fn access_route(&self) -> Result { MultiAddr::from_str(&self.access_route).map_err(|e| ApiError::core(e.to_string())) } + pub fn authority_access_route(&self) -> Result { + match &self.authority_access_route { + Some(authority_access_route) => MultiAddr::from_str(authority_access_route) + .map_err(|e| ApiError::core(e.to_string())), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!( + "no authority has been configured for the project {}", + self.name + ), + )), + } + } + + /// Return the decoded authority change history + /// This method does not verify the change history so it does not require to be async + pub fn authority_change_history(&self) -> Result { + match &self.authority_identity { + Some(authority_identity) => { + let decoded = hex::decode(authority_identity.as_bytes()) + .map_err(|e| Error::new(Origin::Api, Kind::NotFound, e.to_string()))?; + Ok(ChangeHistory::import(&decoded)?) + } + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!( + "no authority has been configured for the project {}", + self.name + ), + )), + } + } + + /// Return the identifier of the project's authority + pub async fn authority_identifier(&self) -> Result { + Ok(self.authority_identity().await?.identifier().clone()) + } + + /// Return the identity of the project's authority + pub async fn authority_identity(&self) -> Result { + match &self.authority_identity { + Some(authority_identity) => { + let decoded = hex::decode(authority_identity.as_bytes()) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + let identities = identities(); + let identifier = identities + .identities_creation() + .import(None, &decoded) + .await?; + Ok(identities.get_identity(&identifier).await?) + } + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!( + "no authority has been configured for the project {}", + self.name + ), + )), + } + } + pub fn has_admin_with_email(&self, email: &str) -> bool { self.user_roles .iter() @@ -117,18 +205,11 @@ impl Project { ma.to_socket_addr() .map_err(|e| ApiError::core(e.to_string())) } - - /// Return the project authority if there is one defined - pub async fn authority(&self) -> Result> { - ProjectAuthority::from_project(self) - .await - .map_err(|e| ApiError::core(e.to_string())) - } } #[derive(Decode, Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[cbor(map)] -pub struct ProjectVersion { +pub struct OrchestratorVersionInfo { /// The version of the Orchestrator Controller #[cbor(n(1))] pub version: Option, @@ -138,6 +219,16 @@ pub struct ProjectVersion { pub project_version: Option, } +impl OrchestratorVersionInfo { + pub fn version(&self) -> String { + self.version.clone().unwrap_or("N/A".to_string()) + } + + pub fn project_version(&self) -> String { + self.project_version.clone().unwrap_or("N/A".to_string()) + } +} + #[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] #[rustfmt::skip] #[cbor(map)] @@ -244,43 +335,58 @@ pub trait Projects { async fn create_project( &self, ctx: &Context, - space_id: String, - name: String, + space_id: &str, + name: &str, users: Vec, ) -> miette::Result; - async fn get_project(&self, ctx: &Context, project_id: String) -> miette::Result; + async fn get_project(&self, ctx: &Context, project_id: &str) -> miette::Result; + + async fn get_project_by_name( + &self, + ctx: &Context, + project_name: &str, + ) -> miette::Result; + + async fn get_project_by_name_or_default( + &self, + ctx: &Context, + project_name: &Option, + ) -> miette::Result; async fn delete_project( &self, ctx: &Context, - space_id: String, - project_id: String, + space_id: &str, + project_id: &str, ) -> miette::Result<()>; - async fn get_project_version(&self, ctx: &Context) -> miette::Result; - - async fn list_projects(&self, ctx: &Context) -> miette::Result>; + async fn delete_project_by_name( + &self, + ctx: &Context, + space_name: &str, + project_name: &str, + ) -> miette::Result<()>; - async fn wait_until_project_is_ready( + async fn get_orchestrator_version_info( &self, ctx: &Context, - project: Project, - ) -> miette::Result; + ) -> miette::Result; + + async fn get_projects(&self, ctx: &Context) -> miette::Result>; } -#[async_trait] -impl Projects for Controller { - async fn create_project( +impl Controller { + pub async fn create_project( &self, ctx: &Context, - space_id: String, - name: String, + space_id: &str, + name: &str, users: Vec, ) -> miette::Result { trace!(target: TARGET, %space_id, project_name = name, "creating project"); let req = Request::post(format!("/v1/spaces/{space_id}/projects")) - .body(CreateProject::new(name, users)); + .body(CreateProject::new(name.to_string(), users)); self.secure_client .ask(ctx, "projects", req) .await @@ -289,7 +395,7 @@ impl Projects for Controller { .into_diagnostic() } - async fn get_project(&self, ctx: &Context, project_id: String) -> miette::Result { + pub async fn get_project(&self, ctx: &Context, project_id: &str) -> miette::Result { trace!(target: TARGET, %project_id, "getting project"); let req = Request::get(format!("/v0/{project_id}")); self.secure_client @@ -300,11 +406,11 @@ impl Projects for Controller { .into_diagnostic() } - async fn delete_project( + pub async fn delete_project( &self, ctx: &Context, - space_id: String, - project_id: String, + space_id: &str, + project_id: &str, ) -> miette::Result<()> { trace!(target: TARGET, %space_id, %project_id, "deleting project"); let req = Request::delete(format!("/v0/{space_id}/{project_id}")); @@ -316,8 +422,11 @@ impl Projects for Controller { .into_diagnostic() } - async fn get_project_version(&self, ctx: &Context) -> miette::Result { - trace!(target: TARGET, "getting project version"); + pub async fn get_orchestrator_version_info( + &self, + ctx: &Context, + ) -> miette::Result { + trace!(target: TARGET, "getting orchestrator version information"); self.secure_client .ask(ctx, "version_info", Request::get("")) .await @@ -326,7 +435,7 @@ impl Projects for Controller { .into_diagnostic() } - async fn list_projects(&self, ctx: &Context) -> miette::Result> { + pub async fn list_projects(&self, ctx: &Context) -> miette::Result> { let req = Request::get("/v0"); self.secure_client .ask(ctx, "projects", req) @@ -336,7 +445,7 @@ impl Projects for Controller { .into_diagnostic() } - async fn wait_until_project_is_ready( + pub async fn wait_until_project_is_ready( &self, ctx: &Context, project: Project, @@ -363,7 +472,7 @@ impl Projects for Controller { .await?; if operation.is_successful() { - self.get_project(ctx, project.id).await + self.get_project(ctx, &project.id).await } else { Err(miette!("Operation failed. Please try again.")) } @@ -446,7 +555,7 @@ mod tests { authority_identity: bool::arbitrary(g) .then(|| hex::encode(>::arbitrary(g))), okta_config: bool::arbitrary(g).then(|| OktaConfig::arbitrary(g)), - confluent_config: bool::arbitrary(g).then(|| ConfluentConfigResponse::arbitrary(g)), + confluent_config: bool::arbitrary(g).then(|| ConfluentConfig::arbitrary(g)), version: Some(String::arbitrary(g)), running: bool::arbitrary(g).then(|| bool::arbitrary(g)), operation_id: bool::arbitrary(g).then(|| String::arbitrary(g)), diff --git a/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs b/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs index 9a7712825e8..648d5a034c2 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs @@ -35,7 +35,7 @@ impl NodeManager { NodeManager::controller_node( &self.tcp_transport, self.secure_channels.clone(), - &self.get_identifier(None).await?, + &self.get_identifier_by_name(None).await?, ) .await } diff --git a/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs b/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs index 76ab70eb51e..b4881af8e5d 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs @@ -3,10 +3,10 @@ use serde::{Deserialize, Serialize}; use ockam_core::Result; -use crate::cli_state::{CliState, StateDirTrait, StateItemTrait}; +use crate::cli_state::CliState; use crate::error::ApiError; use crate::identity::EnrollmentTicket; -use ockam::identity::{identities, Identifier}; +use ockam::identity::Identifier; use super::{RoleInShare, ShareScope}; @@ -50,23 +50,10 @@ impl CreateServiceInvitation { service_route: S, enrollment_ticket: EnrollmentTicket, ) -> Result { - let node_identifier = cli_state.nodes.get(node_name)?.config().identifier()?; - let project = cli_state.projects.get(&project_name)?.config().clone(); - let project_authority_route = project - .authority_access_route - .ok_or(ApiError::core("Project authority route is missing"))?; - let project_authority_identifier = { - let identity = project - .authority_identity - .ok_or(ApiError::core("Project authority identifier is missing"))?; - let as_hex = hex::decode(identity.as_str()).map_err(|_| { - ApiError::core("Project authority identifier is not a valid hex string") - })?; - identities() - .identities_creation() - .import(None, &as_hex) - .await? - }; + let node_identifier = cli_state.get_node_identifier(node_name.as_ref()).await?; + let project = cli_state.get_project_by_name(project_name.as_ref()).await?; + let project_authority_route = project.authority_access_route()?; + let project_authority_identifier = project.authority_identifier().await?; // see also: ockam_command::project::ticket let enrollment_ticket = hex::encode( serde_json::to_vec(&enrollment_ticket) @@ -75,14 +62,12 @@ impl CreateServiceInvitation { Ok(CreateServiceInvitation { enrollment_ticket, expires_at, - project_id: project.id.to_string(), + project_id: project.id(), recipient_email: recipient_email.as_ref().to_string(), - project_identity: project - .identity - .ok_or(ApiError::core("Project identity is missing"))?, - project_route: project.access_route, + project_identity: project.identifier()?, + project_route: project.access_route()?.to_string(), project_authority_identity: project_authority_identifier, - project_authority_route, + project_authority_route: project_authority_route.to_string(), shared_node_identity: node_identifier, shared_node_route: service_route.as_ref().to_string(), }) diff --git a/implementations/rust/ockam/ockam_api/src/cloud/space.rs b/implementations/rust/ockam/ockam_api/src/cloud/space.rs index 5c5a67b4a93..d46cf27056c 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/space.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/space.rs @@ -1,14 +1,17 @@ -use crate::cloud::Controller; use miette::IntoDiagnostic; use minicbor::{Decode, Encode}; +use serde::Serialize; + use ockam_core::api::Request; use ockam_core::async_trait; use ockam_node::Context; -use serde::Serialize; + +use crate::cloud::Controller; +use crate::nodes::InMemoryNode; const TARGET: &str = "ockam_api::cloud::space"; -#[derive(Encode, Decode, Serialize, Debug, Clone)] +#[derive(Encode, Decode, Serialize, Debug, Clone, PartialEq, Eq)] #[rustfmt::skip] #[cbor(map)] pub struct Space { @@ -17,6 +20,16 @@ pub struct Space { #[n(3)] pub users: Vec, } +impl Space { + pub fn space_id(&self) -> String { + self.id.clone() + } + + pub fn space_name(&self) -> String { + self.name.clone() + } +} + #[derive(Encode, Decode, Debug)] #[cfg_attr(test, derive(Clone))] #[rustfmt::skip] @@ -37,27 +50,115 @@ pub trait Spaces { async fn create_space( &self, ctx: &Context, - name: String, - users: Vec, + name: &str, + users: Vec<&str>, ) -> miette::Result; - async fn get_space(&self, ctx: &Context, space_id: String) -> miette::Result; + async fn get_space(&self, ctx: &Context, space_id: &str) -> miette::Result; + + async fn get_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result; - async fn delete_space(&self, ctx: &Context, space_id: String) -> miette::Result<()>; + async fn delete_space(&self, ctx: &Context, space_id: &str) -> miette::Result<()>; - async fn list_spaces(&self, ctx: &Context) -> miette::Result>; + async fn delete_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result<()>; + + async fn get_spaces(&self, ctx: &Context) -> miette::Result>; } #[async_trait] -impl Spaces for Controller { +impl Spaces for InMemoryNode { async fn create_space( &self, ctx: &Context, - name: String, - users: Vec, + name: &str, + users: Vec<&str>, + ) -> miette::Result { + let controller = self.create_controller().await?; + let space = controller.create_space(ctx, name, users).await?; + self.cli_state + .store_space( + &space.id, + &space.name, + space.users.iter().map(|u| u.as_ref()).collect(), + ) + .await?; + Ok(space) + } + + async fn get_space(&self, ctx: &Context, space_id: &str) -> miette::Result { + let controller = self.create_controller().await?; + let space = controller.get_space(ctx, space_id).await?; + self.cli_state + .store_space( + &space.id, + &space.name, + space.users.iter().map(|u| u.as_ref()).collect(), + ) + .await?; + Ok(space) + } + + async fn get_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result { + let space_id = self + .cli_state + .get_space_by_name(space_name) + .await? + .space_id(); + self.get_space(ctx, &space_id).await + } + + async fn delete_space(&self, ctx: &Context, space_id: &str) -> miette::Result<()> { + let controller = self.create_controller().await?; + controller.delete_space(ctx, space_id).await?; + self.cli_state.delete_space(space_id).await?; + Ok(()) + } + + async fn delete_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result<()> { + let space_id = self + .cli_state + .get_space_by_name(space_name) + .await? + .space_id(); + self.delete_space(ctx, &space_id).await + } + + async fn get_spaces(&self, ctx: &Context) -> miette::Result> { + let controller = self.create_controller().await?; + let spaces = controller.list_spaces(ctx).await?; + let default_space = self.cli_state.get_default_space().await.ok(); + for space in &spaces { + self.cli_state + .store_space( + &space.id, + &space.name, + space.users.iter().map(|u| u.as_ref()).collect(), + ) + .await?; + + // make sure that an existing space marked as default is still marked as default + if let Some(default_space) = &default_space { + if space.id == default_space.id { + self.cli_state.set_space_as_default(&space.id).await?; + }; + } + } + Ok(spaces) + } +} + +impl Controller { + pub async fn create_space( + &self, + ctx: &Context, + name: &str, + users: Vec<&str>, ) -> miette::Result { trace!(target: TARGET, space = %name, "creating space"); - let req = Request::post("/v0/").body(CreateSpace::new(name, users)); + let req = Request::post("/v0/").body(CreateSpace::new( + name.into(), + users.iter().map(|u| u.to_string()).collect(), + )); self.secure_client .ask(ctx, "spaces", req) .await @@ -66,7 +167,7 @@ impl Spaces for Controller { .into_diagnostic() } - async fn get_space(&self, ctx: &Context, space_id: String) -> miette::Result { + pub async fn get_space(&self, ctx: &Context, space_id: &str) -> miette::Result { trace!(target: TARGET, space = %space_id, "getting space"); let req = Request::get(format!("/v0/{space_id}")); self.secure_client @@ -77,7 +178,7 @@ impl Spaces for Controller { .into_diagnostic() } - async fn delete_space(&self, ctx: &Context, space_id: String) -> miette::Result<()> { + pub async fn delete_space(&self, ctx: &Context, space_id: &str) -> miette::Result<()> { trace!(target: TARGET, space = %space_id, "deleting space"); let req = Request::delete(format!("/v0/{space_id}")); self.secure_client @@ -88,7 +189,7 @@ impl Spaces for Controller { .into_diagnostic() } - async fn list_spaces(&self, ctx: &Context) -> miette::Result> { + pub async fn list_spaces(&self, ctx: &Context) -> miette::Result> { trace!(target: TARGET, "listing spaces"); self.secure_client .ask(ctx, "spaces", Request::get("/v0/")) diff --git a/implementations/rust/ockam/ockam_api/src/config/cli.rs b/implementations/rust/ockam/ockam_api/src/config/cli.rs index 42210817950..8b137891791 100644 --- a/implementations/rust/ockam/ockam_api/src/config/cli.rs +++ b/implementations/rust/ockam/ockam_api/src/config/cli.rs @@ -1,386 +1 @@ -//! Configuration files used by the ockam CLI -use crate::cli_state::{CliStateError, CredentialState, StateItemTrait}; -use crate::cloud::project::Project; -use crate::config::{lookup::ConfigLookup, ConfigValues}; -use crate::error::ApiError; -use crate::{cli_state, multiaddr_to_transport_route, DefaultAddress, HexByteVec}; -use ockam::identity::{ - identities, AuthorityService, CredentialsMemoryRetriever, CredentialsRetriever, Identifier, - Identities, RemoteCredentialsRetriever, RemoteCredentialsRetrieverInfo, SecureChannels, - TrustContext, -}; -use ockam_core::compat::sync::Arc; -use ockam_core::{Result, Route}; -use ockam_multiaddr::MultiAddr; -use ockam_transport_tcp::TcpTransport; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::str::FromStr; - -use super::lookup::ProjectLookup; - -/// The main ockam CLI configuration -/// -/// Used to determine CLI runtime behaviour and index existing nodes -/// on a system. -/// -/// ## Updates -/// -/// This configuration is read and updated by the user-facing `ockam` -/// CLI. Furthermore the data is only relevant for user-facing -/// `ockam` CLI instances. As such writes to this config don't have -/// to be synchronised to detached consumers. -/// -/// ## Legacy status -/// It's maintained for backwards compatibility with the legacy CLI -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LegacyCliConfig { - /// We keep track of the project directories at runtime but don't - /// persist this data to the configuration - #[serde(skip)] - pub dir: Option, - #[serde(default = "default_lookup")] - pub lookup: ConfigLookup, -} - -fn default_lookup() -> ConfigLookup { - ConfigLookup::default() -} - -impl ConfigValues for LegacyCliConfig { - fn default_values() -> Self { - Self { - dir: Some(Self::dir()), - lookup: default_lookup(), - } - } -} - -impl LegacyCliConfig { - /// Determine the default storage location for the ockam config - pub fn dir() -> PathBuf { - cli_state::CliState::default_dir().unwrap() - } - - /// This function could be zero-copy if we kept the lock on the - /// backing store for as long as we needed it. Because this may - /// have unwanted side-effects, instead we eagerly copy data here. - /// This may be optimised in the future! - pub fn lookup(&self) -> &ConfigLookup { - &self.lookup - } -} - -/// A configuration struct to serialize and deserialize a trust context -/// used within the ockam CLI and ockam node -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct TrustContextConfig { - id: String, - authority: Option, - path: Option, -} - -impl TrustContextConfig { - pub fn new(id: String, authority: Option) -> Self { - Self { - id, - authority, - path: None, - } - } - - pub fn id(&self) -> &str { - &self.id - } - - pub fn path(&self) -> Option<&PathBuf> { - self.path.as_ref() - } - - pub fn set_path(&mut self, path: PathBuf) { - self.path = Some(path); - } - - pub fn authority(&self) -> Result<&TrustAuthorityConfig> { - self.authority - .as_ref() - .ok_or_else(|| ApiError::core("Missing authority on trust context config")) - } - - pub async fn to_trust_context( - &self, - secure_channels: Arc, - tcp_transport: Option, - ) -> Result { - let authority = if let Some(authority_config) = self.authority.as_ref() { - let identifier = authority_config.identifier().await?; - let credential_retriever = - if let Some(retriever_type) = &authority_config.own_credential { - Some( - retriever_type - .to_credential_retriever(secure_channels.clone(), tcp_transport) - .await?, - ) - } else { - None - }; - - Some(AuthorityService::new( - secure_channels.identities().credentials(), - identifier, - credential_retriever, - )) - } else { - None - }; - - Ok(TrustContext::new(self.id.to_string(), authority)) - } - - pub fn from_authority_identity( - authority_identity: &str, - credential: Option, - ) -> Result { - let own_cred = credential.map(CredentialRetrieverConfig::FromPath); - let trust_context = TrustContextConfig::new( - authority_identity.to_string(), - Some(TrustAuthorityConfig::new( - authority_identity.to_string(), - own_cred, - )), - ); - - Ok(trust_context) - } -} - -impl TryFrom for TrustContextConfig { - type Error = CliStateError; - - fn try_from(state: CredentialState) -> std::result::Result { - let issuer = hex::encode(&state.config().encoded_issuer_change_history); - let identifier = state.config().issuer_identifier.clone().to_string(); - let retriever = CredentialRetrieverConfig::FromPath(state); - let authority = TrustAuthorityConfig::new(issuer, Some(retriever)); - Ok(TrustContextConfig::new(identifier, Some(authority))) - } -} - -impl TryFrom for TrustContextConfig { - type Error = CliStateError; - - fn try_from(project_info: Project) -> std::result::Result { - let authority = match ( - &project_info.authority_access_route, - &project_info.authority_identity, - ) { - (Some(route), Some(identity)) => { - let authority_route = MultiAddr::from_str(route) - .map_err(|_| ApiError::core("incorrect multi address"))?; - let retriever = CredentialRetrieverConfig::FromCredentialIssuer( - CredentialIssuerConfig::new(identity.to_string(), authority_route), - ); - let authority = TrustAuthorityConfig::new(identity.to_string(), Some(retriever)); - Some(authority) - } - _ => None, - }; - - Ok(TrustContextConfig::new(project_info.id, authority)) - } -} - -impl TryFrom for TrustContextConfig { - type Error = ApiError; - - fn try_from( - project_lookup: ProjectLookup, - ) -> std::result::Result { - let proj_auth = project_lookup - .authority - .as_ref() - .expect("Project lookup is missing authority"); - let public_identity = hex::encode(proj_auth.identity()); - let authority = { - let retriever = CredentialRetrieverConfig::FromCredentialIssuer( - CredentialIssuerConfig::new(public_identity.clone(), proj_auth.address().clone()), - ); - let authority = TrustAuthorityConfig::new(public_identity, Some(retriever)); - Some(authority) - }; - - Ok(TrustContextConfig::new( - project_lookup.id.clone(), - authority, - )) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TrustAuthorityConfig { - identity: String, - own_credential: Option, -} - -impl TrustAuthorityConfig { - pub fn new(identity: String, own_credential: Option) -> Self { - Self { - identity, - own_credential, - } - } - - pub fn identity_str(&self) -> &str { - &self.identity - } - - pub async fn identifier(&self) -> Result { - identities() - .identities_creation() - .import( - None, - &hex::decode(&self.identity) - .map_err(|_| ApiError::core("unable to decode authority identity"))?, - ) - .await - } - - pub fn own_credential(&self) -> Result<&CredentialRetrieverConfig> { - self.own_credential - .as_ref() - .ok_or_else(|| ApiError::core("Missing own credential on trust authority config")) - } -} - -/// Type of credential retriever -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub enum CredentialRetrieverConfig { - /// Credential is stored in memory - FromMemory(Vec), - /// Path to credential file - FromPath(CredentialState), - /// MultiAddr to Credential Issuer - FromCredentialIssuer(CredentialIssuerConfig), -} - -impl CredentialRetrieverConfig { - async fn to_credential_retriever( - &self, - secure_channels: Arc, - tcp_transport: Option, - ) -> Result> { - match self { - CredentialRetrieverConfig::FromMemory(credential) => Ok(Arc::new( - CredentialsMemoryRetriever::new(minicbor::decode(credential)?), - )), - CredentialRetrieverConfig::FromPath(state) => Ok(Arc::new( - CredentialsMemoryRetriever::new(state.config().credential()?), - )), - CredentialRetrieverConfig::FromCredentialIssuer(issuer_config) => { - let _ = tcp_transport.ok_or_else(|| ApiError::core("TCP Transport was not provided when credential retriever was defined as an issuer."))?; - let credential_issuer_info = RemoteCredentialsRetrieverInfo::new( - issuer_config.resolve_identity().await?, - issuer_config.resolve_route().await?, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ); - - Ok(Arc::new(RemoteCredentialsRetriever::new( - secure_channels, - credential_issuer_info, - ))) - } - } - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct AuthoritiesConfig { - authorities: BTreeMap, -} - -impl AuthoritiesConfig { - pub fn add_authority(&mut self, i: Identifier, a: Authority) { - self.authorities.insert(i, a); - } - - pub fn authorities(&self) -> impl Iterator { - self.authorities.iter() - } - - pub async fn to_identities(&self, identities: Arc) -> Result> { - let mut v = Vec::new(); - for a in self.authorities.values() { - v.push( - identities - .identities_creation() - .import(None, a.identity.as_slice()) - .await?, - ) - } - Ok(v) - } -} - -impl ConfigValues for AuthoritiesConfig { - fn default_values() -> Self { - Self::default() - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct Authority { - identity: HexByteVec, - access: MultiAddr, -} - -impl Authority { - pub fn new(identity: Vec, addr: MultiAddr) -> Self { - Self { - identity: identity.into(), - access: addr, - } - } - - pub fn identity(&self) -> &[u8] { - self.identity.as_slice() - } - - pub fn access_route(&self) -> &MultiAddr { - &self.access - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CredentialIssuerConfig { - pub identity: String, - pub multiaddr: MultiAddr, -} - -impl CredentialIssuerConfig { - pub fn new(encoded_identity: String, multiaddr: MultiAddr) -> CredentialIssuerConfig { - CredentialIssuerConfig { - identity: encoded_identity, - multiaddr, - } - } - - async fn resolve_route(&self) -> Result { - let Some(route) = multiaddr_to_transport_route(&self.multiaddr) else { - let err_msg = format!("Invalid route within trust context: {}", &self.multiaddr); - error!("{err_msg}"); - return Err(ApiError::core(&err_msg)); - }; - Ok(route) - } - - async fn resolve_identity(&self) -> Result { - let encoded = - hex::decode(&self.identity).map_err(|_| ApiError::core("Invalid project authority"))?; - identities() - .identities_creation() - .import(None, &encoded) - .await - } -} diff --git a/implementations/rust/ockam/ockam_api/src/config/lookup.rs b/implementations/rust/ockam/ockam_api/src/config/lookup.rs index 67ac8e4e38f..96312831c1c 100644 --- a/implementations/rust/ockam/ockam_api/src/config/lookup.rs +++ b/implementations/rust/ockam/ockam_api/src/config/lookup.rs @@ -1,14 +1,8 @@ -use crate::cli_state::{ProjectState, StateItemTrait}; -use crate::cloud::project::{OktaAuth0, Project}; -use crate::error::ApiError; -use bytes::Bytes; -use miette::WrapErr; -use ockam::identity::{identities, Identifier}; use ockam_core::compat::collections::VecDeque; +use ockam_multiaddr::proto::{DnsAddr, Ip4, Ip6, Tcp}; use ockam_multiaddr::MultiAddr; use serde::{Deserialize, Serialize}; use std::{ - collections::BTreeMap, fmt, net::{SocketAddr, SocketAddrV4, SocketAddrV6}, str::FromStr, @@ -22,110 +16,7 @@ pub struct LookupMeta { pub type Name = String; -/// A generic lookup mechanism for configuration values -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ConfigLookup { - #[serde(flatten)] - pub map: BTreeMap, -} - -impl Default for ConfigLookup { - fn default() -> Self { - Self::new() - } -} - -impl ConfigLookup { - pub fn new() -> Self { - Self { - map: Default::default(), - } - } - - pub fn spaces(&self) -> impl Iterator + '_ { - self.map.iter().filter_map(|(k, v)| { - if let LookupValue::Space(p) = v { - let name = k.strip_prefix("/space/").unwrap_or(k).to_string(); - Some((name, p.clone())) - } else { - None - } - }) - } - - pub fn set_space(&mut self, id: &str, name: &str) { - self.map.insert( - format!("/space/{name}"), - LookupValue::Space(SpaceLookup { id: id.to_string() }), - ); - } - - pub fn get_space(&self, name: &str) -> Option<&SpaceLookup> { - self.map - .get(&format!("/space/{name}")) - .and_then(|value| match value { - LookupValue::Space(space) => Some(space), - _ => None, - }) - } - - pub fn remove_space(&mut self, name: &str) -> Option { - self.map.remove(&format!("/space/{name}")) - } - - pub fn remove_spaces(&mut self) { - self.map.retain(|k, _| !k.starts_with("/space/")); - } - - /// Store a project route and identifier as lookup - pub fn set_project(&mut self, name: String, proj: ProjectLookup) { - self.map - .insert(format!("/project/{name}"), LookupValue::Project(proj)); - } - - pub fn get_project(&self, name: &str) -> Option<&ProjectLookup> { - self.map - .get(&format!("/project/{name}")) - .and_then(|value| match value { - LookupValue::Project(project) => Some(project), - _ => None, - }) - } - - pub fn remove_project(&mut self, name: &str) -> Option { - self.map.remove(&format!("/project/{name}")) - } - - pub fn remove_projects(&mut self) { - self.map.retain(|k, _| !k.starts_with("/project/")); - } - - pub fn has_unresolved_projects(&self, meta: &LookupMeta) -> bool { - meta.project - .iter() - .any(|name| self.get_project(name).is_none()) - } - - pub fn projects(&self) -> impl Iterator + '_ { - self.map.iter().filter_map(|(k, v)| { - if let LookupValue::Project(p) = v { - let name = k.strip_prefix("/project/").unwrap_or(k).to_string(); - Some((name, p.clone())) - } else { - None - } - }) - } -} - -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum LookupValue { - Address(InternetAddress), - Space(SpaceLookup), - Project(ProjectLookup), -} - +/// A generic lookup /// An internet address abstraction (v6/v4/dns) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum InternetAddress { @@ -137,6 +28,19 @@ pub enum InternetAddress { V6(SocketAddrV6), } +impl InternetAddress { + pub fn multi_addr(&self) -> ockam_core::Result { + let mut m = MultiAddr::default(); + match self { + InternetAddress::Dns(dns, _) => m.push_back(DnsAddr::new(dns))?, + InternetAddress::V4(v4) => m.push_back(Ip4(*v4.ip()))?, + InternetAddress::V6(v6) => m.push_back(Ip6(*v6.ip()))?, + } + m.push_back(Tcp(self.port()))?; + Ok(m) + } +} + impl Default for InternetAddress { fn default() -> Self { InternetAddress::Dns("localhost".to_string(), 6252) @@ -201,115 +105,3 @@ impl From for InternetAddress { } } } - -/// Represents a remote Ockam space lookup -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SpaceLookup { - /// Identifier of this space - pub id: String, -} - -/// Represents a remote Ockam project lookup -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ProjectLookup { - /// How to reach the node hosting this project - pub node_route: Option, - /// Identifier of this project - pub id: String, - /// Name of this project within - pub name: String, - /// Identifier of the IDENTITY of the project (for secure-channel) - pub identity_id: Option, - /// Project authority information. - pub authority: Option, - /// OktaAuth0 information. - pub okta: Option, -} - -impl ProjectLookup { - pub async fn from_project(project: &Project) -> ockam_core::Result { - let node_route: MultiAddr = project.access_route.as_str().try_into()?; - let pid = project - .identity - .as_ref() - .ok_or_else(|| ApiError::message("Project should have identity set"))?; - let authority = project.authority().await?; - let okta = project.okta_config.as_ref().map(|o| OktaAuth0 { - tenant_base_url: o.tenant_base_url.clone(), - client_id: o.client_id.to_string(), - certificate: o.certificate.to_string(), - }); - - Ok(ProjectLookup { - node_route: Some(node_route), - id: project.id.to_string(), - name: project.name.to_string(), - identity_id: Some(pid.clone()), - authority, - okta, - }) - } - - pub async fn from_state(projects: Vec) -> miette::Result> { - let mut lookups = BTreeMap::new(); - for p in projects { - let l = ProjectLookup::from_project(p.config()) - .await - .context("Failed to read project configuration")?; - lookups.insert(l.name.clone(), l); - } - Ok(lookups) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct ProjectAuthority { - id: Identifier, - address: MultiAddr, - identity: Bytes, -} - -impl ProjectAuthority { - pub fn new(id: Identifier, addr: MultiAddr, identity: Vec) -> Self { - Self { - id, - address: addr, - identity: identity.into(), - } - } - - pub async fn from_project(project: &Project) -> ockam_core::Result, ApiError> { - Self::from_raw(&project.authority_access_route, &project.authority_identity).await - } - - pub async fn from_raw( - route: &Option, - identity: &Option, - ) -> ockam_core::Result, ApiError> { - if let Some(r) = route { - let rte = MultiAddr::try_from(r.to_string().as_str())?; - let a = identity - .as_ref() - .ok_or_else(|| ApiError::message("Identity is not set"))? - .to_string(); - let a = hex::decode(a.as_str()) - .map_err(|_| ApiError::message("Invalid project authority"))?; - let p = identities().identities_creation().import(None, &a).await?; - Ok(Some(ProjectAuthority::new(p.clone(), rte, a))) - } else { - Ok(None) - } - } - - pub fn identity(&self) -> &[u8] { - &self.identity - } - - pub fn identity_id(&self) -> &Identifier { - &self.id - } - - pub fn address(&self) -> &MultiAddr { - &self.address - } -} diff --git a/implementations/rust/ockam/ockam_api/src/config/mod.rs b/implementations/rust/ockam/ockam_api/src/config/mod.rs index 77bd1ba1ba4..6e35d432c9d 100644 --- a/implementations/rust/ockam/ockam_api/src/config/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/config/mod.rs @@ -12,7 +12,6 @@ use serde::Serialize; use crate::config::atomic::AtomicUpdater; pub mod atomic; -pub mod cli; pub mod lookup; pub trait ConfigValues: Serialize + DeserializeOwned { diff --git a/implementations/rust/ockam/ockam_api/src/identity.rs b/implementations/rust/ockam/ockam_api/src/identity.rs index b1ac194916e..e81e8597128 100644 --- a/implementations/rust/ockam/ockam_api/src/identity.rs +++ b/implementations/rust/ockam/ockam_api/src/identity.rs @@ -1,3 +1,15 @@ -mod enrollment_ticket; - +pub use credentials_repository::*; +pub use credentials_repository_sql::*; pub use enrollment_ticket::*; +pub use identities_repository::*; +pub use identities_repository_sql::*; +pub use vaults_repository::*; +pub use vaults_repository_sql::*; + +mod credentials_repository; +mod credentials_repository_sql; +mod enrollment_ticket; +mod identities_repository; +mod identities_repository_sql; +mod vaults_repository; +mod vaults_repository_sql; diff --git a/implementations/rust/ockam/ockam_api/src/identity/credentials_repository.rs b/implementations/rust/ockam/ockam_api/src/identity/credentials_repository.rs new file mode 100644 index 00000000000..4106963daf4 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/identity/credentials_repository.rs @@ -0,0 +1,19 @@ +use crate::cli_state::NamedCredential; +use ockam::identity::models::CredentialAndPurposeKey; +use ockam::identity::Identity; +use ockam_core::async_trait; +use ockam_core::Result; + +#[async_trait] +pub trait CredentialsRepository: Send + Sync + 'static { + async fn store_credential( + &self, + name: &str, + issuer: &Identity, + credential: CredentialAndPurposeKey, + ) -> Result<()>; + + async fn get_credential(&self, name: &str) -> Result>; + + async fn get_credentials(&self) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_api/src/identity/credentials_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/identity/credentials_repository_sql.rs new file mode 100644 index 00000000000..577d62759c4 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/identity/credentials_repository_sql.rs @@ -0,0 +1,159 @@ +use std::sync::Arc; + +use sqlx::*; + +use ockam::identity::models::{ChangeHistory, CredentialAndPurposeKey}; +use ockam::identity::{Identifier, Identity}; +use ockam_core::async_trait; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::cli_state::NamedCredential; +use crate::identity::CredentialsRepository; + +#[derive(Clone)] +pub struct CredentialsSqlxDatabase { + database: Arc, +} + +impl CredentialsSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for credentials"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("credentials")))) + } +} + +#[async_trait] +impl CredentialsRepository for CredentialsSqlxDatabase { + async fn store_credential( + &self, + name: &str, + issuer: &Identity, + credential: CredentialAndPurposeKey, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO credential VALUES (?, ?, ?, ?)") + .bind(name.to_sql()) + .bind(issuer.identifier().to_sql()) + .bind(issuer.change_history().to_sql()) + .bind(CredentialAndPurposeKeySql(credential).to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_credential(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM credential WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_credential()).transpose() + } + + async fn get_credentials(&self) -> Result> { + let query = query_as("SELECT * FROM credential"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.iter().map(|r| r.named_credential()).collect() + } +} + +pub struct CredentialAndPurposeKeySql(pub CredentialAndPurposeKey); + +impl ToSqlxType for CredentialAndPurposeKeySql { + fn to_sql(&self) -> SqlxType { + self.0.encode_as_string().unwrap().to_sql() + } +} + +#[derive(sqlx::FromRow)] +struct CredentialRow { + name: String, + issuer_identifier: String, + issuer_change_history: String, + credential: String, +} + +impl CredentialRow { + pub(crate) fn named_credential(&self) -> Result { + Ok(NamedCredential::make( + &self.name, + self.issuer_identifier()?, + self.change_history()?, + self.credential()?, + )) + } + + pub(crate) fn issuer_identifier(&self) -> Result { + self.issuer_identifier.clone().try_into() + } + + pub(crate) fn change_history(&self) -> Result { + ChangeHistory::import_from_string(&self.issuer_change_history) + } + + pub(crate) fn credential(&self) -> Result { + CredentialAndPurposeKey::decode_from_string(&self.credential) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + use std::time::Duration; + + use tempfile::NamedTempFile; + + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identities}; + + use super::*; + + #[tokio::test] + async fn test_credentials_repository() -> Result<()> { + let db_file = NamedTempFile::new().unwrap(); + let repository = create_repository(db_file.path()).await?; + + let identities = identities(); + let issuer_identity = identities.identities_creation().create_identity().await?; + let issuer = identities.get_identity(&issuer_identity).await?; + let credential = create_credential(identities, &issuer_identity).await?; + repository + .store_credential("name", &issuer, credential.clone()) + .await?; + + let result = repository.get_credential("name").await?; + assert_eq!( + result, + Some(NamedCredential::new("name", &issuer, credential)) + ); + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(CredentialsSqlxDatabase::new(Arc::new(db)))) + } + + async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await + } +} diff --git a/implementations/rust/ockam/ockam_api/src/identity/enrollment_ticket.rs b/implementations/rust/ockam/ockam_api/src/identity/enrollment_ticket.rs index 6867170fc10..f5db33562cb 100644 --- a/implementations/rust/ockam/ockam_api/src/identity/enrollment_ticket.rs +++ b/implementations/rust/ockam/ockam_api/src/identity/enrollment_ticket.rs @@ -1,27 +1,21 @@ +use crate::cloud::project::Project; use ockam::identity::OneTimeCode; use ockam_core::Result; use serde::{Deserialize, Serialize}; -use crate::config::{cli::TrustContextConfig, lookup::ProjectLookup}; use crate::error::ApiError; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct EnrollmentTicket { pub one_time_code: OneTimeCode, - pub project: Option, - pub trust_context: Option, + pub project: Option, } impl EnrollmentTicket { - pub fn new( - one_time_code: OneTimeCode, - project: Option, - trust_context: Option, - ) -> Self { + pub fn new(one_time_code: OneTimeCode, project: Option) -> Self { Self { one_time_code, project, - trust_context, } } diff --git a/implementations/rust/ockam/ockam_api/src/identity/identities_repository.rs b/implementations/rust/ockam/ockam_api/src/identity/identities_repository.rs new file mode 100644 index 00000000000..057cdc008f8 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/identity/identities_repository.rs @@ -0,0 +1,120 @@ +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::Result; + +/// The identities repository stores metadata about identities +/// which change history have been stored in the ChangeHistoryRepository. +/// +/// It allows to: +/// +/// - associate a user name to an identity +/// - set one (and one only) identity as the default identity +/// - associate a vault name to an identity so that we know where the identity private keys can be found +/// +#[async_trait] +pub trait IdentitiesRepository: Send + Sync + 'static { + /// Associate a name to an identity + async fn store_named_identity( + &self, + identifier: &Identifier, + name: &str, + vault_name: &str, + ) -> Result; + + /// Delete an identity given its name and return its identifier + async fn delete_identity_by_name(&self, name: &str) -> Result>; + + /// Delete an identity given its identifier and return its name + async fn delete_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result>; + + /// Return the identifier associated to a named identity + async fn get_identifier_by_name(&self, name: &str) -> Result>; + + /// Return the name associated to an identifier + async fn get_identity_name_by_identifier( + &self, + identifier: &Identifier, + ) -> Result>; + + /// Return the named identity associated to an identifier + async fn get_named_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result>; + + /// Return identities which have been given a name + async fn get_named_identities(&self) -> Result>; + + /// Return the named identity with a specific name + async fn get_named_identity(&self, name: &str) -> Result>; + + /// Set an identity as the default one, given its identifier + async fn set_as_default(&self, identifier: &Identifier) -> Result<()>; + + /// Set an identity as the default one, given its name + async fn set_as_default_by_name(&self, name: &str) -> Result<()>; + + /// Return the identifier of the default identity if there is one + async fn get_default_identifier(&self) -> Result>; + + /// Return the default named identity + async fn get_default_named_identity(&self) -> Result>; + + /// Return the name of the default identity if there is one + async fn get_default_identity_name(&self) -> Result>; + + /// Return true if there is an identity with this name and it is the default one + async fn is_default_identity_by_name(&self, name: &str) -> Result; + + /// Return the vault name used to create an identity + async fn get_identifier_vault_name(&self, identifier: &Identifier) -> Result>; +} + +/// A named identity associates a name with a persisted identity. +/// This is a convenience for users since they can refer to an identity by the name "alice" +/// instead of the identifier "I1234561234561234561234561234561234561234" +/// +/// Additionally one identity can be marked as being the default identity and taken to +/// establish a secure channel or create credentials without having to specify it. +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct NamedIdentity { + identifier: Identifier, + name: String, + vault_name: String, + is_default: bool, +} + +impl NamedIdentity { + /// Create a new named identity + pub fn new(identifier: Identifier, name: String, vault_name: String, is_default: bool) -> Self { + Self { + identifier, + name, + vault_name, + is_default, + } + } + + /// Return the identity identifier + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } + + /// Return the identity name + pub fn name(&self) -> String { + self.name.clone() + } + + /// Return the vault name + pub fn vault_name(&self) -> String { + self.vault_name.clone() + } + + /// Return true if this identity is the default one + pub fn is_default(&self) -> bool { + self.is_default + } +} diff --git a/implementations/rust/ockam/ockam_api/src/identity/identities_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/identity/identities_repository_sql.rs new file mode 100644 index 00000000000..e9514604420 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/identity/identities_repository_sql.rs @@ -0,0 +1,344 @@ +use core::str::FromStr; + +use sqlx::*; + +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::identity::identities_repository::{IdentitiesRepository, NamedIdentity}; + +/// Implementation of `IdentitiesRepository` trait based on an underlying database +/// using sqlx as its API, and Sqlite as its driver +#[derive(Clone)] +pub struct IdentitiesSqlxDatabase { + database: Arc, +} + +impl IdentitiesSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for identities"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("identities")))) + } +} + +#[async_trait] +impl IdentitiesRepository for IdentitiesSqlxDatabase { + async fn store_named_identity( + &self, + identifier: &Identifier, + name: &str, + vault_name: &str, + ) -> Result { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_identity_name() + .await? + .map(|n| n == *name) + .unwrap_or(false); + + let query = query("INSERT OR REPLACE INTO named_identity VALUES (?, ?, ?, ?)") + .bind(identifier.to_sql()) + .bind(name.to_sql()) + .bind(vault_name.to_sql()) + .bind(is_already_default.to_sql()); + query.execute(&self.database.pool).await.void()?; + + transaction.commit().await.void()?; + + Ok(NamedIdentity::new( + identifier.clone(), + name.to_string(), + vault_name.to_string(), + is_already_default, + )) + } + + async fn delete_identity_by_name(&self, name: &str) -> Result> { + let identifier = self.get_identifier_by_name(name).await?; + let is_default = self.is_default_identity_by_name(name).await?; + let query = query("DELETE FROM named_identity WHERE name=?").bind(name.to_sql()); + query.execute(&self.database.pool).await.void()?; + + // if the deleted identity was the default one, select another identity to be the default one + if is_default { + let identities = self.get_named_identities().await?; + if let Some(identity) = identities.first() { + self.set_as_default(&identity.identifier()).await?; + }; + } + Ok(identifier) + } + + async fn delete_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result> { + if let Some(name) = self.get_identity_name_by_identifier(identifier).await? { + self.delete_identity_by_name(&name).await?; + Ok(Some(name)) + } else { + Ok(None) + } + } + + async fn get_identifier_by_name(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM named_identity WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.identifier()).transpose() + } + + async fn get_identity_name_by_identifier( + &self, + identifier: &Identifier, + ) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE identifier=$1").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.name())) + } + + async fn get_named_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE identifier=$1").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_identity()).transpose() + } + + async fn get_named_identities(&self) -> Result> { + let query = query_as("SELECT * FROM named_identity"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.iter().map(|r| r.named_identity()).collect() + } + + async fn get_named_identity(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM named_identity WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_identity()).transpose() + } + + async fn set_as_default(&self, identifier: &Identifier) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the identifier as the default one + let query1 = query("UPDATE named_identity SET is_default = ? WHERE identifier = ?") + .bind(true.to_sql()) + .bind(identifier.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE named_identity SET is_default = ? WHERE identifier <> ?") + .bind(false.to_sql()) + .bind(identifier.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn set_as_default_by_name(&self, name: &str) -> Result<()> { + if let Some(identifier) = self.get_identifier_by_name(name).await? { + self.set_as_default(&identifier).await? + }; + Ok(()) + } + + async fn get_default_identifier(&self) -> Result> { + let query = query_as("SELECT * FROM named_identity WHERE is_default=?").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.identifier()).transpose() + } + + async fn get_default_named_identity(&self) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_identity()).transpose() + } + + async fn get_default_identity_name(&self) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.name)) + } + + async fn is_default_identity_by_name(&self, name: &str) -> Result { + let query = query_as("SELECT * FROM named_identity WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.is_default).unwrap_or(false)) + } + + async fn get_identifier_vault_name(&self, identifier: &Identifier) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE identifier=$1").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.vault_name())) + } +} + +#[derive(sqlx::FromRow)] +pub(crate) struct NamedIdentityRow { + identifier: String, + name: String, + vault_name: String, + is_default: bool, +} + +impl NamedIdentityRow { + pub(crate) fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + pub(crate) fn name(&self) -> String { + self.name.clone() + } + + pub(crate) fn vault_name(&self) -> String { + self.vault_name.clone() + } + + pub(crate) fn named_identity(&self) -> Result { + Ok(NamedIdentity::new( + self.identifier()?, + self.name.clone(), + self.vault_name.clone(), + self.is_default, + )) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_identities_repository_named_identities() -> Result<()> { + let identifier1 = + Identifier::from_str("Ie92f183eb4c324804ef4d62962dea94cf095a265").unwrap(); + let identifier2 = + Identifier::from_str("I124ed0b2e5a2be82e267ead6b3279f683616b66d").unwrap(); + let db_file = NamedTempFile::new().unwrap(); + let repository = create_repository(db_file.path()).await?; + + // A name can be associated to an identity + repository + .store_named_identity(&identifier1, "name1", "vault") + .await?; + repository + .store_named_identity(&identifier2, "name2", "vault") + .await?; + + let result = repository.get_identifier_by_name("name1").await?; + assert_eq!(result, Some(identifier1.clone())); + + let result = repository + .get_identity_name_by_identifier(&identifier1) + .await?; + assert_eq!(result, Some("name1".into())); + + let result = repository.get_named_identity("name2").await?; + assert_eq!(result.map(|n| n.identifier()), Some(identifier2.clone())); + + let result = repository.get_named_identities().await?; + assert_eq!( + result.iter().map(|n| n.identifier()).collect::>(), + vec![identifier1.clone(), identifier2.clone()] + ); + + repository.delete_identity_by_name("name1").await?; + let result = repository.get_named_identities().await?; + assert_eq!( + result.iter().map(|n| n.identifier()).collect::>(), + vec![identifier2.clone()] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_identities_repository_default_identities() -> Result<()> { + let identifier1 = + Identifier::from_str("Ie92f183eb4c324804ef4d62962dea94cf095a265").unwrap(); + let identifier2 = + Identifier::from_str("I124ed0b2e5a2be82e267ead6b3279f683616b66d").unwrap(); + let db_file = NamedTempFile::new().unwrap(); + let repository = create_repository(db_file.path()).await?; + + // A name can be associated to an identity + repository + .store_named_identity(&identifier1, "name1", "vault") + .await?; + repository + .store_named_identity(&identifier2, "name2", "vault") + .await?; + + // An identity can be marked as being the default one + repository.set_as_default(&identifier1).await?; + let result = repository.get_default_identifier().await?; + assert_eq!(result, Some(identifier1.clone())); + + // An identity can be marked as being the default one by passing its name + repository.set_as_default_by_name("name2").await?; + let result = repository.get_default_identifier().await?; + assert_eq!(result, Some(identifier2.clone())); + + let result = repository.get_default_named_identity().await?; + assert_eq!(result.map(|n| n.identifier()), Some(identifier2.clone())); + + let result = repository.get_default_identity_name().await?; + assert_eq!(result, Some("name2".into())); + + let result = repository.is_default_identity_by_name("name1").await?; + assert!(!result); + + let result = repository.is_default_identity_by_name("name2").await?; + assert!(result); + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(IdentitiesSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/identity/vaults_repository.rs b/implementations/rust/ockam/ockam_api/src/identity/vaults_repository.rs new file mode 100644 index 00000000000..313c0580864 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/identity/vaults_repository.rs @@ -0,0 +1,81 @@ +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; +use std::sync::Arc; + +use ockam::identity::Vault; +use ockam_core::async_trait; +use ockam_core::Result; +use ockam_vault_aws::AwsSigningVault; + +#[async_trait] +pub trait VaultsRepository: Send + Sync + 'static { + async fn store_vault(&self, name: &str, path: PathBuf, is_aws_kms: bool) -> Result; + async fn delete_vault(&self, name: &str) -> Result<()>; + async fn set_as_default(&self, name: &str) -> Result<()>; + async fn is_default(&self, name: &str) -> Result; + async fn get_named_vaults(&self) -> Result>; + async fn get_vault_by_name(&self, name: &str) -> Result>; + async fn get_default_vault(&self) -> Result>; + async fn get_default_vault_name(&self) -> Result>; +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct NamedVault { + name: String, + path: PathBuf, + is_default: bool, + is_kms: bool, +} + +impl NamedVault { + pub fn new(name: String, path: PathBuf, is_default: bool, is_kms: bool) -> Self { + Self { + name, + path, + is_default, + is_kms, + } + } + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn path(&self) -> PathBuf { + self.path.clone() + } + + pub fn is_default(&self) -> bool { + self.is_default + } + + pub fn is_kms(&self) -> bool { + self.is_kms + } + + pub async fn vault(&self) -> Result { + if self.is_kms { + let mut vault = Vault::create(); + let aws_vault = Arc::new(AwsSigningVault::create().await?); + vault.identity_vault = aws_vault.clone(); + vault.credential_vault = aws_vault; + Ok(vault) + } else { + Ok(Vault::create_with_persistent_storage_path(self.path.as_path()).await?) + } + } +} + +impl Display for NamedVault { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Name: {}", self.name)?; + writeln!( + f, + "Type: {}", + match self.is_kms { + true => "AWS KMS", + false => "OCKAM", + } + )?; + Ok(()) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/identity/vaults_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/identity/vaults_repository_sql.rs new file mode 100644 index 00000000000..d80d8c30210 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/identity/vaults_repository_sql.rs @@ -0,0 +1,191 @@ +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_core::async_trait; +use ockam_core::Result; + +use crate::identity::{NamedVault, VaultsRepository}; + +pub struct VaultsSqlxDatabase { + database: Arc, +} + +impl VaultsSqlxDatabase { + pub fn new(database: Arc) -> Self { + debug!("create a repository for vaults"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("vaults")))) + } +} + +#[async_trait] +impl VaultsRepository for VaultsSqlxDatabase { + async fn store_vault(&self, name: &str, path: PathBuf, is_kms: bool) -> Result { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_vault_name() + .await? + .map(|n| n == *name) + .unwrap_or(false); + let query = query("INSERT OR REPLACE INTO vault VALUES (?1, ?2, ?3, ?4)") + .bind(name.to_sql()) + .bind(path.to_sql()) + .bind(is_kms.to_sql()) + .bind(is_already_default.to_sql()); + query.execute(&self.database.pool).await.void()?; + transaction.commit().await.void()?; + + Ok(NamedVault::new( + name.to_string(), + path.clone(), + is_already_default, + is_kms, + )) + } + + /// Delete a vault by name + async fn delete_vault(&self, name: &str) -> Result<()> { + let is_default = self.is_default(name).await?; + let query = query("DELETE FROM vault WHERE name = $1").bind(name.to_sql()); + query.execute(&self.database.pool).await.void()?; + + // if the deleted vault was the default one, select another vault to be the default one + if is_default { + let vaults = self.get_named_vaults().await?; + if let Some(vault) = vaults.first() { + self.set_as_default(&vault.name()).await?; + }; + } + Ok(()) + } + + async fn set_as_default(&self, name: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the identifier as the default one + let query1 = query("UPDATE vault SET is_default = ? WHERE name = ?") + .bind(true.to_sql()) + .bind(name.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE vault SET is_default = ? WHERE name <> ?") + .bind(false.to_sql()) + .bind(name.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn is_default(&self, name: &str) -> Result { + let query = query_as("SELECT * FROM vault WHERE name = $1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.is_default()).unwrap_or(false)) + } + + async fn get_named_vaults(&self) -> Result> { + let query = query_as("SELECT * FROM vault"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + rows.iter().map(|r| r.named_vault()).collect() + } + + async fn get_vault_by_name(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM vault WHERE name = $1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_vault()).transpose() + } + + async fn get_default_vault(&self) -> Result> { + let query = query_as("SELECT * FROM vault WHERE is_default = $1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_vault()).transpose() + } + + async fn get_default_vault_name(&self) -> Result> { + let query = query("SELECT name FROM vault WHERE is_default = $1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.get(0))) + } +} + +#[derive(FromRow)] +pub(crate) struct VaultRow { + name: String, + path: String, + is_default: bool, + is_kms: bool, +} + +impl VaultRow { + pub(crate) fn named_vault(&self) -> Result { + Ok(NamedVault::new( + self.name.clone(), + PathBuf::from_str(self.path.as_str()).unwrap(), + self.is_default, + self.is_kms, + )) + } + + pub(crate) fn is_default(&self) -> bool { + self.is_default + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + + repository + .store_vault("vault-name", "path".into(), false) + .await?; + let result = repository.get_vault_by_name("vault-name").await?; + + let expected = NamedVault::new("vault-name".into(), "path".into(), false, false); + assert_eq!(result, Some(expected)); + + // a default vault can be set + repository.set_as_default("vault-name").await?; + let result = repository.get_default_vault().await?; + let expected = NamedVault::new("vault-name".into(), "path".into(), true, false); + assert_eq!(result, Some(expected)); + + let result = repository.get_default_vault_name().await?; + assert_eq!(result, Some("vault-name".into())); + + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(VaultsSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs b/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs index 833ad0a5a58..ca35bea7184 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs @@ -48,7 +48,9 @@ impl OutletManagerService { let worker = OutletManagerService { outlet_controller: KafkaOutletController::new(), incoming_access_control: Arc::new(AbacAccessControl::create( - secure_channels.identities().repository(), + secure_channels + .identities() + .identity_attributes_repository(), TRUST_CONTEXT_ID_UTF8, trust_context_id, )), diff --git a/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs b/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs index f675db9fa3f..bb4c1bc1c03 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs @@ -202,7 +202,9 @@ impl KafkaSecureChannelControllerImpl { trust_context_id: String, ) -> KafkaSecureChannelControllerImpl { let access_control = AbacAccessControl::create( - secure_channels.identities().repository(), + secure_channels + .identities() + .identity_attributes_repository(), TRUST_CONTEXT_ID_UTF8, &trust_context_id, ); @@ -412,14 +414,17 @@ impl KafkaSecureChannelControllerImpl { Err(Error::new( Origin::Transport, Kind::Invalid, - "unauthorized secure channel for consumer", + format!( + "unauthorized secure channel for consumer with identifier {}", + entry.their_id() + ), )) } } else { Err(Error::new( Origin::Transport, Kind::Unknown, - "cannot find secure channel entry", + format!("cannot find secure channel entry {producer_encryptor_address}"), )) } } diff --git a/implementations/rust/ockam/ockam_api/src/lib.rs b/implementations/rust/ockam/ockam_api/src/lib.rs index 4a94a61d6d3..47f44f942b9 100644 --- a/implementations/rust/ockam/ockam_api/src/lib.rs +++ b/implementations/rust/ockam/ockam_api/src/lib.rs @@ -3,126 +3,19 @@ //! //! # Configuration //! -//! A `NodeManager` maintains its configuration as a list of directories and files stored under +//! A `NodeManager` maintains its database and log files on disk in //! the `OCKAM_HOME` directory (`~/.ockam`) by default: //! ```shell //! root -//! ├─ credentials -//! │ ├─ c1.json -//! │ ├─ c2.json -//! │ └─ ... -//! ├─ defaults -//! │ ├── credential -> ... -//! │ ├── identity -> ... -//! │ ├── node -> ... -//! │ └── vault -> ... -//! ├─ identities -//! │ ├─ data -//! │ │ ├─ authenticated-storage.lmdb -//! │ │ └─ authenticated-storage.lmdb-lock -//! │ ├─ identity1.json -//! │ ├─ identity2.json -//! │ └─ ... +//! ├─ database.sqlite //! ├─ nodes //! │ ├─ node1 -//! │ │ ├─ default_identity -> ... -//! │ │ ├─ default_vault -> ... -//! │ │ ├─ policies-storage.lmdb -//! │ │ ├─ policies-storage.lmdb-lock -//! │ │ ├─ setup.json //! │ │ ├─ stderr.log //! │ │ ├─ stdout.log -//! │ │ └─ version.log //! │ ├─ node2 //! │ └─ ... -//! ├─ projects -//! │ └─ default.json -//! ├─ trust_contexts -//! │ └─ default.json -//! └─ vaults -//! ├─ vault1.json -//! ├─ vault2.json -//! ├─ ... -//! └─ data -//! ├─ vault1.lmdb -//! ├─ vault1.lmdb-lock -//! ├─ vault2.lmdb -//! ├─ vault2.lmdb-lock -//! └─ ... //! ``` -//! # `credentials` -//! -//! Each file stored under the `credentials` directory contains the credential for a given identity. -//! Those files are created with the `ockam credential store` command. They are then read during the creation of -//! a secure channel to send the credentials to the other party -//! -//! # `defaults` -//! -//! This directory contains symlinks to other files or directories in order to specify which node, -//! identity, credential or vault must be considered as a default when running a command expecting those -//! inputs -//! -//! # `identities` -//! -//! This directory contains one file per identity and a data directory. An identity file is created -//! with the `ockam identity create` command or created by default for some commands (in that case the -//! `defaults/identity` symlink points to that identity). The identity file contains: -//! -//! - the identity identifier -//! - the enrollment status for that identity -//! -//! The `data` directory contains a LMDB database with other information about identities: -//! - the credential attributes that have been verified for this identity. Those attributes are -//! generally used in ABAC rules that are specified on secure channels. For example when sending messages -//! via a secure channel and using the Orchestrator the `project` attribute will be checked and the LMDB database accessed -//! -//! - the list of key changes for each identity. These key changes are created (or updated) when an identity -//! is created either by using the command line or by using the identity service. -//! The key changes are accessed in order to get the latest public key associated to a given identity -//! when checking its signature during the creation of a secure channel. -//! They are also accessed to retrieve the key id associated to that key and then use a Vault to create a signature -//! for an identity -//! -//! Note: for each `.lmdb` file there is a corresponding `lmdb-lock` file which is used to control -//! the exclusive access to the LMDB database even if several OS processes are trying to modify it. -//! For example when several nodes are started using the same `NodeManager`. -//! -//! # `nodes` -//! -//! This directory contains: -//! -//! - symlinks to default values for the node: identity and vault -//! - a database for ABAC policies -//! - a setup file containing some configuration information for the node (is it an authority node?, what is the TCP listener address?,...). -//! That file is created when a node is created and read again if the node is restarted -//! - log files: for system errors and system outputs. The stdout.log file is where almost all the node logs are written -//! - a version number for the configuration -//! -//! # `projects` -//! -//! This directory contains a list of files, one per project that was created, either the default project -//! or via the `ockam project create` command. A project file contains: -//! -//! - the project identifier and the space it belongs to -//! - the authority used by that project (identity, route) -//! - the configuration for the project plugins -//! -//! # `trust_context` -//! -//! This directory contains a list of files, one per trust context. A trust context can created with -//! the `ockam trust_context create` command. It can then be referred to during the creation of a -//! secure channel as a way to specify which authority can attest to the validity of which attributes -//! -//! # `vaults` -//! -//! This directory contains one file per vault that is either created by default or with the `ockam vault create` -//! command. That file contains the configuration for the vault, which for now consists only in -//! declaring if the vault is backed by an AWS KMS or not. -//! -//! The rest of the vault data is stored in an LMDB database under the `data` directory with one `.lmdb` -//! file per vault. A vault contains secrets which are generally used during the creation of secure -//! channels to sign or encrypt data involved in the handshake. -//! + pub mod address; pub mod auth; pub mod authenticator; @@ -140,7 +33,6 @@ pub mod minicbor_url; pub mod nodes; pub mod okta; pub mod port_range; -pub mod trust_context; pub mod uppercase; pub mod authority_node; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/config.rs b/implementations/rust/ockam/ockam_api/src/nodes/config.rs deleted file mode 100644 index e8849194082..00000000000 --- a/implementations/rust/ockam/ockam_api/src/nodes/config.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::fmt::Formatter; -use std::io::Write; -use std::{fmt::Display, fs::File, io::Read, path::Path, str::FromStr}; - -use anyhow::anyhow; - -use crate::config::build_config_path; - -#[derive(Debug, Clone, PartialEq, Eq)] -enum NodeConfigVersion { - V0, - V1, -} - -#[allow(unused)] -impl NodeConfigVersion { - const FILE_NAME: &'static str = "version"; - - fn latest() -> Self { - Self::V1 - } - - fn load(config_dir: &Path) -> anyhow::Result { - let version_path = config_dir.join(Self::FILE_NAME); - let version = if version_path.exists() { - let mut version_file = File::open(version_path)?; - let mut version = String::new(); - version_file.read_to_string(&mut version)?; - NodeConfigVersion::from_str(&version)? - } else { - Self::V0 - }; - debug!(%version, "Loaded config"); - version.upgrade(config_dir) - } - - fn upgrade(&self, config_dir: &Path) -> anyhow::Result { - let from = self; - let mut final_version = from.clone(); - - // Iter through all the versions between `from` and `to` - let f = from.to_string().parse::()?; - let mut t = f + 1; - while let Ok(ref to) = Self::from_str(&t.to_string()) { - debug!(%from, %to, "Upgrading config"); - final_version = to.clone(); - #[allow(clippy::single_match)] - match (from, to) { - (Self::V0, Self::V1) => { - if let (Some(old_config_name), Some(new_config_name)) = - (from.state_config_name(), to.state_config_name()) - { - let old_config_path = build_config_path(config_dir, old_config_name); - // If old config path exists, copy to new config path and keep the old one - if old_config_path.exists() { - let new_config_path = build_config_path(config_dir, new_config_name); - std::fs::copy(old_config_path, new_config_path)?; - } - // Create the version file if doesn't exists - Self::set_version(config_dir, to)?; - } - } - _ => {} - } - t += 1; - } - Ok(final_version) - } - - fn dirs(&self) -> &'static [&'static str] { - match self { - Self::V0 => &["config"], - Self::V1 => &["state", "commands"], - } - } - - fn state_config_name(&self) -> Option<&'static str> { - match self { - Self::V0 => Some("config"), - Self::V1 => Some("state"), - } - } - - fn commands_config_name(&self) -> Option<&'static str> { - match self { - Self::V0 => None, - Self::V1 => Some("commands"), - } - } - - fn set_version(config_dir: &Path, version: &NodeConfigVersion) -> anyhow::Result<()> { - let version_path = config_dir.join(Self::FILE_NAME); - let mut version_file = File::create(version_path)?; - version_file.write_all(version.to_string().as_bytes())?; - Ok(()) - } -} - -impl Display for NodeConfigVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - NodeConfigVersion::V0 => "0", - NodeConfigVersion::V1 => "1", - }) - } -} - -impl FromStr for NodeConfigVersion { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "0" => Ok(Self::V0), - "1" => Ok(Self::V1), - _ => Err(anyhow!("Unknown version: {}", s)), - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/mod.rs index 5f925bf341c..836a635ba3d 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/mod.rs @@ -1,8 +1,10 @@ -pub mod config; pub(crate) mod connection; pub mod models; +mod nodes_repository_sql; pub mod registry; pub mod service; + +pub use nodes_repository_sql::*; pub use service::background_node::*; pub use service::in_memory_node::*; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs index 53c623078d5..2049d17a2da 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs @@ -37,3 +37,9 @@ impl CreateTransportJson { Ok(m) } } + +impl ToString for CreateTransportJson { + fn to_string(&self) -> String { + format!("{}/{}/{}", self.tt, self.tm, self.addr) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs new file mode 100644 index 00000000000..c0e8c1442c3 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs @@ -0,0 +1,368 @@ +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use ockam::identity::Identifier; +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_core::async_trait; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Result; +use ockam_multiaddr::MultiAddr; +use sysinfo::{Pid, ProcessExt, ProcessStatus, System, SystemExt}; + +use crate::config::lookup::InternetAddress; + +#[async_trait] +pub trait NodesRepository: Send + Sync + 'static { + async fn store_node(&self, node_info: &NodeInfo) -> Result<()>; + async fn get_nodes(&self) -> Result>; + async fn get_node(&self, node_name: &str) -> Result>; + async fn get_node_by_identifier(&self, identifier: &Identifier) -> Result>; + async fn get_default_node(&self) -> Result>; + async fn set_default_node(&self, node_name: &str) -> Result<()>; + async fn is_default_node(&self, node_name: &str) -> Result; + async fn delete_node(&self, node_name: &str) -> Result<()>; + async fn delete_default_node(&self) -> Result<()>; + async fn set_tcp_listener_address(&self, node_name: &str, address: &str) -> Result<()>; + async fn get_tcp_listener_address(&self, node_name: &str) -> Result>; + async fn set_node_pid(&self, node_name: &str, pid: u32) -> Result<()>; + async fn set_no_node_pid(&self, node_name: &str) -> Result<()>; + async fn set_node_project_name(&self, node_name: &str, project_name: &str) -> Result<()>; + async fn get_node_project_name(&self, node_name: &str) -> Result>; +} + +pub struct NodesSqlxDatabase { + database: Arc, +} + +impl NodesSqlxDatabase { + pub fn new(database: Arc) -> Self { + debug!("create a repository for nodes"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("nodes")))) + } +} + +#[async_trait] +impl NodesRepository for NodesSqlxDatabase { + async fn store_node(&self, node_info: &NodeInfo) -> Result<()> { + let query = query("INSERT OR REPLACE INTO node VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)") + .bind(node_info.name.to_sql()) + .bind(node_info.identifier.to_sql()) + .bind(node_info.verbosity.to_sql()) + .bind(node_info.is_default.to_sql()) + .bind(node_info.is_authority.to_sql()) + .bind( + node_info + .tcp_listener_address + .as_ref() + .map(|a| a.to_string().to_sql()), + ) + .bind(node_info.pid.map(|p| p.to_sql())); + Ok(query.execute(&self.database.pool).await.void()?) + } + + async fn get_nodes(&self) -> Result> { + let query = query_as("SELECT * FROM node"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + rows.iter().map(|r| r.node_info()).collect() + } + + async fn get_node(&self, node_name: &str) -> Result> { + let query = query_as("SELECT * FROM node WHERE name = ?").bind(node_name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.node_info()).transpose() + } + + async fn get_node_by_identifier(&self, identifier: &Identifier) -> Result> { + let query = query_as("SELECT * FROM node WHERE identifier = ?").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.node_info()).transpose() + } + + async fn get_default_node(&self) -> Result> { + let query = query_as("SELECT * FROM node WHERE is_default = ?").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.node_info()).transpose() + } + + async fn is_default_node(&self, node_name: &str) -> Result { + let query = query("SELECT is_default FROM node WHERE name = ?").bind(node_name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.get(0)).unwrap_or(false)) + } + + async fn set_default_node(&self, node_name: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the node as the default one + let query1 = query("UPDATE node SET is_default = ? WHERE name = ?") + .bind(true.to_sql()) + .bind(node_name.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE node SET is_default = ? WHERE name <> ?") + .bind(false.to_sql()) + .bind(node_name.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn delete_node(&self, node_name: &str) -> Result<()> { + let query = query("DELETE FROM node WHERE name=?").bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_default_node(&self) -> Result<()> { + let query = query("DELETE FROM node WHERE is_default=?").bind(true.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn set_tcp_listener_address(&self, node_name: &str, address: &str) -> Result<()> { + let query = query("UPDATE node SET tcp_listener_address = ? WHERE name = ?") + .bind(address.to_sql()) + .bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_tcp_listener_address(&self, node_name: &str) -> Result> { + Ok(self + .get_node(node_name) + .await? + .and_then(|n| n.tcp_listener_address())) + } + + async fn set_node_pid(&self, node_name: &str, pid: u32) -> Result<()> { + let query = query("UPDATE node SET pid = ? WHERE name = ?") + .bind(pid.to_sql()) + .bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn set_no_node_pid(&self, node_name: &str) -> Result<()> { + let query = query("UPDATE node SET pid = NULL WHERE name = ?").bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn set_node_project_name(&self, node_name: &str, project_name: &str) -> Result<()> { + let query = query("INSERT OR REPLACE INTO node_project VALUES (?1, ?2)") + .bind(node_name.to_sql()) + .bind(project_name.to_sql()); + Ok(query.execute(&self.database.pool).await.void()?) + } + + async fn get_node_project_name(&self, node_name: &str) -> Result> { + let query = query("SELECT project_name FROM node_project WHERE node_name = ?") + .bind(node_name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let project_name: Option = row.map(|r| r.get(0)); + Ok(project_name) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct NodeInfo { + name: String, + identifier: Identifier, + verbosity: u8, + is_default: bool, + is_authority: bool, + tcp_listener_address: Option, + pid: Option, +} + +impl NodeInfo { + pub fn new( + name: String, + identifier: Identifier, + verbosity: u8, + is_default: bool, + is_authority: bool, + tcp_listener_address: Option, + pid: Option, + ) -> Self { + Self { + name, + identifier, + verbosity, + is_default, + is_authority, + tcp_listener_address, + pid, + } + } + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } + + pub fn verbosity(&self) -> u8 { + self.verbosity + } + + pub fn is_default(&self) -> bool { + self.is_default + } + + pub fn is_authority_node(&self) -> bool { + self.is_authority + } + + pub fn tcp_listener_port(&self) -> Option { + self.tcp_listener_address.as_ref().map(|t| t.port()) + } + + pub fn tcp_listener_address(&self) -> Option { + self.tcp_listener_address.clone() + } + + pub fn tcp_listener_multi_address(&self) -> Result { + self.tcp_listener_address + .as_ref() + .ok_or(ockam::Error::new( + Origin::Api, + Kind::Internal, + "no transport has been set on the node".to_string(), + )) + .and_then(|t| t.multi_addr()) + } + + pub fn pid(&self) -> Option { + self.pid + } + + pub fn is_running(&self) -> bool { + if let Some(pid) = self.pid() { + let mut sys = System::new(); + sys.refresh_processes(); + if let Some(p) = sys.process(Pid::from(pid as usize)) { + // Under certain circumstances the process can be in a state where it's not running + // and we are unable to kill it. For example, `kill -9` a process created by + // `node create` in a Docker environment will result in a zombie process. + !matches!(p.status(), ProcessStatus::Dead | ProcessStatus::Zombie) + } else { + false + } + } else { + false + } + } +} + +#[derive(FromRow)] +pub(crate) struct NodeRow { + name: String, + identifier: String, + verbosity: u8, + is_default: bool, + is_authority: bool, + tcp_listener_address: Option, + pid: Option, +} + +impl NodeRow { + pub(crate) fn node_info(&self) -> Result { + let tcp_listener_address = match self.tcp_listener_address.clone() { + None => None, + Some(a) => Some(InternetAddress::new(a.as_str()).ok_or_else(|| { + ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + format!("cannot deserialize the tcp listener address {}", a), + ) + })?), + }; + + Ok(NodeInfo::new( + self.name.clone(), + Identifier::from_str(&self.identifier.clone())?, + self.verbosity, + self.is_default, + self.is_authority, + tcp_listener_address, + self.pid, + )) + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + let identifier = Identifier::from_str("I521f58f591f0dcc69c5a849fd7a93823474aef31").unwrap(); + + let node_info = NodeInfo::new( + "node_name".to_string(), + identifier.clone(), + 0, + false, + false, + InternetAddress::new("127.0.0.1:51591"), + Some(1234), + ); + + repository.store_node(&node_info).await?; + let result = repository.get_nodes().await?; + assert_eq!(result, vec![node_info.clone()]); + + // get the node by name + let result = repository.get_node("node_name").await?; + assert_eq!(result, Some(node_info.clone())); + + // get the node by identifier + let result = repository.get_node_by_identifier(&identifier).await?; + assert_eq!(result, Some(node_info)); + Ok(()) + } + + #[tokio::test] + async fn test_node_project() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + repository + .set_node_project_name("node_name", "project1") + .await?; + let result = repository.get_node_project_name("node_name").await?; + assert_eq!(result, Some("project1".into())); + + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(NodesSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service.rs b/implementations/rust/ockam/ockam_api/src/nodes/service.rs index cfdc2587aa0..e87e46a8e66 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service.rs @@ -1,41 +1,37 @@ //! Node Manager (Node Man, the superhero that we deserve) -use miette::IntoDiagnostic; use std::collections::BTreeMap; use std::error::Error as _; use std::net::SocketAddr; use std::path::PathBuf; use std::time::Duration; +use miette::IntoDiagnostic; use minicbor::{Decoder, Encode}; -pub use node_identities::*; use ockam::identity::models::CredentialAndPurposeKey; -use ockam::identity::CredentialsServerModule; use ockam::identity::TrustContext; use ockam::identity::Vault; -use ockam::identity::{ - Credentials, CredentialsServer, Identities, IdentitiesRepository, IdentityAttributesReader, -}; +use ockam::identity::{ChangeHistoryRepository, Credentials, CredentialsServer, Identities}; +use ockam::identity::{CredentialsServerModule, IdentityAttributesRepository}; use ockam::identity::{Identifier, SecureChannels}; use ockam::{ Address, Context, RelayService, RelayServiceOptions, Result, Routed, TcpTransport, Worker, }; use ockam_abac::expr::{eq, ident, str}; -use ockam_abac::{Action, Env, Expr, PolicyAccessControl, PolicyStorage, Resource}; +use ockam_abac::{Action, Env, Expr, PoliciesRepository, PolicyAccessControl, Resource}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::{string::String, sync::Arc}; use ockam_core::flow_control::FlowControlId; +use ockam_core::AllowAll; use ockam_core::IncomingAccessControl; -use ockam_core::{AllowAll, AsyncTryClone}; use ockam_multiaddr::MultiAddr; -use crate::bootstrapped_identities_store::BootstrapedIdentityStore; +use crate::bootstrapped_identities_store::BootstrapedIdentityAttributesStore; use crate::bootstrapped_identities_store::PreTrustedIdentities; -use crate::cli_state::{CliState, StateDirTrait, StateItemTrait}; +use crate::cli_state::trust_contexts_repository_sql::NamedTrustContext; +use crate::cli_state::CliState; use crate::cloud::{AuthorityNode, ProjectNode}; -use crate::config::cli::TrustContextConfig; -use crate::config::lookup::ProjectLookup; use crate::error::ApiError; use crate::nodes::connection::{ Connection, ConnectionBuilder, PlainTcpInstantiator, ProjectInstantiator, @@ -57,10 +53,10 @@ pub(crate) mod credentials; mod flow_controls; pub(crate) mod in_memory_node; pub mod message; -mod node_identities; mod node_services; mod policy; pub mod portals; +mod projects; pub mod relay; mod secure_channel; mod transport; @@ -94,20 +90,34 @@ pub(crate) fn encode_response>( pub struct NodeManager { pub(crate) cli_state: CliState, node_name: String, + node_identifier: Identifier, api_transport_flow_control_id: FlowControlId, pub(crate) tcp_transport: TcpTransport, - enable_credential_checks: bool, - identifier: Identifier, pub(crate) secure_channels: Arc, trust_context: Option, pub(crate) registry: Registry, - policies: Arc, + policies_repository: Arc, pub(crate) medic_handle: MedicHandle, } impl NodeManager { - pub(super) fn identifier(&self) -> &Identifier { - &self.identifier + pub fn identifier(&self) -> Identifier { + self.node_identifier.clone() + } + + pub(crate) async fn get_identifier_by_name( + &self, + identity_name: Option, + ) -> Result { + if let Some(name) = identity_name { + Ok(self.cli_state.get_identifier_by_name(name.as_ref()).await?) + } else { + Ok(self.identifier()) + } + } + + pub fn trust_context_id(&self) -> Option { + self.trust_context.clone().map(|tc| tc.id().to_string()) } pub fn node_name(&self) -> String { @@ -118,12 +128,8 @@ impl NodeManager { self.secure_channels.identities() } - pub(super) fn identities_repository(&self) -> Arc { - self.identities().repository().clone() - } - - pub(super) fn attributes_reader(&self) -> Arc { - self.identities_repository().as_attributes_reader() + pub(super) fn identity_attributes_repository(&self) -> Arc { + self.identities().identity_attributes_repository().clone() } pub(super) fn credentials(&self) -> Arc { @@ -156,12 +162,10 @@ impl NodeManager { ) } - /// Delete the cli state related to the current node when launched in-memory - pub fn delete_node(&self) -> Result<()> { - Ok(self - .cli_state - .nodes - .delete_sigkill(self.node_name().as_str(), false)?) + /// Delete the current node data + pub async fn delete_node(&self) -> Result<()> { + self.cli_state.remove_node(&self.node_name).await?; + Ok(()) } } @@ -176,7 +180,7 @@ impl NodeManager { authority_identifier, authority_multiaddr, &self - .get_identifier(caller_identity_name) + .get_identifier_by_name(caller_identity_name) .await .into_diagnostic()?, ) @@ -194,7 +198,7 @@ impl NodeManager { project_identifier, project_multiaddr, &self - .get_identifier(caller_identity_name) + .get_identifier_by_name(caller_identity_name) .await .into_diagnostic()?, ) @@ -242,7 +246,7 @@ impl NodeManager { // Check if a policy exists for (resource, action) and if not, then // create or use a default entry: - if self.policies.get_policy(r, a).await?.is_none() { + if self.policies_repository.get_policy(r, a).await?.is_none() { let fallback = match custom_default { Some(e) => e.clone(), None => eq([ @@ -250,17 +254,26 @@ impl NodeManager { ident("subject.trust_context_id"), ]), }; - self.policies.set_policy(r, a, &fallback).await? + self.policies_repository.set_policy(r, a, &fallback).await? } - let policies = self.policies.clone(); + let policies = self.policies_repository.clone(); + debug!( + "set a policy access control for resource '{}' and action '{}'", + &r, &a + ); + Ok(Arc::new(PolicyAccessControl::new( policies, - self.identities_repository(), + self.identity_attributes_repository(), r.clone(), a.clone(), env, ))) } else { + debug!( + "no policy access control set for resource '{}' and action: '{}'", + &r, &a + ); Ok(Arc::new(AllowAll)) } } @@ -330,14 +343,12 @@ impl NodeManagerTransportOptions { } pub struct NodeManagerTrustOptions { - trust_context_config: Option, + trust_context: Option, } impl NodeManagerTrustOptions { - pub fn new(trust_context_config: Option) -> Self { - Self { - trust_context_config, - } + pub fn new(trust_context: Option) -> Self { + Self { trust_context } } } @@ -359,57 +370,70 @@ impl NodeManager { debug!("create the identity repository"); let cli_state = general_options.cli_state; - let node_state = cli_state.nodes.get(&general_options.node_name)?; - let repository: Arc = - cli_state.identities.identities_repository().await?; + let change_history_repository: Arc = + cli_state.change_history_repository().await?; + let identity_attributes_repository: Arc = + cli_state.identity_attributes_repository().await?; + let policies_repository: Arc = + cli_state.policies_repository().await?; //TODO: fix this. Either don't require it to be a bootstrappedidentitystore (and use the //trait instead), or pass it from the general_options always. - let vault: Vault = node_state.config().vault().await?; - let identities_repository: Arc = + let vault: Vault = cli_state.get_node_vault(&general_options.node_name).await?; + let identity_attributes_repository: Arc = Arc::new(match general_options.pre_trusted_identities { - None => BootstrapedIdentityStore::new( + None => BootstrapedIdentityAttributesStore::new( Arc::new(PreTrustedIdentities::new_from_string("{}")?), - repository.clone(), + identity_attributes_repository.clone(), + ), + Some(f) => BootstrapedIdentityAttributesStore::new( + Arc::new(f), + identity_attributes_repository.clone(), ), - Some(f) => BootstrapedIdentityStore::new(Arc::new(f), repository.clone()), }); debug!("create the secure channels service"); let secure_channels = SecureChannels::builder() .with_vault(vault) - .with_identities_repository(identities_repository.clone()) + .with_change_history_repository(change_history_repository.clone()) + .with_identity_attributes_repository(identity_attributes_repository.clone()) + .with_purpose_keys_repository(cli_state.purpose_keys_repository().await?) .build(); - let policies: Arc = Arc::new(node_state.policies_storage().await?); + debug!("start the medic"); let medic_handle = MedicHandle::start_medic(ctx).await?; + debug!("create the trust context"); + let tcp_transport = transport_options.tcp_transport; + let trust_context = match trust_options.trust_context { + None => None, + Some(tc) => Some( + tc.trust_context(&tcp_transport, secure_channels.clone()) + .await?, + ), + }; + + debug!("retrieve the node identifier"); + let node_identifier = cli_state + .get_node(&general_options.node_name) + .await? + .identifier(); + let mut s = Self { cli_state, node_name: general_options.node_name, + node_identifier, api_transport_flow_control_id: transport_options.api_transport_flow_control_id, - tcp_transport: transport_options.tcp_transport, - enable_credential_checks: trust_options.trust_context_config.is_some() - && trust_options - .trust_context_config - .as_ref() - .unwrap() - .authority() - .is_ok(), - identifier: node_state.config().identifier()?, + tcp_transport, secure_channels, - trust_context: None, + trust_context, registry: Default::default(), - policies, + policies_repository, medic_handle, }; - if let Some(tc) = trust_options.trust_context_config { - debug!("configuring trust context"); - s.configure_trust_context(&tc).await?; - } - + debug!("retrieve the node identifier"); s.initialize_services(ctx, general_options.start_default_services) .await?; info!("created a node manager for the node: {}", s.node_name); @@ -417,20 +441,6 @@ impl NodeManager { Ok(s) } - async fn configure_trust_context(&mut self, tc: &TrustContextConfig) -> Result<()> { - self.trust_context = Some( - tc.to_trust_context( - self.secure_channels.clone(), - Some(self.tcp_transport.async_try_clone().await?), - ) - .await?, - ); - - info!("NodeManager::configure_trust_context: trust context configured"); - - Ok(()) - } - async fn initialize_default_services( &self, ctx: &Context, @@ -500,15 +510,11 @@ impl NodeManager { &self, ctx: Arc, addr: &MultiAddr, - identifier: Option, + identifier: Identifier, authorized: Option, credential: Option, timeout: Option, ) -> Result { - let identifier = match identifier { - Some(identifier) => identifier, - None => self.get_identifier(None).await?, - }; let authorized = authorized.map(|authorized| vec![authorized]); self.connect(ctx, addr, identifier, authorized, credential, timeout) .await @@ -549,24 +555,8 @@ impl NodeManager { } pub(crate) async fn resolve_project(&self, name: &str) -> Result<(MultiAddr, Identifier)> { - let projects = ProjectLookup::from_state(self.cli_state.projects.list()?) - .await - .map_err(|e| ApiError::core(format!("Cannot load projects: {:?}", e)))?; - if let Some(info) = projects.get(name) { - let node_route = info - .node_route - .as_ref() - .ok_or_else(|| ApiError::core("Project should have node route set"))? - .clone(); - let identity_id = info - .identity_id - .as_ref() - .ok_or_else(|| ApiError::core("Project should have identity set"))? - .clone(); - Ok((node_route, identity_id)) - } else { - Err(ApiError::core(format!("project {name} not found"))) - } + let project = self.cli_state.get_project_by_name(name).await?; + Ok((project.access_route()?, project.identifier()?)) } } @@ -599,17 +589,14 @@ impl NodeManagerWorker { let r = match (method, path_segments.as_slice()) { // ==*== Basic node information ==*== // TODO: create, delete, destroy remote nodes - (Get, ["node"]) => { - let node_name = &self.node_manager.node_name(); - Response::ok(req) - .body(NodeStatus::new( - node_name, - "Running", - ctx.list_workers().await?.len() as u32, - std::process::id() as i32, - )) - .to_vec()? - } + (Get, ["node"]) => Response::ok(req) + .body(NodeStatus::new( + self.node_manager.node_name.clone(), + "Running", + ctx.list_workers().await?.len() as u32, + std::process::id() as i32, + )) + .to_vec()?, // ==*== Tcp Connection ==*== (Get, ["node", "tcp", "connection"]) => self.get_tcp_connections(req).await.to_vec()?, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs index 0cdcaacccb8..8236ee1febd 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs @@ -1,14 +1,18 @@ -use crate::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use crate::nodes::NODEMANAGER_ADDR; +use std::sync::Arc; +use std::time::Duration; + use miette::IntoDiagnostic; use minicbor::{Decode, Encode}; + +use crate::address::extract_address_value; use ockam_core::api::{Reply, Request}; use ockam_core::{AsyncTryClone, Route}; use ockam_node::api::Client; use ockam_node::Context; use ockam_transport_tcp::{TcpConnectionOptions, TcpTransport}; -use std::sync::Arc; -use std::time::Duration; + +use crate::cli_state::CliState; +use crate::nodes::NODEMANAGER_ADDR; /// This struct represents a node that has been started /// on the same machine with a given node name @@ -28,13 +32,30 @@ impl BackgroundNode { /// Create a new client to send requests to a running background node /// This function instantiates a TcpTransport. Since a TcpTransport can only be created once /// this function must only be called once + /// + /// The optional node name is used to locate the node. It is either + /// a node specified by the user or the default node if no node name is given. pub async fn create( + ctx: &Context, + cli_state: &CliState, + node_name: &Option, + ) -> miette::Result { + let node_name = cli_state.get_node_name_or_default(node_name).await?; + Self::create_to_node(ctx, cli_state, &node_name).await + } + + pub async fn create_to_node( ctx: &Context, cli_state: &CliState, node_name: &str, ) -> miette::Result { let tcp_transport = TcpTransport::create(ctx).await.into_diagnostic()?; - BackgroundNode::new(&tcp_transport, cli_state, node_name).await + BackgroundNode::new( + &tcp_transport, + cli_state, + &extract_address_value(node_name)?, + ) + .await } /// Create a new client to send requests to a running background node @@ -43,7 +64,6 @@ impl BackgroundNode { cli_state: &CliState, node_name: &str, ) -> miette::Result { - cli_state.nodes.get(node_name)?; Ok(BackgroundNode { cli_state: cli_state.clone(), node_name: node_name.to_string(), @@ -53,8 +73,8 @@ impl BackgroundNode { }) } - pub fn delete(&self) -> miette::Result<()> { - Ok(self.cli_state.nodes.delete(self.node_name())?) + pub async fn delete(&self) -> miette::Result<()> { + Ok(self.cli_state.delete_node(&self.node_name(), false).await?) } // Set a different node name @@ -63,6 +83,10 @@ impl BackgroundNode { self } + pub fn node_name(&self) -> String { + self.node_name.clone() + } + /// Use a default timeout for making requests pub fn set_timeout(&mut self, timeout: Duration) -> &Self { self.timeout = Some(timeout); @@ -73,10 +97,6 @@ impl BackgroundNode { &self.cli_state } - pub fn node_name(&self) -> &str { - &self.node_name - } - /// Send a request and expect a decodable response pub async fn ask(&self, ctx: &Context, req: Request) -> miette::Result where @@ -154,12 +174,20 @@ impl BackgroundNode { /// Make a route to the node and connect using TCP async fn create_route(&self) -> miette::Result { let mut route = self.to.clone(); - let node_state = self.cli_state.nodes.get(&self.node_name)?; - let port = node_state.config().setup().api_transport()?.addr.port(); - let addr_str = format!("localhost:{port}"); + let node_info = self.cli_state.get_node(&self.node_name).await?; + let tcp_listener_address = node_info + .tcp_listener_address() + .unwrap_or_else(|| { + panic!( + "an api transport should have been started for node {:?}", + &node_info + ) + }) + .to_string(); + let addr = self .tcp_transport - .connect(addr_str, TcpConnectionOptions::new()) + .connect(tcp_listener_address, TcpConnectionOptions::new()) .await .into_diagnostic()? .sender_address() diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs index 6d459e5bad2..fff4c3726d0 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs @@ -11,7 +11,6 @@ use ockam_core::async_trait; use ockam_multiaddr::MultiAddr; use ockam_node::Context; -use crate::cli_state::traits::StateDirTrait; use crate::cloud::AuthorityNode; use crate::error::ApiError; use crate::local_multiaddr_to_route; @@ -121,24 +120,21 @@ impl NodeManagerWorker { ) -> Result, Response>> { let request: GetCredentialRequest = dec.decode()?; - let identifier = if let Some(identity) = &request.identity_name { - self.node_manager - .cli_state - .identities - .get(identity)? - .identifier() - } else { - self.node_manager.identifier().clone() - }; + let identifier = self + .node_manager + .get_identifier_by_name(request.identity_name) + .await?; match self .node_manager - .trust_context()? - .authority()? - .credential(ctx, &identifier) + .get_credential(ctx, &identifier, None) .await { - Ok(c) => Ok(Either::Right(Response::ok(req).body(c))), + Ok(Some(c)) => Ok(Either::Right(Response::ok(req).body(c))), + Ok(None) => Ok(Either::Left(Response::not_found( + req, + &format!("no credential found for {}", identifier), + ))), Err(e) => Ok(Either::Left(Response::internal_error( req, &format!( @@ -166,12 +162,12 @@ impl NodeManagerWorker { })?; let route = local_multiaddr_to_route(&route)?; + let identifier = self.node_manager.identifier(); let credential = self .node_manager - .trust_context()? - .authority()? - .credential(ctx, self.node_manager.identifier()) - .await?; + .get_credential(ctx, &identifier, None) + .await? + .unwrap_or_else(|| panic!("A credential must be retrieved for {}", identifier)); if request.oneway { self.node_manager @@ -184,11 +180,7 @@ impl NodeManagerWorker { .present_credential_mutual( ctx, route, - self.node_manager - .trust_context()? - .authorities() - .await? - .as_slice(), + &self.node_manager.trust_context()?.authorities(), credential, ) .await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs index a75c8564453..cf3000e32df 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs @@ -1,16 +1,18 @@ -use miette::IntoDiagnostic; use std::ops::Deref; -use std::path::PathBuf; +use futures::executor; +use miette::IntoDiagnostic; + +use ockam::identity::SecureChannels; use ockam::{Context, Result, TcpTransport}; use ockam_core::compat::{string::String, sync::Arc}; use ockam_core::errcode::Kind; use ockam_transport_tcp::TcpListenerOptions; use crate::cli_state::random_name; -use crate::cli_state::{add_project_info_to_node_state, init_node_state, CliState}; +use crate::cli_state::trust_contexts_repository_sql::NamedTrustContext; +use crate::cli_state::CliState; use crate::cloud::Controller; -use crate::config::cli::TrustContextConfig; use crate::nodes::service::{ NodeManagerGeneralOptions, NodeManagerTransportOptions, NodeManagerTrustOptions, }; @@ -51,9 +53,12 @@ impl Drop for InMemoryNode { // stops. Except if they have been started with the `ockam node create` command // because in that case they can be restarted if !self.persistent { - self.node_manager - .delete_node() - .unwrap_or_else(|_| panic!("cannot delete the node {}", self.node_name)); + executor::block_on(async { + self.node_manager + .delete_node() + .await + .unwrap_or_else(|e| panic!("cannot delete the node {}: {e:?}", self.node_name)) + }); } } } @@ -68,40 +73,35 @@ impl InMemoryNode { pub async fn start_with_trust_context( ctx: &Context, cli_state: &CliState, - project_path: Option<&PathBuf>, - trust_context_config: Option, + project_name: Option, + trust_context: Option, ) -> miette::Result { - Self::start_node( - ctx, - cli_state, - None, - None, - project_path, - trust_context_config, - ) - .await + Self::start_node(ctx, cli_state, None, None, project_name, trust_context).await } /// Start an in memory node pub async fn start_node( ctx: &Context, cli_state: &CliState, - vault: Option, - identity: Option, - project_path: Option<&PathBuf>, - trust_context_config: Option, + vault_name: Option, + identity_name: Option, + project_name: Option, + trust_context: Option, ) -> miette::Result { let defaults = NodeManagerDefaults::default(); - init_node_state( - cli_state, - &defaults.node_name, - vault.as_deref(), - identity.as_deref(), - ) - .await?; - - add_project_info_to_node_state(&defaults.node_name, cli_state, project_path).await?; + // if no identity is specified, create one + let identity = cli_state + .create_identity_with_optional_name_and_optional_vault(&identity_name, &vault_name) + .await?; + let node = cli_state + .create_node_with_optional_name_and_optional_vault_and_optional_project( + &Some(defaults.node_name.clone()), + &Some(identity.name()), + &vault_name, + &project_name, + ) + .await?; let tcp = TcpTransport::create(ctx).await.into_diagnostic()?; let bind = defaults.tcp_listener_address; @@ -111,15 +111,9 @@ impl InMemoryNode { let node_manager = Self::new( ctx, - NodeManagerGeneralOptions::new( - cli_state.clone(), - defaults.node_name.clone(), - None, - false, - false, - ), + NodeManagerGeneralOptions::new(cli_state.clone(), node.name(), None, false, false), NodeManagerTransportOptions::new(listener.flow_control_id().clone(), tcp), - NodeManagerTrustOptions::new(trust_context_config), + NodeManagerTrustOptions::new(trust_context), ) .await .into_diagnostic()?; @@ -173,6 +167,10 @@ impl InMemoryNode { persistent, }) } + + pub fn secure_channels(&self) -> Arc { + self.secure_channels.clone() + } } pub struct NodeManagerDefaults { diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs index b58c64f4378..6b1116f0a48 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs @@ -76,7 +76,7 @@ impl MessageSender for NodeManager { let msg_length = message.len(); let connection_ctx = Arc::new(ctx.async_try_clone().await?); let connection = self - .make_connection(connection_ctx, addr, None, None, None, timeout) + .make_connection(connection_ctx, addr, self.identifier(), None, None, timeout) .await?; let route = connection.route(self.tcp_transport()).await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/node_identities.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/node_identities.rs deleted file mode 100644 index 1a11e5383de..00000000000 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/node_identities.rs +++ /dev/null @@ -1,54 +0,0 @@ -use ockam::compat::sync::Arc; -use ockam::identity::{Identifier, Identities, Vault}; -use ockam::Result; - -use crate::cli_state::traits::StateDirTrait; -use crate::cli_state::CliState; - -/// This struct supports identities operation that are either backed by -/// a specific vault or which are using the default vault -pub struct NodeIdentities { - identities: Arc, - cli_state: CliState, -} - -impl NodeIdentities { - pub fn new(identities: Arc, cli_state: CliState) -> NodeIdentities { - NodeIdentities { - identities, - cli_state, - } - } - - pub(super) fn identities_vault(&self) -> Vault { - self.identities.vault() - } - - pub(crate) async fn get_identifier(&self, identity_name: String) -> Result { - let identity_state = self.cli_state.identities.get(identity_name.as_str())?; - Ok(identity_state.identifier()) - } - - /// Return an identities service, possibly backed by a specific vault - pub(crate) async fn get_identities( - &self, - vault_name: Option, - ) -> Result> { - let vault = self.get_identities_vault(vault_name).await?; - let repository = self.cli_state.identities.identities_repository().await?; - Ok(Identities::builder() - .with_vault(vault) - .with_identities_repository(repository) - .build()) - } - - /// Return either the default vault or a specific one - pub(crate) async fn get_identities_vault(&self, vault_name: Option) -> Result { - if let Some(vault) = vault_name { - let existing_vault = self.cli_state.vaults.get(vault.as_str())?.get().await?; - Ok(existing_vault) - } else { - Ok(self.identities_vault()) - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs index 859be671f61..ff236e5a229 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs @@ -39,7 +39,7 @@ use crate::{actions, resources}; use super::NodeManagerWorker; impl NodeManager { - pub(super) async fn start_credentials_service_impl<'a>( + pub(super) async fn start_credentials_service_impl( &self, ctx: &Context, trust_context: TrustContext, @@ -51,13 +51,7 @@ impl NodeManager { } self.credentials_service() - .start( - ctx, - trust_context, - self.identifier().clone(), - addr.clone(), - !oneway, - ) + .start(ctx, trust_context, self.identifier(), addr.clone(), !oneway) .await?; self.registry @@ -84,7 +78,7 @@ impl NodeManager { )); } - let server = Server::new(self.attributes_reader()); + let server = Server::new(self.identity_attributes_repository()); ctx.start_worker(addr.clone(), server).await?; self.registry @@ -538,7 +532,7 @@ impl NodeManagerWorker { // if we are using the project we need to allow safe communication based on the // project identifier self.node_manager - .policies + .policies_repository .set_policy( &resources::INLET, &actions::HANDLE_MESSAGE, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs index 5c375e151c7..a2d233af56f 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs @@ -1,8 +1,6 @@ use either::Either; use minicbor::Decoder; -use crate::cli_state::{StateDirTrait, StateItemTrait}; -use crate::nodes::BackgroundNode; use ockam_abac::expr::{eq, ident, str}; use ockam_abac::{Action, Resource}; use ockam_core::api::{Error, Request, RequestHeader, Response}; @@ -10,6 +8,7 @@ use ockam_core::{async_trait, Result}; use ockam_node::Context; use crate::nodes::models::policy::{Expression, Policy, PolicyList}; +use crate::nodes::BackgroundNode; use super::NodeManager; @@ -24,7 +23,9 @@ impl NodeManager { let p: Policy = dec.decode()?; let r = Resource::new(resource); let a = Action::new(action); - self.policies.set_policy(&r, &a, p.expression()).await?; + self.policies_repository + .set_policy(&r, &a, p.expression()) + .await?; Ok(Response::ok(req)) } @@ -36,7 +37,7 @@ impl NodeManager { ) -> Result, Response>> { let r = Resource::new(resource); let a = Action::new(action); - if let Some(e) = self.policies.get_policy(&r, &a).await? { + if let Some(e) = self.policies_repository.get_policy(&r, &a).await? { Ok(Either::Right(Response::ok(req).body(Policy::new(e)))) } else { Ok(Either::Left(Response::not_found(req, "policy not found"))) @@ -49,7 +50,10 @@ impl NodeManager { res: &str, ) -> Result, Response> { let r = Resource::new(res); - let p = self.policies.policies(&r).await?; + let p = self + .policies_repository + .get_policies_by_resource(&r) + .await?; let p = p.into_iter().map(|(a, e)| Expression::new(a, e)).collect(); Ok(Response::ok(req).body(PolicyList::new(p))) } @@ -62,7 +66,7 @@ impl NodeManager { ) -> Result, Response> { let r = Resource::new(res); let a = Action::new(act); - self.policies.del_policy(&r, &a).await?; + self.policies_repository.delete_policy(&r, &a).await?; Ok(Response::ok(req)) } } @@ -86,12 +90,9 @@ impl Policies for BackgroundNode { ) -> miette::Result<()> { let project_id = match self .cli_state() - .nodes - .get(self.node_name())? - .config() - .setup() - .project - .to_owned() + .get_node_project(&self.node_name()) + .await + .ok() { None => return Ok(()), Some(p) => p.id, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs index 2de7a17bc63..596fbe7baed 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs @@ -1,7 +1,8 @@ -use minicbor::Decoder; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::Duration; + +use minicbor::Decoder; use tokio::time::timeout; use ockam::identity::Identifier; @@ -15,8 +16,6 @@ use ockam_multiaddr::{MultiAddr, Protocol}; use ockam_node::Context; use ockam_transport_tcp::{TcpInletOptions, TcpOutletOptions}; -use crate::cli_state::StateDirTrait; -use crate::config::lookup::ProjectLookup; use crate::error::ApiError; use crate::nodes::connection::Connection; use crate::nodes::models::portal::{ @@ -185,8 +184,8 @@ impl NodeManager { reachable_from_default_secure_channel: bool, ) -> Result { info!( - "Handling request to create outlet portal at {:?}", - socket_addr + "Handling request to create outlet portal at {:?} with worker {:?}", + socket_addr, worker_addr ); let resource = alias .as_deref() @@ -205,19 +204,17 @@ impl NodeManager { )); } - let check_credential = self.enable_credential_checks; - let trust_context_id = if check_credential { - Some(self.trust_context()?.id()) - } else { - None - }; - let access_control = self - .access_control(&resource, &actions::HANDLE_MESSAGE, trust_context_id, None) + .access_control( + &resource, + &actions::HANDLE_MESSAGE, + self.trust_context_id().as_deref(), + None, + ) .await?; let options = TcpOutletOptions::new().with_incoming_access_control(access_control); - let options = if !check_credential { + let options = if self.trust_context_id().is_none() { options.as_consumer(&self.api_transport_flow_control_id) } else { options @@ -359,36 +356,39 @@ impl NodeManager { let outlet_route = connection.route(self.tcp_transport()).await?; let outlet_route = route![prefix_route.clone(), outlet_route, suffix_route.clone()]; - let projects = self.cli_state.projects.list()?; - let projects = ProjectLookup::from_state(projects) - .await - .map_err(|e| ockam_core::Error::new(Origin::Node, Kind::NotFound, e))?; - let check_credential = self.enable_credential_checks; - let project_id = if check_credential { - let pid = outlet_addr - .first() - .and_then(|p| { - if let Some(p) = p.cast::() { - projects.get(&*p).map(|info| &*info.id) - } else { - None - } - }) - .or_else(|| Some(self.trust_context().ok()?.id())); - if pid.is_none() { - let message = "Credential check requires a project or trust context"; - return Err(ockam_core::Error::new(Origin::Node, Kind::Invalid, message)); + let projects = self.cli_state.get_projects_grouped_by_name().await?; + + let project_id = match self.trust_context_id() { + Some(trust_context_id) => { + let pid = outlet_addr + .first() + .and_then(|p| { + if let Some(p) = p.cast::() { + projects.get(&*p).map(|project| project.id()) + } else { + None + } + }) + .or(Some(trust_context_id)); + if pid.is_none() { + let message = "Credential check requires a project or trust context"; + return Err(ockam_core::Error::new(Origin::Node, Kind::Invalid, message)); + } + pid } - pid - } else { - None + None => None, }; let resource = requested_alias .map(|a| Resource::new(a.as_str())) .unwrap_or(resources::INLET); let access_control = self - .access_control(&resource, &actions::HANDLE_MESSAGE, project_id, None) + .access_control( + &resource, + &actions::HANDLE_MESSAGE, + project_id.as_deref(), + None, + ) .await?; let options = TcpInletOptions::new().with_incoming_access_control(access_control.clone()); @@ -550,7 +550,7 @@ impl InMemoryNode { .make_connection( connection_ctx.clone(), &outlet_addr, - None, + self.identifier(), authorized.clone(), None, Some(duration), @@ -670,7 +670,7 @@ impl InMemoryNode { .make_connection( ctx.clone(), &addr, - None, + node_manager.identifier(), authorized, None, Some(MAX_CONNECT_TIME), diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/projects.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/projects.rs new file mode 100644 index 00000000000..9072d5dbe13 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/projects.rs @@ -0,0 +1,95 @@ +use ockam_core::async_trait; +use ockam_node::Context; + +use crate::cloud::project::{OrchestratorVersionInfo, Project, Projects}; +use crate::nodes::InMemoryNode; + +#[async_trait] +impl Projects for InMemoryNode { + async fn create_project( + &self, + ctx: &Context, + space_name: &str, + project_name: &str, + users: Vec, + ) -> miette::Result { + let space = self.cli_state.get_space_by_name(space_name).await?; + let controller = self.create_controller().await?; + let project = controller + .create_project(ctx, &space.space_id(), project_name, users) + .await?; + self.cli_state.store_project(project.clone()).await?; + Ok(project) + } + + async fn get_project(&self, ctx: &Context, project_id: &str) -> miette::Result { + let controller = self.create_controller().await?; + let project = controller.get_project(ctx, project_id).await?; + self.cli_state.store_project(project.clone()).await?; + Ok(project) + } + + async fn get_project_by_name_or_default( + &self, + ctx: &Context, + project_name: &Option, + ) -> miette::Result { + let project_id = self + .cli_state + .get_project_by_name_or_default(project_name) + .await? + .id(); + self.get_project(ctx, &project_id).await + } + + async fn get_project_by_name( + &self, + ctx: &Context, + project_name: &str, + ) -> miette::Result { + let project_id = self.cli_state.get_project_by_name(project_name).await?.id(); + self.get_project(ctx, &project_id).await + } + + async fn delete_project( + &self, + ctx: &Context, + space_id: &str, + project_id: &str, + ) -> miette::Result<()> { + let controller = self.create_controller().await?; + controller.delete_project(ctx, space_id, project_id).await?; + Ok(self.cli_state.delete_project(project_id).await?) + } + + async fn delete_project_by_name( + &self, + ctx: &Context, + space_name: &str, + project_name: &str, + ) -> miette::Result<()> { + let space = self.cli_state.get_space_by_name(space_name).await?; + let project = self.cli_state.get_project_by_name(project_name).await?; + self.delete_project(ctx, &space.space_id(), &project.id()) + .await + } + + async fn get_orchestrator_version_info( + &self, + ctx: &Context, + ) -> miette::Result { + Ok(self + .create_controller() + .await? + .get_orchestrator_version_info(ctx) + .await?) + } + + async fn get_projects(&self, ctx: &Context) -> miette::Result> { + let projects = self.create_controller().await?.list_projects(ctx).await?; + for project in &projects { + self.cli_state.store_project(project.clone()).await? + } + Ok(projects) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs index 82d7f888548..0cdaa090235 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs @@ -1,7 +1,8 @@ -use miette::IntoDiagnostic; use std::sync::Arc; use std::time::Duration; +use miette::IntoDiagnostic; + use ockam::compat::sync::Mutex; use ockam::identity::Identifier; use ockam::remote::{RemoteRelay, RemoteRelayOptions}; @@ -233,7 +234,7 @@ impl InMemoryNode { .make_connection( connection_ctx.clone(), &address.clone(), - None, + self.identifier(), authorized.clone(), None, None, @@ -337,7 +338,7 @@ impl InMemoryNode { .make_connection( ctx.clone(), &addr, - None, + node_manager.identifier(), authorized, None, Some(MAX_CONNECT_TIME), diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs index 38ff5f15b94..667f9c89dd4 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs @@ -7,7 +7,7 @@ use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::TrustEveryonePolicy; use ockam::identity::Vault; use ockam::identity::{ - Identifier, Identities, SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, + Identifier, SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, TrustMultiIdentifiersPolicy, }; use ockam::identity::{SecureChannel, SecureChannelListener}; @@ -19,8 +19,6 @@ use ockam_core::AsyncTryClone; use ockam_multiaddr::MultiAddr; use ockam_node::Context; -use crate::cli_state::traits::StateDirTrait; -use crate::cli_state::StateItemTrait; use crate::nodes::models::secure_channel::{ CreateSecureChannelListenerRequest, CreateSecureChannelRequest, CreateSecureChannelResponse, DeleteSecureChannelListenerRequest, DeleteSecureChannelListenerResponse, @@ -29,7 +27,6 @@ use crate::nodes::models::secure_channel::{ ShowSecureChannelResponse, }; use crate::nodes::registry::{SecureChannelInfo, SecureChannelListenerInfo}; -use crate::nodes::service::NodeIdentities; use crate::nodes::{NodeManager, NodeManagerWorker}; use crate::DefaultAddress; @@ -80,7 +77,7 @@ impl NodeManagerWorker { return Err(Response::bad_request( req, &format!("Incorrect multi-address {}", addr), - )) + )); } }; let sc = self @@ -266,9 +263,9 @@ impl NodeManager { credential_name: Option, timeout: Option, ) -> Result { - let identifier = self.get_identifier(identity_name.clone()).await?; + let identifier = self.get_identifier_by_name(identity_name.clone()).await?; let credential = self - .get_credential(ctx, &identifier, credential_name, timeout) + .retrieve_credential(ctx, &identifier, credential_name, timeout) .await?; let connection_ctx = Arc::new(ctx.async_try_clone().await?); @@ -276,7 +273,7 @@ impl NodeManager { .make_connection( connection_ctx, &addr, - Some(identifier.clone()), + identifier.clone(), None, credential.clone(), timeout, @@ -297,7 +294,7 @@ impl NodeManager { Ok(sc) } - pub async fn get_credential( + pub async fn retrieve_credential( &self, ctx: &Context, identifier: &Identifier, @@ -305,35 +302,42 @@ impl NodeManager { timeout: Option, ) -> Result> { debug!("getting a credential"); - let credential = if let Some(credential_name) = credential_name { + if let Some(credential_name) = credential_name { debug!( "get the credential using a credential name {}", &credential_name ); - Some( + Ok(Some( self.cli_state - .credentials - .get(credential_name)? - .config() - .credential()?, - ) + .get_credential_by_name(&credential_name) + .await? + .credential_and_purpose_key(), + )) } else { - match self.trust_context().ok() { - Some(tc) => { - if let Some(t) = timeout { - ockam_node::compat::timeout(t, tc.get_credential(ctx, identifier)) - .await - .map_err(|e| { - ockam_core::Error::new(Origin::Api, Kind::Timeout, e.to_string()) - })? - } else { - tc.get_credential(ctx, identifier).await - } - } - None => None, + self.get_credential(ctx, identifier, timeout).await + } + } + + pub async fn get_credential( + &self, + ctx: &Context, + identifier: &Identifier, + timeout: Option, + ) -> Result> { + if let Some(tc) = self.trust_context.as_ref() { + debug!("getting a credential"); + if let Some(t) = timeout { + ockam_node::compat::timeout(t, tc.get_credential(ctx, identifier)) + .await + .map_err(|e| { + ockam_core::Error::new(Origin::Api, Kind::Timeout, e.to_string()) + })? + } else { + tc.get_credential(ctx, identifier).await } - }; - Ok(credential) + } else { + Ok(None) + } } pub(crate) async fn create_secure_channel_internal( @@ -356,8 +360,7 @@ impl NodeManager { let options = if let Some(credential) = credential { options.with_credential(credential) - } else if let Some(credential) = self.get_credential(ctx, identifier, None, timeout).await? - { + } else if let Some(credential) = self.get_credential(ctx, identifier, timeout).await? { options.with_credential(credential) } else { options @@ -422,7 +425,7 @@ impl NodeManager { ); let secure_channels = self.build_secure_channels(vault_name.clone()).await?; - let identifier = self.get_identifier(identity_name.clone()).await?; + let identifier = self.get_identifier_by_name(identity_name.clone()).await?; let options = SecureChannelListenerOptions::new().as_consumer(&self.api_transport_flow_control_id); @@ -503,34 +506,19 @@ impl NodeManager { return Ok(self.secure_channels.clone()); } let vault = self.get_secure_channels_vault(vault_name.clone()).await?; - let identities = self.get_identities(vault_name).await?; + let identity_repository = self.cli_state.change_history_repository().await?; let registry = self.secure_channels.secure_channel_registry(); Ok(SecureChannels::builder() .with_vault(vault) - .with_identities(identities) + .with_change_history_repository(identity_repository) .with_secure_channels_registry(registry) + .with_purpose_keys_repository(self.cli_state.purpose_keys_repository().await?) .build()) } - pub fn node_identities(&self) -> NodeIdentities { - NodeIdentities::new(self.identities(), self.cli_state.clone()) - } - - pub async fn get_identifier(&self, identity_name: Option) -> Result { - if let Some(name) = identity_name { - self.node_identities().get_identifier(name.clone()).await - } else { - Ok(self.identifier().clone()) - } - } - - async fn get_identities(&self, vault_name: Option) -> Result> { - self.node_identities().get_identities(vault_name).await - } - async fn get_secure_channels_vault(&self, vault_name: Option) -> Result { - if let Some(vault) = vault_name { - let existing_vault = self.cli_state.vaults.get(vault.as_str())?.get().await?; + if let Some(vault_name) = vault_name { + let existing_vault = self.cli_state.get_vault_by_name(&vault_name).await?; Ok(existing_vault) } else { Ok(self.secure_channels_vault()) diff --git a/implementations/rust/ockam/ockam_api/src/okta/mod.rs b/implementations/rust/ockam/ockam_api/src/okta/mod.rs index 753b9398295..bd99c8fcfc8 100644 --- a/implementations/rust/ockam/ockam_api/src/okta/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/okta/mod.rs @@ -2,10 +2,8 @@ use crate::error::ApiError; use core::str; use minicbor::Decoder; use ockam::identity::utils::now; -use ockam::identity::TRUST_CONTEXT_ID; -use ockam::identity::{ - AttributesEntry, Identifier, IdentityAttributesWriter, IdentitySecureChannelLocalInfo, -}; +use ockam::identity::{AttributesEntry, Identifier, IdentitySecureChannelLocalInfo}; +use ockam::identity::{IdentityAttributesRepository, TRUST_CONTEXT_ID}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; use ockam_core::{self, Result, Routed, Worker}; @@ -15,7 +13,7 @@ use std::collections::HashMap; use tracing::trace; pub struct Server { - attributes_writer: Arc, + identity_attributes_repository: Arc, project: String, tenant_base_url: String, certificate: reqwest::Certificate, @@ -42,7 +40,7 @@ impl Worker for Server { impl Server { pub fn new( - attributes_writer: Arc, + identity_attributes_repository: Arc, project: String, tenant_base_url: &str, certificate: &str, @@ -51,7 +49,7 @@ impl Server { let certificate = reqwest::Certificate::from_pem(certificate.as_bytes()) .map_err(|err| ApiError::core(err.to_string()))?; Ok(Server { - attributes_writer, + identity_attributes_repository, project, tenant_base_url: tenant_base_url.to_string(), certificate, @@ -102,7 +100,9 @@ impl Server { None, None, ); - self.attributes_writer.put_attributes(from, entry).await?; + self.identity_attributes_repository + .put_attributes(from, entry) + .await?; Response::ok(&req).to_vec()? } else { Response::forbidden(&req, "Forbidden").to_vec()? diff --git a/implementations/rust/ockam/ockam_api/src/trust_context.rs b/implementations/rust/ockam/ockam_api/src/trust_context.rs deleted file mode 100644 index 6cb05deaabf..00000000000 --- a/implementations/rust/ockam/ockam_api/src/trust_context.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::cli_state::{CliState, ProjectConfigCompact, StateDirTrait, StateItemTrait}; -use crate::cloud::project::Project; -use crate::config::cli::TrustContextConfig; -use miette::{IntoDiagnostic, WrapErr}; -use std::path::PathBuf; - -#[derive(Debug, Clone)] -pub struct TrustContextConfigBuilder { - pub cli_state: CliState, - pub project_path: Option, - pub trust_context: Option, - pub project: Option, - pub authority_identity: Option, - pub credential_name: Option, - pub use_default_trust_context: bool, -} - -impl TrustContextConfigBuilder { - pub fn new(cli_state: &CliState) -> Self { - Self { - cli_state: cli_state.clone(), - project_path: None, - trust_context: None, - project: None, - authority_identity: None, - credential_name: None, - use_default_trust_context: false, - } - } - - pub fn with_authority_identity(&mut self, authority_identity: Option<&String>) -> &mut Self { - self.authority_identity = authority_identity.map(|s| s.to_string()); - self - } - - pub fn with_credential_name(&mut self, credential_name: Option<&String>) -> &mut Self { - self.credential_name = credential_name.map(|s| s.to_string()); - self - } - - pub fn use_default_trust_context(&mut self, use_default_trust_context: bool) -> &mut Self { - self.use_default_trust_context = use_default_trust_context; - self - } - - pub fn build(&self) -> Option { - self.trust_context - .clone() - .or_else(|| self.get_from_project_path(self.project_path.as_ref()?)) - .or_else(|| self.get_from_project_name()) - .or_else(|| self.get_from_authority_identity()) - .or_else(|| self.get_from_credential()) - .or_else(|| self.get_from_default_trust_context()) - .or_else(|| self.get_from_default_project()) - } - - fn get_from_project_path(&self, path: &PathBuf) -> Option { - let s = std::fs::read_to_string(path) - .into_diagnostic() - .context("Failed to read project file") - .ok()?; - let proj_info = serde_json::from_str::(&s) - .into_diagnostic() - .context("Failed to parse project info") - .ok()?; - let proj: Project = (&proj_info).into(); - proj.try_into().ok() - } - - fn get_from_project_name(&self) -> Option { - let project = self.cli_state.projects.get(self.project.as_ref()?).ok()?; - project.config().clone().try_into().ok() - } - - fn get_from_authority_identity(&self) -> Option { - let authority_identity = self.authority_identity.clone(); - let credential = match &self.credential_name { - Some(c) => Some(self.cli_state.credentials.get(c).ok()?), - None => None, - }; - - TrustContextConfig::from_authority_identity(&authority_identity?, credential).ok() - } - - fn get_from_credential(&self) -> Option { - let cred_name = self.credential_name.clone()?; - let cred_state = self.cli_state.credentials.get(cred_name).ok()?; - - cred_state.try_into().ok() - } - - fn get_from_default_trust_context(&self) -> Option { - if !self.use_default_trust_context { - return None; - } - - let tc = self - .cli_state - .trust_contexts - .default() - .ok()? - .config() - .clone(); - Some(tc) - } - - fn get_from_default_project(&self) -> Option { - let proj = self.cli_state.projects.default().ok()?; - self.get_from_project_path(proj.path()) - } -} diff --git a/implementations/rust/ockam/ockam_api/src/util.rs b/implementations/rust/ockam/ockam_api/src/util.rs index 1b94529e370..c6226323c8f 100644 --- a/implementations/rust/ockam/ockam_api/src/util.rs +++ b/implementations/rust/ockam/ockam_api/src/util.rs @@ -1,6 +1,7 @@ -use miette::miette; use std::net::{SocketAddrV4, SocketAddrV6}; +use miette::miette; + use ockam::TcpTransport; use ockam_core::errcode::{Kind, Origin}; use ockam_core::flow_control::FlowControlId; @@ -49,7 +50,7 @@ pub fn local_multiaddr_to_route(ma: &MultiAddr) -> Result { Origin::Api, Kind::Invalid, "unexpected code: node. clean_multiaddr should have been called", - )) + )); } code @ (Ip4::CODE | Ip6::CODE | DnsAddr::CODE) => { @@ -57,7 +58,7 @@ pub fn local_multiaddr_to_route(ma: &MultiAddr) -> Result { Origin::Api, Kind::Invalid, format!("unexpected code: {code}. The address must be a local address {ma}"), - )) + )); } other => { @@ -373,22 +374,17 @@ pub fn local_worker(code: &Code) -> Result { #[cfg(test)] pub mod test_utils { - use ockam::identity::storage::InMemoryStorage; use ockam::identity::utils::AttributesBuilder; - use ockam::identity::{Identifier, MAX_CREDENTIAL_VALIDITY}; + use ockam::identity::MAX_CREDENTIAL_VALIDITY; use ockam::identity::{SecureChannels, PROJECT_MEMBER_SCHEMA, TRUST_CONTEXT_ID}; use ockam::Result; use ockam_core::compat::sync::Arc; use ockam_core::flow_control::FlowControls; use ockam_core::AsyncTryClone; - use ockam_node::Context; use ockam_transport_tcp::TcpTransport; - use crate::cli_state::{ - random_name, traits::*, CliState, IdentityConfig, NodeConfig, VaultConfig, - }; - use crate::config::cli::{CredentialRetrieverConfig, TrustAuthorityConfig, TrustContextConfig}; + use crate::cli_state::{random_name, CliState}; use crate::nodes::service::{ NodeManagerGeneralOptions, NodeManagerTransportOptions, NodeManagerTrustOptions, }; @@ -405,12 +401,11 @@ pub mod test_utils { pub node_manager: Arc, pub tcp: TcpTransport, pub secure_channels: Arc, - pub identifier: Identifier, } impl Drop for NodeManagerHandle { fn drop(&mut self) { - CliState::delete_at(&self.cli_state.dir).expect("cannot delete cli state"); + self.cli_state.delete().expect("cannot delete cli state"); } } @@ -421,42 +416,22 @@ pub mod test_utils { // #[must_use] make sense to enable only on rust 1.67+ pub async fn start_manager_for_tests(context: &mut Context) -> Result { let tcp = TcpTransport::create(context).await?; - let cli_state = CliState::test()?; + let cli_state = CliState::test().await?; - let vault_name = random_name(); - let vault = cli_state - .vaults - .create_async(&vault_name.clone(), VaultConfig::default()) - .await? - .get() - .await?; - - let identity_name = random_name(); + let node_name = random_name(); + cli_state.create_node(&node_name).await.unwrap(); // Premise: we need an identity and a credential before the node manager starts. - // Since the LMDB can trigger some race conditions, we first use the memory storage - // export the identity and credentials,then import in the LMDB after secure-channel - // has been re-created - let secure_channels = SecureChannels::builder() - .with_vault(vault) - .with_identities_repository(cli_state.identities.identities_repository().await?) - .with_identities_storage(InMemoryStorage::create()) - .build(); - - let identifier = create_random_identity(&secure_channels).await?; - - let exported_identity = secure_channels - .identities() - .get_identity(&identifier) - .await? - .export()?; + let identifier = cli_state.get_node_identifier(&node_name).await?; + let identity = cli_state.get_identity(&identifier).await?; let attributes = AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA) .with_attribute(TRUST_CONTEXT_ID.to_vec(), b"test_trust_context_id".to_vec()) .build(); - let credential = secure_channels - .identities() + let credential = cli_state + .get_identities() + .await? .credentials() .credentials_creation() .issue_credential( @@ -468,14 +443,19 @@ pub mod test_utils { .await .unwrap(); - drop(secure_channels); - - let config = IdentityConfig::new(&identifier).await; - cli_state.identities.create(&identity_name, config).unwrap(); + cli_state + .store_credential("credential", &identity, credential) + .await?; - let node_name = random_name(); - let node_config = NodeConfig::try_from(&cli_state).unwrap(); - cli_state.nodes.create(&node_name, node_config)?; + let trust_context = cli_state + .create_trust_context( + Some("trust-context".to_string()), + None, + Some("credential".to_string()), + None, + None, + ) + .await?; let node_manager = InMemoryNode::new( context, @@ -484,46 +464,23 @@ pub mod test_utils { FlowControls::generate_flow_control_id(), // FIXME tcp.async_try_clone().await?, ), - NodeManagerTrustOptions::new(Some(TrustContextConfig::new( - "test_trust_context".to_string(), - Some(TrustAuthorityConfig::new( - hex::encode(&exported_identity), - Some(CredentialRetrieverConfig::FromMemory(minicbor::to_vec( - &credential, - )?)), - )), - ))), + NodeManagerTrustOptions::new(Some(trust_context)), ) .await?; + let node_manager = Arc::new(node_manager); let node_manager_worker = NodeManagerWorker::new(node_manager.clone()); - let secure_channels = node_manager.secure_channels.clone(); - - // Import identity, since it doesn't exist in the LMDB storage - let _ = secure_channels - .identities() - .identities_creation() - .import(Some(&identifier), &exported_identity) - .await?; context .start_worker(NODEMANAGER_ADDR, node_manager_worker) .await?; + let secure_channels = node_manager.secure_channels(); Ok(NodeManagerHandle { cli_state, node_manager, tcp: tcp.async_try_clone().await?, - secure_channels: secure_channels.clone(), - identifier, + secure_channels, }) } - - async fn create_random_identity(secure_channels: &Arc) -> Result { - secure_channels - .identities() - .identities_creation() - .create_identity() - .await - } } diff --git a/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs b/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs index 6643f6dc707..7f1e43b02c3 100644 --- a/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs +++ b/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs @@ -1,4 +1,4 @@ -use ockam::identity::{Identifier, IdentityAttributesReader}; +use ockam::identity::{Identifier, IdentityAttributesRepository}; use ockam_api::auth; use ockam_api::auth::AuthorizationApi; use ockam_api::bootstrapped_identities_store::PreTrustedIdentities; @@ -15,7 +15,7 @@ async fn auth_smoke(ctx: &mut Context) -> Result<()> { "I224ed0b2e5a2be82e267ead6b3279f683616b66d":{"attr":"value2"} }"#, )?; - let s: Arc = Arc::new(s); + let s: Arc = Arc::new(s); ctx.start_worker("auth", auth::Server::new(s)).await?; let client = Client::new(&route!["auth"], None); diff --git a/implementations/rust/ockam/ockam_api/tests/authority.rs b/implementations/rust/ockam/ockam_api/tests/authority.rs index 217a0bdbacd..2a9acffea41 100644 --- a/implementations/rust/ockam/ockam_api/tests/authority.rs +++ b/implementations/rust/ockam/ockam_api/tests/authority.rs @@ -1,3 +1,8 @@ +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + use ockam::identity::utils::now; use ockam::identity::{secure_channels, AttributesEntry, Identifier, SecureChannels}; use ockam::AsyncTryClone; @@ -5,6 +10,7 @@ use ockam_api::authenticator::enrollment_tokens::Members; use ockam_api::authority_node::{Authority, Configuration}; use ockam_api::bootstrapped_identities_store::PreTrustedIdentities; use ockam_api::cloud::AuthorityNode; +use ockam_api::config::lookup::InternetAddress; use ockam_api::nodes::NodeManager; use ockam_api::{authority_node, DefaultAddress}; use ockam_core::{Address, Result}; @@ -12,10 +18,6 @@ use ockam_multiaddr::MultiAddr; use ockam_node::Context; use ockam_transport_tcp::TcpTransport; use rand::{thread_rng, Rng}; -use std::collections::BTreeMap; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; use tempfile::NamedTempFile; #[ockam_macros::test] @@ -319,7 +321,6 @@ async fn two_admins_two_members_exist_in_one_global_scope(ctx: &mut Context) -> // with freshly created Authority Identifier and temporary files for storage and vault async fn default_configuration() -> Result { let storage_path = NamedTempFile::new().unwrap().keep().unwrap().1; - let vault_path = NamedTempFile::new().unwrap().keep().unwrap().1; let port = thread_rng().gen_range(10000..65535); @@ -330,10 +331,9 @@ async fn default_configuration() -> Result { let mut configuration = authority_node::Configuration { identifier: "I4dba4b2e53b2ed95967b3bab350b6c9ad9c624e5".try_into()?, - storage_path, - vault_path, + database_path: storage_path, project_identifier: "123456".to_string(), - tcp_listener_address: format!("127.0.0.1:{}", port), + tcp_listener_address: InternetAddress::new(&format!("127.0.0.1:{}", port)).unwrap(), secure_channel_listener_name: None, authenticator_name: None, trusted_identities, diff --git a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs index 6f7e936beeb..850507a6be5 100644 --- a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs +++ b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs @@ -7,7 +7,9 @@ use ockam::identity::{ SecureChannels, }; use ockam::route; -use ockam_api::bootstrapped_identities_store::{BootstrapedIdentityStore, PreTrustedIdentities}; +use ockam_api::bootstrapped_identities_store::{ + BootstrapedIdentityAttributesStore, PreTrustedIdentities, +}; use ockam_core::api::Request; use ockam_core::compat::collections::{BTreeMap, HashMap}; use ockam_core::compat::sync::Arc; @@ -38,16 +40,17 @@ async fn credential(ctx: &mut Context) -> Result<()> { ), )]); - let bootstrapped = BootstrapedIdentityStore::new( + let bootstrapped = BootstrapedIdentityAttributesStore::new( Arc::new(PreTrustedIdentities::from(pre_trusted)), - identities.repository(), + identities.identity_attributes_repository(), ); // Now recreate the identities services with the previous vault // (so that the authority can verify its signature) // and the repository containing the trusted identities let identities = Identities::builder() - .with_identities_repository(Arc::new(bootstrapped)) + .with_change_history_repository(identities.change_history_repository()) + .with_identity_attributes_repository(Arc::new(bootstrapped)) .with_vault(identities.vault()) .with_purpose_keys_repository(identities.purpose_keys_repository()) .build(); @@ -65,7 +68,7 @@ async fn credential(ctx: &mut Context) -> Result<()> { ctx.flow_controls() .add_consumer(auth_worker_addr.clone(), &sc_flow_control_id); let auth = CredentialsIssuer::new( - identities.repository(), + identities.identity_attributes_repository(), identities.credentials(), &auth_identifier, "project42".into(), diff --git a/implementations/rust/ockam/ockam_app/Cargo.toml b/implementations/rust/ockam/ockam_app/Cargo.toml index 75a692806e4..7a59094072b 100644 --- a/implementations/rust/ockam/ockam_app/Cargo.toml +++ b/implementations/rust/ockam/ockam_app/Cargo.toml @@ -44,6 +44,7 @@ open = "5.0.0" percent-encoding = "2.3.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } tauri = { version = "=2.0.0-alpha.11", features = ["tray-icon", "icon-png"] } tauri-plugin-log = { version = "=2.0.0-alpha.1", optional = true } tauri-plugin-notification = "=2.0.0-alpha.1" diff --git a/implementations/rust/ockam/ockam_app/src/app/state/mod.rs b/implementations/rust/ockam/ockam_app/src/app/state/mod.rs index ac17be91e19..5bf46f211ac 100644 --- a/implementations/rust/ockam/ockam_app/src/app/state/mod.rs +++ b/implementations/rust/ockam/ockam_app/src/app/state/mod.rs @@ -8,25 +8,21 @@ use tauri::async_runtime::{block_on, spawn, RwLock}; use tauri::{AppHandle, Manager, Runtime}; use tracing::{error, info, trace, warn}; -pub(crate) use crate::app::state::model::ModelState; -pub(crate) use crate::app::state::repository::{LmdbModelStateRepository, ModelStateRepository}; -use crate::background_node::{BackgroundNodeClient, Cli}; use ockam::Context; use ockam::{NodeBuilder, TcpListenerOptions, TcpTransport}; -use ockam_api::cli_state::{ - add_project_info_to_node_state, init_node_state, CliState, StateDirTrait, StateItemTrait, -}; +use ockam_api::cli_state::CliState; use ockam_api::cloud::enroll::auth0::UserInfo; use ockam_api::cloud::Controller; use ockam_api::nodes::models::portal::OutletStatus; -use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType}; use ockam_api::nodes::service::{ NodeManagerGeneralOptions, NodeManagerTransportOptions, NodeManagerTrustOptions, }; use ockam_api::nodes::InMemoryNode; use ockam_api::nodes::NODEMANAGER_ADDR; -use ockam_api::trust_context::TrustContextConfigBuilder; +pub(crate) use crate::app::state::model::ModelState; +pub(crate) use crate::app::state::repository::{ModelStateRepository, ModelStateSqlxDatabase}; +use crate::background_node::{BackgroundNodeClient, Cli}; use crate::Result; mod model; @@ -66,7 +62,7 @@ impl Default for AppState { impl AppState { /// Create a new AppState pub fn new() -> AppState { - let cli_state = CliState::initialize().unwrap_or_else(|_| { + let cli_state = CliState::with_default_dir().unwrap_or_else(|_| { CliState::backup_and_reset().expect( "Failed to initialize CliState. Try to manually remove the '~/.ockam' directory", ) @@ -112,24 +108,6 @@ impl AppState { let mut writer = self.event_manager.write().unwrap(); writer.events.clear(); } - - // recreate the model state repository since the cli state has changed - { - let mut writer = self.model_state.write().await; - *writer = ModelState::default(); - } - let identity_path = self - .state() - .await - .identities - .identities_repository_path() - .expect("Failed to get the identities repository path"); - let new_state_repository = LmdbModelStateRepository::new(identity_path).await?; - { - let mut writer = self.model_state_repository.write().await; - *writer = Arc::new(new_state_repository); - } - Ok(()) } @@ -189,7 +167,7 @@ impl AppState { } pub async fn is_enrolled(&self) -> Result { - self.state().await.is_enrolled().map_err(|e| { + self.state().await.is_enrolled().await.map_err(|e| { warn!(%e, "Failed to check if user is enrolled"); e.into() }) @@ -202,14 +180,7 @@ impl AppState { } pub async fn user_info(&self) -> Result { - Ok(self - .state - .read() - .await - .users_info - .default()? - .config() - .clone()) + Ok(self.state.read().await.get_default_user().await?) } pub async fn user_email(&self) -> Result { @@ -291,8 +262,6 @@ pub(crate) async fn make_node_manager( ctx: Arc, cli_state: &CliState, ) -> miette::Result { - init_node_state(cli_state, NODE_NAME, None, None).await?; - let tcp = TcpTransport::create(&ctx).await.into_diagnostic()?; let options = TcpListenerOptions::new(); let listener = tcp @@ -300,26 +269,15 @@ pub(crate) async fn make_node_manager( .await .into_diagnostic()?; - add_project_info_to_node_state(NODE_NAME, cli_state, None).await?; - - let node_state = cli_state.nodes.get(NODE_NAME)?; - node_state.set_setup( - &node_state.config().setup_mut().set_api_transport( - CreateTransportJson::new( - TransportType::Tcp, - TransportMode::Listen, - &listener.socket_address().to_string(), - ) - .into_diagnostic()?, - ), - )?; - let trust_context_config = TrustContextConfigBuilder::new(cli_state).build(); + cli_state + .set_tcp_listener_address(NODE_NAME, listener.socket_address().to_string()) + .await?; let node_manager = InMemoryNode::new( &ctx, NodeManagerGeneralOptions::new(cli_state.clone(), NODE_NAME.to_string(), None, true, true), NodeManagerTransportOptions::new(listener.flow_control_id().clone(), tcp), - NodeManagerTrustOptions::new(trust_context_config), + NodeManagerTrustOptions::new(cli_state.get_default_trust_context().await.ok()), ) .await .into_diagnostic()?; @@ -331,17 +289,7 @@ pub(crate) async fn make_node_manager( /// Create the repository containing the model state fn create_model_state_repository(state: &CliState) -> Arc { - let identity_path = state - .identities - .identities_repository_path() - .expect("Failed to get the identities repository path"); - match block_on(async move { LmdbModelStateRepository::new(identity_path).await }) { - Ok(model_state_repository) => Arc::new(model_state_repository), - Err(e) => { - error!(%e, "Cannot create a model state repository manager"); - panic!("Cannot create a model state repository manager: {e:?}"); - } - } + Arc::new(ModelStateSqlxDatabase::create(state.database())) } /// Load a previously persisted ModelState @@ -359,7 +307,6 @@ fn load_model_state( block_on(async { match model_state_repository.load().await { Ok(model_state) => { - let model_state = model_state.unwrap_or(ModelState::default()); crate::shared_service::tcp_outlet::load_model_state( context.clone(), node_manager.clone(), @@ -379,6 +326,7 @@ fn load_model_state( pub type EventName = String; type IsProcessing = AtomicBool; + struct Event { name: EventName, is_processing: IsProcessing, diff --git a/implementations/rust/ockam/ockam_app/src/app/state/model.rs b/implementations/rust/ockam/ockam_app/src/app/state/model.rs index 4c01422be58..ae1f5e957f9 100644 --- a/implementations/rust/ockam/ockam_app/src/app/state/model.rs +++ b/implementations/rust/ockam/ockam_app/src/app/state/model.rs @@ -1,5 +1,11 @@ +use crate::Result; +use miette::IntoDiagnostic; use ockam_api::nodes::models::portal::OutletStatus; +use ockam_core::Address; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use std::net::SocketAddr; +use std::str::FromStr; /// The ModelState stores all the data which is not maintained by the NodeManager. #[derive(Serialize, Deserialize, Clone)] @@ -19,3 +25,38 @@ impl ModelState { Self { tcp_outlets } } } + +#[derive(FromRow)] +pub struct TcpOutletRow { + socket_addr: String, + worker_addr: String, + alias: String, + payload: Option, +} + +impl TcpOutletRow { + pub(crate) fn socket_addr(&self) -> Result { + Ok(SocketAddr::from_str(&self.socket_addr)?) + } + + pub(crate) fn worker_addr(&self) -> Result
{ + Ok(Address::from_str(&self.worker_addr).into_diagnostic()?) + } + + pub(crate) fn alias(&self) -> String { + self.alias.clone() + } + + pub(crate) fn payload(&self) -> Option { + self.payload.clone() + } + + pub(crate) fn tcp_outlet_status(&self) -> Result { + Ok(OutletStatus::new( + self.socket_addr()?, + self.worker_addr()?, + self.alias(), + self.payload(), + )) + } +} diff --git a/implementations/rust/ockam/ockam_app/src/app/state/repository.rs b/implementations/rust/ockam/ockam_app/src/app/state/repository.rs index c6c46f108b4..42884c5ecf5 100644 --- a/implementations/rust/ockam/ockam_app/src/app/state/repository.rs +++ b/implementations/rust/ockam/ockam_app/src/app/state/repository.rs @@ -1,65 +1,62 @@ use std::path::Path; +use std::sync::Arc; -use miette::miette; - -use crate::app::state::model::ModelState; -use ockam::identity::storage::Storage; -use ockam::LmdbStorage; -use ockam_core::async_trait; +use miette::{miette, IntoDiagnostic}; +use ockam::{SqlxDatabase, ToSqlxType}; +use crate::app::state::model::{ModelState, TcpOutletRow}; use crate::Result; +use ockam_core::async_trait; +use sqlx::*; -const MODEL_STATE_ID: &str = "model_state"; -const MODEL_STATE_KEY: &str = "model_state_key"; - -/// The ModelStateRepository is responsible for storing and loading -/// ModelState data (user information, shared services etc...) -/// The state must be stored everytime it is modified (see set_user_info in AppState for example) -/// so that it can be loaded again when the application starts up #[async_trait] pub trait ModelStateRepository: Send + Sync + 'static { async fn store(&self, model_state: &ModelState) -> Result<()>; - async fn load(&self) -> Result>; + async fn load(&self) -> Result; } -/// This implementation of the ModelStateRepository piggy-backs for now on the LMDB storage -/// which is used to store all the data related to identities. -/// We will possibly store all data eventually using SQLite and in that case the ModelData -/// can be a set of tables dedicated to the desktop application -pub struct LmdbModelStateRepository { - storage: LmdbStorage, +pub struct ModelStateSqlxDatabase { + database: Arc, } -impl LmdbModelStateRepository { +impl ModelStateSqlxDatabase { + #[allow(unused)] pub async fn new>(path: P) -> Result { - Ok(Self { - storage: LmdbStorage::new(path).await.map_err(|e| miette!(e))?, - }) + Ok(Self::create(Arc::new( + SqlxDatabase::create(path).await.map_err(|e| miette!(e))?, + ))) + } + + pub fn create(database: Arc) -> Self { + Self { database } } } -/// The implementation simply serializes / deserializes the ModelState as JSON #[async_trait] -impl ModelStateRepository for LmdbModelStateRepository { +impl ModelStateRepository for ModelStateSqlxDatabase { async fn store(&self, model_state: &ModelState) -> Result<()> { - self.storage - .set( - MODEL_STATE_ID, - MODEL_STATE_KEY.to_string(), - serde_json::to_vec(model_state)?, - ) - .await - .map_err(|e| miette!(e))?; + for tcp_outlet in &model_state.tcp_outlets { + let query = query("INSERT INTO tcp_outlet VALUES (?, ?, ?, ?)") + .bind(tcp_outlet.socket_addr.to_sql()) + .bind(tcp_outlet.worker_addr.to_sql()) + .bind(tcp_outlet.alias.to_sql()) + .bind(tcp_outlet.payload.as_ref().map(|p| p.to_sql())); + query + .execute(&self.database.pool) + .await + .map(|_| ()) + .map_err(|e| miette!(e))?; + } Ok(()) } - async fn load(&self) -> Result> { - match self.storage.get(MODEL_STATE_ID, MODEL_STATE_KEY).await { - Err(e) => Err(miette!(e).into()), - Ok(None) => Ok(None), - Ok(Some(bytes)) => { - Ok(serde_json::from_slice(bytes.as_slice()).map_err(|e| miette!(e))?) - } - } + async fn load(&self) -> Result { + let query = query_as("SELECT * FROM tcp_outlet"); + let rows: Vec = query + .fetch_all(&self.database.pool) + .await + .into_diagnostic()?; + let values: Result> = rows.iter().map(|r| r.tcp_outlet_status()).collect(); + Ok(ModelState::new(values?)) } } diff --git a/implementations/rust/ockam/ockam_app/src/enroll/enroll_user.rs b/implementations/rust/ockam/ockam_app/src/enroll/enroll_user.rs index a9f353ebef6..6557737daa8 100644 --- a/implementations/rust/ockam/ockam_app/src/enroll/enroll_user.rs +++ b/implementations/rust/ockam/ockam_app/src/enroll/enroll_user.rs @@ -4,10 +4,8 @@ use tauri_plugin_notification::NotificationExt; use tracing::{debug, error, info}; use ockam_api::cli_state; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::{add_project_info_to_node_state, update_enrolled_identity, SpaceConfig}; -use ockam_api::cloud::project::{Project, Projects}; -use ockam_api::cloud::space::{Space, Spaces}; +use ockam_api::cloud::project::Project; +use ockam_api::cloud::space::Space; use ockam_api::enroll::enrollment::Enrollment; use ockam_api::enroll::oidc_service::OidcService; @@ -72,9 +70,7 @@ async fn enroll_with_token(app: &AppHandle, app_state: &AppState) let user_info = oidc_service.get_user_info(&token).await?; info!(?user_info, "User info retrieved successfully"); let cli_state = app_state.state().await; - cli_state - .users_info - .overwrite(&user_info.email, user_info.clone())?; + cli_state.store_user(&user_info).await?; // enroll the current user using that token on the controller { @@ -87,10 +83,15 @@ async fn enroll_with_token(app: &AppHandle, app_state: &AppState) let space = retrieve_space(app_state).await?; system_tray_on_update_with_enroll_status(app, "Retrieving project...")?; retrieve_project(app, app_state, &space).await?; - let identifier = update_enrolled_identity(&cli_state, NODE_NAME) + + let cli_state = app_state.state().await; + cli_state + .set_node_as_enrolled(NODE_NAME) .await .into_diagnostic()?; + let identifier = cli_state.get_node_identifier(NODE_NAME).await?; info!(%identifier, "User enrolled successfully"); + app.notification() .builder() .title("Enrolled successfully!") @@ -123,17 +124,11 @@ async fn retrieve_space(app_state: &AppState) -> Result { None => { let space_name = cli_state::random_name(); controller - .create_space(&app_state.context(), space_name, vec![]) + .create_space(&app_state.context(), &space_name, vec![]) .await .map_err(|e| miette!(e))? } }; - app_state - .state() - .await - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; - Ok(space) } @@ -169,16 +164,11 @@ async fn retrieve_project( .unwrap_or_else(|e| error!(?e, "Failed to create push notification")); let ctx = &app_state.context(); let project = controller - .create_project(ctx, space.id.to_string(), PROJECT_NAME.to_string(), vec![]) + .create_project(ctx, &space.id, PROJECT_NAME, vec![]) .await .map_err(|e| miette!(e))?; controller.wait_until_project_is_ready(ctx, project).await? } }; - let cli_state = app_state.state().await; - cli_state - .projects - .overwrite(&project.name, project.clone())?; - add_project_info_to_node_state(NODE_NAME, &cli_state, None).await?; Ok(project) } diff --git a/implementations/rust/ockam/ockam_app/src/invitations/commands.rs b/implementations/rust/ockam/ockam_app/src/invitations/commands.rs index de7d5d9ab1d..5f24e7d7e71 100644 --- a/implementations/rust/ockam/ockam_app/src/invitations/commands.rs +++ b/implementations/rust/ockam/ockam_app/src/invitations/commands.rs @@ -8,8 +8,7 @@ use tauri::{AppHandle, Manager, Runtime, State}; use tracing::{debug, info, trace, warn}; use ockam_api::address::get_free_address; -use ockam_api::cli_state::{CliState, StateDirTrait}; -use ockam_api::cloud::project::Project; +use ockam_api::cli_state::CliState; use ockam_api::cloud::share::InvitationListKind; use ockam_api::cloud::share::{CreateServiceInvitation, InvitationWithAccess, Invitations}; @@ -62,7 +61,7 @@ async fn accept_invitation_impl(id: String, app: &AppHandle) -> c debug!(?i, "Invitation was already accepted"); Ok(()) } - } + }; } } } @@ -214,7 +213,9 @@ async fn refresh_inlets(app: &AppHandle) -> crate::Result<()> { &cli_state, invitation, &invitations_state.accepted.inlets, - ) { + ) + .await + { Ok(i) => match i { Some(mut i) => { if !i.enabled { @@ -223,7 +224,7 @@ async fn refresh_inlets(app: &AppHandle) -> crate::Result<()> { } debug!(node = %i.local_node_name, "Checking node status"); - if let Ok(node) = cli_state.nodes.get(&i.local_node_name) { + if let Ok(node) = cli_state.get_node(&i.local_node_name).await { if node.is_running() { debug!(node = %i.local_node_name, "Node already running"); if let Ok(inlet) = background_node_client @@ -360,7 +361,7 @@ pub(crate) struct InletDataFromInvitation { } impl InletDataFromInvitation { - pub fn new( + pub async fn new( cli_state: &CliState, invitation: &InvitationWithAccess, inlets: &HashMap, @@ -385,9 +386,7 @@ impl InletDataFromInvitation { if let Some(project) = enrollment_ticket.project { // At this point, the project name will be the project id. - let project = cli_state - .projects - .overwrite(project.name.clone(), Project::from(project.clone()))?; + cli_state.store_project(project.clone()).await?; assert_eq!( project.name(), project.id(), @@ -431,17 +430,18 @@ impl InletDataFromInvitation { #[cfg(test)] mod tests { - use super::*; use ockam::identity::OneTimeCode; + use ockam_api::cloud::project::Project; use ockam_api::cloud::share::{ ReceivedInvitation, RoleInShare, ServiceAccessDetails, ShareScope, }; - use ockam_api::config::lookup::ProjectLookup; use ockam_api::identity::EnrollmentTicket; - #[test] - fn test_inlet_data_from_invitation() { - let cli_state = CliState::test().unwrap(); + use super::*; + + #[tokio::test] + async fn test_inlet_data_from_invitation() -> ockam_core::Result<()> { + let cli_state = CliState::test().await.unwrap(); let mut inlets = HashMap::new(); let mut invitation = InvitationWithAccess { invitation: ReceivedInvitation { @@ -459,6 +459,7 @@ mod tests { // InletDataFromInvitation will be none because `service_access_details` is none assert!( InletDataFromInvitation::new(&cli_state, &invitation, &inlets) + .await .unwrap() .is_none() ); @@ -471,29 +472,38 @@ mod tests { project_authority_identity: "Iabcdefabcdefabcdefabcdefabcdefabcdefabcd" .try_into() .unwrap(), - project_authority_route: "project_authority_route".to_string(), + project_authority_route: "/project/authority_route".to_string(), shared_node_identity: "I12ab34cd56ef12ab34cd56ef12ab34cd56ef12ab" .try_into() .unwrap(), shared_node_route: "shared_node_route".to_string(), enrollment_ticket: EnrollmentTicket::new( OneTimeCode::new(), - Some(ProjectLookup { - node_route: None, - id: "project_identity".to_string(), + Some(Project { + id: "project_id".to_string(), name: "project_name".to_string(), - identity_id: None, - authority: None, - okta: None, + space_name: "space_name".to_string(), + access_route: "route".to_string(), + users: vec![], + space_id: "space_id".to_string(), + identity: None, + authority_access_route: Some("/project/authority_route".to_string()), + authority_identity: Some("81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805".to_string()), + okta_config: None, + confluent_config: None, + version: None, + running: None, + operation_id: None, + user_roles: vec![], }), - None, ) - .hex_encoded() - .unwrap(), + .hex_encoded() + .unwrap(), }); // Validate the inlet data, with no prior inlet data let inlet_data = InletDataFromInvitation::new(&cli_state, &invitation, &inlets) + .await .unwrap() .unwrap(); assert!(inlet_data.socket_addr.is_none()); @@ -509,8 +519,10 @@ mod tests { }, ); let inlet_data = InletDataFromInvitation::new(&cli_state, &invitation, &inlets) + .await .unwrap() .unwrap(); assert!(inlet_data.socket_addr.is_some()); + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_app/src/invitations/mod.rs b/implementations/rust/ockam/ockam_app/src/invitations/mod.rs index 0fefc4fa860..0da6b91ac81 100644 --- a/implementations/rust/ockam/ockam_app/src/invitations/mod.rs +++ b/implementations/rust/ockam/ockam_app/src/invitations/mod.rs @@ -9,7 +9,6 @@ pub(crate) use tray_menu::*; use crate::app::{AppState, NODE_NAME}; use crate::Error; -use ockam_api::cli_state::StateDirTrait; use ockam_api::cloud::share::CreateServiceInvitation; use ockam_api::identity::EnrollmentTicket; use tauri::{AppHandle, Manager, Runtime, State}; @@ -32,15 +31,15 @@ pub(crate) async fn build_args_for_create_service_invitation( }) .await .ok_or::("outlet should exist".into())??; - let project = cli_state.projects.default()?; + let project = cli_state.get_default_project().await?; Ok(CreateServiceInvitation::new( &cli_state, None, project.name(), - recipient_email, - NODE_NAME, - service_route.to_string().as_str(), + recipient_email.to_string(), + NODE_NAME.to_string(), + service_route.to_string(), enrollment_ticket, ) .await?) diff --git a/implementations/rust/ockam/ockam_app/src/projects/commands.rs b/implementations/rust/ockam/ockam_app/src/projects/commands.rs index e807ea6f492..25f8d983233 100644 --- a/implementations/rust/ockam/ockam_app/src/projects/commands.rs +++ b/implementations/rust/ockam/ockam_app/src/projects/commands.rs @@ -3,11 +3,10 @@ use std::sync::Arc; use tauri::{async_runtime::RwLock, AppHandle, Manager, Runtime, State}; use tracing::{debug, error, info, trace, warn}; -use ockam_api::cloud::project::Projects; -use ockam_api::{cli_state::StateDirTrait, cloud::project::Project, identity::EnrollmentTicket}; +use ockam_api::{cloud::project::Project, identity::EnrollmentTicket}; use crate::app::AppState; -use crate::projects::error::Error::{InternalFailure, ListingFailed, StateSaveFailed}; +use crate::projects::error::Error::{InternalFailure, ListingFailed}; use super::error::{Error, Result}; use super::State as ProjectState; @@ -73,13 +72,6 @@ pub(crate) async fn refresh_projects(app: AppHandle) -> Result<() debug!("Projects fetched"); trace!(?projects); - let cli_projects = state.state().await.projects; - for project in &projects { - cli_projects - .overwrite(&project.name, project.clone()) - .map_err(|_| StateSaveFailed)?; - } - let project_state: State<'_, SyncAdminProjectsState> = app.state(); let mut writer = project_state.write().await; *writer = projects; diff --git a/implementations/rust/ockam/ockam_app/src/shared_service/relay/create.rs b/implementations/rust/ockam/ockam_app/src/shared_service/relay/create.rs index fd07e4aa5a6..0bf2aeacf37 100644 --- a/implementations/rust/ockam/ockam_app/src/shared_service/relay/create.rs +++ b/implementations/rust/ockam/ockam_app/src/shared_service/relay/create.rs @@ -2,7 +2,7 @@ use crate::app::NODE_NAME; use crate::Result; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{CliState, StateDirTrait}; +use ockam_api::cli_state::CliState; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::InMemoryNode; use ockam_multiaddr::MultiAddr; @@ -39,11 +39,11 @@ async fn create_relay_impl( node_manager: Arc, ) -> Result> { trace!("Creating relay"); - if !cli_state.is_enrolled().unwrap_or(false) { + if !cli_state.is_enrolled().await.unwrap_or(false) { trace!("Not enrolled, skipping relay creation"); return Ok(None); } - match cli_state.projects.default() { + match cli_state.get_default_project().await { Ok(project) => { if let Some(relay) = get_relay(node_manager.clone()).await { debug!(project = %project.name(), "Relay already exists"); diff --git a/implementations/rust/ockam/ockam_app/src/shared_service/tcp_outlet/state.rs b/implementations/rust/ockam/ockam_app/src/shared_service/tcp_outlet/state.rs index 51091890f2e..2e5a8a0dfad 100644 --- a/implementations/rust/ockam/ockam_app/src/shared_service/tcp_outlet/state.rs +++ b/implementations/rust/ockam/ockam_app/src/shared_service/tcp_outlet/state.rs @@ -26,7 +26,7 @@ pub(crate) async fn load_model_state( model_state: &ModelState, cli_state: &CliState, ) { - if !cli_state.is_enrolled().unwrap_or(false) { + if !cli_state.is_enrolled().await.unwrap_or(false) { return; } for tcp_outlet in model_state.get_tcp_outlets() { diff --git a/implementations/rust/ockam/ockam_app_lib/Cargo.toml b/implementations/rust/ockam/ockam_app_lib/Cargo.toml index 0808700a1ea..4c80cb8afee 100644 --- a/implementations/rust/ockam/ockam_app_lib/Cargo.toml +++ b/implementations/rust/ockam/ockam_app_lib/Cargo.toml @@ -41,6 +41,7 @@ ockam_multiaddr = { path = "../ockam_multiaddr", version = "0.37.0", features = ockam_transport_tcp = { path = "../ockam_transport_tcp", version = "^0.96.0" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "migrate"] } thiserror = "1.0" tokio = { version = "1.31.0", features = ["full"] } tokio-retry = "0.3" @@ -49,5 +50,8 @@ tracing-appender = "0.2.2" tracing-error = "0.2" tracing-subscriber = { version = "0.3.18", features = ["json"] } +[dev-dependencies] +tempfile = { version = "3.8.0" } + [build-dependencies] cbindgen = "0.26" diff --git a/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs b/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs index ba6a10c0b89..1d8bdd4a09d 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs @@ -4,10 +4,8 @@ use tracing::{debug, error, info}; use crate::api::notification::rust::{Kind, Notification}; use crate::api::state::OrchestratorStatus; use ockam_api::cli_state; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::{add_project_info_to_node_state, update_enrolled_identity, SpaceConfig}; -use ockam_api::cloud::project::{Project, Projects}; -use ockam_api::cloud::space::{Space, Spaces}; +use ockam_api::cloud::project::Project; +use ockam_api::cloud::space::Space; use ockam_api::enroll::enrollment::Enrollment; use ockam_api::enroll::oidc_service::OidcService; @@ -112,10 +110,8 @@ impl AppState { } let cli_state = self.state().await; - cli_state - .users_info - .overwrite(&user_info.email, user_info.clone())?; - cli_state.users_info.set_default(&user_info.email)?; + cli_state.store_user(&user_info).await?; + cli_state.set_default_user(&user_info.email).await?; // enroll the current user using that token on the controller { @@ -132,9 +128,12 @@ impl AppState { self.publish_state().await; self.retrieve_project(&space).await?; - let identifier = update_enrolled_identity(&cli_state, NODE_NAME) + let cli_state = self.state().await; + cli_state + .set_node_as_enrolled(NODE_NAME) .await .into_diagnostic()?; + let identifier = cli_state.get_node_identifier(NODE_NAME).await?; info!(%identifier, "User enrolled successfully"); Ok(EnrollmentOutcome::Successful) @@ -164,15 +163,11 @@ impl AppState { None => { let space_name = cli_state::random_name(); controller - .create_space(&context, space_name, vec![]) + .create_space(&self.context(), &space_name, vec![]) .await .map_err(|e| miette!(e))? } }; - self.state() - .await - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; Ok(space) } @@ -201,17 +196,12 @@ impl AppState { }); let ctx = &self.context(); let project = controller - .create_project(ctx, space.id.to_string(), PROJECT_NAME.to_string(), vec![]) + .create_project(ctx, &space.id, PROJECT_NAME, vec![]) .await .map_err(|e| miette!(e))?; controller.wait_until_project_is_ready(ctx, project).await? } }; - let cli_state = self.state().await; - cli_state - .projects - .overwrite(&project.name, project.clone())?; - add_project_info_to_node_state(NODE_NAME, &cli_state, None).await?; Ok(project) } } diff --git a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs index 66e96dc550d..72d7744b51d 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs @@ -1,17 +1,19 @@ -use crate::background_node::BackgroundNodeClient; -use crate::incoming_services::state::{IncomingService, Port}; -use crate::state::AppState; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + use miette::IntoDiagnostic; +use tracing::{debug, info, warn}; + use ockam_api::address::get_free_address; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::service::portals::Inlets; use ockam_api::ConnectionStatus; use ockam_core::api::Reply; use ockam_multiaddr::MultiAddr; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; -use tracing::{debug, info, warn}; + +use crate::background_node::BackgroundNodeClient; +use crate::incoming_services::state::{IncomingService, Port}; +use crate::state::AppState; impl AppState { pub(crate) async fn refresh_inlets(&self) -> crate::Result<()> { @@ -86,7 +88,7 @@ impl AppState { /// Returns true if the inlet is already connected to the destination node /// if any error occurs, it returns false async fn is_connected(&self, service: &IncomingService, inlet_node_name: &str) -> bool { - if self.state().await.nodes.exists(inlet_node_name) { + if self.state().await.get_node(inlet_node_name).await.is_ok() { if let Ok(mut inlet_node) = self.background_node(inlet_node_name).await { inlet_node.set_timeout(Duration::from_secs(5)); if let Ok(Reply::Successful(inlet)) = inlet_node diff --git a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs index 2683298be5f..261b8165cd4 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs @@ -2,4 +2,4 @@ mod commands; mod state; pub use state::IncomingServicesState; -pub use state::PersistentIncomingServiceState; +pub use state::PersistentIncomingService; diff --git a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs index 9eb45ae5fa2..921368947d0 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs @@ -13,7 +13,7 @@ pub type Port = u16; #[derive(Clone, Debug, Decode, Encode, Serialize, Deserialize, PartialEq)] #[rustfmt::skip] #[cbor(map)] -pub struct PersistentIncomingServiceState { +pub struct PersistentIncomingService { #[n(1)] pub(crate) invitation_id: String, #[n(2)] pub(crate) enabled: bool, #[n(3)] pub(crate) name: Option, @@ -35,10 +35,7 @@ impl IncomingServicesState { } impl ModelState { - pub(crate) fn upsert_incoming_service( - &mut self, - id: &str, - ) -> &mut PersistentIncomingServiceState { + pub(crate) fn upsert_incoming_service(&mut self, id: &str) -> &mut PersistentIncomingService { match self .incoming_services .iter_mut() @@ -47,7 +44,7 @@ impl ModelState { // we have to use index, see https://github.com/rust-lang/rust/issues/21906 Some(index) => &mut self.incoming_services[index], None => { - self.incoming_services.push(PersistentIncomingServiceState { + self.incoming_services.push(PersistentIncomingService { invitation_id: id.to_string(), enabled: true, name: None, @@ -268,17 +265,16 @@ impl IncomingService { #[cfg(test)] mod tests { - use crate::incoming_services::PersistentIncomingServiceState; + use crate::incoming_services::PersistentIncomingService; use crate::state::AppState; - use ockam::identity::{Identifier, OneTimeCode}; + use ockam::identity::OneTimeCode; use ockam::Context; use ockam_api::cli_state::CliState; + use ockam_api::cloud::project::Project; use ockam_api::cloud::share::{ InvitationWithAccess, ReceivedInvitation, RoleInShare, ServiceAccessDetails, ShareScope, }; - use ockam_api::config::lookup::ProjectLookup; use ockam_api::identity::EnrollmentTicket; - use std::str::FromStr; fn create_invitation_with( service_access_details: Option, @@ -313,20 +309,26 @@ mod tests { shared_node_route: "remote_service_name".to_string(), enrollment_ticket: EnrollmentTicket::new( OneTimeCode::new(), - Some(ProjectLookup { - node_route: None, + Some(Project { id: "project_id".to_string(), name: "project_name".to_string(), - identity_id: Some( - Identifier::from_str("I1234561234561234561234561234561234561234").unwrap(), - ), - authority: None, - okta: None, + space_name: "space_name".to_string(), + access_route: "route".to_string(), + users: vec![], + space_id: "space_id".to_string(), + identity: None, + authority_access_route: Some("/project/authority_route".to_string()), + authority_identity: Some("81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805".to_string()), + okta_config: None, + confluent_config: None, + version: None, + running: None, + operation_id: None, + user_roles: vec![], }), - None, ) - .hex_encoded() - .unwrap(), + .hex_encoded() + .unwrap(), } } @@ -334,7 +336,7 @@ mod tests { async fn test_inlet_data_from_invitation(context: &mut Context) -> ockam::Result<()> { // in this test we want to validate data loading from the accepted invitation // as well as using the related persistent data - let app_state = AppState::test(context, CliState::test().unwrap()).await; + let app_state = AppState::test(context, CliState::test().await?).await; let mut invitation = create_invitation_with(None); @@ -391,7 +393,7 @@ mod tests { // let's load another invitation, but persistent state for it already exists app_state .model_mut(|m| { - m.incoming_services.push(PersistentIncomingServiceState { + m.incoming_services.push(PersistentIncomingService { invitation_id: "second_invitation_id".to_string(), enabled: false, name: Some("custom_user_name".to_string()), diff --git a/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs b/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs index ff065c932ee..eef00be04c8 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs @@ -1,13 +1,15 @@ -use miette::IntoDiagnostic; use std::net::SocketAddr; use std::str::FromStr; + +use miette::IntoDiagnostic; use tracing::{debug, info, trace, warn}; +use ockam_api::cloud::share::{CreateServiceInvitation, InvitationListKind, Invitations}; + use crate::api::notification::rust::Notification; use crate::api::notification::Kind; use crate::invitations::state::ReceivedInvitationStatus; use crate::state::{AppState, StateKind, PROJECT_NAME}; -use ockam_api::cloud::share::{CreateServiceInvitation, InvitationListKind, Invitations}; impl AppState { /// Fetch received, accept and sent invitations from the orchestrator @@ -108,7 +110,7 @@ impl AppState { debug!(?i, "Invitation is in status {s:?}, skipping..."); Ok(()) } - } + }; } } } diff --git a/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs index f5e0910ba79..0eef7a211aa 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs @@ -6,7 +6,6 @@ use tracing::{debug, warn}; use crate::state::{AppState, NODE_NAME}; use crate::Error; -use ockam_api::cli_state::StateDirTrait; use ockam_api::cloud::share::CreateServiceInvitation; use ockam_api::identity::EnrollmentTicket; @@ -33,15 +32,15 @@ impl AppState { .ok_or::( format!("The outlet {outlet_socket_addr} wasn't found in the App state").into(), )??; - let project = cli_state.projects.default()?; + let project = cli_state.get_default_project().await?; Ok(CreateServiceInvitation::new( &cli_state, None, project.name(), - recipient_email, - NODE_NAME, - service_route.to_string().as_str(), + recipient_email.to_string(), + NODE_NAME.to_string(), + service_route.to_string(), enrollment_ticket, ) .await?) diff --git a/implementations/rust/ockam/ockam_app_lib/src/log.rs b/implementations/rust/ockam/ockam_app_lib/src/log.rs index c916992fe38..824bced243f 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/log.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/log.rs @@ -20,7 +20,6 @@ impl AppState { .runtime() .block_on(async move { this.state().await }); state - .nodes .stdout_logs(NODE_NAME) .expect("Failed to get stdout log path for node") }; diff --git a/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs b/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs index 58a42f7ba14..96bb57715e1 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs @@ -4,14 +4,14 @@ use std::time::Duration; use tracing::{debug, info, trace, warn}; use ockam_api::authenticator::enrollment_tokens::TokenIssuer; -use ockam_api::cloud::project::Projects; -use ockam_api::config::lookup::{ProjectAuthority, ProjectLookup}; -use ockam_api::{cli_state::StateDirTrait, cloud::project::Project, identity::EnrollmentTicket}; +use ockam_api::{cloud::project::Project, identity::EnrollmentTicket}; -use super::error::{Error, Result}; use crate::projects::error::Error::{InternalFailure, ListingFailed}; use crate::state::{AppState, StateKind}; +use super::error::{Error, Result}; + +// Store the user's admin projects impl AppState { pub(crate) async fn create_enrollment_ticket( &self, @@ -25,16 +25,10 @@ impl AppState { .find(|p| p.id == project_id) .ok_or_else(|| Error::ProjectNotFound(project_id.to_owned()))? .clone(); - let project_authority = ProjectAuthority::from_project(&project) - .await - .into_diagnostic()? - .ok_or(Error::ProjectInvalidState( - "project has no authority set".to_string(), - ))?; let authority_node = self .authority_node( - project_authority.identity_id(), - project_authority.address(), + &project.authority_identifier().await.into_diagnostic()?, + &project.authority_access_route().into_diagnostic()?, None, ) .await @@ -47,9 +41,7 @@ impl AppState { None, ) .await?; - let project_lookup = ProjectLookup::from_project(&project).await.ok(); - let trust_context = project.try_into().ok(); - Ok(EnrollmentTicket::new(otc, project_lookup, trust_context)) + Ok(EnrollmentTicket::new(otc, Some(project))) } pub(crate) async fn refresh_projects(&self) -> Result<()> { @@ -79,13 +71,6 @@ impl AppState { debug!("Projects fetched"); trace!(?projects); - let cli_projects = self.state().await.projects; - for project in &projects { - cli_projects - .overwrite(&project.name, project.clone()) - .map_err(|_| Error::StateSaveFailed)?; - } - *self.projects().write().await = projects; self.mark_as_loaded(StateKind::Projects); self.publish_state().await; diff --git a/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs b/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs index 797faafbeda..7e619e373b1 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs @@ -1,15 +1,18 @@ -use crate::api::state::OrchestratorStatus; -use crate::state::AppState; -use crate::Result; +use std::str::FromStr; +use std::sync::Arc; + use miette::IntoDiagnostic; +use tracing::{debug, info, trace, warn}; + use ockam::Context; -use ockam_api::cli_state::{CliState, StateDirTrait}; +use ockam_api::cli_state::CliState; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::InMemoryNode; use ockam_multiaddr::MultiAddr; -use std::str::FromStr; -use std::sync::Arc; -use tracing::{debug, info, trace, warn}; + +use crate::api::state::OrchestratorStatus; +use crate::state::AppState; +use crate::Result; impl AppState { /// Try to create a relay until it succeeds. @@ -66,7 +69,7 @@ impl AppState { node_manager: Arc, ) -> Result<()> { trace!("Creating relay"); - match cli_state.projects.default() { + match cli_state.get_default_project().await { Ok(project) => { if let Some(_relay) = get_relay(&node_manager, cli_state).await? { debug!(project = %project.name(), "Relay already exists"); @@ -83,7 +86,7 @@ impl AppState { .create_relay( context, &project_address, - Some(bare_relay_name(cli_state)?), + Some(bare_relay_name(cli_state).await?), false, None, ) @@ -108,7 +111,7 @@ async fn delete_relay( node_manager: &InMemoryNode, cli_state: &CliState, ) -> ockam::Result> { - let relay_name = relay_name(cli_state)?; + let relay_name = relay_name(cli_state).await?; node_manager.delete_relay(&context, &relay_name).await } @@ -116,7 +119,7 @@ async fn get_relay( node_manager: &InMemoryNode, cli_state: &CliState, ) -> ockam::Result> { - let relay_name = relay_name(cli_state)?; + let relay_name = relay_name(cli_state).await?; Ok(node_manager .get_relays() .await @@ -124,15 +127,15 @@ async fn get_relay( .find(|r| r.remote_address() == relay_name)) } -fn relay_name(cli_state: &CliState) -> ockam::Result { - let bare_relay_name = bare_relay_name(cli_state)?; +async fn relay_name(cli_state: &CliState) -> ockam::Result { + let bare_relay_name = bare_relay_name(cli_state).await?; Ok(format!("forward_to_{bare_relay_name}")) } -fn bare_relay_name(cli_state: &CliState) -> ockam::Result { +async fn bare_relay_name(cli_state: &CliState) -> ockam::Result { Ok(cli_state - .identities - .get_or_default(None)? + .get_default_named_identity() + .await? .identifier() .to_string()) } diff --git a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs index 54cddac4da8..fca2039c433 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs @@ -1,6 +1,9 @@ +use tracing::{debug, error}; + +#[cfg(test)] +use crate::incoming_services::PersistentIncomingService; use crate::state::{AppState, ModelState}; use ockam_api::nodes::models::portal::OutletStatus; -use tracing::{debug, error}; impl ModelState { pub fn add_tcp_outlet(&mut self, status: OutletStatus) { @@ -14,12 +17,17 @@ impl ModelState { pub fn get_tcp_outlets(&self) -> &[OutletStatus] { &self.tcp_outlets } + + #[cfg(test)] + pub fn add_incoming_service(&mut self, service: PersistentIncomingService) { + self.incoming_services.push(service); + } } impl AppState { pub(crate) async fn restore_tcp_outlets(&self) { let cli_state = self.state().await; - if !cli_state.is_enrolled().unwrap_or(false) { + if !cli_state.is_enrolled().await.ok().unwrap_or(false) { debug!("Not enrolled, skipping outlet restoration"); return; } diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs index 41ce86a3c5d..808279b63f8 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs @@ -1,8 +1,3 @@ -mod kind; -mod model; -mod repository; -mod tasks; - use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; @@ -11,42 +6,43 @@ use tokio::sync::RwLock; use tracing::{error, info, trace, warn}; use tracing_appender::non_blocking::WorkerGuard; -use crate::api::notification::rust::{Notification, NotificationCallback}; -use crate::api::state::rust::{ - ApplicationState, ApplicationStateCallback, Invitation, Invitee, LocalService, Service, - ServiceGroup, -}; -use crate::background_node::{BackgroundNodeClient, Cli}; -use crate::invitations::state::{InvitationState, ReceivedInvitationStatus}; -pub(crate) use crate::state::model::ModelState; -pub(crate) use crate::state::repository::{LmdbModelStateRepository, ModelStateRepository}; use ockam::identity::Identifier; use ockam::Context; use ockam::{NodeBuilder, TcpListenerOptions, TcpTransport}; -use ockam_api::cli_state::{ - add_project_info_to_node_state, init_node_state, CliState, StateDirTrait, StateItemTrait, -}; +use ockam_api::cli_state::CliState; use ockam_api::cloud::enroll::auth0::UserInfo; use ockam_api::cloud::project::Project; use ockam_api::cloud::{AuthorityNode, Controller}; use ockam_api::nodes::models::portal::OutletStatus; -use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType}; use ockam_api::nodes::service::{ NodeManagerGeneralOptions, NodeManagerTransportOptions, NodeManagerTrustOptions, }; use ockam_api::nodes::{BackgroundNode, InMemoryNode, NodeManagerWorker, NODEMANAGER_ADDR}; -use ockam_api::trust_context::TrustContextConfigBuilder; use ockam_multiaddr::MultiAddr; +use crate::api::notification::rust::{Notification, NotificationCallback}; +use crate::api::state::rust::{ + ApplicationState, ApplicationStateCallback, Invitation, Invitee, LocalService, Service, + ServiceGroup, +}; use crate::api::state::OrchestratorStatus; +use crate::background_node::{BackgroundNodeClient, Cli}; use crate::incoming_services::IncomingServicesState; +use crate::invitations::state::{InvitationState, ReceivedInvitationStatus}; use crate::scheduler::Scheduler; +pub(crate) use crate::state::model::ModelState; +pub(crate) use crate::state::repository::{ModelStateRepository, ModelStateSqlxDatabase}; use crate::state::tasks::{ RefreshInletsTask, RefreshInvitationsTask, RefreshProjectsTask, RefreshRelayTask, }; use crate::{api, Result}; pub use kind::StateKind; +mod kind; +mod model; +mod repository; +mod tasks; + pub const NODE_NAME: &str = "ockam_app"; // TODO: static project name of "default" is an unsafe default behavior due to backend uniqueness requirements pub const PROJECT_NAME: &str = "default"; @@ -99,7 +95,8 @@ impl AppState { application_state_callback: ApplicationStateCallback, notification_callback: NotificationCallback, ) -> Result { - let cli_state = CliState::initialize()?; + let cli_state = + CliState::with_default_dir().expect("Failed to load the local Ockam configuration"); let (context, mut executor) = NodeBuilder::new().no_logging().build(); let context = Arc::new(context); @@ -146,7 +143,6 @@ impl AppState { let model_state = model_state_repository .load() .await - .expect("Failed to load the model state") .unwrap_or(ModelState::default()); info!("AppState initialized"); @@ -288,16 +284,11 @@ impl AppState { let mut writer = self.model_state.write().await; *writer = ModelState::default(); } - let identity_path = self - .state() - .await - .identities - .identities_repository_path() - .expect("Failed to get the identities repository path"); - let new_state_repository = LmdbModelStateRepository::new(identity_path).await?; + let cli_state = &self.state().await; + let new_state_repository = create_model_state_repository(cli_state).await; { let mut writer = self.model_state_repository.write().await; - *writer = Arc::new(new_state_repository); + *writer = new_state_repository; } self.update_orchestrator_status(OrchestratorStatus::default()); self.publish_state().await; @@ -397,15 +388,15 @@ impl AppState { } pub async fn background_node(&self, node_name: &str) -> Result { - Ok(BackgroundNode::create(&self.context(), &self.state().await, node_name).await?) + Ok(BackgroundNode::create_to_node(&self.context(), &self.state().await, node_name).await?) } pub async fn delete_background_node(&self, node_name: &str) -> Result<()> { - Ok(self.state().await.nodes.delete(node_name)?) + Ok(self.state().await.delete_node(node_name, true).await?) } pub async fn is_enrolled(&self) -> Result { - self.state().await.is_enrolled().map_err(|e| { + self.state().await.is_enrolled().await.map_err(|e| { warn!(%e, "Failed to check if user is enrolled"); e.into() }) @@ -418,14 +409,7 @@ impl AppState { } pub async fn user_info(&self) -> Result { - Ok(self - .state - .read() - .await - .users_info - .default()? - .config() - .clone()) + Ok(self.state.read().await.get_default_user().await?) } pub async fn user_email(&self) -> Result { @@ -684,8 +668,6 @@ pub(crate) async fn make_node_manager( ctx: Arc, cli_state: &CliState, ) -> miette::Result> { - init_node_state(cli_state, NODE_NAME, None, None).await?; - let tcp = TcpTransport::create(&ctx).await.into_diagnostic()?; let options = TcpListenerOptions::new(); let listener = tcp @@ -693,20 +675,7 @@ pub(crate) async fn make_node_manager( .await .into_diagnostic()?; - add_project_info_to_node_state(NODE_NAME, cli_state, None).await?; - - let node_state = cli_state.nodes.get(NODE_NAME)?; - node_state.set_setup( - &node_state.config().setup_mut().set_api_transport( - CreateTransportJson::new( - TransportType::Tcp, - TransportMode::Listen, - &listener.socket_address().to_string(), - ) - .into_diagnostic()?, - ), - )?; - let trust_context_config = TrustContextConfigBuilder::new(cli_state).build(); + let _ = cli_state.create_node(NODE_NAME).await?; let node_manager = Arc::new( InMemoryNode::new( @@ -719,7 +688,7 @@ pub(crate) async fn make_node_manager( true, ), NodeManagerTransportOptions::new(listener.flow_control_id().clone(), tcp), - NodeManagerTrustOptions::new(trust_context_config), + NodeManagerTrustOptions::new(cli_state.get_default_trust_context().await.ok()), ) .await .into_diagnostic()?, @@ -737,13 +706,10 @@ pub(crate) async fn make_node_manager( /// Create the repository containing the model state async fn create_model_state_repository(state: &CliState) -> Arc { - let identity_path = state - .identities - .identities_repository_path() - .expect("Failed to get the identities repository path"); + let database_path = state.database_path(); - match LmdbModelStateRepository::new(identity_path).await { - Ok(model_state_repository) => Arc::new(model_state_repository), + match ModelStateSqlxDatabase::create_at(database_path).await { + Ok(model_state_repository) => model_state_repository, Err(e) => { error!(%e, "Cannot create a model state repository manager"); panic!("Cannot create a model state repository manager: {e:?}"); diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/model.rs b/implementations/rust/ockam/ockam_app_lib/src/state/model.rs index 422f744a57f..ddd736813c5 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/model.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/model.rs @@ -1,4 +1,4 @@ -use crate::incoming_services::PersistentIncomingServiceState; +use crate::incoming_services::PersistentIncomingService; use ockam_api::nodes::models::portal::OutletStatus; use serde::{Deserialize, Serialize}; @@ -9,7 +9,7 @@ pub struct ModelState { pub(crate) tcp_outlets: Vec, #[serde(default = "Vec::new")] - pub(crate) incoming_services: Vec, + pub(crate) incoming_services: Vec, } impl Default for ModelState { @@ -21,7 +21,7 @@ impl Default for ModelState { impl ModelState { pub fn new( tcp_outlets: Vec, - incoming_services: Vec, + incoming_services: Vec, ) -> Self { Self { tcp_outlets, diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs b/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs index 992678f69e8..0a16c537cfa 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs @@ -1,15 +1,20 @@ -use crate::state::model::ModelState; -use miette::miette; -use ockam::identity::storage::Storage; -use ockam::LmdbStorage; -use ockam_core::async_trait; +use std::net::SocketAddr; use std::path::Path; -use tracing::trace; +use std::str::FromStr; +use std::sync::Arc; -use crate::Result; +use sqlx::*; +use tracing::debug; + +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_api::nodes::models::portal::OutletStatus; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_core::{async_trait, Address}; -const MODEL_STATE_ID: &str = "model_state"; -const MODEL_STATE_KEY: &str = "model_state_key"; +use crate::incoming_services::PersistentIncomingService; +use crate::state::model::ModelState; +use crate::Result; /// The ModelStateRepository is responsible for storing and loading /// ModelState data (user information, shared services etc...) @@ -18,51 +23,118 @@ const MODEL_STATE_KEY: &str = "model_state_key"; #[async_trait] pub trait ModelStateRepository: Send + Sync + 'static { async fn store(&self, model_state: &ModelState) -> Result<()>; - async fn load(&self) -> Result>; + async fn load(&self) -> Result; } -/// This implementation of the ModelStateRepository piggy-backs for now on the LMDB storage -/// which is used to store all the data related to identities. -/// We will possibly store all data eventually using SQLite and in that case the ModelData -/// can be a set of tables dedicated to the desktop application -pub struct LmdbModelStateRepository { - storage: LmdbStorage, +#[derive(Clone)] +pub struct ModelStateSqlxDatabase { + database: Arc, } -impl LmdbModelStateRepository { - pub async fn new>(path: P) -> Result { - Ok(Self { - storage: LmdbStorage::new(path).await.map_err(|e| miette!(e))?, - }) +impl ModelStateSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for model state"); + Self { database } + } + + /// Create a database on the specified path + pub async fn create_at>(path: P) -> Result> { + Ok(Arc::new(Self::new(Arc::new( + SqlxDatabase::create(path).await?, + )))) + } + + /// Create a new in-memory database + #[allow(unused)] + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("model state")))) } } /// The implementation simply serializes / deserializes the ModelState as JSON #[async_trait] -impl ModelStateRepository for LmdbModelStateRepository { +impl ModelStateRepository for ModelStateSqlxDatabase { async fn store(&self, model_state: &ModelState) -> Result<()> { - self.storage - .set( - MODEL_STATE_ID, - MODEL_STATE_KEY.to_string(), - serde_json::to_vec(model_state)?, - ) - .await - .map_err(|e| miette!(e))?; - trace!(?model_state, "stored model state"); + let transaction = self.database.begin().await.into_core()?; + + for tcp_outlet_status in &model_state.tcp_outlets { + let query = query("INSERT OR REPLACE INTO tcp_outlet_status VALUES (?, ?, ?, ?)") + .bind(tcp_outlet_status.alias.to_sql()) + .bind(tcp_outlet_status.socket_addr.to_sql()) + .bind(tcp_outlet_status.worker_addr.to_sql()) + .bind(tcp_outlet_status.payload.as_ref().map(|p| p.to_sql())); + query.execute(&self.database.pool).await.void()?; + } + + for incoming_service in &model_state.incoming_services { + let query = query("INSERT OR REPLACE INTO incoming_service VALUES (?, ?, ?)") + .bind(incoming_service.invitation_id.to_sql()) + .bind(incoming_service.enabled.to_sql()) + .bind(incoming_service.name.as_ref().map(|n| n.to_sql())); + query.execute(&self.database.pool).await.void()?; + } + transaction.commit().await.void()?; + Ok(()) } - async fn load(&self) -> Result> { - match self.storage.get(MODEL_STATE_ID, MODEL_STATE_KEY).await { - Err(e) => Err(miette!(e).into()), - Ok(None) => Ok(None), - Ok(Some(bytes)) => { - let state = serde_json::from_slice(bytes.as_slice()).map_err(|e| miette!(e))?; - trace!(?state, "loaded model state"); - Ok(state) - } - } + async fn load(&self) -> Result { + let query1 = query_as("SELECT * FROM tcp_outlet_status"); + let result: Vec = + query1.fetch_all(&self.database.pool).await.into_core()?; + let tcp_outlets = result + .into_iter() + .map(|r| r.tcp_outlet_status()) + .collect::>>()?; + + let query2 = query_as("SELECT * FROM incoming_service"); + let result: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let incoming_services = result + .into_iter() + .map(|r| r.persistent_incoming_service()) + .collect::>>()?; + Ok(ModelState::new(tcp_outlets, incoming_services)) + } +} + +#[derive(sqlx::FromRow)] +struct TcpOutletStatusRow { + alias: String, + socket_addr: String, + worker_addr: String, + payload: Option, +} + +impl TcpOutletStatusRow { + fn tcp_outlet_status(&self) -> Result { + let socket_addr = SocketAddr::from_str(&self.socket_addr) + .map_err(|e| Error::new(Origin::Application, Kind::Serialization, e.to_string()))?; + let worker_addr = Address::from_string(&self.worker_addr); + Ok(OutletStatus { + alias: self.alias.clone(), + socket_addr, + worker_addr, + payload: self.payload.clone(), + }) + } +} + +#[derive(sqlx::FromRow)] +struct PersistentIncomingServiceRow { + invitation_id: String, + enabled: bool, + name: Option, +} + +impl PersistentIncomingServiceRow { + fn persistent_incoming_service(&self) -> Result { + Ok(PersistentIncomingService { + invitation_id: self.invitation_id.clone(), + enabled: self.enabled, + name: self.name.clone(), + }) } } @@ -71,30 +143,37 @@ mod tests { use super::*; use ockam_api::nodes::models::portal::OutletStatus; use ockam_core::Address; + use std::path::Path; + use tempfile::NamedTempFile; #[tokio::test] - async fn store_and_load_tcp_outlets() { - let path = std::env::temp_dir().join("ockam_app_lib_test"); - let _ = std::fs::remove_dir_all(&path); + async fn store_and_load() -> Result<()> { + let db_file = NamedTempFile::new()?; + let repository = create_repository(db_file.path()).await?; - // Initial state - let repo = LmdbModelStateRepository::new(&path).await.unwrap(); let mut state = ModelState::default(); - repo.store(&state).await.unwrap(); - let loaded = repo.load().await.unwrap().unwrap(); + repository.store(&state).await?; + let loaded = repository.load().await?; assert!(state.tcp_outlets.is_empty()); assert_eq!(state, loaded); // Add a tcp outlet state.add_tcp_outlet(OutletStatus::new( - "127.0.0.1:1001".parse().unwrap(), + "127.0.0.1:1001".parse()?, Address::from_string("s1"), "s1", None, )); - repo.store(&state).await.unwrap(); - let loaded = repo.load().await.unwrap().unwrap(); + // Add an incoming service + state.add_incoming_service(PersistentIncomingService { + invitation_id: "1235".to_string(), + enabled: true, + name: Some("aws".to_string()), + }); + repository.store(&state).await?; + let loaded = repository.load().await?; assert_eq!(state.tcp_outlets.len(), 1); + assert_eq!(state.incoming_services.len(), 1); assert_eq!(state, loaded); // Add a few more @@ -105,16 +184,25 @@ mod tests { &format!("s{i}"), None, )); - repo.store(&state).await.unwrap(); + repository.store(&state).await.unwrap(); } - let loaded = repo.load().await.unwrap().unwrap(); + let loaded = repository.load().await?; assert_eq!(state.tcp_outlets.len(), 5); assert_eq!(state, loaded); // Reload from DB scratch to emulate an app restart - let repo = LmdbModelStateRepository::new(&path).await.unwrap(); - let loaded = repo.load().await.unwrap().unwrap(); + let repository = create_repository(db_file.path()).await?; + let loaded = repository.load().await?; assert_eq!(state.tcp_outlets.len(), 5); + assert_eq!(state.incoming_services.len(), 1); assert_eq!(state, loaded); + + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(ModelStateSqlxDatabase::new(Arc::new(db)))) } } diff --git a/implementations/rust/ockam/ockam_command/Cargo.toml b/implementations/rust/ockam/ockam_command/Cargo.toml index 27ce403318e..0d184954105 100644 --- a/implementations/rust/ockam/ockam_command/Cargo.toml +++ b/implementations/rust/ockam/ockam_command/Cargo.toml @@ -67,6 +67,7 @@ ctrlc = { version = "3.4.1", features = ["termination"] } dialoguer = "0.11.0" duct = "0.13" flate2 = "1.0.28" +futures = "0.3.28" hex = "0.4" home = "0.5" indicatif = "0.17.7" diff --git a/implementations/rust/ockam/ockam_command/src/authenticated.rs b/implementations/rust/ockam/ockam_command/src/authenticated.rs index 2db61f78234..c81d014fd6b 100644 --- a/implementations/rust/ockam/ockam_command/src/authenticated.rs +++ b/implementations/rust/ockam/ockam_command/src/authenticated.rs @@ -1,4 +1,3 @@ -use crate::node::get_node_name; use crate::output::Output; use crate::util::node_rpc; use crate::util::parsers::identity_identifier_parser; @@ -8,7 +7,6 @@ use clap::{Args, Subcommand}; use miette::{miette, Context as _}; use ockam::identity::{AttributesEntry, Identifier}; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::auth::AuthorizationApi; use ockam_api::is_local_node; use ockam_api::nodes::BackgroundNode; @@ -78,9 +76,7 @@ async fn make_background_node_client( addr: &MultiAddr, ) -> Result { is_local_node(addr).context("The address must point to a local node")?; - let to = get_node_name(&opts.state, &Some(addr.to_string())); - let node_name = extract_address_value(&to)?; - Ok(BackgroundNode::create(ctx, &opts.state, &node_name).await?) + Ok(BackgroundNode::create_to_node(ctx, &opts.state, &addr.to_string()).await?) } struct IdentifierWithAttributes { diff --git a/implementations/rust/ockam/ockam_command/src/authority/create.rs b/implementations/rust/ockam/ockam_command/src/authority/create.rs index 0af6513dbda..37a75f163c6 100644 --- a/implementations/rust/ockam/ockam_command/src/authority/create.rs +++ b/implementations/rust/ockam/ockam_command/src/authority/create.rs @@ -1,25 +1,29 @@ -use crate::node::util::run_ockam; -use crate::util::{embedded_node_that_is_not_stopped, exitcode}; -use crate::util::{local_cmd, node_rpc}; -use crate::{docs, identity, CommandGlobalOpts, Result}; -use clap::{ArgGroup, Args}; -use miette::Context as _; +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; +use std::process; + +use clap::ArgGroup; +use clap::Args; use miette::{miette, IntoDiagnostic}; +use serde::{Deserialize, Serialize}; +use tracing::debug; + use ockam::identity::{AttributesEntry, Identifier}; use ockam::Context; use ockam_api::authority_node; use ockam_api::authority_node::{OktaConfiguration, TrustedIdentity}; use ockam_api::bootstrapped_identities_store::PreTrustedIdentities; -use ockam_api::cli_state::init_node_state; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType}; +use ockam_api::config::lookup::InternetAddress; +use ockam_api::nodes::NodeInfo; use ockam_api::DefaultAddress; use ockam_core::compat::collections::HashMap; use ockam_core::compat::fmt; -use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::path::PathBuf; -use tracing::debug; + +use crate::node::util::run_ockam; +use crate::util::parsers::internet_address_parser; +use crate::util::{embedded_node_that_is_not_stopped, exitcode}; +use crate::util::{local_cmd, node_rpc}; +use crate::{docs, CommandGlobalOpts, Result}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -28,9 +32,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create an Authority node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] #[clap(group(ArgGroup::new("trusted").required(true).args(& ["trusted_identities", "reload_from_trusted_identities_file"])))] pub struct CreateCommand { @@ -44,13 +48,14 @@ pub struct CreateCommand { /// TCP listener address #[arg( - display_order = 900, - long, - short, - id = "SOCKET_ADDRESS", - default_value = "127.0.0.1:4000" + display_order = 900, + long, + short, + id = "SOCKET_ADDRESS", + default_value = "127.0.0.1:4000", + value_parser = internet_address_parser )] - tcp_listener_address: String, + tcp_listener_address: InternetAddress, /// `authority create` started a child process to run this node in foreground. #[arg(long, hide = true)] @@ -107,14 +112,14 @@ async fn spawn_background_node( opts: &CommandGlobalOpts, cmd: &CreateCommand, ) -> miette::Result<()> { - // Create node state, including the vault and identity if they don't exist - init_node_state( - &opts.state, - &cmd.node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), - ) - .await?; + opts.state + .create_node_with_optional_name_and_optional_vault_and_optional_project( + &Some(cmd.node_name.clone()), + &cmd.identity, + &cmd.vault, + &None, + ) + .await?; // Construct the arguments list and re-execute the ockam // CLI in foreground mode to start the newly created node @@ -128,7 +133,7 @@ async fn spawn_background_node( "--project-identifier".to_string(), cmd.project_identifier.clone(), "--tcp-listener-address".to_string(), - cmd.tcp_listener_address.clone(), + cmd.tcp_listener_address.to_string(), "--foreground".to_string(), "--child-process".to_string(), ]; @@ -187,7 +192,7 @@ async fn spawn_background_node( } args.push(cmd.node_name.to_string()); - run_ockam(opts, &cmd.node_name, args, cmd.logging_to_file()) + run_ockam(opts, &cmd.node_name, args, cmd.logging_to_file()).await } impl CreateCommand { @@ -259,43 +264,37 @@ async fn start_authority_node( ) -> miette::Result<()> { let (opts, cmd) = args; - // Create node state, including the vault and identity if they don't exist - if !opts.state.nodes.exists(&cmd.node_name) { - init_node_state( - &opts.state, - &cmd.node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), - ) - .await?; - }; - // Retrieve the authority identity if it has been created before // otherwise create a new one - let identifier = match &cmd.identity { - Some(identity_name) => { - debug!(name=%identity_name, "getting identity from state"); - opts.state - .identities - .get(identity_name) - .context("Identity not found")? - .config() - .identifier() - } - None => { - debug!("getting default identity from state"); - match opts.state.identities.default() { - Ok(state) => state.config().identifier(), - Err(_) => { - debug!("creating default identity"); - let cmd = identity::CreateCommand::new("authority".into(), None, None); - cmd.create_identity(opts.clone()).await? - } - } - } + let identity_name = cmd.identity.clone().unwrap_or("authority".to_string()); + let identifier = match opts.state.get_identifier_by_name(&identity_name).await.ok() { + Some(identifier) => identifier, + None => opts + .state + .create_identity_with_optional_name_and_optional_vault( + &Some("authority".into()), + &cmd.vault, + ) + .await? + .identifier(), }; debug!(identifier=%identifier, "authority identifier"); + // persist the node state and mark it as an authority node + // That flag allows the node to be seen as UP when listing the nodes with the + // the `ockam node list` command, without having to send a TCP query to open a connection + // because this would fail if there is no intention to create a secure channel + let node = NodeInfo::new( + cmd.node_name.clone(), + identifier.clone(), + 0, + false, + true, + Some(cmd.tcp_listener_address.clone()), + Some(process::id()), + ); + opts.state.store_node(&node).await?; + let okta_configuration = match (&cmd.tenant_base_url, &cmd.certificate, &cmd.attributes) { (Some(tenant_base_url), Some(certificate), Some(attributes)) => Some(OktaConfiguration { address: DefaultAddress::OKTA_IDENTITY_PROVIDER.to_string(), @@ -306,34 +305,11 @@ async fn start_authority_node( _ => None, }; - // persist the node state and mark it as an authority node - // That flag allows the node to be seen as UP when listing the nodes with the - // the `ockam node list` command, without having to send a TCP query to open a connection - // because this would fail if there is no intention to create a secure channel - debug!("updating node state's setup config"); - let node_state = opts.state.nodes.get(&cmd.node_name)?; - node_state.set_setup( - &node_state - .config() - .setup_mut() - .set_verbose(opts.global_args.verbose) - .set_authority_node() - .set_api_transport( - CreateTransportJson::new( - TransportType::Tcp, - TransportMode::Listen, - cmd.tcp_listener_address.as_str(), - ) - .into_diagnostic()?, - ), - )?; - let trusted_identities = cmd.trusted_identities(&identifier)?; let configuration = authority_node::Configuration { identifier, - storage_path: opts.state.identities.identities_repository_path()?, - vault_path: opts.state.vaults.default()?.vault_file_path().clone(), + database_path: opts.state.database_path(), project_identifier: cmd.project_identifier, tcp_listener_address: cmd.tcp_listener_address, secure_channel_listener_name: None, @@ -362,10 +338,11 @@ fn parse_trusted_identities(values: &str) -> Result { #[cfg(test)] mod tests { - use super::*; use ockam::identity::Identifier; use ockam_core::compat::collections::HashMap; + use super::*; + #[test] fn test_parse_trusted_identities() { let identity1 = Identifier::try_from("Ie86be15e83d1c93e24dd1967010b01b6df491b45").unwrap(); @@ -386,7 +363,9 @@ mod tests { TrustedIdentity::new(&identity2, &attributes2), TrustedIdentity::new(&identity1, &attributes1), ]; - assert_eq!(actual.trusted_identities(), expected); + let mut trusted_identities = actual.trusted_identities(); + trusted_identities.sort_by_key(|t| t.identifier()); + assert_eq!(trusted_identities, expected); } } diff --git a/implementations/rust/ockam/ockam_command/src/configuration/get.rs b/implementations/rust/ockam/ockam_command/src/configuration/get.rs index 69b659d7de2..c4b8ff5f197 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/get.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/get.rs @@ -1,7 +1,9 @@ -use crate::util::local_cmd; -use crate::CommandGlobalOpts; use clap::Args; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] pub struct GetCommand { @@ -11,13 +13,19 @@ pub struct GetCommand { impl GetCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options, self)); + node_rpc(run_impl, (options, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: GetCommand) -> miette::Result<()> { - let node_state = opts.state.nodes.get(cmd.alias)?; - let addr = &node_state.config().setup().api_transport()?.addr; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, GetCommand), +) -> miette::Result<()> { + let node_info = opts.state.get_node(&cmd.alias).await?; + let addr = &node_info + .tcp_listener_address() + .map(|a| a.to_string()) + .unwrap_or("N/A".to_string()); println!("Address: {addr}"); Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs b/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs index 1b449321a3d..7572b8b663e 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs @@ -1,6 +1,7 @@ use clap::Args; +use ockam_node::Context; -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] @@ -8,11 +9,16 @@ pub struct GetDefaultNodeCommand {} impl GetDefaultNodeCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options)); + node_rpc(run_impl, options); } } -fn run_impl(_opts: CommandGlobalOpts) -> miette::Result<()> { - // TODO: get from opts.state.nodes().default() - todo!() +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { + let node_info = opts.state.get_default_node().await?; + let addr = &node_info + .tcp_listener_address() + .map(|a| a.to_string()) + .unwrap_or("N/A".to_string()); + println!("Address: {addr}"); + Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/configuration/list.rs b/implementations/rust/ockam/ockam_command/src/configuration/list.rs index 9727cd51668..bad27cf9037 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/list.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/list.rs @@ -1,19 +1,21 @@ -use crate::util::local_cmd; -use crate::CommandGlobalOpts; use clap::Args; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] pub struct ListCommand {} impl ListCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options, self)); + node_rpc(run_impl, options); } } -fn run_impl(opts: CommandGlobalOpts, _cmd: ListCommand) -> miette::Result<()> { - for node in opts.state.nodes.list()? { +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { + for node in opts.state.get_nodes().await? { opts.terminal.write(format!("Node: {}\n", node.name()))?; } Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs b/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs index dfa0249a213..8c0cf34dfeb 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs @@ -1,6 +1,8 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::CommandGlobalOpts; use clap::Args; +use miette::IntoDiagnostic; +use ockam_node::Context; #[derive(Clone, Debug, Args)] pub struct SetDefaultNodeCommand { @@ -9,12 +11,17 @@ pub struct SetDefaultNodeCommand { } impl SetDefaultNodeCommand { - pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(&self.name, &options)); + pub fn run(self, opts: CommandGlobalOpts) { + node_rpc(run_impl, (opts, self)); } } -fn run_impl(_name: &str, _options: &CommandGlobalOpts) -> miette::Result<()> { - // TODO: add symlink to options.state.defaults().node - todo!() +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, SetDefaultNodeCommand), +) -> miette::Result<()> { + opts.state + .set_default_node(&cmd.name) + .await + .into_diagnostic() } diff --git a/implementations/rust/ockam/ockam_command/src/credential/get.rs b/implementations/rust/ockam/ockam_command/src/credential/get.rs index 97f5e44af21..52c1f2947dc 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/get.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/get.rs @@ -3,7 +3,7 @@ use clap::Args; use ockam::Context; use ockam_api::nodes::{BackgroundNode, Credentials}; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -22,7 +22,6 @@ pub struct GetCommand { impl GetCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -32,8 +31,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, GetCommand)) -> miet } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: GetCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; node.get_credential(ctx, cmd.overwrite, cmd.identity) .await?; Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/credential/issue.rs b/implementations/rust/ockam/ockam_command/src/credential/issue.rs index 75a7deafe4f..6af0639e015 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/issue.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/issue.rs @@ -1,20 +1,17 @@ -use ockam_core::compat::collections::HashMap; - -use crate::identity::{get_identity_name, initialize_identity_if_default}; -use crate::{ - util::{node_rpc, parsers::identity_identifier_parser}, - vault::default_vault_name, - CommandGlobalOpts, Result, -}; use clap::Args; - -use crate::output::{CredentialAndPurposeKeyDisplay, EncodeFormat}; use miette::{miette, IntoDiagnostic}; + use ockam::identity::utils::AttributesBuilder; use ockam::identity::Identifier; use ockam::identity::{MAX_CREDENTIAL_VALIDITY, PROJECT_MEMBER_SCHEMA, TRUST_CONTEXT_ID}; use ockam::Context; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; +use ockam_core::compat::collections::HashMap; + +use crate::output::{CredentialAndPurposeKeyDisplay, EncodeFormat}; +use crate::{ + util::{node_rpc, parsers::identity_identifier_parser}, + CommandGlobalOpts, Result, +}; #[derive(Clone, Debug, Args)] pub struct IssueCommand { @@ -40,7 +37,6 @@ pub struct IssueCommand { impl IssueCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.as_identity); node_rpc(run_impl, (opts, self)); } @@ -64,23 +60,18 @@ async fn run_impl( _ctx: Context, (opts, cmd): (CommandGlobalOpts, IssueCommand), ) -> miette::Result<()> { - let identity_name = get_identity_name(&opts.state, &cmd.as_identity); - let ident_state = opts.state.identities.get(&identity_name)?; - let auth_identity_identifier = ident_state.config().identifier().clone(); - - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); - let vault = opts.state.vaults.get(&vault_name)?.get().await?; - let identities = opts.state.get_identities(vault).await?; - let issuer = ident_state.identifier(); + let authority = opts + .state + .get_identifier_by_optional_name(&cmd.as_identity) + .await?; + + let identities = opts + .state + .get_identities_with_optional_vault_name(&cmd.vault) + .await?; let mut attributes_builder = AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA) - .with_attribute( - TRUST_CONTEXT_ID.to_vec(), - auth_identity_identifier.to_string(), - ); + .with_attribute(TRUST_CONTEXT_ID.to_vec(), authority.to_string()); for (key, value) in cmd.attributes()? { attributes_builder = attributes_builder.with_attribute(key.as_bytes().to_vec(), value.as_bytes().to_vec()); @@ -90,7 +81,7 @@ async fn run_impl( .credentials() .credentials_creation() .issue_credential( - &issuer, + &authority, cmd.identity_identifier(), attributes_builder.build(), MAX_CREDENTIAL_VALIDITY, diff --git a/implementations/rust/ockam/ockam_command/src/credential/list.rs b/implementations/rust/ockam/ockam_command/src/credential/list.rs index 238f3e4a8de..871d2bd5fb8 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/list.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/list.rs @@ -2,11 +2,8 @@ use clap::{arg, Args}; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; -use crate::{ - fmt_log, terminal::OckamColor, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, -}; +use crate::{fmt_log, terminal::OckamColor, util::node_rpc, CommandGlobalOpts}; use super::CredentialOutput; @@ -30,15 +27,12 @@ async fn run_impl( opts.terminal .write_line(&fmt_log!("Listing Credentials...\n"))?; - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); + let vault_name = opts.state.get_vault_name_or_default(&cmd.vault).await?; let mut credentials: Vec = Vec::new(); - for cred_state in opts.state.credentials.list()? { - let cred = CredentialOutput::try_from_state(&opts, &cred_state, &vault_name).await?; - credentials.push(cred); + for credential in opts.state.get_credentials().await? { + let credential_output = CredentialOutput::new(credential).await; + credentials.push(credential_output); } let list = opts.terminal.build_list( diff --git a/implementations/rust/ockam/ockam_command/src/credential/mod.rs b/implementations/rust/ockam/ockam_command/src/credential/mod.rs index f0bc9680f7b..61a44417e99 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/mod.rs @@ -1,29 +1,25 @@ -pub(crate) mod get; -pub(crate) mod issue; -pub(crate) mod list; -pub(crate) mod present; -pub(crate) mod show; -pub(crate) mod store; -pub(crate) mod verify; - +use clap::{Args, Subcommand}; use colorful::Colorful; + pub(crate) use get::GetCommand; pub(crate) use issue::IssueCommand; pub(crate) use list::ListCommand; -use ockam::identity::{Identifier, Identities}; -use ockam_api::cli_state::{CredentialState, StateItemTrait}; +use ockam_api::cli_state::NamedCredential; pub(crate) use present::PresentCommand; pub(crate) use show::ShowCommand; -use std::sync::Arc; pub(crate) use store::StoreCommand; pub(crate) use verify::VerifyCommand; use crate::output::{CredentialAndPurposeKeyDisplay, Output}; use crate::{CommandGlobalOpts, Result}; -use clap::{Args, Subcommand}; -use miette::IntoDiagnostic; -use ockam::identity::models::CredentialAndPurposeKey; -use ockam_api::cli_state::traits::StateDirTrait; + +pub(crate) mod get; +pub(crate) mod issue; +pub(crate) mod list; +pub(crate) mod present; +pub(crate) mod show; +pub(crate) mod store; +pub(crate) mod verify; /// Manage Credentials #[derive(Clone, Debug, Args)] @@ -59,40 +55,6 @@ impl CredentialCommand { } } -pub async fn identities(vault_name: &str, opts: &CommandGlobalOpts) -> Result> { - let vault = opts.state.vaults.get(vault_name)?.get().await?; - let identities = opts.state.get_identities(vault).await?; - - Ok(identities) -} - -pub async fn identity(identity: &str, identities: Arc) -> Result { - let identity_as_bytes = hex::decode(identity)?; - - let identifier = identities - .identities_creation() - .import(None, &identity_as_bytes) - .await?; - - Ok(identifier) -} - -pub async fn validate_encoded_cred( - encoded_cred: &[u8], - identities: Arc, - issuer: &Identifier, -) -> Result<()> { - let cred: CredentialAndPurposeKey = minicbor::decode(encoded_cred)?; - - identities - .credentials() - .credentials_verification() - .verify_credential(None, &[issuer.clone()], &cred) - .await?; - - Ok(()) -} - pub struct CredentialOutput { name: String, credential: String, @@ -100,33 +62,15 @@ pub struct CredentialOutput { } impl CredentialOutput { - pub async fn try_from_state( - opts: &CommandGlobalOpts, - state: &CredentialState, - vault_name: &str, - ) -> Result { - let config = state.config(); - - let identities = identities(vault_name, opts).await.into_diagnostic()?; - - let is_verified = validate_encoded_cred( - &config.encoded_credential, - identities, - &config.issuer_identifier, - ) - .await - .is_ok(); - - let credential = config.credential()?; - let credential = format!("{}", CredentialAndPurposeKeyDisplay(credential)); - - let output = Self { - name: state.name().to_string(), - credential, - is_verified, - }; - - Ok(output) + pub async fn new(credential: NamedCredential) -> Self { + Self { + name: credential.name(), + credential: format!( + "{}", + CredentialAndPurposeKeyDisplay(credential.credential_and_purpose_key()) + ), + is_verified: true, + } } } diff --git a/implementations/rust/ockam/ockam_command/src/credential/present.rs b/implementations/rust/ockam/ockam_command/src/credential/present.rs index bb5a2a69554..cb9f60cb5d0 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/present.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/present.rs @@ -4,7 +4,7 @@ use ockam::Context; use ockam_api::nodes::{BackgroundNode, Credentials}; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -22,7 +22,6 @@ pub struct PresentCommand { impl PresentCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -36,8 +35,7 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: PresentCommand, ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; node.present_credential(ctx, &cmd.to, cmd.oneway).await?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/credential/show.rs b/implementations/rust/ockam/ockam_command/src/credential/show.rs index 5160f3fdd5b..27f10c452bd 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/show.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/show.rs @@ -1,15 +1,10 @@ use clap::{arg, Args}; use colorful::Colorful; use indoc::formatdoc; -use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; -use crate::credential::identities; use crate::output::CredentialAndPurposeKeyDisplay; -use crate::{ - credential::validate_encoded_cred, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, -}; +use crate::{util::node_rpc, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct ShowCommand { @@ -31,43 +26,20 @@ async fn run_impl( _ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); + let named_credential = opts + .state + .get_credential_by_name(&cmd.credential_name) + .await?; - let cred_name = &cmd.credential_name; - let cred = opts.state.credentials.get(cred_name)?; - let cred_config = cred.config(); - - let identities = identities(&vault_name, &opts).await?; - identities - .identities_creation() - .import( - Some(&cred_config.issuer_identifier), - &cred_config.encoded_issuer_change_history, - ) - .await - .into_diagnostic()?; - - let is_verified = match validate_encoded_cred( - &cred_config.encoded_credential, - identities, - &cred_config.issuer_identifier, - ) - .await - { - Ok(_) => "✔︎".light_green(), - Err(_) => "✕".light_red(), - }; - - let cred = cred_config.credential()?; + let is_verified = "✔︎".light_green(); + let credential = named_credential.credential_and_purpose_key(); let plain = formatdoc!( r#" - Credential: {cred_name} {is_verified} + Credential: {} {is_verified} {} "#, - CredentialAndPurposeKeyDisplay(cred) + &cmd.credential_name, + CredentialAndPurposeKeyDisplay(credential) ); opts.terminal.stdout().plain(plain).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/credential/store.rs b/implementations/rust/ockam/ockam_command/src/credential/store.rs index 29f5f9d786c..858520e08bc 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/store.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/store.rs @@ -1,16 +1,17 @@ -use crate::credential::{identities, identity}; -use crate::{ - credential::validate_encoded_cred, fmt_log, fmt_ok, terminal::OckamColor, util::node_rpc, - vault::default_vault_name, CommandGlobalOpts, -}; +use std::path::PathBuf; + use clap::Args; use colorful::Colorful; -use miette::miette; +use miette::IntoDiagnostic; +use tokio::sync::Mutex; +use tokio::try_join; + +use ockam::identity::Identity; use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::{CredentialConfig, StateDirTrait}; -use std::path::PathBuf; -use tokio::{sync::Mutex, try_join}; + +use crate::credential::verify::verify_credential; +use crate::{fmt_log, fmt_ok, terminal::OckamColor, util::node_rpc, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct StoreCommand { @@ -50,57 +51,29 @@ async fn run_impl( let is_finished: Mutex = Mutex::new(false); let send_req = async { - let cred_as_str = match (&cmd.credential, &cmd.credential_path) { - (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path) - .await? - .trim() - .to_string(), - (Some(credential), _) => credential.to_string(), - _ => { - *is_finished.lock().await = true; - return crate::Result::Err( - miette!("Credential or Credential Path argument must be provided").into(), - ); - } - }; - - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); - - let identities = match identities(&vault_name, &opts).await { - Ok(i) => i, - Err(_) => { - *is_finished.lock().await = true; - return Err(miette!("Invalid state").into()); - } - }; - - let issuer = match identity(&cmd.issuer, identities.clone()).await { - Ok(i) => i, - Err(_) => { - *is_finished.lock().await = true; - return Err(miette!("Issuer is invalid {}", &cmd.issuer).into()); - } - }; - - let issuer_exported = identities.export_identity(&issuer).await?; - let cred = hex::decode(&cred_as_str)?; - if let Err(e) = validate_encoded_cred(&cred, identities, &issuer).await { - *is_finished.lock().await = true; - return Err(miette!("Credential is invalid\n{}", e).into()); - } - + let issuer = verify_issuer(&opts, &cmd.issuer, &cmd.vault).await?; + let credential_and_purpose_key = verify_credential( + &opts, + issuer.identifier(), + &cmd.credential, + &cmd.credential_path, + &cmd.vault, + ) + .await?; // store - opts.state.credentials.create( - &cmd.credential_name, - CredentialConfig::new(issuer.clone(), issuer_exported, cred)?, - )?; + opts.state + .store_credential( + &cmd.credential_name, + &issuer, + credential_and_purpose_key.clone(), + ) + .await + .into_diagnostic()?; *is_finished.lock().await = true; - - Ok(cred_as_str) + Ok(credential_and_purpose_key + .encode_as_string() + .into_diagnostic()?) }; let output_messages = vec![format!("Storing credential...")]; @@ -113,7 +86,7 @@ async fn run_impl( opts.terminal .stdout() - .machine(credential.to_string()) + .machine(credential.clone()) .json(serde_json::json!( { "name": cmd.credential_name, @@ -131,3 +104,24 @@ async fn run_impl( Ok(()) } + +async fn verify_issuer( + opts: &CommandGlobalOpts, + issuer: &str, + vault: &Option, +) -> miette::Result { + let identities = opts + .state + .get_identities_with_optional_vault_name(vault) + .await?; + let identifier = identities + .identities_creation() + .import(None, &hex::decode(issuer).into_diagnostic()?) + .await + .into_diagnostic()?; + let identity = identities + .get_identity(&identifier) + .await + .into_diagnostic()?; + Ok(identity) +} diff --git a/implementations/rust/ockam/ockam_command/src/credential/verify.rs b/implementations/rust/ockam/ockam_command/src/credential/verify.rs index 6df92389e26..71b45aa2849 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/verify.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/verify.rs @@ -1,20 +1,17 @@ use std::path::PathBuf; +use std::sync::Arc; -use crate::{ - fmt_err, fmt_log, fmt_ok, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, -}; -use miette::miette; - -use crate::credential::identities; use clap::Args; use colorful::Colorful; -use ockam::identity::Identifier; -use ockam::Context; +use miette::{miette, IntoDiagnostic}; use tokio::{sync::Mutex, try_join}; -use crate::util::parsers::identity_identifier_parser; +use ockam::identity::models::CredentialAndPurposeKey; +use ockam::identity::{Identifier, Identities}; +use ockam::Context; -use super::validate_encoded_cred; +use crate::util::parsers::identity_identifier_parser; +use crate::{fmt_err, fmt_log, fmt_ok, util::node_rpc, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct VerifyCommand { @@ -46,13 +43,46 @@ async fn run_impl( _ctx: Context, (opts, cmd): (CommandGlobalOpts, VerifyCommand), ) -> miette::Result<()> { + let (is_valid, plain_text) = match verify_credential( + &opts, + cmd.issuer(), + &cmd.credential, + &cmd.credential_path, + &cmd.vault, + ) + .await + { + Ok(_) => (true, fmt_ok!("Credential is")), + Err(e) => ( + false, + fmt_err!("Credential is not valid\n") + &fmt_log!("{}", e), + ), + }; + + opts.terminal + .stdout() + .machine(is_valid.to_string()) + .json(serde_json::json!({ "is_valid": is_valid })) + .plain(plain_text) + .write_line()?; + + Ok(()) +} + +pub async fn verify_credential( + opts: &CommandGlobalOpts, + issuer: &Identifier, + credential: &Option, + credential_path: &Option, + vault: &Option, +) -> miette::Result { opts.terminal .write_line(&fmt_log!("Verifying credential...\n"))?; let is_finished: Mutex = Mutex::new(false); let send_req = async { - let cred_as_str = match (&cmd.credential, &cmd.credential_path) { + let credential_as_str = match (&credential, &credential_path) { (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path) .await? .trim() @@ -60,35 +90,27 @@ async fn run_impl( (Some(credential), _) => credential.clone(), _ => { *is_finished.lock().await = true; - return crate::Result::Err( + return Err( miette!("Credential or Credential Path argument must be provided").into(), ); } }; - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); - - let issuer = cmd.issuer(); - - let identities = match identities(&vault_name, &opts).await { + let identities = match opts + .state + .get_identities_with_optional_vault_name(vault) + .await + { Ok(i) => i, - Err(_) => { + Err(e) => { *is_finished.lock().await = true; - return Err(miette!("Invalid state").into()); + return Err(e.into()); } }; - let cred = hex::decode(&cred_as_str)?; - let is_valid = match validate_encoded_cred(&cred, identities, issuer).await { - Ok(_) => (true, String::new()), - Err(e) => (false, e.to_string()), - }; - + let result = validate_encoded_credential(identities, issuer, &credential_as_str).await; *is_finished.lock().await = true; - Ok(is_valid) + result.map_err(|e| miette!("Credential is invalid\n{}", e).into()) }; let output_messages = vec![format!("Verifying credential...")]; @@ -97,18 +119,22 @@ async fn run_impl( .terminal .progress_output(&output_messages, &is_finished); - let ((is_valid, reason), _) = try_join!(send_req, progress_output)?; - let plain_text = match is_valid { - true => fmt_ok!("Credential is valid"), - false => fmt_err!("Credential is not valid\n") + &fmt_log!("{reason}"), - }; + let (credential_and_purpose_key, _) = try_join!(send_req, progress_output)?; - opts.terminal - .stdout() - .machine(is_valid.to_string()) - .json(serde_json::json!({ "is_valid": is_valid })) - .plain(plain_text) - .write_line()?; + Ok(credential_and_purpose_key) +} - Ok(()) +async fn validate_encoded_credential( + identities: Arc, + issuer: &Identifier, + credential_as_str: &str, +) -> miette::Result { + let verification = identities.credentials().credentials_verification(); + let credential_and_purpose_key: CredentialAndPurposeKey = + minicbor::decode(&hex::decode(credential_as_str).into_diagnostic()?).into_diagnostic()?; + verification + .verify_credential(None, &[issuer.clone()], &credential_and_purpose_key) + .await + .into_diagnostic()?; + Ok(credential_and_purpose_key) } diff --git a/implementations/rust/ockam/ockam_command/src/enroll/command.rs b/implementations/rust/ockam/ockam_command/src/enroll/command.rs index 7a48785a5fd..7b8fc2acecc 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/command.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/command.rs @@ -10,10 +10,8 @@ use tokio::try_join; use tracing::info; use tracing::log::warn; -use ockam::identity::Identifier; use ockam::Context; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::{random_name, update_enrolled_identity, SpaceConfig}; +use ockam_api::cli_state::random_name; use ockam_api::cloud::enroll::auth0::*; use ockam_api::cloud::project::{Project, Projects}; use ockam_api::cloud::space::{Space, Spaces}; @@ -23,7 +21,6 @@ use ockam_api::enroll::oidc_service::OidcService; use ockam_api::nodes::InMemoryNode; use crate::enroll::OidcServiceExt; -use crate::identity::initialize_identity_if_default; use crate::operation::util::check_for_completion; use crate::output::OutputFormat; use crate::project::util::check_project_readiness; @@ -52,7 +49,6 @@ pub struct EnrollCommand { impl EnrollCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.identity); node_rpc(rpc, (opts, self)); } } @@ -76,7 +72,7 @@ fn ctrlc_handler(opts: CommandGlobalOpts) { "\n{} Received Ctrl+C again. Cancelling {}. Please try again.", "!".red(), "ockam enroll".bold().light_yellow() ) - .as_str(), + .as_str(), ); process::exit(2); } else { @@ -85,12 +81,12 @@ fn ctrlc_handler(opts: CommandGlobalOpts) { "\n{} {} is still in progress. If you would like to stop the enrollment process, press Ctrl+C again.", "!".red(), "ockam enroll".bold().light_yellow() ) - .as_str(), + .as_str(), ); is_confirmation.store(true, Ordering::Relaxed); } }) - .expect("Error setting Ctrl-C handler"); + .expect("Error setting Ctrl-C handler"); } async fn run_impl( @@ -115,9 +111,7 @@ async fn run_impl( let user_info = oidc_service .wait_for_email_verification(&token, Some(&opts.terminal)) .await?; - opts.state - .users_info - .overwrite(&user_info.email, user_info.clone())?; + opts.state.store_user(&user_info).await?; let node = InMemoryNode::start(ctx, &opts.state).await?; let controller = node.create_controller().await?; @@ -126,7 +120,19 @@ async fn run_impl( .await .wrap_err("Failed to enroll your local identity with Ockam Orchestrator")?; - let identifier = retrieve_user_project(&opts, ctx, &node).await?; + let project = retrieve_user_project(&opts, ctx, &node).await?; + let identifier = node.identifier(); + opts.state + .set_identifier_as_enrolled(&identifier) + .await + .wrap_err(format!( + "Unable to set the local identity as enrolled with project {}", + project + .name + .to_string() + .color(OckamColor::PrimaryResource.color()) + ))?; + info!("Enrolled a user with the Identifier {}", identifier); opts.terminal.write_line(&fmt_ok!( "Enrolled {} as one of the Ockam identities of your Orchestrator account {}.", @@ -142,13 +148,18 @@ pub async fn retrieve_user_project( opts: &CommandGlobalOpts, ctx: &Context, node: &InMemoryNode, -) -> Result { - let space = default_space(opts, ctx, &node.create_controller().await?) +) -> Result { + // return the default project if there is one already stored locally + if let Ok(project) = opts.state.get_default_project().await { + return Ok(project); + }; + + let space = get_user_space(opts, ctx, node) .await .wrap_err("Unable to retrieve and set a space as default")?; info!("Retrieved the user default space {:?}", space); - let project = default_project(opts, ctx, node, &space) + let project = get_user_project(opts, ctx, node, &space) .await .wrap_err(format!( "Unable to retrieve and set a project as default with space {}", @@ -158,19 +169,7 @@ pub async fn retrieve_user_project( .color(OckamColor::PrimaryResource.color()) ))?; info!("Retrieved the user default project {:?}", project); - - let identifier = update_enrolled_identity(&opts.state, &node.node_name()) - .await - .wrap_err(format!( - "Unable to set the local identity as enrolled with project {}", - project - .name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; - info!("Enrolled a user with the Identifier {}", identifier); - - Ok(identifier) + Ok(project) } /// Enroll a user with a token, using the controller @@ -189,17 +188,23 @@ pub async fn enroll_with_node( Ok(()) } -async fn default_space( +async fn get_user_space( opts: &CommandGlobalOpts, ctx: &Context, - controller: &Controller, + node: &InMemoryNode, ) -> Result { - // Get available spaces for node's identity + // return the default space if there is one already stored locally + if let Ok(space) = opts.state.get_default_space().await { + return Ok(space); + }; + + // Otherwise get the available spaces for node's identity + // Those spaces might have been created previously and all the local state reset opts.terminal .write_line(&fmt_log!("Getting available spaces in your account..."))?; let is_finished = Mutex::new(false); let get_spaces = async { - let spaces: Vec = controller.list_spaces(ctx).await?; + let spaces = node.get_spaces(ctx).await?; *is_finished.lock().await = true; Ok(spaces) }; @@ -207,79 +212,65 @@ async fn default_space( let message = vec![format!("Checking for any existing spaces...")]; let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (mut available_spaces, _) = try_join!(get_spaces, progress_output)?; + let (spaces, _) = try_join!(get_spaces, progress_output)?; // If the identity has no spaces, create one - let default_space = if available_spaces.is_empty() { - opts.terminal - .write_line(&fmt_para!("No spaces are defined in your account."))? - .write_line(&fmt_para!( - "Creating a trial space for you ({}) ...", - "everything in it will be deleted in 15 days" - .to_string() - .color(OckamColor::FmtWARNBackground.color()) - ))? - .write_line(&fmt_para!( - "To learn more about production ready spaces in Ockam Orchestrator, contact us at: {}", - "hello@ockam.io".to_string().color(OckamColor::PrimaryResource.color()) - ))?; - - let is_finished = Mutex::new(false); - let name = random_name(); - let space_name = name.clone(); - let create_space = async { - let space = controller.create_space(ctx, space_name, vec![]).await?; - *is_finished.lock().await = true; - Ok(space) - }; - - let message = vec![format!( - "Creating space {}...", - name.color(OckamColor::PrimaryResource.color()) - )]; - let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (space, _) = try_join!(create_space, progress_output)?; - space - } - // If it has, return the first one on the list - else { - for space in &available_spaces { - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(space))?; - } - - let space = available_spaces - .drain(..1) - .next() - .expect("already checked that is not empty"); - - opts.terminal.write_line(&fmt_log!( - "Found space {}.", + let space = match spaces.first() { + None => { + opts.terminal + .write_line(&fmt_para!("No spaces are defined in your account."))? + .write_line(&fmt_para!( + "Creating a trial space for you ({}) ...", + "everything in it will be deleted in 15 days" + .to_string() + .color(OckamColor::FmtWARNBackground.color()) + ))? + .write_line(&fmt_para!( + "To learn more about production ready spaces in Ockam Orchestrator, contact us at: {}", + "hello@ockam.io".to_string().color(OckamColor::PrimaryResource.color())))?; + + let is_finished = Mutex::new(false); + let space_name = random_name(); + let create_space = async { + let space = node.create_space(ctx, &space_name, vec![]).await?; + *is_finished.lock().await = true; + Ok(space) + }; + + let message = vec![format!( + "Creating space {}...", + space_name + .clone() + .color(OckamColor::PrimaryResource.color()) + )]; + let progress_output = opts.terminal.progress_output(&message, &is_finished); + let (space, _) = try_join!(create_space, progress_output)?; space - .name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; - space + } + Some(space) => { + opts.terminal.write_line(&fmt_log!( + "Found space {}.", + space + .name + .clone() + .color(OckamColor::PrimaryResource.color()) + ))?; + space.clone() + } }; - opts.state - .spaces - .overwrite(&default_space.name, SpaceConfig::from(&default_space))?; + opts.state.set_space_as_default(&space.id).await?; opts.terminal.write_line(&fmt_ok!( "Marked this space as your default space, on this machine.\n" ))?; - Ok(default_space) + Ok(space) } -async fn default_project( +async fn get_user_project( opts: &CommandGlobalOpts, ctx: &Context, node: &InMemoryNode, space: &Space, ) -> Result { - let controller = node.create_controller().await?; - // Get available project for the given space opts.terminal.write_line(&fmt_log!( "Getting available projects in space {}...", @@ -291,7 +282,7 @@ async fn default_project( let is_finished = Mutex::new(false); let get_projects = async { - let projects = controller.list_projects(ctx).await?; + let projects = node.get_projects(ctx).await?; *is_finished.lock().await = true; Ok(projects) }; @@ -299,85 +290,68 @@ async fn default_project( let message = vec![format!("Checking for any existing projects...")]; let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (mut available_projects, _) = try_join!(get_projects, progress_output)?; + let (projects, _) = try_join!(get_projects, progress_output)?; // If the space has no projects, create one - let default_project = if available_projects.is_empty() { - opts.terminal - .write_line(&fmt_para!( - "No projects are defined in the space {}.", - space - .name + let project = match projects.first() { + None => { + opts.terminal + .write_line(&fmt_para!( + "No projects are defined in the space {}.", + space + .name + .to_string() + .color(OckamColor::PrimaryResource.color()) + ))? + .write_line(&fmt_para!("Creating a project for you..."))?; + + let is_finished = Mutex::new(false); + let project_name = "default".to_string(); + let get_project = async { + let project = node + .create_project(ctx, &space.id, &project_name, vec![]) + .await?; + *is_finished.lock().await = true; + Ok(project) + }; + + let message = vec![format!( + "Creating project {}...", + project_name .to_string() .color(OckamColor::PrimaryResource.color()) - ))? - .write_line(&fmt_para!("Creating a project for you..."))?; - - let is_finished = Mutex::new(false); - let project_name = "default".to_string(); - let get_project = async { - let project = controller - .create_project(ctx, space.id.clone(), project_name.clone(), vec![]) - .await?; - *is_finished.lock().await = true; - Ok(project) - }; + )]; + let progress_output = opts.terminal.progress_output(&message, &is_finished); + let (project, _) = try_join!(get_project, progress_output)?; - let message = vec![format!( - "Creating project {}...", - project_name - .to_string() - .color(OckamColor::PrimaryResource.color()) - )]; - let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (project, _) = try_join!(get_project, progress_output)?; - - opts.terminal.write_line(&fmt_ok!( - "Created project {}.", - project_name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; + opts.terminal.write_line(&fmt_ok!( + "Created project {}.", + project_name + .to_string() + .color(OckamColor::PrimaryResource.color()) + ))?; - let operation_id = project.operation_id.clone().unwrap(); - check_for_completion(opts, ctx, &controller, &operation_id).await?; + let operation_id = project.operation_id.clone().unwrap(); + check_for_completion(opts, ctx, &node.create_controller().await?, &operation_id) + .await?; - project.to_owned() - } - // If it has, return the "default" project or first one on the list - else { - for project in &available_projects { - opts.state - .projects - .overwrite(&project.name, project.clone())?; + project.to_owned() + } + Some(project) => { + opts.terminal.write_line(&fmt_log!( + "Found project {}.", + project + .project_name() + .color(OckamColor::PrimaryResource.color()) + ))?; + project.clone() } - let p = match available_projects.iter().find(|ns| ns.name == "default") { - None => available_projects - .drain(..1) - .next() - .expect("already checked that is not empty"), - Some(p) => p.to_owned(), - }; - opts.terminal.write_line(&fmt_log!( - "Found project {}.", - p.name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; - p }; - let project = check_project_readiness(opts, ctx, node, default_project).await?; + check_project_readiness(opts, ctx, node, project.clone()).await?; opts.terminal.write_line(&fmt_ok!( "Marked this project as your default project, on this machine.\n" ))?; - - opts.state - .projects - .overwrite(&project.name, project.clone())?; - opts.state - .trust_contexts - .overwrite(&project.name, project.clone().try_into()?)?; Ok(project) } diff --git a/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs b/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs index bd155e463aa..8d5c21cf5d9 100644 --- a/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs +++ b/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs @@ -1,12 +1,11 @@ use clap::Args; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::nodes::BackgroundNode; use ockam_core::flow_control::FlowControlId; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, NodeOpts}; +use crate::node::NodeOpts; use crate::util::{api, node_rpc}; use crate::CommandGlobalOpts; @@ -41,9 +40,7 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: AddConsumerCommand, ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; node.tell(ctx, api::add_consumer(cmd.flow_control_id, cmd.address)) .await?; diff --git a/implementations/rust/ockam/ockam_command/src/identity/create.rs b/implementations/rust/ockam/ockam_command/src/identity/create.rs index df41cf9bebd..0fe359b05b1 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/create.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/create.rs @@ -1,16 +1,15 @@ -use crate::terminal::OckamColor; -use crate::util::node_rpc; -use crate::{docs, fmt_log, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use miette::miette; +use tokio::sync::Mutex; +use tokio::try_join; + use ockam::identity::Identifier; use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_vault::{HandleToSecret, SigningSecretKeyHandle}; -use tokio::sync::Mutex; -use tokio::try_join; + +use crate::terminal::OckamColor; +use crate::util::node_rpc; +use crate::{docs, fmt_log, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -66,53 +65,30 @@ impl CreateCommand { let is_finished: Mutex = Mutex::new(false); let send_req = async { - let default_vault_created = - self.vault.is_none() && opts.state.vaults.default().is_err(); - let vault_state = opts.state.create_vault_state(self.vault.as_deref()).await?; - if default_vault_created { - opts.terminal.write_line(&fmt_log!( - "Default vault created: {}\n", - &vault_state - .name() - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; - } - - let vault = vault_state.get().await?; - - let identities_creation = opts - .state - .get_identities(vault) - .await? - .identities_creation(); + let result = opts.state.create_named_vault(&self.vault).await?; + let vault_name = match result { + Ok(v) => { + opts.terminal.write_line(&fmt_log!( + "Default vault created: {}\n", + v.name().color(OckamColor::PrimaryResource.color()) + ))?; + v.name() + } + Err(name) => name, + }; - // Create an identity using the KMS key, if provided. let identifier = match &self.key_id { Some(key_id) => { - if !vault_state.config().is_aws() { - Err(miette!( - "Vault {} is not an AWS KMS vault", - self.vault.clone().unwrap_or("default".to_string()), - )) - } else { - let handle = SigningSecretKeyHandle::ECDSASHA256CurveP256( - HandleToSecret::new(key_id.as_bytes().to_vec()), - ); - - Ok(identities_creation - .identity_builder() - .with_existing_key(handle) - .build() - .await?) - } + opts.state + .create_identity_with_key_id(&self.name, &vault_name, key_id.as_ref()) + .await? } - None => Ok(identities_creation.create_identity().await?), - }?; - - opts.state - .create_identity_state(&identifier, Some(&self.name)) - .await?; + None => { + opts.state + .create_identity_with_name_and_vault(&self.name, &vault_name) + .await? + } + }; *is_finished.lock().await = true; Ok(identifier) diff --git a/implementations/rust/ockam/ockam_command/src/identity/default.rs b/implementations/rust/ockam/ockam_command/src/identity/default.rs index 9f79a0f8d94..666d9ee18dc 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/default.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/default.rs @@ -1,9 +1,11 @@ -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -21,42 +23,41 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options, self)); + node_rpc(run_impl, (options, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { - if let Some(name) = cmd.name { - let state = opts.state.identities; - let idt = state.get(&name)?; - // If it's already the default, warn the user and exit - if state.is_default(idt.name())? { - Err(miette!( - "The identity named '{}' is already the default", - &name - )) +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { + match cmd.name { + Some(name) => { + if opts.state.is_default_identity_by_name(&name).await? { + Err(miette!( + "The identity named '{}' is already the default", + &name + ))? + } else { + opts.state.set_as_default_identity(&name).await?; + opts.terminal + .stdout() + .plain(fmt_ok!("The identity named '{}' is now the default", &name)) + .machine(&name) + .write_line()?; + } } - // Otherwise, set it as default - else { - state.set_default(idt.name())?; + None => { + let identity = opts.state.get_default_named_identity().await?; opts.terminal .stdout() - .plain(fmt_ok!("The identity named '{}' is now the default", &name)) - .machine(&name) + .plain(fmt_ok!( + "The name of the default identity is '{}'", + identity.name() + )) .write_line()?; - Ok(()) } - } - // No argument provided, show default identity name - else { - let state = opts.state.identities.get_or_default(None)?; - opts.terminal - .stdout() - .plain(fmt_ok!( - "The name of the default identity is '{}'", - state.name() - )) - .write_line()?; - Ok(()) - } + }; + + Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/identity/delete.rs b/implementations/rust/ockam/ockam_command/src/identity/delete.rs index 1bb0db354e0..60e431a5002 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/delete.rs @@ -2,9 +2,7 @@ use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; - use ockam::Context; -use ockam_api::cli_state::traits::StateDirTrait; const LONG_ABOUT: &str = include_str!("./static/delete/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -35,13 +33,11 @@ async fn run_impl( _ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let state = opts.state; - let idt = state.identities.get(&cmd.name)?; if opts .terminal .confirmed_with_flag_or_prompt(cmd.yes, "Are you sure you want to delete this identity?")? { - state.delete_identity(idt)?; + opts.state.delete_identity_by_name(&cmd.name).await?; opts.terminal .stdout() .plain(fmt_ok!( diff --git a/implementations/rust/ockam/ockam_command/src/identity/list.rs b/implementations/rust/ockam/ockam_command/src/identity/list.rs index 8856ab41dbe..ea0950755f5 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/list.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/list.rs @@ -6,14 +6,10 @@ use crate::{docs, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::traits::StateDirTrait; - use ockam_node::Context; use serde::Serialize; use serde_json::json; use std::fmt::Write; -use tokio::sync::Mutex; -use tokio::try_join; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -38,37 +34,20 @@ impl ListCommand { options: (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { let (opts, _cmd) = options; - let mut identities: Vec = Vec::new(); - - let idts = opts.state.identities.list()?; - for identity in idts.iter() { - let is_finished: Mutex = Mutex::new(false); - - let send_req = async { - let i = IdentityListOutput::new( - identity.name().to_string(), - identity.identifier().to_string(), - opts.state.identities.default()?.name() == identity.name(), - ); - *is_finished.lock().await = true; - Ok(i) - }; - - let output_messages = vec![format!( - "Retrieving identity {}...\n", - &identity.name().color(OckamColor::PrimaryResource.color()) - )]; - - let progress_output = opts - .terminal - .progress_output(&output_messages, &is_finished); - - let (identity_states, _) = try_join!(send_req, progress_output)?; - identities.push(identity_states); + let mut identities_list: Vec = Vec::new(); + + let identities = opts.state.get_named_identities().await?; + for identity in identities.iter() { + let identity_output = IdentityListOutput::new( + identity.name(), + identity.identifier().to_string(), + identity.is_default(), + ); + identities_list.push(identity_output); } let list = opts.terminal.build_list( - &identities, + &identities_list, "Identities", "No identities found on this system.", )?; diff --git a/implementations/rust/ockam/ockam_command/src/identity/mod.rs b/implementations/rust/ockam/ockam_command/src/identity/mod.rs index 5c038bd2561..8937b8daef8 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/mod.rs @@ -1,8 +1,4 @@ -mod create; -mod default; -mod delete; -mod list; -mod show; +use clap::{Args, Subcommand}; pub use create::CreateCommand; pub(crate) use delete::DeleteCommand; @@ -11,9 +7,12 @@ pub(crate) use show::ShowCommand; use crate::identity::default::DefaultCommand; use crate::{docs, CommandGlobalOpts}; -use clap::{Args, Subcommand}; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::CliState; + +mod create; +mod default; +mod delete; +mod list; +mod show; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); @@ -49,65 +48,3 @@ impl IdentityCommand { } } } - -/// If the required identity is the default identity but if it has not been initialized yet -/// then initialize it -pub fn initialize_identity_if_default(opts: &CommandGlobalOpts, name: &Option) { - let name = get_identity_name(&opts.state, name); - if name == "default" && opts.state.identities.default().is_err() { - create_default_identity(opts); - } -} - -/// Return the name if identity_name is Some otherwise return the name of the default identity -pub fn get_identity_name(cli_state: &CliState, identity_name: &Option) -> String { - identity_name - .clone() - .unwrap_or_else(|| get_default_identity_name(cli_state)) -} - -/// Return the name of the default identity -pub fn get_default_identity_name(cli_state: &CliState) -> String { - cli_state - .identities - .default() - .map(|i| i.name().to_string()) - .unwrap_or_else(|_| "default".to_string()) -} - -/// Create the default identity -pub fn create_default_identity(opts: &CommandGlobalOpts) { - let default = "default"; - let create_command = CreateCommand::new(default.into(), None, None); - create_command.run(opts.clone().set_quiet()); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::GlobalArgs; - use ockam_api::cli_state::StateItemTrait; - - #[test] - fn test_initialize() { - let state = CliState::test().unwrap(); - let opts = CommandGlobalOpts::new_for_test(GlobalArgs::default(), state); - - // on start-up there is no default identity - assert!(opts.state.identities.default().is_err()); - - // if no name is given then the default identity is initialized - initialize_identity_if_default(&opts, &None); - assert!(opts.state.identities.default().is_ok()); - - // if "default" is given as a name the default identity is initialized - opts.state.identities.default().unwrap().delete().unwrap(); - initialize_identity_if_default(&opts, &Some("default".into())); - assert!(opts.state.identities.default().is_ok()); - - // if the name of another identity is given then the default identity is not initialized - opts.state.identities.default().unwrap().delete().unwrap(); - initialize_identity_if_default(&opts, &Some("other".into())); - assert!(opts.state.identities.default().is_err()); - } -} diff --git a/implementations/rust/ockam/ockam_command/src/identity/show.rs b/implementations/rust/ockam/ockam_command/src/identity/show.rs index 66dfcf1ffbc..dfebb9c8f3a 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/show.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/show.rs @@ -1,6 +1,5 @@ use std::fmt::Display; -use crate::identity::get_identity_name; use crate::identity::list::IdentityListOutput; use crate::output::{EncodeFormat, IdentifierDisplay, Output, VerifyingPublicKeyDisplay}; use crate::util::node_rpc; @@ -8,8 +7,8 @@ use crate::{docs, CommandGlobalOpts}; use clap::Args; use miette::IntoDiagnostic; use ockam::identity::verified_change::VerifiedChange; -use ockam::identity::{Identifier, Identity, Vault}; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; +use ockam::identity::{Identifier, Identity}; +use ockam_api::identity::NamedIdentity; use ockam_node::Context; use serde::Serialize; use serde_json::{json, to_string_pretty}; @@ -21,9 +20,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show the details of an identity #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { #[arg()] @@ -53,26 +52,32 @@ impl ShowCommand { let (opts, cmd) = options; if cmd.name.is_some() || !opts.terminal.can_ask_for_user_input() { - let name = get_identity_name(&opts.state, &cmd.name); - Self::show_single_identity(&opts, &name, cmd.full, cmd.encoding).await?; + Self::show_single_identity(&opts, &cmd.name, cmd.full, cmd.encoding).await?; return Ok(()); } - let id_names: Vec = opts.state.identities.list_items_names()?; - match id_names.len() { + let identities: Vec = opts.state.get_named_identities().await?; + let identities_names: Vec = identities.iter().map(|i| i.name()).collect(); + match identities_names.len() { 0 => { opts.terminal .stdout() - .plain("There are no nodes to show") + .plain("There are no identities to show") .write_line()?; } 1 => { - Self::show_single_identity(&opts, &id_names[0], cmd.full, cmd.encoding).await?; + Self::show_single_identity( + &opts, + &identities_names.first().cloned(), + cmd.full, + cmd.encoding, + ) + .await?; } _ => { let selected_names = opts.terminal.select_multiple( "Select one or more identities that you want to show".to_string(), - id_names, + identities_names, ); if selected_names.is_empty() { @@ -97,40 +102,25 @@ impl ShowCommand { async fn show_single_identity( opts: &CommandGlobalOpts, - name: &str, + name: &Option, full: bool, encoding: Option, ) -> miette::Result<()> { - let state = opts.state.identities.get(name)?; - let identifier = state.config().identifier(); + let identity = opts.state.get_identity_by_optional_name(name).await?; + let (plain, json) = if full { - let change_history = opts - .state - .identities - .identities_repository() - .await? - .get_identity(&identifier) - .await - .into_diagnostic()?; + let change_history = identity.change_history(); if Some(EncodeFormat::Hex) == encoding { let encoded = hex::encode(change_history.export().into_diagnostic()?); let json = to_string_pretty(&json!({"encoded": &encoded})); (encoded, json) } else { - let identity: ShowIdentity = Identity::import_from_change_history( - Some(&identifier), - change_history, - Vault::create_verifying_vault(), - ) - .await - .into_diagnostic()? - .into(); - + let identity: ShowIdentity = identity.into(); (identity.to_string(), to_string_pretty(&identity)) } } else { - let identifier_display = IdentifierDisplay(identifier); + let identifier_display = IdentifierDisplay(identity.identifier().clone()); ( identifier_display.to_string(), to_string_pretty(&json!({"identifier": &identifier_display})), @@ -154,11 +144,13 @@ impl ShowCommand { let mut identities: Vec = Vec::new(); for name in selected_names { - let state = opts.state.identities.get(&name)?; - let identifier = state.config().identifier().to_string(); - let is_default = opts.state.identities.is_default(&name)?; - let identity = IdentityListOutput::new(name, identifier, is_default); - identities.push(identity); + let identity = opts.state.get_named_identity(&name).await?; + let identity_list_output = IdentityListOutput::new( + identity.name(), + identity.identifier().to_string(), + identity.is_default(), + ); + identities.push(identity_list_output); } let list = opts.terminal.build_list( diff --git a/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs index f027eb5d604..efd767682fe 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs @@ -6,7 +6,6 @@ use ockam_api::port_range::PortRange; use ockam_multiaddr::MultiAddr; use crate::kafka::util::{rpc, ArgOpts}; -use crate::node::initialize_node_if_default; use crate::{ kafka::{ kafka_consumer_default_addr, kafka_default_consumer_port_range, @@ -41,7 +40,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); let arg_opts = ArgOpts { endpoint: "/node/services/kafka_consumer".to_string(), kafka_entity: "KafkaConsumer".to_string(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs b/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs index bdcfe33963a..c8114ecac2f 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs @@ -5,8 +5,7 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -24,7 +23,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -33,10 +31,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::delete("/node/services/kafka_consumer").body( models::services::DeleteServiceRequest::new(cmd.address.clone()), ); diff --git a/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs b/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs index 96846d56f81..05ea3edb9f0 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs @@ -1,16 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::ServiceList; use ockam_api::nodes::BackgroundNode; use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::node_rpc; use crate::{docs, fmt_err, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -19,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Kafka Consumers #[derive(Args, Clone, Debug)] #[command( - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { #[command(flatten)] @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -38,14 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let services: ServiceList = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs index 979e362ee13..25974bd26a5 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs @@ -1,7 +1,6 @@ use std::net::SocketAddr; use crate::kafka::direct::rpc::{start, ArgOpts}; -use crate::node::initialize_node_if_default; use crate::{ kafka::{ kafka_default_consumer_port_range, kafka_default_consumer_server, @@ -41,7 +40,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); let arg_opts = ArgOpts { endpoint: "/node/services/kafka_direct".to_string(), kafka_entity: "KafkaDirect".to_string(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs index d89136e8bac..771e3008210 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs @@ -5,8 +5,7 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -24,7 +23,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -33,10 +31,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::delete("/node/services/kafka_direct").body( models::services::DeleteServiceRequest::new(cmd.address.clone()), ); diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs index e16fb51bd0a..96214bdccf7 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs @@ -1,16 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::ServiceList; use ockam_api::nodes::BackgroundNode; use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::node_rpc; use crate::{docs, fmt_err, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -19,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Kafka Consumers #[derive(Args, Clone, Debug)] #[command( - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { #[command(flatten)] @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -38,14 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let services: ServiceList = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs index 758e9335f5a..91915dc786d 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs @@ -10,7 +10,7 @@ use ockam_api::port_range::PortRange; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, NodeOpts}; +use crate::node::NodeOpts; use crate::service::start::start_service_impl; use crate::terminal::OckamColor; use crate::util::process_nodes_multiaddr; @@ -45,15 +45,14 @@ pub async fn start(ctx: Context, (opts, args): (CommandGlobalOpts, ArgOpts)) -> display_parse_logs(&opts); let consumer_route = if let Some(consumer_route) = consumer_route { - Some(process_nodes_multiaddr(&consumer_route, &opts.state)?) + Some(process_nodes_multiaddr(&consumer_route, &opts.state).await?) } else { None }; let is_finished = Mutex::new(false); let send_req = async { - let node_name = get_node_name(&opts.state, &node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &node_opts.at_node).await?; let payload = StartKafkaDirectRequest::new( bind_address.to_owned(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs index 6dc9c83bcc7..ab6e616ab11 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs @@ -10,7 +10,6 @@ use ockam_api::nodes::models::services::StartServiceRequest; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::{ fmt_log, fmt_ok, kafka::{kafka_default_outlet_addr, kafka_default_outlet_server}, @@ -53,8 +52,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m let payload = StartKafkaOutletRequest::new(bootstrap_server); let payload = StartServiceRequest::new(payload, &addr); let req = Request::post("/node/services/kafka_outlet").body(payload); - let node_name = get_node_name(&opts.state, &node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &node_opts.at_node).await?; start_service_impl(&ctx, &node, "KafkaOutlet", req).await?; *is_finished.lock().await = true; diff --git a/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs index 9d8b19e3891..a17b6cc673d 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs @@ -6,7 +6,6 @@ use ockam_api::port_range::PortRange; use ockam_multiaddr::MultiAddr; use crate::kafka::util::{rpc, ArgOpts}; -use crate::node::initialize_node_if_default; use crate::{ kafka::{ kafka_default_producer_port_range, kafka_default_producer_server, @@ -41,7 +40,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); let arg_opts = ArgOpts { endpoint: "/node/services/kafka_producer".to_string(), kafka_entity: "KafkaProducer".to_string(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs b/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs index ad677238342..53e81172d6a 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs @@ -5,8 +5,7 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -24,7 +23,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -33,10 +31,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::delete("/node/services/kafka_producer").body( models::services::DeleteServiceRequest::new(cmd.address.clone()), ); diff --git a/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs b/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs index 4f9d3ecc8bf..bbb2bab50dc 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs @@ -1,16 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::ServiceList; use ockam_api::nodes::BackgroundNode; use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::node_rpc; use crate::{docs, fmt_err, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -19,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Kafka Producers #[derive(Args, Clone, Debug)] #[command( - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { #[command(flatten)] @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -38,14 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let services: ServiceList = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/kafka/util.rs b/implementations/rust/ockam/ockam_command/src/kafka/util.rs index 9809febc467..d74a90f97c7 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/util.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/util.rs @@ -10,7 +10,7 @@ use ockam_api::port_range::PortRange; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, NodeOpts}; +use crate::node::NodeOpts; use crate::service::start::start_service_impl; use crate::terminal::OckamColor; use crate::util::process_nodes_multiaddr; @@ -42,12 +42,11 @@ pub async fn rpc(ctx: Context, (opts, args): (CommandGlobalOpts, ArgOpts)) -> mi display_parse_logs(&opts); - let project_route = process_nodes_multiaddr(&project_route, &opts.state)?; + let project_route = process_nodes_multiaddr(&project_route, &opts.state).await?; let is_finished = Mutex::new(false); let send_req = async { - let node_name = get_node_name(&opts.state, &node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &node_opts.at_node).await?; let payload = StartKafkaProducerRequest::new( bootstrap_server.to_owned(), diff --git a/implementations/rust/ockam/ockam_command/src/lease/create.rs b/implementations/rust/ockam/ockam_command/src/lease/create.rs index b1c898d83de..8ee47bc6e0c 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/create.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/create.rs @@ -9,7 +9,6 @@ use time::PrimitiveDateTime; use tokio::sync::Mutex; use tokio::try_join; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::terminal::OckamColor; use crate::util::api::{CloudOpts, TrustContextOpts}; @@ -26,7 +25,6 @@ pub struct CreateCommand {} impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lease/list.rs b/implementations/rust/ockam/ockam_command/src/lease/list.rs index ba7dd501b5f..1e7ce001086 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/list.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/list.rs @@ -11,7 +11,6 @@ use time::PrimitiveDateTime; use tokio::sync::Mutex; use tokio::try_join; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::output::Output; use crate::terminal::OckamColor; @@ -28,7 +27,6 @@ pub struct ListCommand; impl ListCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lease/mod.rs b/implementations/rust/ockam/ockam_command/src/lease/mod.rs index 4a6e026e866..39883565330 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/mod.rs @@ -1,28 +1,24 @@ -mod create; -mod list; -mod revoke; -mod show; +use clap::{Args, Subcommand}; +use miette::IntoDiagnostic; pub use create::CreateCommand; pub use list::ListCommand; -pub use show::ShowCommand; - -use clap::{Args, Subcommand}; -use miette::{miette, Context, IntoDiagnostic}; - -use ockam_api::cli_state::{ProjectConfigCompact, StateDirTrait, StateItemTrait}; +use ockam_api::cloud::project::Projects; use ockam_api::cloud::ProjectNode; -use ockam_api::config::lookup::ProjectLookup; use ockam_api::nodes::Credentials; use ockam_api::nodes::InMemoryNode; - -use crate::identity::get_identity_name; +pub use show::ShowCommand; use crate::util::api::{CloudOpts, TrustContextOpts}; use crate::CommandGlobalOpts; use self::revoke::RevokeCommand; +mod create; +mod list; +mod revoke; +mod show; + #[derive(Clone, Debug, Args)] #[command(arg_required_else_help = true, subcommand_required = true)] pub struct LeaseCommand { @@ -61,31 +57,37 @@ async fn authenticate( cloud_opts: &CloudOpts, trust_opts: &TrustContextOpts, ) -> miette::Result { - let trust_context_config = trust_opts.to_config(&opts.state)?.build(); + let trust_context = opts + .state + .retrieve_trust_context( + &trust_opts.trust_context, + &trust_opts.project_name, + &None, + &None, + ) + .await?; + let node = InMemoryNode::start_with_trust_context( ctx, &opts.state, - trust_opts.project_path.as_ref(), - trust_context_config, + trust_opts.project_name(), + trust_context, ) .await?; - let identity = get_identity_name(&opts.state, &cloud_opts.identity); - let project_info = retrieve_project_info(opts, trust_opts).await?; - let project_authority = project_info - .authority - .as_ref() - .ok_or(miette!("Project Authority is required"))?; - let project_identifier = project_info - .identity_id - .ok_or(miette!("Project identifier is required"))?; - let project_addr = project_info - .node_route - .ok_or(miette!("Project route is required"))?; + let identity = opts + .state + .get_identity_name_or_default(&cloud_opts.identity) + .await?; + let project = node + .get_project_by_name_or_default(ctx, &trust_opts.project_name()) + .await?; + + let authority_identity = project.authority_identity().await.into_diagnostic()?; let authority_node = node .create_authority_client( - project_authority.identity_id(), - project_authority.address(), + authority_identity.identifier(), + &project.authority_access_route().into_diagnostic()?, Some(identity.clone()), ) .await?; @@ -93,32 +95,10 @@ async fn authenticate( authority_node .authenticate(ctx, Some(identity.clone())) .await?; - node.create_project_client(&project_identifier, &project_addr, Some(identity.clone())) - .await -} - -async fn retrieve_project_info( - opts: &CommandGlobalOpts, - trust_context_opts: &TrustContextOpts, -) -> miette::Result { - let project_path = match &trust_context_opts.project_path { - Some(p) => p.clone(), - None => { - let default_project = opts - .state - .projects - .default() - .context("A default project or project parameter is required")?; - - default_project.path().clone() - } - }; - // Read (okta and authority) project parameters from project.json - let s = tokio::fs::read_to_string(project_path) - .await - .into_diagnostic()?; - let proj_info: ProjectConfigCompact = serde_json::from_str(&s).into_diagnostic()?; - ProjectLookup::from_project(&(&proj_info).into()) - .await - .into_diagnostic() + node.create_project_client( + &project.identifier().into_diagnostic()?, + &project.access_route().into_diagnostic()?, + Some(identity.clone()), + ) + .await } diff --git a/implementations/rust/ockam/ockam_command/src/lease/revoke.rs b/implementations/rust/ockam/ockam_command/src/lease/revoke.rs index 25af5141d3e..70c3e1ec513 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/revoke.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/revoke.rs @@ -2,7 +2,6 @@ use clap::Args; use ockam::Context; use ockam_api::InfluxDbTokenLease; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::util::api::{CloudOpts, TrustContextOpts}; use crate::util::node_rpc; @@ -21,7 +20,6 @@ pub struct RevokeCommand { impl RevokeCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, self, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lease/show.rs b/implementations/rust/ockam/ockam_command/src/lease/show.rs index 508230dfe63..2c7f9b3b1b3 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/show.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/show.rs @@ -3,7 +3,6 @@ use clap::Args; use ockam::Context; use ockam_api::InfluxDbTokenLease; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::output::Output; use crate::util::api::{CloudOpts, TrustContextOpts}; @@ -23,7 +22,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, self, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lib.rs b/implementations/rust/ockam/ockam_command/src/lib.rs index 8d0596e5c1e..62664ecb1b9 100644 --- a/implementations/rust/ockam/ockam_command/src/lib.rs +++ b/implementations/rust/ockam/ockam_command/src/lib.rs @@ -17,6 +17,65 @@ //! cd implementations/rust/ockam/ockam_command && cargo install --path . //! ``` +use std::{path::PathBuf, sync::Mutex}; + +use clap::{ArgAction, Args, Parser, Subcommand}; +use colorful::Colorful; +use console::Term; +use miette::GraphicalReportHandler; +use once_cell::sync::Lazy; + +use authenticated::AuthenticatedCommand; +use completion::CompletionCommand; +use configuration::ConfigurationCommand; +use credential::CredentialCommand; +use enroll::EnrollCommand; +use environment::EnvironmentCommand; +use error::{Error, Result}; +use identity::IdentityCommand; +use kafka::consumer::KafkaConsumerCommand; +use kafka::producer::KafkaProducerCommand; +use lease::LeaseCommand; +use manpages::ManpagesCommand; +use markdown::MarkdownCommand; +use message::MessageCommand; +use node::NodeCommand; +use ockam_api::cli_state::CliState; +use ockam_core::env::get_env_with_default; +use policy::PolicyCommand; +use project::ProjectCommand; +use relay::RelayCommand; +use reset::ResetCommand; +use secure_channel::{listener::SecureChannelListenerCommand, SecureChannelCommand}; +use service::ServiceCommand; +#[cfg(feature = "orchestrator")] +use share::ShareCommand; +use space::SpaceCommand; +use status::StatusCommand; +use tcp::{ + connection::TcpConnectionCommand, inlet::TcpInletCommand, listener::TcpListenerCommand, + outlet::TcpOutletCommand, +}; +use trust_context::TrustContextCommand; +use upgrade::check_if_an_upgrade_is_available; +use util::{exitcode, exitcode::ExitCode}; +use vault::VaultCommand; +use version::Version; +use worker::WorkerCommand; + +use crate::admin::AdminCommand; +use crate::authority::AuthorityCommand; +use crate::flow_control::FlowControlCommand; +use crate::kafka::direct::KafkaDirectCommand; +use crate::kafka::outlet::KafkaOutletCommand; +use crate::logs::setup_logging; +use crate::node::NodeSubcommand; +use crate::output::{Output, OutputFormat}; +use crate::run::RunCommand; +use crate::sidecar::SidecarCommand; +use crate::subscription::SubscriptionCommand; +pub use crate::terminal::{OckamColor, Terminal, TerminalStream}; + mod admin; mod authenticated; mod authority; @@ -62,63 +121,6 @@ mod vault; mod version; mod worker; -use crate::admin::AdminCommand; -use crate::authority::AuthorityCommand; -use crate::flow_control::FlowControlCommand; -use crate::logs::setup_logging; -use crate::node::NodeSubcommand; -use crate::run::RunCommand; -use crate::subscription::SubscriptionCommand; -pub use crate::terminal::{OckamColor, Terminal, TerminalStream}; -use authenticated::AuthenticatedCommand; -use clap::{ArgAction, Args, Parser, Subcommand}; - -use crate::kafka::direct::KafkaDirectCommand; -use crate::kafka::outlet::KafkaOutletCommand; -use crate::output::{Output, OutputFormat}; -use crate::sidecar::SidecarCommand; -use colorful::Colorful; -use completion::CompletionCommand; -use configuration::ConfigurationCommand; -use console::Term; -use credential::CredentialCommand; -use enroll::EnrollCommand; -use environment::EnvironmentCommand; -use error::{Error, Result}; -use identity::IdentityCommand; -use kafka::consumer::KafkaConsumerCommand; -use kafka::producer::KafkaProducerCommand; -use lease::LeaseCommand; -use manpages::ManpagesCommand; -use markdown::MarkdownCommand; -use message::MessageCommand; -use miette::GraphicalReportHandler; -use node::NodeCommand; -use ockam_api::cli_state::CliState; -use ockam_core::env::get_env_with_default; -use once_cell::sync::Lazy; -use policy::PolicyCommand; -use project::ProjectCommand; -use relay::RelayCommand; -use reset::ResetCommand; -use secure_channel::{listener::SecureChannelListenerCommand, SecureChannelCommand}; -use service::ServiceCommand; -#[cfg(feature = "orchestrator")] -use share::ShareCommand; -use space::SpaceCommand; -use status::StatusCommand; -use std::{path::PathBuf, sync::Mutex}; -use tcp::{ - connection::TcpConnectionCommand, inlet::TcpInletCommand, listener::TcpListenerCommand, - outlet::TcpOutletCommand, -}; -use trust_context::TrustContextCommand; -use upgrade::check_if_an_upgrade_is_available; -use util::{exitcode, exitcode::ExitCode}; -use vault::VaultCommand; -use version::Version; -use worker::WorkerCommand; - const ABOUT: &str = include_str!("./static/about.txt"); const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt"); @@ -240,7 +242,7 @@ pub struct CommandGlobalOpts { impl CommandGlobalOpts { pub fn new(global_args: GlobalArgs) -> Self { - let state = match CliState::initialize() { + let state = match CliState::with_default_dir() { Ok(state) => state, Err(err) => { eprintln!("Failed to initialize state: {}", err); @@ -250,7 +252,7 @@ impl CommandGlobalOpts { let state = CliState::backup_and_reset().expect( "Failed to initialize CliState. Try to manually remove the '~/.ockam' directory", ); - let dir = &state.dir; + let dir = state.dir(); let backup_dir = CliState::backup_default_dir().unwrap(); eprintln!( "The {dir:?} directory has been reset and has been backed up to {backup_dir:?}" @@ -494,13 +496,9 @@ impl OckamCommand { } // In the case where a node is explicitly created in foreground mode, we need // to initialize the node directories before we can get the log path. - let path = opts - .state - .nodes - .stdout_logs(&c.node_name) - .unwrap_or_else(|_| { - panic!("Failed to initialize logs file for node {}", c.node_name) - }); + let path = opts.state.stdout_logs(&c.node_name).unwrap_or_else(|_| { + panic!("Failed to initialize logs file for node {}", c.node_name) + }); return Some(path); } } @@ -518,6 +516,7 @@ pub(crate) fn display_parse_logs(opts: &CommandGlobalOpts) { logs.clear(); } } + pub(crate) fn replace_hyphen_with_stdin(s: String) -> String { let input_stream = std::io::stdin(); if s.contains("/-") { diff --git a/implementations/rust/ockam/ockam_command/src/message/send.rs b/implementations/rust/ockam/ockam_command/src/message/send.rs index 49a32529617..2df05629237 100644 --- a/implementations/rust/ockam/ockam_command/src/message/send.rs +++ b/implementations/rust/ockam/ockam_command/src/message/send.rs @@ -2,17 +2,15 @@ use core::time::Duration; use clap::Args; use miette::{Context as _, IntoDiagnostic}; +use tracing::info; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::nodes::service::message::{MessageSender, SendMessage}; use ockam_api::nodes::BackgroundNode; use ockam_api::nodes::InMemoryNode; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::project::util::{ clean_projects_multiaddr, get_projects_secure_channels_from_config_lookup, }; @@ -59,7 +57,6 @@ pub struct SendCommand { impl SendCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.cloud_opts.identity); node_rpc(rpc, (opts, self)) } } @@ -67,8 +64,9 @@ impl SendCommand { async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, SendCommand)) -> miette::Result<()> { async fn go(ctx: &Context, opts: CommandGlobalOpts, cmd: SendCommand) -> miette::Result<()> { // Process `--to` Multiaddr - let (to, meta) = - clean_nodes_multiaddr(&cmd.to, &opts.state).context("Argument '--to' is invalid")?; + let (to, meta) = clean_nodes_multiaddr(&cmd.to, &opts.state) + .await + .context("Argument '--to' is invalid")?; let msg_bytes = if cmd.hex { hex::decode(cmd.message) @@ -78,28 +76,45 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, SendCommand)) -> mie cmd.message.as_bytes().to_vec() }; - // Setup environment depending on whether we are sending the message from an background node + // Setup environment depending on whether we are sending the message from a background node // or an in-memory node let response: Vec = if let Some(node) = &cmd.from { - let node_name = extract_address_value(node)?; - BackgroundNode::create(ctx, &opts.state, &node_name) + BackgroundNode::create_to_node(ctx, &opts.state, node.as_str()) .await? .set_timeout(cmd.timeout) .ask(ctx, req(&to, msg_bytes)) .await? } else { - let identity_name = get_identity_name(&opts.state, &cmd.cloud_opts.identity); - let trust_context_config = cmd.trust_context_opts.to_config(&opts.state)?.build(); + let identity_name = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; + + info!("retrieving the trust context"); + + let named_trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_context_opts.trust_context, + &cmd.trust_context_opts.project_name, + &None, + &None, + ) + .await?; + info!("retrieved the trust context: {named_trust_context:?}"); + + info!("starting an in memory node to send a message"); let node_manager = InMemoryNode::start_node( ctx, &opts.state, None, Some(identity_name.clone()), - cmd.trust_context_opts.project_path.as_ref(), - trust_context_config, + cmd.trust_context_opts.project_name, + named_trust_context, ) .await?; + info!("started an in memory node to send a message"); // Replace `/project/` occurrences with their respective secure channel addresses let projects_sc = get_projects_secure_channels_from_config_lookup( @@ -112,6 +127,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, SendCommand)) -> mie ) .await?; let to = clean_projects_multiaddr(to, projects_sc)?; + info!("sending to {to}"); node_manager .send_message(ctx, &to, msg_bytes, Some(cmd.timeout)) .await diff --git a/implementations/rust/ockam/ockam_command/src/node/create.rs b/implementations/rust/ockam/ockam_command/src/node/create.rs index 864d07a0a88..02ebe65c951 100644 --- a/implementations/rust/ockam/ockam_command/src/node/create.rs +++ b/implementations/rust/ockam/ockam_command/src/node/create.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use std::{path::PathBuf, process, str::FromStr}; +use std::{path::PathBuf, str::FromStr}; use clap::Args; use colorful::Colorful; @@ -9,18 +9,17 @@ use minicbor::{Decoder, Encode}; use tokio::sync::Mutex; use tokio::time::{sleep, Duration}; use tokio::try_join; +use tracing::{debug, info}; +use ockam::identity::Identity; use ockam::{Address, AsyncTryClone, TcpListenerOptions}; use ockam::{Context, TcpTransport}; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_api::cli_state::{add_project_info_to_node_state, init_node_state, random_name}; -use ockam_api::nodes::models::transport::CreateTransportJson; +use ockam_api::cli_state::random_name; use ockam_api::nodes::service::NodeManagerTrustOptions; use ockam_api::nodes::BackgroundNode; use ockam_api::nodes::InMemoryNode; use ockam_api::{ bootstrapped_identities_store::PreTrustedIdentities, - nodes::models::transport::{TransportMode, TransportType}, nodes::{ service::{NodeManagerGeneralOptions, NodeManagerTransportOptions}, NodeManagerWorker, NODEMANAGER_ADDR, @@ -29,18 +28,18 @@ use ockam_api::{ use ockam_core::api::{Request, ResponseHeader, Status}; use ockam_core::{route, LOCAL}; +use crate::node::show::is_node_up; use crate::node::util::{spawn_node, NodeManagerDefaults}; use crate::secure_channel::listener::create as secure_channel_listener; use crate::service::config::Config; use crate::terminal::OckamColor; use crate::util::api::TrustContextOpts; -use crate::util::{api, parse_node_name}; -use crate::util::{embedded_node_that_is_not_stopped, exitcode}; +use crate::util::embedded_node_that_is_not_stopped; +use crate::util::parsers::identity_parser; +use crate::util::{api, exitcode}; use crate::util::{local_cmd, node_rpc}; -use crate::{docs, shutdown, CommandGlobalOpts, Result}; -use crate::{fmt_log, fmt_ok}; - -use super::show::is_node_up; +use crate::{docs, fmt_log, fmt_ok}; +use crate::{shutdown, CommandGlobalOpts, Result}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -101,8 +100,8 @@ pub struct CreateCommand { #[arg(long = "identity", value_name = "IDENTITY_NAME")] identity: Option, - #[arg(long)] - pub authority_identity: Option, + #[arg(long, value_name = "IDENTITY", value_parser = identity_parser)] + pub authority_identity: Option, #[arg(long = "credential", value_name = "CREDENTIAL_NAME")] pub credential: Option, @@ -123,10 +122,10 @@ impl Default for CreateCommand { launch_config: None, vault: None, identity: None, + authority_identity: None, trusted_identities: None, trusted_identities_file: None, reload_from_trusted_identities_file: None, - authority_identity: None, credential: None, trust_context_opts: node_manager_defaults.trust_context_opts, } @@ -135,17 +134,6 @@ impl Default for CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - if !self.child_process { - if let Ok(state) = opts.state.nodes.get(&self.node_name) { - if state.is_running() { - eprintln!( - "{:?}", - miette!("Node {} is already running", self.node_name) - ); - std::process::exit(exitcode::SOFTWARE); - } - } - } if self.foreground { local_cmd(foreground_mode(opts, self)); } else { @@ -187,12 +175,14 @@ pub(crate) async fn background_mode( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let node_name = &parse_node_name(&cmd.node_name)?; + check_if_node_is_not_already_running(&opts, &cmd).await; + + let node_name = cmd.node_name.clone(); + debug!("create node in background mode"); + opts.terminal.write_line(&fmt_log!( "Creating Node {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node_name.clone().color(OckamColor::PrimaryResource.color()) ))?; if cmd.child_process { @@ -205,8 +195,8 @@ pub(crate) async fn background_mode( let send_req = async { spawn_background_node(&opts, cmd.clone()).await?; - let mut node = BackgroundNode::create(&ctx, &opts.state, node_name).await?; - let is_node_up = is_node_up(&ctx, node_name, &mut node, opts.state.clone(), true).await?; + let mut node = BackgroundNode::create_to_node(&ctx, &opts.state, &node_name).await?; + let is_node_up = is_node_up(&ctx, &mut node, true).await?; *is_finished.lock().await = true; Ok(is_node_up) }; @@ -229,9 +219,7 @@ pub(crate) async fn background_mode( .plain( fmt_ok!( "Node {} created successfully\n\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node_name.color(OckamColor::PrimaryResource.color()) ) + &fmt_log!("To see more details on this node, run:\n") + &fmt_log!( "{}", @@ -253,33 +241,36 @@ async fn run_foreground_node( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let node_name = parse_node_name(&cmd.node_name)?; - - // This node was initially created as a foreground node - // and there is no existing state for it yet. - if !cmd.child_process && !opts.state.nodes.exists(&node_name) { - init_node_state( - &opts.state, - &node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), - ) - .await?; - } + check_if_node_is_not_already_running(&opts, &cmd).await; - add_project_info_to_node_state( - &node_name, - &opts.state, - cmd.trust_context_opts.project_path.as_ref(), - ) - .await?; + let node_name = cmd.node_name.clone(); + debug!("create node {node_name} in foreground mode"); - let trust_context_config = cmd - .trust_context_opts - .to_config(&opts.state)? - .with_authority_identity(cmd.authority_identity.as_ref()) - .with_credential_name(cmd.credential.as_ref()) - .build(); + if opts.state.is_node_running(&node_name).await? { + eprintln!("{:?}", miette!("Node {} is already running", &node_name)); + std::process::exit(exitcode::SOFTWARE); + }; + + let node_info = opts + .state + .create_node_with_optional_name_and_optional_vault_and_optional_project( + &Some(node_name.clone()), + &cmd.identity, + &cmd.vault, + &cmd.trust_context_opts.project_name, + ) + .await?; + debug!("created node {node_info:?}"); + + let named_trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_context_opts.trust_context, + &cmd.trust_context_opts.project_name, + &cmd.authority_identity, + &cmd.credential, + ) + .await?; let tcp = TcpTransport::create(&ctx).await.into_diagnostic()?; let options = TcpListenerOptions::new(); @@ -288,22 +279,13 @@ async fn run_foreground_node( .await .into_diagnostic()?; - let node_state = opts.state.nodes.get(&node_name)?; - node_state.set_pid(process::id() as i32)?; - node_state.set_setup( - &node_state - .config() - .setup_mut() - .set_verbose(opts.global_args.verbose) - .set_api_transport( - CreateTransportJson::new( - TransportType::Tcp, - TransportMode::Listen, - &listener.socket_address().to_string(), - ) - .into_diagnostic()?, - ), - )?; + opts.state + .set_tcp_listener_address(&node_name, listener.socket_address().to_string()) + .await?; + debug!( + "set the node {node_name} listener address to {:?}", + listener.socket_address() + ); let pre_trusted_identities = load_pre_trusted_identities(&cmd)?; @@ -311,7 +293,7 @@ async fn run_foreground_node( &ctx, NodeManagerGeneralOptions::new( opts.state.clone(), - cmd.node_name.clone(), + node_name.clone(), pre_trusted_identities, cmd.launch_config.is_none(), true, @@ -320,7 +302,7 @@ async fn run_foreground_node( listener.flow_control_id().clone(), tcp.async_try_clone().await.into_diagnostic()?, ), - NodeManagerTrustOptions::new(trust_context_config), + NodeManagerTrustOptions::new(named_trust_context), ) .await .into_diagnostic()?; @@ -360,9 +342,7 @@ async fn run_foreground_node( .await?; // Try to stop node; it might have already been stopped or deleted (e.g. when running `node delete --all`) - if let Ok(state) = opts.state.nodes.get(&node_name) { - let _ = state.kill_process(false); - } + opts.state.stop_node(&node_name, true).await?; ctx.stop().await.into_diagnostic()?; opts.terminal .write_line(format!("{}Node stopped successfully", "✔︎".light_green()).as_str()) @@ -441,43 +421,49 @@ pub async fn spawn_background_node( opts: &CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let node_name = parse_node_name(&cmd.node_name)?; - // Create node state, including the vault and identity if don't exist - init_node_state( - &opts.state, - &node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), - ) - .await?; - - let trust_context_path = match cmd.trust_context_opts.trust_context.clone() { + let trust_context = match cmd.trust_context_opts.trust_context.clone() { Some(tc) => { - let config = opts.state.trust_contexts.read_config_from_path(&tc)?; - Some(config.path().unwrap().clone()) + let trust_context = opts.state.get_trust_context(&tc).await?; + Some(trust_context) } None => None, }; // Construct the arguments list and re-execute the ockam // CLI in foreground mode to start the newly created node + info!("spawing a new node {}", &cmd.node_name); spawn_node( opts, - &node_name, + &cmd.node_name, + &cmd.identity, + &cmd.vault, &cmd.tcp_listener_address, - cmd.trust_context_opts.project_path.as_ref(), cmd.trusted_identities.as_ref(), cmd.trusted_identities_file.as_ref(), cmd.reload_from_trusted_identities_file.as_ref(), cmd.launch_config .as_ref() .map(|config| serde_json::to_string(config).unwrap()), - cmd.authority_identity.as_ref(), cmd.credential.as_ref(), - trust_context_path.as_ref(), - cmd.trust_context_opts.project.as_ref(), + trust_context.as_ref(), + cmd.trust_context_opts.project_name.clone(), cmd.logging_to_file(), - )?; + ) + .await?; Ok(()) } + +async fn check_if_node_is_not_already_running(opts: &CommandGlobalOpts, cmd: &CreateCommand) { + if !cmd.child_process { + if let Ok(node) = opts.state.get_node(&cmd.node_name).await { + if node.is_running() { + eprintln!( + "{:?}", + miette!("Node {} is already running", &cmd.node_name) + ); + std::process::exit(exitcode::SOFTWARE); + } + } + } +} diff --git a/implementations/rust/ockam/ockam_command/src/node/default.rs b/implementations/rust/ockam/ockam_command/src/node/default.rs index 433d127fff7..05a00771959 100644 --- a/implementations/rust/ockam/ockam_command/src/node/default.rs +++ b/implementations/rust/ockam/ockam_command/src/node/default.rs @@ -1,11 +1,11 @@ -use crate::node::{get_default_node_name, get_node_name}; -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; - use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -13,8 +13,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt /// Change the default node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct DefaultCommand { /// Name of the node to set as default @@ -23,25 +23,27 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { +async fn run_impl( + _cxt: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { if let Some(node_name) = cmd.node_name { - let name = get_node_name(&opts.state, &Some(node_name.clone())); - if opts.state.nodes.is_default(&name)? { - return Err(miette!("The node '{name}' is already the default")); + if opts.state.is_default_node(&node_name).await? { + return Err(miette!("The node '{node_name}' is already the default")); } else { - opts.state.nodes.set_default(&name)?; + opts.state.set_default_node(&node_name).await?; opts.terminal .stdout() - .plain(fmt_ok!("The node '{name}' is now the default")) - .machine(&name) + .plain(fmt_ok!("The node '{node_name}' is now the default")) + .machine(&node_name) .write_line()?; } } else { - let default_node_name = get_default_node_name(&opts.state); + let default_node_name = opts.state.get_default_node_name().await?; let _ = opts .terminal .stdout() diff --git a/implementations/rust/ockam/ockam_command/src/node/delete.rs b/implementations/rust/ockam/ockam_command/src/node/delete.rs index f2214e06837..7fb8d47bfe6 100644 --- a/implementations/rust/ockam/ockam_command/src/node/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/node/delete.rs @@ -1,10 +1,8 @@ use clap::Args; use colorful::Colorful; use console::Term; -use ockam_api::cli_state::StateDirTrait; use ockam_node::Context; -use crate::node::get_node_name; use crate::terminal::tui::DeleteCommandTui; use crate::util::node_rpc; use crate::{docs, fmt_ok, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; @@ -82,18 +80,29 @@ impl DeleteCommandTui for DeleteTui { } async fn get_arg_item_name_or_default(&self) -> miette::Result { - Ok(get_node_name(&self.opts.state, &self.cmd.node_name)) + Ok(self + .opts + .state + .get_node_name_or_default(&self.cmd.node_name) + .await?) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.nodes.list_items_names()?) + Ok(self + .opts + .state + .get_nodes() + .await? + .iter() + .map(|n| n.name()) + .collect()) } async fn delete_single(&self, item_name: &str) -> miette::Result<()> { self.opts .state - .nodes - .delete_sigkill(item_name, self.cmd.force)?; + .delete_node(item_name, self.cmd.force) + .await?; self.terminal() .stdout() .plain(fmt_ok!( @@ -107,23 +116,26 @@ impl DeleteCommandTui for DeleteTui { } async fn delete_multiple(&self, items_names: Vec) -> miette::Result<()> { - let plain = items_names - .into_iter() - .map(|name| { - if self - .opts - .state - .nodes - .delete_sigkill(&name, self.cmd.force) - .is_ok() - { - fmt_ok!("Node {} deleted\n", name.light_magenta()) - } else { - fmt_warn!("Failed to delete node {}\n", name.light_magenta()) - } - }) - .collect::(); - self.terminal().stdout().plain(plain).write_line()?; + let mut plain: Vec = vec![]; + for name in items_names { + let result = if self + .opts + .state + .delete_node(&name, self.cmd.force) + .await + .is_ok() + { + fmt_ok!("Node {} deleted\n", name.light_magenta()) + } else { + fmt_warn!("Failed to delete node {}\n", name.light_magenta()) + }; + plain.push(result); + } + + self.terminal() + .stdout() + .plain(plain.join("\n")) + .write_line()?; Ok(()) } } diff --git a/implementations/rust/ockam/ockam_command/src/node/list.rs b/implementations/rust/ockam/ockam_command/src/node/list.rs index 5def071cd20..6e6670060a2 100644 --- a/implementations/rust/ockam/ockam_command/src/node/list.rs +++ b/implementations/rust/ockam/ockam_command/src/node/list.rs @@ -1,21 +1,17 @@ use clap::Args; use colorful::Colorful; use indoc::formatdoc; -use miette::Context as _; use miette::IntoDiagnostic; use serde::Serialize; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; -use ockam_api::nodes::models::base::NodeStatus; -use ockam_api::nodes::BackgroundNode; +use ockam_api::nodes::NodeInfo; -use crate::node::get_default_node_name; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::{api, node_rpc}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts, Result}; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); @@ -25,9 +21,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List nodes #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand {} @@ -38,7 +34,7 @@ impl ListCommand { } async fn run_impl( - ctx: Context, + _ctx: Context, (opts, _cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { // Before printing node states we verify them. @@ -48,47 +44,28 @@ async fn run_impl( // This should only happen if the node has failed in the past, // and has been restarted by something that is not this CLI. let node_names: Vec<_> = { - let nodes_states = opts.state.nodes.list()?; - nodes_states.iter().map(|s| s.name().to_string()).collect() + let nodes = opts.state.get_nodes().await?; + nodes.iter().map(|n| n.name()).collect() }; - let nodes = get_nodes_info(&ctx, &opts, node_names).await?; + let nodes = get_nodes_info(&opts, node_names).await?; print_nodes_info(&opts, nodes)?; - Ok(()) } pub async fn get_nodes_info( - ctx: &Context, opts: &CommandGlobalOpts, node_names: Vec, ) -> Result> { let mut nodes: Vec = Vec::new(); - let default_node_name = get_default_node_name(&opts.state); - for node_name in node_names { - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + for node_name in node_names { let is_finished: Mutex = Mutex::new(false); let get_node_status = async { - let result: miette::Result = node.ask(ctx, api::query_status()).await; - let node_status = match result { - Ok(node_status) => { - if let Ok(node_state) = opts.state.nodes.get(&node_name) { - // Update the persisted configuration data with the pids - // responded by nodes. - if node_state.pid()? != Some(node_status.pid) { - node_state - .set_pid(node_status.pid) - .context("Failed to update pid for node {node_name}")?; - } - } - node_status - } - Err(_) => NodeStatus::new(node_name.to_string(), "Not running".to_string(), 0, 0), - }; + let node = opts.state.get_node(&node_name).await?; *is_finished.lock().await = true; - Ok(node_status) + Ok(node) }; let output_messages = vec![format!( @@ -101,14 +78,9 @@ pub async fn get_nodes_info( .terminal .progress_output(&output_messages, &is_finished); - let (node_status, _) = try_join!(get_node_status, progress_output)?; + let (node, _) = try_join!(get_node_status, progress_output)?; - nodes.push(NodeListOutput::new( - node_status.node_name.to_string(), - node_status.status.to_string(), - node_status.pid, - node_status.node_name == default_node_name, - )); + nodes.push(NodeListOutput::from_node_info(&node)); } Ok(nodes) @@ -138,12 +110,12 @@ pub fn print_nodes_info( pub struct NodeListOutput { pub node_name: String, pub status: String, - pub pid: i32, + pub pid: Option, pub is_default: bool, } impl NodeListOutput { - pub fn new(node_name: String, status: String, pid: i32, is_default: bool) -> Self { + pub fn new(node_name: String, status: String, pid: Option, is_default: bool) -> Self { Self { node_name, status, @@ -151,18 +123,30 @@ impl NodeListOutput { is_default, } } + + pub fn from_node_info(node_info: &NodeInfo) -> Self { + let status = if node_info.is_running() { + "Running" + } else { + "Not running" + }; + Self::new( + node_info.name(), + status.to_string(), + node_info.pid(), + node_info.is_default(), + ) + } } impl Output for NodeListOutput { fn output(&self) -> Result { - let (status, pid) = match self.status.as_str() { - "Running" => ( + let (status, pid) = match self.pid { + Some(pid) => ( "UP".color(OckamColor::Success.color()), format!( "Process id {}", - self.pid - .to_string() - .color(OckamColor::PrimaryResource.color()) + pid.to_string().color(OckamColor::PrimaryResource.color()) ), ), _ => ( diff --git a/implementations/rust/ockam/ockam_command/src/node/logs.rs b/implementations/rust/ockam/ockam_command/src/node/logs.rs index d0b944a79d4..ec61f3e3be0 100644 --- a/implementations/rust/ockam/ockam_command/src/node/logs.rs +++ b/implementations/rust/ockam/ockam_command/src/node/logs.rs @@ -1,10 +1,11 @@ -use crate::fmt_ok; -use crate::node::get_node_name; -use crate::util::local_cmd; -use crate::{docs, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::fmt_ok; +use crate::util::node_rpc; +use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/logs/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -13,9 +14,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/logs/after_long_help.txt"); /// Get the stdout/stderr log file of a node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct LogCommand { /// Name of the node to retrieve the logs from. @@ -24,14 +25,16 @@ pub struct LogCommand { impl LogCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: LogCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_name); - let node_state = opts.state.nodes.get(node_name)?; - let log_path = node_state.stdout_log().display().to_string(); +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, LogCommand), +) -> miette::Result<()> { + let node_name = opts.state.get_node_name_or_default(&cmd.node_name).await?; + let log_path = opts.state.stdout_logs(&node_name)?.display().to_string(); opts.terminal .stdout() .plain(fmt_ok!("The path for the log file is: {log_path}")) diff --git a/implementations/rust/ockam/ockam_command/src/node/mod.rs b/implementations/rust/ockam/ockam_command/src/node/mod.rs index ebd836ddad6..747cd000766 100644 --- a/implementations/rust/ockam/ockam_command/src/node/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/node/mod.rs @@ -1,17 +1,16 @@ use clap::{Args, Subcommand}; -use colorful::Colorful; pub use create::CreateCommand; +pub use create::*; use default::DefaultCommand; use delete::DeleteCommand; use list::ListCommand; use logs::LogCommand; -use ockam_api::cli_state::{CliState, StateDirTrait}; use show::ShowCommand; use start::StartCommand; use stop::StopCommand; -use crate::{docs, fmt_log, terminal::OckamColor, CommandGlobalOpts, PARSER_LOGS}; +use crate::{docs, CommandGlobalOpts}; mod create; mod default; @@ -23,7 +22,6 @@ mod show; mod start; mod stop; pub mod util; -pub use create::*; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt"); @@ -31,10 +29,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt"); /// Manage Nodes #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - subcommand_required = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = true, +subcommand_required = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct NodeCommand { #[command(subcommand)] @@ -81,82 +79,3 @@ pub struct NodeOpts { #[arg(global = true, id = "at", value_name = "NODE_NAME", long)] pub at_node: Option, } - -/// If the required node name is the default node but that node has not been initialized yet -/// then initialize it -pub fn initialize_node_if_default(opts: &CommandGlobalOpts, node_name: &Option) { - let node_name = get_node_name(&opts.state, node_name); - if node_name == "default" && opts.state.nodes.default().is_err() { - spawn_default_node(opts) - } -} - -/// Return the node_name if Some otherwise return the default node name -pub fn get_node_name<'a>(cli_state: &CliState, node_name: impl Into<&'a Option>) -> String { - node_name - .into() - .clone() - .unwrap_or_else(|| get_default_node_name(cli_state)) -} - -/// Return the default node name -pub fn get_default_node_name(cli_state: &CliState) -> String { - cli_state - .nodes - .default() - .map(|n| n.name().to_string()) - .unwrap_or_else(|_| "default".to_string()) -} - -/// Start the default node -fn spawn_default_node(opts: &CommandGlobalOpts) { - let mut create_command = CreateCommand::default(); - - let default = "default"; - create_command.node_name = default.into(); - create_command.run(opts.clone().set_quiet()); - - if let Ok(mut logs) = PARSER_LOGS.lock() { - logs.push(fmt_log!( - "There is no node, on this machine, marked as your default." - )); - logs.push(fmt_log!("Creating a new Ockam node for you...")); - logs.push(fmt_log!( - "Created a new node named {}", - default.color(OckamColor::PrimaryResource.color()) - )); - logs.push(fmt_log!( - "Marked this node as your default, on this machine.\n" - )); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::GlobalArgs; - use ockam_api::cli_state::StateItemTrait; - - #[test] - fn test_initialize() { - let opts = CommandGlobalOpts::new(GlobalArgs::default()).set_quiet(); - - // on start-up there is no default node - let _ = opts.state.nodes.default().and_then(|n| n.delete()); - assert!(opts.state.nodes.default().is_err()); - - // if no name is given then the default node is initialized - initialize_node_if_default(&opts, &None); - assert!(opts.state.nodes.default().is_ok()); - - // if "default" is given as a name the default node is initialized - opts.state.nodes.default().unwrap().delete().unwrap(); - initialize_node_if_default(&opts, &Some("default".into())); - assert!(opts.state.nodes.default().is_ok()); - - // if the name of another identity is given then the default node is not initialized - opts.state.nodes.default().unwrap().delete().unwrap(); - initialize_node_if_default(&opts, &Some("other".into())); - assert!(opts.state.nodes.default().is_err()); - } -} diff --git a/implementations/rust/ockam/ockam_command/src/node/show.rs b/implementations/rust/ockam/ockam_command/src/node/show.rs index 2b4209c3d85..5257983d0fa 100644 --- a/implementations/rust/ockam/ockam_command/src/node/show.rs +++ b/implementations/rust/ockam/ockam_command/src/node/show.rs @@ -1,18 +1,17 @@ use clap::Args; use console::Term; use miette::IntoDiagnostic; +use tokio_retry::strategy::FixedInterval; +use tracing::{info, trace, warn}; + +use ockam_api::nodes::models::base::NodeStatus; +use ockam_api::nodes::models::portal::{InletList, OutletList}; use ockam_api::nodes::models::secure_channel::SecureChannelListenersList; use ockam_api::nodes::models::services::ServiceList; use ockam_api::nodes::models::transport::TransportList; use ockam_api::nodes::BackgroundNode; use ockam_node::Context; -use tokio_retry::strategy::FixedInterval; -use tracing::{info, trace, warn}; - -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use ockam_api::nodes::models::portal::{InletList, OutletList}; -use crate::node::get_node_name; use crate::node::list; use crate::terminal::tui::ShowCommandTui; use crate::util::{api, node_rpc}; @@ -34,9 +33,9 @@ const IS_NODE_UP_MAX_ATTEMPTS: usize = 60; // 3 seconds /// Show the details of a node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { /// Name of the node to retrieve the details from @@ -90,21 +89,34 @@ impl ShowCommandTui for ShowTui { } async fn get_arg_item_name_or_default(&self) -> miette::Result { - Ok(get_node_name(&self.opts.state, &self.node_name)) + Ok(self + .opts + .state + .get_node_name_or_default(&self.node_name) + .await?) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.nodes.list_items_names()?) + Ok(self + .opts + .state + .get_nodes() + .await? + .iter() + .map(|n| n.name()) + .collect()) } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let mut node = BackgroundNode::create(&self.ctx, &self.opts.state, item_name).await?; - print_query_status(&self.opts, &self.ctx, item_name, &mut node, false).await?; + let mut node = + BackgroundNode::create(&self.ctx, &self.opts.state, &Some(item_name.to_string())) + .await?; + print_query_status(&self.opts, &self.ctx, &mut node, false).await?; Ok(()) } async fn show_multiple(&self, items_names: Vec) -> miette::Result<()> { - let nodes = list::get_nodes_info(&self.ctx, &self.opts, items_names).await?; + let nodes = list::get_nodes_info(&self.opts, items_names).await?; list::print_nodes_info(&self.opts, nodes)?; Ok(()) } @@ -113,90 +125,76 @@ impl ShowCommandTui for ShowTui { pub async fn print_query_status( opts: &CommandGlobalOpts, ctx: &Context, - node_name: &str, node: &mut BackgroundNode, wait_until_ready: bool, ) -> miette::Result<()> { let cli_state = opts.state.clone(); - let is_default = opts.state.nodes.is_default(node_name)?; - - let node_info = - if !is_node_up(ctx, node_name, node, cli_state.clone(), wait_until_ready).await? { - let node_state = cli_state.nodes.get(node_name)?; - let node_port = node_state - .config() - .setup() - .api_transport() - .ok() - .map(|listener| listener.addr.port()); - - // it is expected to not be able to open an arbitrary TCP connection on an authority node - // so in that case we display an UP status - let is_authority_node = node_state.config().setup().authority_node.unwrap_or(false); - - ShowNodeResponse::new(is_default, node_name, is_authority_node, node_port) - } else { - let node_state = cli_state.nodes.get(node_name)?; - let node_port = node_state - .config() - .setup() - .api_transport() - .ok() - .map(|listener| listener.addr.port()); - - let mut node_info = ShowNodeResponse::new(is_default, node_name, true, node_port); - - // Get short id for the node - node_info.identity = Some(match node_state.config().identity_config() { - Ok(resp) => resp.identifier().to_string(), - Err(_) => String::from("None"), - }); - - // Get list of services for the node - let services: ServiceList = node.ask(ctx, api::list_services()).await?; - node_info.services = services - .list - .into_iter() - .map(ShowServiceStatus::from) - .collect(); - - // Get list of TCP listeners for node - let transports: TransportList = node.ask(ctx, api::list_tcp_listeners()).await?; - node_info.transports = transports - .list - .into_iter() - .map(ShowTransportStatus::from) - .collect(); - - // Get list of Secure Channel Listeners - let listeners: SecureChannelListenersList = - node.ask(ctx, api::list_secure_channel_listener()).await?; - node_info.secure_channel_listeners = listeners - .list - .into_iter() - .map(ShowSecureChannelListener::from) - .collect(); - - // Get list of inlets - let inlets: InletList = node.ask(ctx, api::list_inlets()).await?; - node_info.inlets = inlets.list.into_iter().map(ShowInletStatus::from).collect(); - - // Get list of outlets - let outlets: OutletList = node.ask(ctx, api::list_outlets()).await?; - node_info.outlets = outlets - .list - .into_iter() - .map(ShowOutletStatus::from) - .collect(); - - node_info - }; + let node_name = node.node_name(); + let node_info = cli_state.get_node(&node_name).await?; + + let show_node = if !is_node_up(ctx, node, wait_until_ready).await? { + // it is expected to not be able to open an arbitrary TCP connection on an authority node + // so in that case we display an UP status + let is_authority_node = cli_state.is_authority_node(&node_name).await?; + + ShowNodeResponse::new( + node_info.is_default(), + &node_name, + is_authority_node, + node_info.tcp_listener_port(), + ) + } else { + let mut show_node = ShowNodeResponse::new( + node_info.is_default(), + &node_name, + true, + node_info.tcp_listener_port(), + ); + // Get list of services for the node + let services: ServiceList = node.ask(ctx, api::list_services()).await?; + show_node.services = services + .list + .into_iter() + .map(ShowServiceStatus::from) + .collect(); + + // Get list of TCP listeners for node + let transports: TransportList = node.ask(ctx, api::list_tcp_listeners()).await?; + show_node.transports = transports + .list + .into_iter() + .map(ShowTransportStatus::from) + .collect(); + + // Get list of Secure Channel Listeners + let listeners: SecureChannelListenersList = + node.ask(ctx, api::list_secure_channel_listener()).await?; + show_node.secure_channel_listeners = listeners + .list + .into_iter() + .map(ShowSecureChannelListener::from) + .collect(); + + // Get list of inlets + let inlets: InletList = node.ask(ctx, api::list_inlets()).await?; + show_node.inlets = inlets.list.into_iter().map(ShowInletStatus::from).collect(); + + // Get list of outlets + let outlets: OutletList = node.ask(ctx, api::list_outlets()).await?; + show_node.outlets = outlets + .list + .into_iter() + .map(ShowOutletStatus::from) + .collect(); + + show_node + }; opts.terminal .clone() .stdout() - .plain(&node_info) - .json(serde_json::to_string_pretty(&node_info).into_diagnostic()?) + .plain(&show_node) + .json(serde_json::to_string_pretty(&show_node).into_diagnostic()?) .write_line()?; Ok(()) @@ -211,9 +209,7 @@ pub async fn print_query_status( /// allow a node time to start up and become ready. pub async fn is_node_up( ctx: &Context, - node_name: &str, - node: &mut BackgroundNode, - cli_state: CliState, + node_client: &mut BackgroundNode, wait_until_ready: bool, ) -> Result { let attempts = match wait_until_ready { @@ -224,27 +220,30 @@ pub async fn is_node_up( let retries = FixedInterval::from_millis(IS_NODE_UP_TIME_BETWEEN_CHECKS_MS as u64).take(attempts); - let cli_state = cli_state.clone(); let now = std::time::Instant::now(); + let node_name = node_client.node_name(); + for timeout_duration in retries { - let node_state = cli_state.nodes.get(node_name)?; - // The node is down if it has not stored its default tcp listener in its state file. - if node_state.config().setup().api_transport().is_err() { - trace!(%node_name, "node has not been initialized"); - tokio::time::sleep(timeout_duration).await; - continue; + // The node is down if its default tcp listener has not been started yet + let node = node_client.cli_state().get_node(&node_name).await.ok(); + if let Some(node) = node { + if node.tcp_listener_address().is_none() { + trace!(%node_name, "node has not been initialized"); + tokio::time::sleep(timeout_duration).await; + continue; + } } // Test if node is up // If node is down, we expect it won't reply and the timeout // will trigger the next loop (i.e. no need to sleep here). - let result = node + let result = node_client .set_timeout(timeout_duration) - .tell(ctx, api::query_status()) + .ask::<(), NodeStatus>(ctx, api::query_status()) .await; - if result.is_ok() { + if let Ok(node_status) = result { let elapsed = now.elapsed(); - info!(%node_name, ?elapsed, "node is up"); + info!(%node_name, ?elapsed, "node is up {:?}", node_status); return Ok(true); } else { trace!(%node_name, "node is initializing"); diff --git a/implementations/rust/ockam/ockam_command/src/node/start.rs b/implementations/rust/ockam/ockam_command/src/node/start.rs index 7dc89f565ba..b3f98615c8d 100644 --- a/implementations/rust/ockam/ockam_command/src/node/start.rs +++ b/implementations/rust/ockam/ockam_command/src/node/start.rs @@ -1,7 +1,6 @@ use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::nodes::BackgroundNode; use ockam_node::Context; @@ -10,8 +9,6 @@ use crate::node::util::spawn_node; use crate::util::node_rpc; use crate::{docs, fmt_err, fmt_info, fmt_log, fmt_ok, fmt_warn, CommandGlobalOpts, OckamColor}; -use super::get_node_name; - const LONG_ABOUT: &str = include_str!("./static/start/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/start/after_long_help.txt"); @@ -19,9 +16,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/start/after_long_help.txt") /// Start a node that was previously stopped #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct StartCommand { /// Name of the node to be started @@ -42,12 +39,12 @@ async fn run_impl( (opts, cmd): (CommandGlobalOpts, StartCommand), ) -> miette::Result<()> { if cmd.node_name.is_some() || !opts.terminal.can_ask_for_user_input() { - let node_name = &get_node_name(&opts.state, &cmd.node_name); - start_single_node(node_name, opts, &ctx).await?; + let node_name = opts.state.get_node_name_or_default(&cmd.node_name).await?; + start_single_node(&node_name, opts, &ctx).await?; return Ok(()); } - let inactive_nodes = get_inactive_nodes(&opts)?; + let inactive_nodes = get_inactive_nodes(&opts).await?; match inactive_nodes.len() { 0 => { opts.terminal @@ -104,12 +101,12 @@ async fn start_single_node( mut opts: CommandGlobalOpts, ctx: &Context, ) -> miette::Result<()> { - let node_state = opts.state.nodes.get(node_name)?; + let node_info = opts.state.get_node(node_name).await?; - opts.global_args.verbose = node_state.config().setup().verbose; + opts.global_args.verbose = node_info.verbosity(); // Abort if node is already running - if node_state.is_running() { + if node_info.is_running() { opts.terminal .stdout() .plain(fmt_err!( @@ -121,7 +118,7 @@ async fn start_single_node( } let mut node: BackgroundNode = run_node(node_name, ctx, &opts).await?; - print_query_status(&opts, ctx, node_name, &mut node, true).await?; + print_query_status(&opts, ctx, &mut node, true).await?; Ok(()) } @@ -149,40 +146,44 @@ async fn start_multiple_nodes( Ok(node_starts_output) } -/// Run a single node. Return the BackgroundNode istance of the created node or error +/// Run a single node. Return the BackgroundNode instance of the created node or error async fn run_node( node_name: &str, ctx: &Context, opts: &CommandGlobalOpts, ) -> miette::Result { - let node_state = opts.state.nodes.get(node_name)?; - node_state.kill_process(false)?; - let node_setup = node_state.config().setup(); - let node_name = node_state.name(); + let node_info = opts.state.get_node(node_name).await?; + opts.state.stop_node(node_name, false).await?; + let node_address = node_info + .tcp_listener_address() + .map(|a| a.to_string()) + .unwrap_or("no transport address".to_string()); + // Restart node spawn_node( opts, - node_name, // The selected node name - &node_setup.api_transport()?.addr.to_string(), // The selected node api address - None, // No project information available - None, // No trusted identities - None, // " - None, // " - None, // Launch config - None, // Authority Identity - None, // Credential - None, // Trust Context - None, // Project Name - true, // Restarted nodes will log to files - )?; - - let node = BackgroundNode::create(ctx, &opts.state, node_name).await?; + node_name, // The selected node name + &None, // Use the default identity + &None, // Use the default vault + &node_address, // The selected node api address + None, // No project information available + None, // No trusted identities + None, // " + None, // Launch config + None, // Authority Identity + None, // Credential + None, // Trust Context + true, // Restarted nodes will log to files + ) + .await?; + + let node = BackgroundNode::create_to_node(ctx, &opts.state, node_name).await?; Ok(node) } /// Get a list of the inactive_nodes -fn get_inactive_nodes(opts: &CommandGlobalOpts) -> miette::Result> { - let node_list = opts.state.nodes.list()?; +async fn get_inactive_nodes(opts: &CommandGlobalOpts) -> miette::Result> { + let node_list = opts.state.get_nodes().await?; Ok(node_list .iter() .filter(|node_state| !(node_state.is_running())) diff --git a/implementations/rust/ockam/ockam_command/src/node/stop.rs b/implementations/rust/ockam/ockam_command/src/node/stop.rs index 3fa847aa050..b3dcf096329 100644 --- a/implementations/rust/ockam/ockam_command/src/node/stop.rs +++ b/implementations/rust/ockam/ockam_command/src/node/stop.rs @@ -1,9 +1,10 @@ -use crate::node::get_node_name; -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/stop/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -12,10 +13,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/stop/after_long_help.txt"); /// Stop a running node #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct StopCommand { /// Name of the node. @@ -27,14 +28,16 @@ pub struct StopCommand { impl StopCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: StopCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_name); - let node_state = opts.state.nodes.get(&node_name)?; - node_state.kill_process(cmd.force)?; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, StopCommand), +) -> miette::Result<()> { + let node_name = opts.state.get_node_name_or_default(&cmd.node_name).await?; + opts.state.stop_node(&node_name, cmd.force).await?; opts.terminal .stdout() .plain(fmt_ok!("Stopped node '{}'", &node_name)) diff --git a/implementations/rust/ockam/ockam_command/src/node/util.rs b/implementations/rust/ockam/ockam_command/src/node/util.rs index 9329b03a6cb..150bc638859 100644 --- a/implementations/rust/ockam/ockam_command/src/node/util.rs +++ b/implementations/rust/ockam/ockam_command/src/node/util.rs @@ -3,11 +3,11 @@ use std::fs::OpenOptions; use std::path::PathBuf; use std::process::{Command, Stdio}; -use miette::Context as _; -use miette::{miette, IntoDiagnostic}; +use miette::IntoDiagnostic; +use miette::{miette, Context as _}; use rand::random; -use ockam_api::cli_state::StateDirTrait; +use ockam_api::cli_state::trust_contexts_repository_sql::NamedTrustContext; use ockam_core::env::get_env_with_default; use crate::util::api::TrustContextOpts; @@ -29,12 +29,12 @@ impl Default for NodeManagerDefaults { } } -pub fn delete_all_nodes(opts: &CommandGlobalOpts, force: bool) -> miette::Result<()> { - let nodes_states = opts.state.nodes.list()?; +pub async fn delete_all_nodes(opts: &CommandGlobalOpts, force: bool) -> miette::Result<()> { + let nodes = opts.state.get_nodes().await?; let mut deletion_errors = Vec::new(); - for s in nodes_states { - if let Err(e) = opts.state.nodes.delete_sigkill(s.name(), force) { - deletion_errors.push((s.name().to_string(), e)); + for n in nodes { + if let Err(e) = opts.state.delete_node(&n.name(), force).await { + deletion_errors.push((n.name(), e)); } } if !deletion_errors.is_empty() { @@ -46,28 +46,28 @@ pub fn delete_all_nodes(opts: &CommandGlobalOpts, force: bool) -> miette::Result Ok(()) } -pub fn check_default(opts: &CommandGlobalOpts, name: &str) -> bool { - if let Ok(default) = opts.state.nodes.default() { - return default.name() == name; +pub async fn check_default(opts: &CommandGlobalOpts, name: &str) -> bool { + if let Ok(default_name) = opts.state.get_default_node_name().await { + return default_name == name; } false } /// A utility function to spawn a new node into foreground mode #[allow(clippy::too_many_arguments)] -pub fn spawn_node( +pub async fn spawn_node( opts: &CommandGlobalOpts, name: &str, + identity_name: &Option, + vault_name: &Option, address: &str, - project: Option<&PathBuf>, trusted_identities: Option<&String>, trusted_identities_file: Option<&PathBuf>, reload_from_trusted_identities_file: Option<&PathBuf>, launch_config: Option, - authority_identity: Option<&String>, credential: Option<&String>, - trust_context: Option<&PathBuf>, - project_name: Option<&String>, + trust_context: Option<&NamedTrustContext>, + project_name: Option, logging_to_file: bool, ) -> miette::Result<()> { let mut args = vec![ @@ -87,12 +87,14 @@ pub fn spawn_node( args.push("--no-color".to_string()); } - if let Some(path) = project { - args.push("--project-path".to_string()); - let p = path - .to_str() - .unwrap_or_else(|| panic!("unsupported path {path:?}")); - args.push(p.to_string()) + if let Some(identity_name) = identity_name { + args.push("--identity".to_string()); + args.push(identity_name.to_string()); + } + + if let Some(vault_name) = vault_name { + args.push("--vault".to_string()); + args.push(vault_name.to_string()); } if let Some(l) = launch_config { @@ -119,11 +121,6 @@ pub fn spawn_node( ); } - if let Some(ai) = authority_identity { - args.push("--authority-identity".to_string()); - args.push(ai.to_string()); - } - if let Some(credential) = credential { args.push("--credential".to_string()); args.push(credential.to_string()); @@ -131,12 +128,7 @@ pub fn spawn_node( if let Some(trust_context) = trust_context { args.push("--trust-context".to_string()); - args.push( - trust_context - .to_str() - .unwrap_or_else(|| panic!("unsupported path {trust_context:?}")) - .to_string(), - ); + args.push(trust_context.name()); } if let Some(project_name) = project_name { @@ -146,11 +138,11 @@ pub fn spawn_node( args.push(name.to_owned()); - run_ockam(opts, name, args, logging_to_file) + run_ockam(opts, name, args, logging_to_file).await } /// Run the ockam command line with specific arguments -pub fn run_ockam( +pub async fn run_ockam( opts: &CommandGlobalOpts, node_name: &str, args: Vec, @@ -161,12 +153,16 @@ pub fn run_ockam( // deterministic way of starting a node. let ockam_exe = get_env_with_default("OCKAM", current_exe().unwrap_or_else(|_| "ockam".into())) .into_diagnostic()?; - let node_state = opts.state.nodes.get(node_name)?; let mut cmd = Command::new(ockam_exe); if logging_to_file { - let (mlog, elog) = { (node_state.stdout_log(), node_state.stderr_log()) }; + let (mlog, elog) = { + ( + opts.state.stdout_logs(node_name)?, + opts.state.stderr_logs(node_name)?, + ) + }; let main_log_file = OpenOptions::new() .create(true) .append(true) @@ -182,14 +178,10 @@ pub fn run_ockam( cmd.stdout(main_log_file).stderr(stderr_log_file); } - let child = cmd - .args(args) + cmd.args(args) .stdin(Stdio::null()) .spawn() .into_diagnostic() .context("failed to spawn node")?; - - node_state.set_pid(child.id() as i32)?; - Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/output/output.rs b/implementations/rust/ockam/ockam_command/src/output/output.rs index b1a6b8b385a..579c1834ecf 100644 --- a/implementations/rust/ockam/ockam_command/src/output/output.rs +++ b/implementations/rust/ockam/ockam_command/src/output/output.rs @@ -7,16 +7,16 @@ use colorful::Colorful; use miette::miette; use miette::IntoDiagnostic; use minicbor::Encode; +use serde::{Serialize, Serializer}; + use ockam::identity::models::{ CredentialAndPurposeKey, CredentialData, CredentialVerifyingKey, PurposeKeyAttestation, PurposeKeyAttestationData, PurposePublicKey, }; use ockam::identity::{Credential, Identifier, Identity, TimestampInSeconds}; -use serde::{Serialize, Serializer}; - -use ockam_api::cli_state::{ProjectConfigCompact, StateItemTrait, VaultState}; use ockam_api::cloud::project::Project; use ockam_api::cloud::space::Space; +use ockam_api::identity::NamedVault; use ockam_api::nodes::models::portal::{InletStatus, OutletStatus}; use ockam_api::nodes::models::secure_channel::{ CreateSecureChannelResponse, ShowSecureChannelResponse, @@ -145,7 +145,7 @@ impl Output for Project { write!(w, "Project")?; write!(w, "\n Id: {}", self.id)?; write!(w, "\n Name: {}", self.name)?; - write!(w, "\n Access route: {}", self.access_route)?; + write!(w, "\n Access route: {}", self.access_route()?)?; write!( w, "\n Identity identifier: {}", @@ -179,17 +179,28 @@ Space {}"#, } } +#[derive(Debug, Clone, Serialize)] +pub struct ProjectConfigCompact(pub Project); + impl Output for ProjectConfigCompact { fn output(&self) -> Result { let pi = self - .identity - .as_ref() + .0 + .identifier() .map(|i| i.to_string()) - .unwrap_or_else(|| "N/A".to_string()); - let ar = self.authority_access_route.as_deref().unwrap_or("N/A"); - let ai = self.authority_identity.as_deref().unwrap_or("N/A"); + .unwrap_or_else(|_| "N/A".to_string()); + let ar = self + .0 + .authority_access_route() + .map(|r| r.to_string()) + .unwrap_or_else(|_| "N/A".to_string()); + let ai = self + .0 + .authority_change_history() + .map(|r| r.to_string()) + .unwrap_or_else(|_| "N/A".to_string()); let mut w = String::new(); - writeln!(w, "{}: {}", "Project ID".bold(), self.id)?; + writeln!(w, "{}: {}", "Project ID".bold(), self.0.id())?; writeln!(w, "{}: {}", "Project identity".bold(), pi)?; writeln!(w, "{}: {}", "Authority address".bold(), ar)?; write!(w, "{}: {}", "Authority identity".bold(), ai)?; @@ -361,14 +372,14 @@ From {} to {}"#, } } -impl Output for VaultState { +impl Output for NamedVault { fn output(&self) -> Result { let mut output = String::new(); writeln!(output, "Name: {}", self.name())?; writeln!( output, "Type: {}", - match self.config().is_aws() { + match self.is_kms() { true => "AWS KMS", false => "OCKAM", } diff --git a/implementations/rust/ockam/ockam_command/src/policy/create.rs b/implementations/rust/ockam/ockam_command/src/policy/create.rs index 62a48b35d84..7d7caa7e15d 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/create.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/create.rs @@ -6,9 +6,8 @@ use ockam_api::nodes::models::policy::Policy; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::policy::policy_path; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] @@ -41,11 +40,9 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.at).await?; let bdy = Policy::new(cmd.expression); let req = Request::post(policy_path(&cmd.resource, &cmd.action)).body(bdy); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; node.tell(ctx, req).await?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/policy/delete.rs b/implementations/rust/ockam/ockam_command/src/policy/delete.rs index e25d2512aa4..5b9d721ceb9 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/delete.rs @@ -6,9 +6,8 @@ use ockam_abac::{Action, Resource}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::policy::policy_path; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{fmt_ok, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] @@ -42,15 +41,13 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: DeleteCommand, ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; if opts .terminal .confirmed_with_flag_or_prompt(cmd.yes, "Are you sure you want to delete this policy?")? { + let node = BackgroundNode::create(ctx, &opts.state, &cmd.at).await?; let policy_path = policy_path(&cmd.resource, &cmd.action); let req = Request::delete(&policy_path); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; node.tell(ctx, req).await?; opts.terminal @@ -63,7 +60,7 @@ async fn run_impl( .json(serde_json::json!({ "resource": &cmd.resource.to_string(), "action": &cmd.action.to_string(), - "at": &node_name} + "at": &node.node_name()} )) .write_line()?; } diff --git a/implementations/rust/ockam/ockam_command/src/policy/list.rs b/implementations/rust/ockam/ockam_command/src/policy/list.rs index 8722351e4bd..f981279ffbf 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/list.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/list.rs @@ -2,21 +2,18 @@ use std::fmt::Write; use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; use ockam_abac::Resource; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::policy::{Expression, PolicyList}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{CommandGlobalOpts, Result}; #[derive(Clone, Debug, Args)] @@ -39,18 +36,10 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let resource = cmd.resource; - - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", &node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.at).await?; let is_finished: Mutex = Mutex::new(false); + let resource = cmd.resource; let get_policies = async { let req = Request::get(format!("/policy/{resource}")); let policies: PolicyList = node.ask(ctx, req).await?; @@ -59,7 +48,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing Policies on {} for Resource {}...\n", - node_name + node.node_name() .to_string() .color(OckamColor::PrimaryResource.color()), resource @@ -75,8 +64,8 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let list = opts.terminal.build_list( policies.expressions(), - &format!("Policies on Node {} for {}", &node_name, resource), - &format!("No Policies on Node {} for {}", &node_name, resource), + &format!("Policies on Node {} for {}", &node.node_name(), resource), + &format!("No Policies on Node {} for {}", &node.node_name(), resource), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/policy/mod.rs b/implementations/rust/ockam/ockam_command/src/policy/mod.rs index 2922815c5b8..518c7ba2d9a 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/mod.rs @@ -3,9 +3,9 @@ use clap::{Args, Subcommand}; use ockam::Context; use ockam_abac::expr::{eq, ident, str}; use ockam_abac::{Action, Resource}; +use ockam_api::nodes::models::policy::Policy; use ockam_api::nodes::models::policy::PolicyList; use ockam_api::nodes::BackgroundNode; -use ockam_api::{config::lookup::ProjectLookup, nodes::models::policy::Policy}; use ockam_core::api::Request; use crate::policy::create::CreateCommand; @@ -55,8 +55,8 @@ pub(crate) async fn has_policy( opts: &CommandGlobalOpts, resource: &Resource, ) -> Result { + let node = BackgroundNode::create_to_node(ctx, &opts.state, node_name).await?; let req = Request::get(format!("/policy/{resource}")); - let node = BackgroundNode::create(ctx, &opts.state, node_name).await?; let policies: PolicyList = node.ask(ctx, req).await?; Ok(!policies.expressions().is_empty()) } @@ -65,17 +65,15 @@ pub(crate) async fn add_default_project_policy( node_name: &str, ctx: &Context, opts: &CommandGlobalOpts, - project: ProjectLookup, + project_id: String, resource: &Resource, ) -> miette::Result<()> { - let expr = eq([ - ident("subject.trust_context_id"), - str(project.id.to_string()), - ]); + let node = BackgroundNode::create_to_node(ctx, &opts.state, node_name).await?; + + let expr = eq([ident("subject.trust_context_id"), str(project_id)]); let bdy = Policy::new(expr); let req = Request::post(policy_path(resource, &Action::new("handle_message"))).body(bdy); - let node = BackgroundNode::create(ctx, &opts.state, node_name).await?; node.tell(ctx, req).await?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/policy/show.rs b/implementations/rust/ockam/ockam_command/src/policy/show.rs index 0c0eb1ea592..84ee62f5d16 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/show.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/show.rs @@ -2,7 +2,6 @@ use clap::Args; use ockam::Context; use ockam_abac::{Action, Resource}; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::policy::Policy; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; @@ -34,9 +33,8 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ShowCommand) -> miette::Result<()> { - let node_name = extract_address_value(&cmd.at)?; + let node = BackgroundNode::create_to_node(ctx, &opts.state, &cmd.at).await?; let req = Request::get(policy_path(&cmd.resource, &cmd.action)); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; let policy: Policy = node.ask(ctx, req).await?; println!("{}", policy.expression()); Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs b/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs index 07111e3d476..f920b113a75 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs @@ -6,7 +6,7 @@ use ockam::Context; use ockam_api::cloud::addon::{Addons, ConfluentConfig}; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::{check_configuration_completion, get_project_id}; +use crate::project::addon::check_configuration_completion; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; @@ -54,16 +54,16 @@ async fn run_impl( project_name, bootstrap_server, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let config = ConfluentConfig::new(bootstrap_server); let node = InMemoryNode::start(&ctx, &opts.state).await?; let controller = node.create_controller().await?; let response = controller - .configure_confluent_addon(&ctx, project_id.clone(), config) + .configure_confluent_addon(&ctx, project_id, config) .await?; - check_configuration_completion(&opts, &ctx, &node, project_id, response.operation_id).await?; + check_configuration_completion(&opts, &ctx, &node, project_id, &response.operation_id).await?; opts.terminal .write_line(&fmt_ok!("Confluent addon configured successfully"))?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs b/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs index c853ad1fa73..ef6d7a69b19 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs @@ -10,7 +10,7 @@ use ockam_api::cloud::addon::Addons; use ockam_api::cloud::project::InfluxDBTokenLeaseManagerConfig; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::{check_configuration_completion, get_project_id}; +use crate::project::addon::check_configuration_completion; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; @@ -133,7 +133,7 @@ async fn run_impl( user_access_role, admin_access_role, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let perms = match (permissions, permissions_path) { (_, Some(p)) => std::fs::read_to_string(p).into_diagnostic()?, @@ -159,9 +159,9 @@ async fn run_impl( let controller = node.create_controller().await?; let response = controller - .configure_influxdb_addon(&ctx, project_id.clone(), config) + .configure_influxdb_addon(&ctx, project_id, config) .await?; - check_configuration_completion(&opts, &ctx, &node, project_id, response.operation_id).await?; + check_configuration_completion(&opts, &ctx, &node, project_id, &response.operation_id).await?; opts.terminal .write_line(&fmt_ok!("InfluxDB addon configured successfully"))?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs b/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs index e0b23a31f46..b2d0710b52a 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs @@ -16,7 +16,7 @@ use ockam_api::enroll::okta_oidc_provider::OktaOidcProvider; use ockam_api::minicbor_url::Url; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::{check_configuration_completion, get_project_id}; +use crate::project::addon::check_configuration_completion; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts, Result}; @@ -96,7 +96,7 @@ async fn run_impl( client_id, attributes, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let base_url = Url::parse(tenant.as_str()) .into_diagnostic() @@ -122,9 +122,9 @@ async fn run_impl( let controller = node.create_controller().await?; let response = controller - .configure_okta_addon(&ctx, project_id.clone(), okta_config) + .configure_okta_addon(&ctx, project_id, okta_config) .await?; - check_configuration_completion(&opts, &ctx, &node, project_id, response.operation_id).await?; + check_configuration_completion(&opts, &ctx, &node, project_id, &response.operation_id).await?; opts.terminal .write_line(&fmt_ok!("Okta addon configured successfully"))?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs b/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs index 28a51ecc554..183b363c185 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs @@ -7,7 +7,6 @@ use ockam_api::cloud::addon::Addons; use ockam_api::nodes::InMemoryNode; use crate::operation::util::check_for_completion; -use crate::project::addon::get_project_id; use crate::util::node_rpc; use crate::{fmt_ok, CommandGlobalOpts}; @@ -47,11 +46,13 @@ async fn run_impl( project_name, addon_id, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let node = InMemoryNode::start(&ctx, &opts.state).await?; let controller = node.create_controller().await?; - let response = controller.disable_addon(&ctx, project_id, addon_id).await?; + let response = controller + .disable_addon(&ctx, project_id, &addon_id) + .await?; let operation_id = response.operation_id; check_for_completion(&opts, &ctx, &controller, &operation_id).await?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/list.rs b/implementations/rust/ockam/ockam_command/src/project/addon/list.rs index 4a4823f9567..ab631282d21 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/list.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/list.rs @@ -5,7 +5,6 @@ use ockam::Context; use ockam_api::cloud::addon::Addons; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::get_project_id; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -33,7 +32,7 @@ async fn run_impl( (opts, cmd): (CommandGlobalOpts, AddonListSubcommand), ) -> miette::Result<()> { let project_name = cmd.project_name; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let node = InMemoryNode::start(&ctx, &opts.state).await?; let controller = node.create_controller().await?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs b/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs index 31b78894e2c..fa0dc172184 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs @@ -1,34 +1,28 @@ -mod configure_confluent; -mod configure_influxdb; -mod configure_okta; -mod disable; -mod list; - use core::fmt::Write; use clap::{Args, Subcommand}; -use miette::Context as _; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; use ockam_api::cloud::addon::Addon; -use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; - use ockam_node::Context; +use crate::operation::util::check_for_completion; +use crate::output::Output; use crate::project::addon::configure_confluent::AddonConfigureConfluentSubcommand; use crate::project::addon::configure_influxdb::AddonConfigureInfluxdbSubcommand; use crate::project::addon::configure_okta::AddonConfigureOktaSubcommand; use crate::project::addon::disable::AddonDisableSubcommand; use crate::project::addon::list::AddonListSubcommand; - -use crate::output::Output; -use crate::util::api::CloudOpts; - -use crate::operation::util::check_for_completion; use crate::project::util::check_project_readiness; +use crate::util::api::CloudOpts; use crate::{CommandGlobalOpts, Result}; +mod configure_confluent; +mod configure_influxdb; +mod configure_okta; +mod disable; +mod list; + /// Manage addons for a project #[derive(Clone, Debug, Args)] #[command(arg_required_else_help = true, subcommand_required = true)] @@ -103,27 +97,15 @@ impl Output for Vec { } } -pub fn get_project_id(cli_state: &CliState, project_name: &str) -> Result { - Ok(cli_state - .projects - .get(project_name) - .context(format!( - "Failed to get project {project_name} from config lookup" - ))? - .config() - .id - .clone()) -} - async fn check_configuration_completion( opts: &CommandGlobalOpts, ctx: &Context, node: &InMemoryNode, - project_id: String, - operation_id: String, + project_id: &str, + operation_id: &str, ) -> Result<()> { let controller = node.create_controller().await?; - check_for_completion(opts, ctx, &controller, &operation_id).await?; + check_for_completion(opts, ctx, &controller, operation_id).await?; let project = controller.get_project(ctx, project_id).await?; let _ = check_project_readiness(opts, ctx, node, project).await?; Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/project/create.rs b/implementations/rust/ockam/ockam_command/src/project/create.rs index 1a24db57fd7..e180256b4a0 100644 --- a/implementations/rust/ockam/ockam_command/src/project/create.rs +++ b/implementations/rust/ockam/ockam_command/src/project/create.rs @@ -2,14 +2,14 @@ use clap::Args; use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; use crate::operation::util::check_for_completion; use crate::project::util::check_project_readiness; use crate::util::api::CloudOpts; -use crate::util::{api, node_rpc}; +use crate::util::node_rpc; +use crate::util::parsers::validate_project_name; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -50,33 +50,14 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let space_id = opts.state.spaces.get(&cmd.space_name)?.config().id.clone(); let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - - let project = controller - .create_project(ctx, space_id, cmd.project_name, vec![]) + let project = node + .create_project(ctx, &cmd.space_name, &cmd.project_name, vec![]) .await?; let operation_id = project.operation_id.clone().unwrap(); + let controller = node.create_controller().await?; check_for_completion(&opts, ctx, &controller, &operation_id).await?; let project = check_project_readiness(&opts, ctx, &node, project).await?; - opts.state - .projects - .overwrite(&project.name, project.clone())?; - opts.state - .trust_contexts - .overwrite(&project.name, project.clone().try_into()?)?; opts.println(&project)?; Ok(()) } - -fn validate_project_name(s: &str) -> Result { - match api::validate_cloud_resource_name(s) { - Ok(_) => Ok(s.to_string()), - Err(_e)=> Err(String::from( - "project name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ - Separators must occur between alphanumeric characters. This implies that separators can't \ - occur at the start or end of the name, nor they can occur in sequence.", - )), - } -} diff --git a/implementations/rust/ockam/ockam_command/src/project/delete.rs b/implementations/rust/ockam/ockam_command/src/project/delete.rs index 5bb6c42c5b4..128a7358108 100644 --- a/implementations/rust/ockam/ockam_command/src/project/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/project/delete.rs @@ -2,12 +2,10 @@ use clap::Args; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; -use crate::project::util::refresh_projects; use crate::util::api::CloudOpts; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; @@ -57,32 +55,9 @@ async fn run_impl( .terminal .confirmed_with_flag_or_prompt(cmd.yes, "Are you sure you want to delete this project?")? { - let space_id = opts.state.spaces.get(&cmd.space_name)?.config().id.clone(); let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - - // Lookup project - let project_id = match opts.state.projects.get(&cmd.project_name) { - Ok(state) => state.config().id.clone(), - Err(_) => { - // The project is not in the config file. - // Fetch all available projects from the cloud. - refresh_projects(&opts, ctx, &controller).await?; - - // If the project is not found in the lookup, then it must not exist in the cloud, so we exit the command. - match opts.state.projects.get(&cmd.project_name) { - Ok(state) => state.config().id.clone(), - Err(_) => { - return Ok(()); - } - } - } - }; - - // Send request - controller.delete_project(ctx, space_id, project_id).await?; - - opts.state.projects.delete(&cmd.project_name)?; + node.delete_project_by_name(ctx, &cmd.space_name, &cmd.project_name) + .await?; opts.terminal .stdout() .plain(fmt_ok!( diff --git a/implementations/rust/ockam/ockam_command/src/project/enroll.rs b/implementations/rust/ockam/ockam_command/src/project/enroll.rs index aa6783f9258..f5a64e01a49 100644 --- a/implementations/rust/ockam/ockam_command/src/project/enroll.rs +++ b/implementations/rust/ockam/ockam_command/src/project/enroll.rs @@ -5,7 +5,6 @@ use miette::Context as _; use miette::{miette, IntoDiagnostic}; use ockam::Context; -use ockam_api::cli_state::{ProjectConfigCompact, StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::{OktaAuth0, Project}; use ockam_api::cloud::AuthorityNode; use ockam_api::enroll::enrollment::Enrollment; @@ -15,8 +14,6 @@ use ockam_api::identity::EnrollmentTicket; use ockam_api::nodes::InMemoryNode; use crate::enroll::OidcServiceExt; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::output::CredentialAndPurposeKeyDisplay; use crate::util::api::{CloudOpts, TrustContextOpts}; use crate::util::node_rpc; @@ -69,7 +66,6 @@ pub fn parse_enroll_ticket(hex_encoded_data_or_path: &str) -> Result miette::Result { let project = retrieve_project(opts, &cmd).await?; - let project_authority = project - .authority() - .await - .into_diagnostic()? - .ok_or_else(|| miette!("Authority details not configured"))?; - let identity_name = get_identity_name(&opts.state, &cmd.cloud_opts.identity); + let identity_name = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; // Create secure channel to the project's authority node - let trust_context_config = cmd.trust_opts.to_config(&opts.state)?.build(); + let trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_opts.trust_context, + &cmd.trust_opts.project_name, + &None, + &None, + ) + .await?; let node = InMemoryNode::start_with_trust_context( ctx, &opts.state, - cmd.trust_opts.project_path.as_ref(), - trust_context_config, + cmd.trust_opts.project_name, + trust_context, ) .await?; let authority_node: AuthorityNode = node .create_authority_client( - project_authority.identity_id(), - project_authority.address(), + &project.authority_identifier().await.into_diagnostic()?, + &project.authority_access_route().into_diagnostic()?, Some(identity_name), ) .await?; @@ -140,41 +142,25 @@ pub async fn project_enroll( } async fn retrieve_project(opts: &CommandGlobalOpts, cmd: &EnrollCommand) -> Result { - let project_as_string: String; - // Retrieve project info from the enrollment ticket or project.json in the case of okta auth - let proj: ProjectConfigCompact = if let Some(ticket) = &cmd.enroll_ticket { + let project = if let Some(ticket) = &cmd.enroll_ticket { ticket .project .as_ref() .expect("Enrollment ticket is invalid. Ticket does not contain a project.") .clone() - .try_into()? } else { // OKTA AUTHENTICATION FLOW | PREVIOUSLY ENROLLED FLOW // currently okta auth does not use an enrollment token // however, it could be worked to use one in the future // // REQUIRES Project passed or default project - let path = match cmd.trust_opts.project_path.as_ref() { - Some(p) => p.clone(), - None => { - let default_project = opts - .state - .projects - .default() - .context("A default project or project parameter is required.")?; - default_project.path().clone() - } - }; - - // Read (okta and authority) project parameters from project.json - project_as_string = tokio::fs::read_to_string(path).await.into_diagnostic()?; - serde_json::from_str(&project_as_string).into_diagnostic()? + opts.state + .get_project_by_name_or_default(&cmd.trust_opts.project_name) + .await + .context("A default project or project parameter is required.")? }; - let project: Project = (&proj).into(); - let trust_context_name = if let Some(trust_context_name) = &cmd.new_trust_context_name { trust_context_name } else { @@ -182,8 +168,8 @@ async fn retrieve_project(opts: &CommandGlobalOpts, cmd: &EnrollCommand) -> Resu }; if !cmd.force { - if let Ok(trust_context) = opts.state.trust_contexts.get(trust_context_name) { - if trust_context.config().id() != project.id { + if let Ok(trust_context) = opts.state.get_trust_context(trust_context_name).await { + if trust_context.trust_context_id() != project.id { return Err(miette!( "A trust context with the name {} already exists and is associated with a different project. Please choose a different name.", trust_context_name @@ -193,12 +179,14 @@ async fn retrieve_project(opts: &CommandGlobalOpts, cmd: &EnrollCommand) -> Resu } opts.state - .projects - .overwrite(&project.name, project.clone())?; - - opts.state - .trust_contexts - .overwrite(trust_context_name, project.clone().try_into()?)?; + .create_trust_context( + Some(trust_context_name.clone()), + Some(project.id()), + None, + project.authority_identity().await.ok(), + project.authority_access_route().ok(), + ) + .await?; Ok(project) } diff --git a/implementations/rust/ockam/ockam_command/src/project/import.rs b/implementations/rust/ockam/ockam_command/src/project/import.rs new file mode 100644 index 00000000000..fff74befe5e --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/project/import.rs @@ -0,0 +1,95 @@ +use clap::Args; +use colorful::Colorful; + +use ockam::identity::{Identifier, Identity}; +use ockam::Context; +use ockam_multiaddr::MultiAddr; + +use crate::util::node_rpc; +use crate::util::parsers::{ + identity_identifier_parser, identity_parser, multiaddr_parser, validate_project_name, +}; +use crate::{docs, fmt_err, fmt_ok, CommandGlobalOpts}; + +const LONG_ABOUT: &str = include_str!("./static/import/long_about.txt"); +const AFTER_LONG_HELP: &str = include_str!("./static/import/after_long_help.txt"); + +/// Import projects +#[derive(Clone, Debug, Args)] +#[command( +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), +)] +pub struct ImportCommand { + /// Project name + #[arg(long, value_parser = validate_project_name)] + pub project_name: String, + + /// Project id + #[arg(long)] + pub project_id: String, + + /// Project identifier + #[arg(long, value_name = "IDENTIFIER", value_parser = identity_identifier_parser)] + pub project_identifier: Option, + + /// Project access route + #[arg(long, value_name = "MULTIADDR", value_parser = multiaddr_parser)] + pub project_access_route: MultiAddr, + + /// Authority identity + #[arg(long, value_name = "IDENTITY", value_parser = identity_parser)] + pub authority_identity: Option, + + /// Authority access route + #[arg(long, value_name = "MULTIADDR", value_parser = multiaddr_parser)] + pub authority_access_route: Option, +} + +impl ImportCommand { + pub fn run(self, options: CommandGlobalOpts) { + node_rpc(rpc, (options, self)); + } +} + +async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ImportCommand)) -> miette::Result<()> { + run_impl(&ctx, opts, cmd).await +} + +async fn run_impl( + _ctx: &Context, + opts: CommandGlobalOpts, + cmd: ImportCommand, +) -> miette::Result<()> { + match opts + .state + .import_project( + &cmd.project_id, + &cmd.project_name, + &cmd.project_identifier, + &cmd.project_access_route, + &cmd.authority_identity, + &cmd.authority_access_route, + ) + .await + { + Ok(_) => opts + .terminal + .stdout() + .plain(fmt_ok!( + "Successfully imported project {}", + &cmd.project_name + )) + .write_line()?, + Err(e) => opts + .terminal + .stdout() + .plain(fmt_err!( + "The project {} could not be imported: {}", + &cmd.project_name, + e.to_string() + )) + .write_line()?, + }; + Ok(()) +} diff --git a/implementations/rust/ockam/ockam_command/src/project/info.rs b/implementations/rust/ockam/ockam_command/src/project/info.rs index 375e0d21074..d943e5edb95 100644 --- a/implementations/rust/ockam/ockam_command/src/project/info.rs +++ b/implementations/rust/ockam/ockam_command/src/project/info.rs @@ -2,13 +2,11 @@ use clap::Args; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{ProjectConfigCompact, StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; -use crate::output::Output; -use crate::project::util::refresh_projects; +use crate::output::{Output, ProjectConfigCompact}; use crate::util::api::CloudOpts; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -38,19 +36,8 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, InfoCommand)) -> mie async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: InfoCommand) -> miette::Result<()> { let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - - // Lookup project - let id = match opts.state.projects.get(&cmd.name) { - Ok(state) => state.config().id.clone(), - Err(_) => { - refresh_projects(&opts, ctx, &controller).await?; - opts.state.projects.get(&cmd.name)?.config().id.clone() - } - }; - - let project = controller.get_project(ctx, id).await?; - let info: ProjectConfigCompact = project.into(); + let project = node.get_project_by_name(ctx, &cmd.name).await?; + let info = ProjectConfigCompact(project); opts.terminal .stdout() .plain(info.output()?) diff --git a/implementations/rust/ockam/ockam_command/src/project/list.rs b/implementations/rust/ockam/ockam_command/src/project/list.rs index 550dee8a70b..3bbc2c604d3 100644 --- a/implementations/rust/ockam/ockam_command/src/project/list.rs +++ b/implementations/rust/ockam/ockam_command/src/project/list.rs @@ -4,9 +4,7 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::cloud::project::Projects; - use ockam_api::nodes::InMemoryNode; use crate::util::api::CloudOpts; @@ -20,9 +18,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List projects #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ListCommand { #[command(flatten)] @@ -40,15 +38,14 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> miette::Result<()> { - if !opts.state.is_enrolled()? { + if !opts.state.is_enrolled().await? { return Err(miette!("You must enroll before you can list your projects")); } let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; let is_finished: Mutex = Mutex::new(false); let get_projects = async { - let projects = controller.list_projects(ctx).await?; + let projects = node.get_projects(ctx).await?; *is_finished.lock().await = true; Ok(projects) }; @@ -66,12 +63,6 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> .build_list(&projects, "Projects", "No projects found on this system.")?; let json = serde_json::to_string_pretty(&projects).into_diagnostic()?; - for project in &projects { - opts.state - .projects - .overwrite(&project.name, project.clone())?; - } - opts.terminal .stdout() .plain(plain) diff --git a/implementations/rust/ockam/ockam_command/src/project/mod.rs b/implementations/rust/ockam/ockam_command/src/project/mod.rs index 25a311deeaa..8ec34266515 100644 --- a/implementations/rust/ockam/ockam_command/src/project/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/project/mod.rs @@ -1,38 +1,40 @@ -mod addon; -mod create; -mod delete; -pub(crate) mod enroll; -mod info; -mod list; -mod show; -mod ticket; -pub mod util; -mod version; - +use crate::docs; use clap::{Args, Subcommand}; -pub use crate::credential::get::GetCommand; pub use addon::AddonCommand; pub use create::CreateCommand; pub use delete::DeleteCommand; pub use enroll::EnrollCommand; +pub use import::ImportCommand; pub use info::InfoCommand; pub use list::ListCommand; pub use show::ShowCommand; pub use ticket::TicketCommand; pub use version::VersionCommand; -use crate::docs; +pub use crate::credential::get::GetCommand; use crate::CommandGlobalOpts; +mod addon; +mod create; +mod delete; +pub(crate) mod enroll; +mod import; +mod info; +mod list; +mod show; +mod ticket; +pub mod util; +mod version; + const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); /// Manage Projects in Ockam Orchestrator #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - subcommand_required = true, - long_about = docs::about(LONG_ABOUT), +arg_required_else_help = true, +subcommand_required = true, +long_about = docs::about(LONG_ABOUT), )] pub struct ProjectCommand { #[command(subcommand)] @@ -42,6 +44,7 @@ pub struct ProjectCommand { #[derive(Clone, Debug, Subcommand)] pub enum ProjectSubcommand { Create(CreateCommand), + Import(ImportCommand), Delete(DeleteCommand), List(ListCommand), Show(ShowCommand), @@ -49,13 +52,14 @@ pub enum ProjectSubcommand { Information(InfoCommand), Ticket(TicketCommand), Addon(AddonCommand), - Enroll(EnrollCommand), + Enroll(Box), } impl ProjectCommand { pub fn run(self, options: CommandGlobalOpts) { match self.subcommand { ProjectSubcommand::Create(c) => c.run(options), + ProjectSubcommand::Import(c) => c.run(options), ProjectSubcommand::Delete(c) => c.run(options), ProjectSubcommand::List(c) => c.run(options), ProjectSubcommand::Show(c) => c.run(options), diff --git a/implementations/rust/ockam/ockam_command/src/project/show.rs b/implementations/rust/ockam/ockam_command/src/project/show.rs index 8d9ebed4cb3..8865603f2fa 100644 --- a/implementations/rust/ockam/ockam_command/src/project/show.rs +++ b/implementations/rust/ockam/ockam_command/src/project/show.rs @@ -2,13 +2,11 @@ use clap::Args; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::Projects; - use ockam_api::nodes::InMemoryNode; -use crate::output::Output; -use crate::project::util::refresh_projects; +use crate::output::output::Output; +use crate::output::ProjectConfigCompact; use crate::util::api::CloudOpts; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -20,9 +18,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show projects #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ShowCommand { /// Name of the project. @@ -45,27 +43,13 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand)) -> mie async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ShowCommand) -> miette::Result<()> { let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - - // Lookup project - let id = match &opts.state.projects.get(&cmd.name) { - Ok(state) => state.config().id.clone(), - Err(_) => { - refresh_projects(&opts, ctx, &controller).await?; - opts.state.projects.get(&cmd.name)?.config().id.clone() - } - }; - - // Send request - let project = controller.get_project(ctx, id).await?; + let project = node.get_project_by_name(ctx, &cmd.name).await?; + let project_output = ProjectConfigCompact(project); opts.terminal .stdout() - .plain(project.output()?) - .json(serde_json::to_string_pretty(&project).into_diagnostic()?) + .plain(project_output.output()?) + .json(serde_json::to_string_pretty(&project_output).into_diagnostic()?) .write_line()?; - opts.state - .projects - .overwrite(&project.name, project.clone())?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/project/static/import/after_long_help.txt b/implementations/rust/ockam/ockam_command/src/project/static/import/after_long_help.txt new file mode 100644 index 00000000000..f5badf53a93 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/project/static/import/after_long_help.txt @@ -0,0 +1,10 @@ +```sh +# To import a project +$ ockam project import + --project-name name \\ + --project-id 12345 \\ + --project-identifier I1234561234561234561234561234561234561234 \\ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \\ + --authority-identity I1234561234561234561234561234561234561234 \\ + --authority-access-route /dnsaddr/127.0.0.1/tcp/5000/service/api +``` diff --git a/implementations/rust/ockam/ockam_command/src/project/static/import/long_about.txt b/implementations/rust/ockam/ockam_command/src/project/static/import/long_about.txt new file mode 100644 index 00000000000..c3389fdb523 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/project/static/import/long_about.txt @@ -0,0 +1 @@ +This command will import a project in the local database. If the project already exists, an error is returned diff --git a/implementations/rust/ockam/ockam_command/src/project/ticket.rs b/implementations/rust/ockam/ockam_command/src/project/ticket.rs index 2617fdbae2b..955b1ec25bd 100644 --- a/implementations/rust/ockam/ockam_command/src/project/ticket.rs +++ b/implementations/rust/ockam/ockam_command/src/project/ticket.rs @@ -1,23 +1,20 @@ -use crate::util::duration::duration_parser; -use clap::Args; -use ockam_api::config::cli::TrustContextConfig; -use ockam_api::identity::EnrollmentTicket; use std::collections::HashMap; use std::time::Duration; +use clap::Args; use miette::{miette, IntoDiagnostic}; + use ockam::identity::Identifier; use ockam::Context; use ockam_api::authenticator::enrollment_tokens::{Members, TokenIssuer}; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use ockam_api::config::lookup::{ProjectAuthority, ProjectLookup}; +use ockam_api::cli_state::CliState; +use ockam_api::cloud::project::Project; +use ockam_api::identity::EnrollmentTicket; use ockam_api::nodes::InMemoryNode; - use ockam_multiaddr::{proto, MultiAddr, Protocol}; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::util::api::{CloudOpts, TrustContextOpts}; +use crate::util::duration::duration_parser; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts, Result}; @@ -27,8 +24,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/ticket/after_long_help.txt" /// Add members to a project as an authorised enroller. #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct TicketCommand { /// Orchestrator address to resolve projects present in the `at` argument @@ -48,7 +45,7 @@ pub struct TicketCommand { #[arg(short, long = "attribute", value_name = "ATTRIBUTE")] attributes: Vec, - #[arg(long = "expires-in", value_name = "DURATION", conflicts_with = "member", value_parser=duration_parser)] + #[arg(long = "expires-in", value_name = "DURATION", conflicts_with = "member", value_parser = duration_parser)] expires_in: Option, #[arg( @@ -61,7 +58,6 @@ pub struct TicketCommand { impl TicketCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.cloud_opts.identity); node_rpc(run_impl, (opts, self)); } @@ -81,51 +77,60 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, TicketCommand), ) -> miette::Result<()> { - let trust_context_config = cmd.trust_opts.to_config(&opts.state)?.build(); + let trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_opts.trust_context, + &cmd.trust_opts.project_name, + &None, + &None, + ) + .await?; let node = InMemoryNode::start_with_trust_context( &ctx, &opts.state, - cmd.trust_opts.project_path.as_ref(), - trust_context_config, + cmd.trust_opts.project_name.clone(), + trust_context, ) .await?; - let mut project: Option = None; - let mut trust_context: Option = None; + let mut project: Option = None; - let authority_node = if let Some(tc) = cmd.trust_opts.trust_context.as_ref() { - let tc = &opts.state.trust_contexts.read_config_from_path(tc)?; - trust_context = Some(tc.clone()); - let cred_retr = tc + let authority_node = if let Some(name) = cmd.trust_opts.trust_context.as_ref() { + let authority = if let Some(authority) = opts + .state + .get_trust_context(name) + .await? .authority() + .await .into_diagnostic()? - .own_credential() - .into_diagnostic()?; - let addr = match cred_retr { - ockam_api::config::cli::CredentialRetrieverConfig::FromCredentialIssuer(c) => { - &c.multiaddr - } - _ => { - return Err(miette!( - "Trust context must be configured with a credential issuer" - )); - } + { + authority + } else { + return Err(miette!( + "Trust context must be configured with a credential issuer" + )); }; - let identity = get_identity_name(&opts.state, &cmd.cloud_opts.identity); - let authority_identifier = tc - .authority() - .into_diagnostic()? - .identifier() - .await - .into_diagnostic()?; - node.create_authority_client(&authority_identifier, addr, Some(identity)) - .await? - } else if let (Some(p), Some(a)) = get_project(&opts.state, &cmd.to).await? { - let identity = get_identity_name(&opts.state, &cmd.cloud_opts.identity); - project = Some(p); - node.create_authority_client(a.identity_id(), a.address(), Some(identity)) + let identity = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; + + node.create_authority_client(&authority.identifier(), &authority.route(), Some(identity)) .await? + } else if let Some(p) = get_project(&opts.state, &cmd.to).await? { + let identity = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; + project = Some(p.clone()); + node.create_authority_client( + &p.authority_identifier().await.into_diagnostic()?, + &p.authority_access_route().into_diagnostic()?, + Some(identity), + ) + .await? } else { return Err(miette!("Cannot create a ticket. Please specify a route to your project or to an authority node")); }; @@ -141,7 +146,7 @@ async fn run_impl( .create_token(&ctx, cmd.attributes()?, cmd.expires_in, cmd.usage_count) .await?; - let ticket = EnrollmentTicket::new(token, project, trust_context); + let ticket = EnrollmentTicket::new(token, project); let ticket_serialized = ticket.hex_encoded().into_diagnostic()?; opts.terminal .clone() @@ -156,28 +161,27 @@ async fn run_impl( /// Get the project authority from the first address protocol. /// /// If the first protocol is a `/project`, look up the project's config. -async fn get_project( - cli_state: &CliState, - input: &MultiAddr, -) -> Result<(Option, Option)> { +async fn get_project(cli_state: &CliState, input: &MultiAddr) -> Result> { if let Some(proto) = input.first() { if proto.code() == proto::Project::CODE { - let proj = proto.cast::().expect("project protocol"); - return if let Ok(p) = cli_state.projects.get(proj.to_string()) { - let c = p.config(); - let a = - ProjectAuthority::from_raw(&c.authority_access_route, &c.authority_identity) - .await?; - if a.is_some() { - let p = ProjectLookup::from_project(c).await?; - Ok((Some(p), a)) - } else { - Err(miette!("missing authority in project {:?}", &*proj).into()) + let project_name = proto.cast::().expect("project protocol"); + match cli_state.get_project_by_name(&project_name).await.ok() { + None => Err(miette!("unknown project {}", project_name.to_string()).into()), + Some(project) => { + if project.authority_identifier().await.is_err() { + Err( + miette!("missing authority in project {}", project_name.to_string()) + .into(), + ) + } else { + Ok(Some(project)) + } } - } else { - Err(miette!("unknown project {}", &*proj).into()) - }; + } + } else { + Ok(None) } + } else { + Ok(None) } - Ok((None, None)) } diff --git a/implementations/rust/ockam/ockam_command/src/project/util.rs b/implementations/rust/ockam/ockam_command/src/project/util.rs index dce94373a92..c0b337db0ca 100644 --- a/implementations/rust/ockam/ockam_command/src/project/util.rs +++ b/implementations/rust/ockam/ockam_command/src/project/util.rs @@ -1,14 +1,13 @@ use indicatif::ProgressBar; +use miette::miette; use miette::Context as _; -use miette::{miette, IntoDiagnostic}; use std::iter::Take; use std::time::Duration; use tokio_retry::strategy::FixedInterval; use tokio_retry::Retry; use tracing::debug; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; -use ockam_api::cloud::project::{Project, Projects}; +use ockam_api::cloud::project::Project; use ockam_api::cloud::{Controller, ORCHESTRATOR_AWAIT_TIMEOUT}; use ockam_api::config::lookup::LookupMeta; use ockam_api::error::ApiError; @@ -16,7 +15,6 @@ use ockam_api::nodes::service::relay::SecureChannelsCreation; use ockam_api::nodes::InMemoryNode; use ockam_api::route_to_multiaddr; -use ockam_core::compat::str::FromStr; use ockam_core::route; use ockam_multiaddr::{MultiAddr, Protocol}; use ockam_node::Context; @@ -64,19 +62,13 @@ pub async fn get_projects_secure_channels_from_config_lookup( // Get the project node's access route + identity id from the config let (project_access_route, project_identity_id) = { // This shouldn't fail, as we did a refresh above if we found any missing project. - let p = opts + let project = opts .state - .projects - .get(name) - .context(format!("Failed to get project {name} from config lookup"))? - .config() - .clone(); - let id = p - .identity - .ok_or(miette!("Project should have identity set"))?; - let node_route = MultiAddr::from_str(&p.access_route) - .into_diagnostic() - .wrap_err("Invalid project node route")?; + .get_project_by_name(name) + .await + .context(format!("Failed to get project {name}"))?; + let id = project.identifier()?; + let node_route = project.access_route()?; (node_route, id) }; @@ -113,11 +105,6 @@ pub async fn check_project_readiness( let retry_strategy = FixedInterval::from_millis(5000) .take((ORCHESTRATOR_AWAIT_TIMEOUT.as_millis() / 5000) as usize); - // Persist project config prior to checking readiness which might take a while - opts.state - .projects - .overwrite(&project.name, project.clone())?; - let spinner_option = opts.terminal.progress_spinner(); let project = check_project_ready( ctx, @@ -142,11 +129,6 @@ pub async fn check_project_readiness( if let Some(spinner) = spinner_option.as_ref() { spinner.finish_and_clear(); } - - // Persist project config with all its fields - opts.state - .projects - .overwrite(&project.name, project.clone())?; Ok(project) } @@ -166,11 +148,11 @@ async fn check_project_ready( return Ok(project); }; - let project_id = project.id.clone(); + let project_id = project.id(); let project: Project = Retry::spawn(retry_strategy.clone(), || async { // Handle the project show request result // so we can provide better errors in the case orchestrator does not respond timely - let project = controller.get_project(ctx, project_id.clone()).await?; + let project = controller.get_project(ctx, &project_id).await?; let result: miette::Result = if project.is_ready() { Ok(project) } else { @@ -238,13 +220,12 @@ async fn check_authority_node_accessible( retry_strategy: Take, spinner_option: Option, ) -> Result { - let authority = project - .authority() - .await? - .ok_or(miette!("Project does not have an authority defined."))?; - let authority_node = node - .create_authority_client(authority.identity_id(), authority.address(), None) + .create_authority_client( + &project.authority_identifier().await?, + &project.authority_access_route()?, + None, + ) .await?; if let Some(spinner) = spinner_option.as_ref() { @@ -252,25 +233,11 @@ async fn check_authority_node_accessible( } Retry::spawn(retry_strategy.clone(), || async { if authority_node.check_secure_channel(ctx).await.is_ok() { - Ok(()) - } else { - Err(miette!("Timed out while trying to establish a secure channel to the project authority. Please try again.")) - } - }) - .await?; + Ok(()) + } else { + Err(miette!("Timed out while trying to establish a secure channel to the project authority. Please try again.")) + } + }) + .await?; Ok(project) } - -pub async fn refresh_projects( - opts: &CommandGlobalOpts, - ctx: &Context, - controller: &Controller, -) -> miette::Result<()> { - let projects = controller.list_projects(ctx).await?; - for project in projects { - opts.state - .projects - .overwrite(&project.name, project.clone())?; - } - Ok(()) -} diff --git a/implementations/rust/ockam/ockam_command/src/project/version.rs b/implementations/rust/ockam/ockam_command/src/project/version.rs index caf992ff731..3c297765135 100644 --- a/implementations/rust/ockam/ockam_command/src/project/version.rs +++ b/implementations/rust/ockam/ockam_command/src/project/version.rs @@ -3,7 +3,6 @@ use colorful::Colorful; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; use crate::util::api::CloudOpts; @@ -16,8 +15,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/version/after_long_help.txt /// Return the version of the Orchestrator Controller and the Projects #[derive(Clone, Debug, Args)] #[command( - long_about=docs::about(LONG_ABOUT), - after_long_help=docs::about(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::about(AFTER_LONG_HELP) )] pub struct VersionCommand { #[command(flatten)] @@ -38,7 +37,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> // Send request let node = InMemoryNode::start(ctx, &opts.state).await?; let controller = node.create_controller().await?; - let project_version = controller.get_project_version(ctx).await?; + let project_version = controller.get_orchestrator_version_info(ctx).await?; let json = serde_json::to_string(&project_version).into_diagnostic()?; let project_version = project_version diff --git a/implementations/rust/ockam/ockam_command/src/relay/create.rs b/implementations/rust/ockam/ockam_command/src/relay/create.rs index ee7c98e7937..63a280c578e 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/create.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/create.rs @@ -10,7 +10,6 @@ use tracing::info; use ockam::identity::Identifier; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::is_local_node; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::service::relay::Relays; @@ -18,12 +17,11 @@ use ockam_api::nodes::BackgroundNode; use ockam_multiaddr::proto::Project; use ockam_multiaddr::{MultiAddr, Protocol}; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::output::Output; use crate::terminal::OckamColor; use crate::util::{node_rpc, process_nodes_multiaddr}; -use crate::{display_parse_logs, docs, fmt_ok, CommandGlobalOpts}; -use crate::{fmt_log, Result}; +use crate::{display_parse_logs, fmt_ok, CommandGlobalOpts}; +use crate::{docs, fmt_log, Result}; const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -55,7 +53,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.to); node_rpc(rpc, (opts, self)); } } @@ -80,11 +77,9 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m display_parse_logs(&opts); - let to = get_node_name(&opts.state, &cmd.to); - let node_name = extract_address_value(&to)?; let at_rust_node = is_local_node(&cmd.at).wrap_err("Argument --at is not valid")?; - let ma = process_nodes_multiaddr(&cmd.at, &opts.state)?; + let ma = process_nodes_multiaddr(&cmd.at, &opts.state).await?; let alias = if at_rust_node { format!("forward_to_{}", cmd.relay_name) } else { @@ -93,13 +88,13 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m let is_finished: Mutex = Mutex::new(false); + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.to).await?; let get_relay_info = async { let relay_info = { - if cmd.at.matches(0, &[Project::CODE.into()]) && cmd.authorized.is_some() { + if cmd.at.starts_with(Project::CODE) && cmd.authorized.is_some() { return Err(miette!("--authorized can not be used with project addresses").into()); }; - info!("creating a relay at {} to {node_name}", cmd.at); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + info!("creating a relay at {} to {}", cmd.at, node.node_name()); node.create_relay(&ctx, &ma, Some(alias.clone()), cmd.authorized) .await? }; @@ -116,9 +111,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m ), format!( "Setting up receiving relay mailbox on node {}...", - &node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + &node.node_name().color(OckamColor::PrimaryResource.color()) ), ]; let progress_output = opts @@ -138,7 +131,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m .color(OckamColor::PrimaryResource.color()); let formatted_to = format!( "/node/{}{}", - &node_name, + &node.node_name(), &relay.remote_address_ma().into_diagnostic()?.to_string() ) .color(OckamColor::PrimaryResource.color()); diff --git a/implementations/rust/ockam/ockam_command/src/relay/delete.rs b/implementations/rust/ockam/ockam_command/src/relay/delete.rs index 74766d8f1c8..b6de4d82f3a 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/delete.rs @@ -8,10 +8,9 @@ use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::relay::util::relay_name_parser; use crate::terminal::tui::DeleteCommandTui; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -59,11 +58,7 @@ impl DeleteTui { opts: CommandGlobalOpts, cmd: DeleteCommand, ) -> miette::Result<()> { - let node_name = { - let name = get_node_name(&opts.state, &cmd.at); - parse_node_name(&name)? - }; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let tui = Self { ctx, opts, @@ -147,13 +142,13 @@ impl DeleteCommandTui for DeleteTui { plain.push_str(&fmt_ok!( "Relay with name {} on Node {} has been deleted\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } else { plain.push_str(&fmt_warn!( "Failed to delete relay with name {} on Node {}\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } } diff --git a/implementations/rust/ockam/ockam_command/src/relay/list.rs b/implementations/rust/ockam/ockam_command/src/relay/list.rs index 82d019e4925..8c7f5eb0c4d 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/list.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/list.rs @@ -1,18 +1,15 @@ use clap::Args; use colorful::Colorful; -use miette::{miette, IntoDiagnostic}; +use miette::IntoDiagnostic; use tokio::sync::Mutex; use tokio::try_join; use tracing::trace; use ockam::Context; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::terminal::OckamColor; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -37,7 +34,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, options: CommandGlobalOpts) { - initialize_node_if_default(&options, &self.to); node_rpc(run_impl, (options, self)); } } @@ -46,14 +42,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let to = get_node_name(&opts.state, &cmd.to); - let node_name = extract_address_value(&to)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.to).await?; let is_finished: Mutex = Mutex::new(false); let get_relays = async { @@ -64,9 +53,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing Relays on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -78,8 +65,8 @@ async fn run_impl( let plain = opts.terminal.build_list( &relays, - &format!("Relays on Node {node_name}"), - &format!("No Relays found on node {node_name}."), + &format!("Relays on Node {}", node.node_name()), + &format!("No Relays found on node {}.", node.node_name()), )?; let json = serde_json::to_string_pretty(&relays).into_diagnostic()?; diff --git a/implementations/rust/ockam/ockam_command/src/relay/show.rs b/implementations/rust/ockam/ockam_command/src/relay/show.rs index 021435d9626..0b5c1eb750b 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/show.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/show.rs @@ -11,11 +11,10 @@ use ockam_multiaddr::MultiAddr; use serde::Serialize; -use crate::node::get_node_name; use crate::output::Output; use crate::relay::util::relay_name_parser; use crate::terminal::tui::ShowCommandTui; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts, Terminal, TerminalStream}; const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -63,11 +62,7 @@ impl ShowTui { opts: CommandGlobalOpts, cmd: ShowCommand, ) -> miette::Result<()> { - let node_name = { - let name = get_node_name(&opts.state, &cmd.at); - parse_node_name(&name)? - }; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let tui = Self { ctx, opts, diff --git a/implementations/rust/ockam/ockam_command/src/reset.rs b/implementations/rust/ockam/ockam_command/src/reset.rs index 8c17bffb999..dbb43e7b4a7 100644 --- a/implementations/rust/ockam/ockam_command/src/reset.rs +++ b/implementations/rust/ockam/ockam_command/src/reset.rs @@ -1,14 +1,14 @@ -use crate::terminal::ConfirmResult; -use crate::util::node_rpc; -use crate::{fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use ockam_api::cloud::space::Spaces; + use ockam_api::nodes::InMemoryNode; use ockam_node::Context; +use crate::terminal::ConfirmResult; +use crate::util::node_rpc; +use crate::{fmt_ok, CommandGlobalOpts}; + /// Removes the local Ockam configuration including all Identities and Nodes #[derive(Clone, Debug, Args)] pub struct ResetCommand { @@ -32,7 +32,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ResetCommand)) -> mi } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ResetCommand) -> miette::Result<()> { - let delete_orchestrator_resources = cmd.with_orchestrator && opts.state.is_enrolled()?; + let delete_orchestrator_resources = cmd.with_orchestrator && opts.state.is_enrolled().await?; if !cmd.yes { let msg = if delete_orchestrator_resources { "This will delete the local Ockam configuration and remove your spaces from the Orchestrator. Are you sure?" @@ -57,14 +57,12 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ResetCommand) -> } let node = InMemoryNode::start(ctx, &opts.state).await?; let controller = node.create_controller().await?; - for space in opts.state.spaces.list()? { + for space in opts.state.get_spaces().await? { if let Some(ref s) = spinner { - s.set_message(format!("Deleting space '{}'...", space.name())) + s.set_message(format!("Deleting space '{}'...", space.name)) } - controller - .delete_space(ctx, space.config().id.clone()) - .await?; - let _ = opts.state.spaces.delete(space.name()); + controller.delete_space(ctx, &space.id).await?; + opts.state.delete_space(&space.id).await? } if let Some(ref s) = spinner { s.finish_and_clear(); @@ -72,7 +70,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ResetCommand) -> opts.terminal .write_line(fmt_ok!("Orchestrator spaces deleted"))?; } - CliState::delete()?; + opts.state.delete()?; opts.terminal .stdout() .plain(fmt_ok!("Local Ockam configuration deleted")) diff --git a/implementations/rust/ockam/ockam_command/src/run/parser.rs b/implementations/rust/ockam/ockam_command/src/run/parser.rs index 8e6d9b88dff..cfbbdf100a8 100644 --- a/implementations/rust/ockam/ockam_command/src/run/parser.rs +++ b/implementations/rust/ockam/ockam_command/src/run/parser.rs @@ -1,14 +1,16 @@ -use crate::{shutdown, CommandGlobalOpts}; +use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::fmt::Debug; + use duct::Expression; use miette::IntoDiagnostic; -use ockam_api::cli_state::StateDirTrait; -use ockam_core::compat::collections::HashMap; use once_cell::sync::Lazy; use serde::Deserialize; -use std::collections::{BTreeMap, HashSet, VecDeque}; -use std::fmt::Debug; use tracing::debug; +use ockam_core::compat::collections::HashMap; + +use crate::{shutdown, CommandGlobalOpts}; + pub struct ConfigRunner { commands_sorted: Vec, commands_index: BTreeMap, @@ -131,10 +133,8 @@ impl ConfigRunner { // Send a SIGTERM to all nodes if they are still running for node_name in spawned_nodes { - if let Ok(node) = opts.state.nodes.get(node_name) { - if node.is_running() { - let _ = node.kill_process(false); - } + if opts.state.is_node_running(&node_name).await? { + opts.state.stop_node(&node_name, false).await?; } } } diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs index cf87a7b859b..c714fcc4a82 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs @@ -6,7 +6,6 @@ use tokio::{sync::Mutex, try_join}; use ockam::identity::DEFAULT_TIMEOUT; use ockam::{identity::Identifier, route, Context}; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::secure_channel::{ CreateSecureChannelRequest, CreateSecureChannelResponse, }; @@ -15,15 +14,13 @@ use ockam_api::route_to_multiaddr; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::docs; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::project::util::{ clean_projects_multiaddr, get_projects_secure_channels_from_config_lookup, }; use crate::util::api::CloudOpts; use crate::util::clean_nodes_multiaddr; use crate::{ + docs, error::Error, fmt_log, fmt_ok, terminal::OckamColor, @@ -37,9 +34,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create Secure Channels #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct CreateCommand { /// Node from which to initiate the secure channel @@ -65,7 +62,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.cloud_opts.identity); node_rpc(rpc, (opts, self)); } @@ -78,10 +74,12 @@ impl CreateCommand { node: &BackgroundNode, ) -> miette::Result { let (to, meta) = clean_nodes_multiaddr(&self.to, &opts.state) - .into_diagnostic() + .await .wrap_err(format!("Could not convert {} into route", &self.to))?; - - let identity_name = get_identity_name(&opts.state, &self.cloud_opts.identity); + let identity_name = opts + .state + .get_identity_name_or_default(&self.cloud_opts.identity) + .await?; let projects_sc = get_projects_secure_channels_from_config_lookup( opts, @@ -96,27 +94,24 @@ impl CreateCommand { .into_diagnostic() .wrap_err("Could not parse projects from route") } - - // Read the `from` argument and return node name - fn parse_from_node(&self) -> miette::Result { - extract_address_value(&self.from).into_diagnostic() - } } async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> miette::Result<()> { + let node = BackgroundNode::create_to_node(&ctx, &opts.state, &cmd.from).await?; + opts.terminal .write_line(&fmt_log!("Creating Secure Channel...\n"))?; // Delegate the request to create a secure channel to the from node. let is_finished: Mutex = Mutex::new(false); - - let from = cmd.parse_from_node()?; - let node = BackgroundNode::create(&ctx, &opts.state, &from).await?; let to = cmd.parse_to_route(&opts, &ctx, &node).await?; let authorized_identifiers = cmd.authorized.clone(); let create_secure_channel = async { - let identity_name = get_identity_name(&opts.state, &cmd.cloud_opts.identity); + let identity_name = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; let payload = CreateSecureChannelRequest::new( &to, authorized_identifiers, @@ -145,7 +140,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m ) })?; - let from = format!("/node/{}", from); + let from = format!("/node/{}", node.node_name()); opts.terminal .stdout() .plain( diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs index ccdedb80c4a..8113814be14 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs @@ -10,8 +10,7 @@ use ockam_api::{nodes::models::secure_channel::DeleteSecureChannelResponse, rout use ockam_core::{Address, AddressParseError}; use crate::docs; -use crate::node::get_node_name; -use crate::util::{is_tty, parse_node_name}; +use crate::util::is_tty; use crate::{ util::{api, exitcode, node_rpc}, CommandGlobalOpts, OutputFormat, @@ -152,13 +151,11 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand)) -> m cmd.yes, "Are you sure you want to delete this secure channel?", )? { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let address = &cmd.address; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; let response: DeleteSecureChannelResponse = node.ask(&ctx, api::delete_secure_channel(address)).await?; - cmd.print_output(&node_name, address, &opts, response); + cmd.print_output(&node.node_name(), address, &opts, response); } Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs index 69592589ca2..ef23e4fd5f5 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs @@ -7,16 +7,13 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::secure_channel::ShowSecureChannelResponse; use ockam_api::nodes::BackgroundNode; use ockam_api::route_to_multiaddr; use ockam_core::{route, Address}; -use crate::node::get_node_name; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::parse_node_name; use crate::{ docs, util::{api, node_rpc}, @@ -30,10 +27,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Secure Channels #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ListCommand { /// Node at which the returned secure channels were initiated @@ -84,16 +81,9 @@ impl ListCommand { } async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let is_finished: Mutex = Mutex::new(false); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; - let get_secure_channel_identifiers = async { let secure_channel_identifiers: Vec = node.ask(&ctx, api::list_secure_channels()).await?; @@ -115,7 +105,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie let request = api::show_secure_channel(&Address::from(channel_addr)); let show_response: ShowSecureChannelResponse = node.ask(&ctx, request).await?; let secure_channel_output = - cmd.build_output(&node_name, channel_addr, show_response)?; + cmd.build_output(&node.node_name(), channel_addr, show_response)?; *is_finished.lock().await = true; Ok(secure_channel_output) }; @@ -136,8 +126,8 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie let list = opts.terminal.build_list( &responses, - &format!("Secure Channels on {}", node_name), - &format!("No secure channels found on {}", node_name), + &format!("Secure Channels on {}", node.node_name()), + &format!("No secure channels found on {}", node.node_name()), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs index 8ba28311888..080470f79aa 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs @@ -9,8 +9,8 @@ use ockam_api::nodes::{BackgroundNode, NODEMANAGER_ADDR}; use ockam_core::api::{Request, Status}; use ockam_core::{Address, Route}; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{api, exitcode, node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::{api, exitcode, node_rpc}; use crate::{docs, fmt_log, fmt_ok, terminal::OckamColor, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -19,9 +19,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create Secure Channel Listeners #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct CreateCommand { #[command(flatten)] @@ -45,7 +45,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -58,9 +57,7 @@ async fn run_impl( ctx: &Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::post("/node/secure_channel_listener").body( CreateSecureChannelListenerRequest::new( &cmd.address, @@ -83,9 +80,7 @@ async fn run_impl( .color(OckamColor::PrimaryResource.color()) ) + &fmt_log!( "At node /node/{}", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) ), ) .machine(address.to_string()) diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs index f6f9c7e296d..1126249b2e4 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs @@ -6,8 +6,8 @@ use ockam_api::nodes::models::secure_channel::DeleteSecureChannelListenerRespons use ockam_api::nodes::BackgroundNode; use ockam_core::Address; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::{api, node_rpc}; use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/delete/long_about.txt"); @@ -16,9 +16,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt" /// Delete Secure Channel Listeners #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct DeleteCommand { /// Address at which the channel listener to be deleted is running @@ -30,7 +30,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -43,16 +42,15 @@ async fn run_impl( ctx: &Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = api::delete_secure_channel_listener(&cmd.address); let response: DeleteSecureChannelListenerResponse = node.ask(ctx, req).await?; let addr = response.addr; opts.terminal .stdout() .plain(fmt_ok!( - "Deleted secure-channel listener with address '{addr}' on node '{node_name}'" + "Deleted secure-channel listener with address '{addr}' on node '{}'", + node.node_name() )) .machine(addr) .write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs index f394a78c74f..9d1b741521d 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs @@ -5,7 +5,6 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::secure_channel::{ SecureChannelListenersList, ShowSecureChannelListenerResponse, }; @@ -13,11 +12,11 @@ use ockam_api::nodes::BackgroundNode; use ockam_api::route_to_multiaddr; use ockam_core::route; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::terminal::OckamColor; +use crate::util::api; use crate::util::node_rpc; -use crate::util::{api, parse_node_name}; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); @@ -27,10 +26,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Secure Channel Listeners #[derive(Args, Clone, Debug)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ListCommand { /// Node of which secure listeners shall be listed @@ -40,7 +39,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -50,14 +48,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_listeners = async { @@ -69,9 +60,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing secure channel listeners on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -82,8 +71,11 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let list = opts.terminal.build_list( &secure_channel_listeners.list, - &format!("Secure Channel Listeners at Node {}", node_name), - &format!("No secure channel listeners found at node {}.", node_name), + &format!("Secure Channel Listeners at Node {}", node.node_name()), + &format!( + "No secure channel listeners found at node {}.", + node.node_name() + ), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs index 702cc05ffc1..6b373e2fa70 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs @@ -4,8 +4,8 @@ use ockam::Context; use ockam_api::nodes::BackgroundNode; use ockam_core::Address; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::{api, node_rpc}; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/show/long_about.txt"); @@ -15,10 +15,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show Secure Channel Listener #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ShowCommand { /// Address of the channel listener @@ -30,7 +30,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -43,11 +42,8 @@ async fn run_impl( ctx: &Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let address = &cmd.address; - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; let req = api::show_secure_channel_listener(address); node.tell(ctx, req).await?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs index 3a1da7ec893..2f0e7d10fdf 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs @@ -6,9 +6,7 @@ use ockam_api::nodes::models::secure_channel::ShowSecureChannelResponse; use ockam_api::nodes::BackgroundNode; use ockam_core::Address; -use crate::node::get_node_name; use crate::output::Output; -use crate::util::parse_node_name; use crate::{ docs, util::{api, node_rpc}, @@ -22,10 +20,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show Secure Channels #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ShowCommand { /// Node at which the secure channel was initiated @@ -44,11 +42,9 @@ impl ShowCommand { } async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand)) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; - let address = &cmd.address; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let address = &cmd.address; let response: ShowSecureChannelResponse = node.ask(&ctx, api::show_secure_channel(address)).await?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/service/list.rs b/implementations/rust/ockam/ockam_command/src/service/list.rs index 59afdf5a9ed..c604c525f71 100644 --- a/implementations/rust/ockam/ockam_command/src/service/list.rs +++ b/implementations/rust/ockam/ockam_command/src/service/list.rs @@ -2,20 +2,18 @@ use std::fmt::Write; use clap::Args; use colorful::Colorful; -use miette::miette; use miette::IntoDiagnostic; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::{ServiceList, ServiceStatus}; use ockam_api::nodes::BackgroundNode; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::util::{api, node_rpc}; use crate::CommandGlobalOpts; /// List service(s) of a given node @@ -27,7 +25,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -37,14 +34,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_services = async { @@ -55,9 +45,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing Services on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -68,8 +56,8 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let plain = opts.terminal.build_list( &services.list, - &format!("Services on {}", node_name), - &format!("No services found on {}", node_name), + &format!("Services on {}", node.node_name()), + &format!("No services found on {}", node.node_name()), )?; let json = serde_json::to_string_pretty(&services.list).into_diagnostic()?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/service/start.rs b/implementations/rust/ockam/ockam_command/src/service/start.rs index bb91d2ddd38..d1ed3618185 100644 --- a/implementations/rust/ockam/ockam_command/src/service/start.rs +++ b/implementations/rust/ockam/ockam_command/src/service/start.rs @@ -8,7 +8,7 @@ use ockam_api::nodes::BackgroundNode; use ockam_api::DefaultAddress; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; use crate::util::{api, node_rpc}; use crate::{fmt_ok, CommandGlobalOpts}; @@ -70,7 +70,6 @@ fn authenticator_default_addr() -> String { impl StartCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -80,8 +79,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, StartCommand)) -> mi } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: StartCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let mut is_hop_service = false; let addr = match cmd.create_subcommand { StartSubCommand::Hop { addr, .. } => { diff --git a/implementations/rust/ockam/ockam_command/src/space/create.rs b/implementations/rust/ockam/ockam_command/src/space/create.rs index 9af19eadb1a..f3e377feacf 100644 --- a/implementations/rust/ockam/ockam_command/src/space/create.rs +++ b/implementations/rust/ockam/ockam_command/src/space/create.rs @@ -1,14 +1,14 @@ use clap::Args; -use ockam::Context; -use ockam_api::cloud::space::Spaces; +use colorful::Colorful; +use miette::miette; use crate::output::Output; use crate::util::api::{self, CloudOpts}; -use crate::util::{is_enrolled_guard, node_rpc}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; -use colorful::Colorful; +use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait}; +use ockam_api::cloud::space::Spaces; use ockam_api::nodes::InMemoryNode; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -17,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create a new space #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct CreateCommand { /// Name of the space - must be unique across all Ockam Orchestrator users. @@ -48,7 +48,15 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - is_enrolled_guard(&opts.state, None)?; + if !opts + .state + .is_identity_enrolled(&cmd.cloud_opts.identity) + .await? + { + return Err(miette!( + "Please enroll using 'ockam enroll' before using this command" + )); + }; opts.terminal.write_line(format!( "\n{}", @@ -61,24 +69,26 @@ async fn run_impl( ))?; let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - let space = controller.create_space(ctx, cmd.name, cmd.admins).await?; + let space = node + .create_space( + ctx, + &cmd.name, + cmd.admins.iter().map(|a| a.as_ref()).collect(), + ) + .await?; opts.terminal .stdout() .plain(space.output()?) .json(serde_json::json!(&space)) .write_line()?; - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; Ok(()) } fn validate_space_name(s: &str) -> Result { match api::validate_cloud_resource_name(s) { Ok(_) => Ok(s.to_string()), - Err(_e)=> Err(String::from( + Err(_e) => Err(String::from( "space name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ Separators must occur between alphanumeric characters. This implies that separators can't \ occur at the start or end of the name, nor they can occur in sequence.", diff --git a/implementations/rust/ockam/ockam_command/src/space/delete.rs b/implementations/rust/ockam/ockam_command/src/space/delete.rs index f8fcd6d4aae..173aa54639b 100644 --- a/implementations/rust/ockam/ockam_command/src/space/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/space/delete.rs @@ -2,7 +2,6 @@ use clap::Args; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::space::Spaces; use ockam_api::nodes::InMemoryNode; @@ -53,14 +52,9 @@ async fn run_impl( .terminal .confirmed_with_flag_or_prompt(cmd.yes, "Are you sure you want to delete this space?")? { - let space_id = opts.state.spaces.get(&cmd.name)?.config().id.clone(); let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - controller.delete_space(ctx, space_id).await?; + node.delete_space_by_name(ctx, &cmd.name).await?; - let _ = opts.state.spaces.delete(&cmd.name); - // TODO: remove projects associated to the space. - // Currently we are not storing that association in the project config file. opts.terminal .stdout() .plain(fmt_ok!("Space with name '{}' has been deleted.", &cmd.name)) diff --git a/implementations/rust/ockam/ockam_command/src/space/list.rs b/implementations/rust/ockam/ockam_command/src/space/list.rs index 12918ba8e61..9f1b4890983 100644 --- a/implementations/rust/ockam/ockam_command/src/space/list.rs +++ b/implementations/rust/ockam/ockam_command/src/space/list.rs @@ -4,7 +4,6 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait}; use ockam_api::cloud::space::Spaces; use ockam_api::nodes::InMemoryNode; @@ -42,10 +41,9 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> miette::Result<()> { let is_finished: Mutex = Mutex::new(false); let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; let get_spaces = async { - let spaces = controller.list_spaces(ctx).await?; + let spaces = node.get_spaces(ctx).await?; *is_finished.lock().await = true; Ok(spaces) }; @@ -65,12 +63,6 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> )?; let json = serde_json::to_string_pretty(&spaces).into_diagnostic()?; - for space in spaces { - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; - } - opts.terminal .stdout() .plain(plain) diff --git a/implementations/rust/ockam/ockam_command/src/space/mod.rs b/implementations/rust/ockam/ockam_command/src/space/mod.rs index c72cac8a515..1e05a5d97bf 100644 --- a/implementations/rust/ockam/ockam_command/src/space/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/space/mod.rs @@ -11,7 +11,6 @@ mod create; mod delete; mod list; mod show; -mod util; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); diff --git a/implementations/rust/ockam/ockam_command/src/space/show.rs b/implementations/rust/ockam/ockam_command/src/space/show.rs index ae03764f90a..df959c15107 100644 --- a/implementations/rust/ockam/ockam_command/src/space/show.rs +++ b/implementations/rust/ockam/ockam_command/src/space/show.rs @@ -3,9 +3,7 @@ use console::Term; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait, StateItemTrait}; use ockam_api::cloud::space::{Space, Spaces}; -use ockam_api::cloud::Controller; use ockam_api::nodes::InMemoryNode; use crate::output::Output; @@ -21,9 +19,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show the details of a space #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { /// Name of the space @@ -51,7 +49,7 @@ pub struct ShowTui { ctx: Context, opts: CommandGlobalOpts, space_name: Option, - controller: Controller, + node: InMemoryNode, } impl ShowTui { @@ -61,12 +59,11 @@ impl ShowTui { cmd: ShowCommand, ) -> miette::Result<()> { let node = InMemoryNode::start(&ctx, &opts.state).await?; - let controller = node.create_controller().await?; let tui = Self { ctx, opts, space_name: cmd.name, - controller, + node, }; tui.show().await } @@ -86,23 +83,24 @@ impl ShowCommandTui for ShowTui { async fn get_arg_item_name_or_default(&self) -> miette::Result { let space_name = match &self.space_name { - None => self.opts.state.spaces.default()?.name().to_string(), + None => self.opts.state.get_default_space().await?.space_name(), Some(n) => n.to_string(), }; Ok(space_name) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.spaces.list_items_names()?) + Ok(self + .node + .get_spaces(&self.ctx) + .await? + .iter() + .map(|s| s.space_name()) + .collect()) } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let id = self.opts.state.spaces.get(item_name)?.config().id.clone(); - let space = self.controller.get_space(&self.ctx, id).await?; - self.opts - .state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; + let space = self.node.get_space_by_name(&self.ctx, item_name).await?; self.terminal() .stdout() .plain(space.output()?) @@ -113,13 +111,7 @@ impl ShowCommandTui for ShowTui { } async fn show_multiple(&self, items_names: Vec) -> miette::Result<()> { - let spaces: Vec = self.controller.list_spaces(&self.ctx).await?; - for space in &spaces { - self.opts - .state - .spaces - .overwrite(&space.name, SpaceConfig::from(space))?; - } + let spaces: Vec = self.node.get_spaces(&self.ctx).await?; let filtered: Vec = spaces .into_iter() .filter(|s| items_names.contains(&s.name)) diff --git a/implementations/rust/ockam/ockam_command/src/space/util.rs b/implementations/rust/ockam/ockam_command/src/space/util.rs deleted file mode 100644 index 24a09e4f42e..00000000000 --- a/implementations/rust/ockam/ockam_command/src/space/util.rs +++ /dev/null @@ -1,21 +0,0 @@ -use ockam::Context; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait}; -use ockam_api::cloud::space::Spaces; -use ockam_api::cloud::Controller; - -use crate::CommandGlobalOpts; - -#[allow(dead_code)] -async fn refresh_spaces( - ctx: &Context, - opts: &CommandGlobalOpts, - controller: &Controller, -) -> miette::Result<()> { - let spaces = controller.list_spaces(ctx).await?; - for space in spaces { - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; - } - Ok(()) -} diff --git a/implementations/rust/ockam/ockam_command/src/status.rs b/implementations/rust/ockam/ockam_command/src/status.rs index df33ed42a8a..0cc32f1bb05 100644 --- a/implementations/rust/ockam/ockam_command/src/status.rs +++ b/implementations/rust/ockam/ockam_command/src/status.rs @@ -2,20 +2,14 @@ use std::io::Write; use std::time::Duration; use clap::Args; -use miette::miette; -use minicbor::{Decode, Decoder, Encode}; use tracing::warn; -use ockam::identity::{Identifier, SecureChannelOptions, TrustIdentifierPolicy}; -use ockam::{Context, Node, TcpConnectionOptions, TcpTransport}; -use ockam_api::cli_state::identities::IdentityState; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_api::cli_state::NodeState; +use ockam::identity::{Identifier, TimestampInSeconds}; +use ockam::Context; +use ockam_api::cli_state::enrollment::{EnrollmentStatus, IdentityEnrollment}; +use ockam_api::cloud::project::OrchestratorVersionInfo; use ockam_api::nodes::models::base::NodeStatus as NodeStatusModel; -use ockam_api::nodes::{BackgroundNode, NodeManager}; -use ockam_core::api::{Request, ResponseHeader, Status}; -use ockam_core::route; -use ockam_node::MessageSendReceiveOptions; +use ockam_api::nodes::{BackgroundNode, InMemoryNode}; use crate::util::{api, node_rpc}; use crate::CommandGlobalOpts; @@ -48,32 +42,40 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: StatusCommand, ) -> miette::Result<()> { - let identities_details = get_identities_details(&opts, cmd.all)?; + let identities_details = get_identities_details(&opts, cmd.all).await?; let nodes_details = get_nodes_details(ctx, &opts).await?; - let orchestrator_version = - get_orchestrator_version(ctx, &opts, Duration::from_secs(cmd.timeout)).await; + + let node = InMemoryNode::start(ctx, &opts.state).await?; + let controller = node.create_controller().await?; + + let orchestrator_version = controller + .get_orchestrator_version_info(ctx) + .await + .map_err(|e| warn!(%e, "Failed to retrieve orchestrator version")) + .unwrap_or_default(); let status = StatusData::from_parts(orchestrator_version, identities_details, nodes_details)?; - print_output(opts, cmd, status)?; + print_output(opts, cmd, status).await?; Ok(()) } async fn get_nodes_details(ctx: &Context, opts: &CommandGlobalOpts) -> Result> { let mut node_details: Vec = vec![]; - let node_states = opts.state.nodes.list()?; - if node_states.is_empty() { + let nodes = opts.state.get_nodes().await?; + if nodes.is_empty() { return Ok(node_details); } - let default_node_name = opts.state.nodes.default()?.name().to_string(); - let mut node = BackgroundNode::create(ctx, &opts.state, &default_node_name).await?; - node.set_timeout(Duration::from_millis(200)); + let default_node_name = opts.state.get_default_node_name().await?; + let mut node_client = + BackgroundNode::create_to_node(ctx, &opts.state, &default_node_name).await?; + node_client.set_timeout(Duration::from_millis(200)); - for node_state in &node_states { - node.set_node_name(node_state.name()); + for node in nodes { + node_client.set_node_name(&node.name()); let node_infos = NodeDetails { - identifier: node_state.config().identifier()?, - state: node_state.clone(), - status: get_node_status(ctx, &node).await?, + identifier: node.identifier(), + name: node.name(), + status: get_node_status(ctx, &node_client).await?, }; node_details.push(node_infos); } @@ -89,94 +91,27 @@ async fn get_node_status(ctx: &Context, node: &BackgroundNode) -> Result .unwrap_or("Stopped".to_string())) } -fn get_identities_details(opts: &CommandGlobalOpts, all: bool) -> Result> { - let mut identities_details: Vec = vec![]; - for identity in opts.state.identities.list()? { - if all { - identities_details.push(identity) - } else { - match &identity.config().enrollment_status { - Some(_enrollment) => identities_details.push(identity), - None => (), - } - } - } - Ok(identities_details) -} - -async fn get_orchestrator_version( - ctx: &Context, +async fn get_identities_details( opts: &CommandGlobalOpts, - timeout: Duration, -) -> Result { - // for new we get the controller address directly until we - // access a Controller interface from the NodeManager - let controller_addr = NodeManager::controller_multiaddr(); - let controller_identifier = NodeManager::load_controller_identifier()?; - let controller_tcp_addr = controller_addr.to_socket_addr()?; - let tcp = TcpTransport::create(ctx).await?; - let connection = tcp - .connect(controller_tcp_addr, TcpConnectionOptions::new()) - .await?; - - // Create node that will be used to send the request - let node = { - // Get or create a vault to store the identity - let vault = match opts.state.vaults.default() { - Ok(v) => v, - Err(_) => opts.state.create_vault_state(None).await?, - } - .get() - .await?; - let identities_repository = opts.state.identities.identities_repository().await?; - Node::builder() - .with_vault(vault) - .with_identities_repository(identities_repository) - .build(ctx) - .await? + all: bool, +) -> Result> { + let enrollment_status = if all { + EnrollmentStatus::Any + } else { + EnrollmentStatus::Enrolled }; - - // Establish secure channel with controller - let node_identifier = opts + Ok(opts .state - .default_identities() - .await? - .identities_creation() - .create_identity() - .await?; - let secure_channel_options = SecureChannelOptions::new() - .with_trust_policy(TrustIdentifierPolicy::new(controller_identifier)) - .with_timeout(timeout); - let secure_channel = node - .create_secure_channel( - &node_identifier, - route![connection, "api"], - secure_channel_options, - ) - .await?; - - // Send request - let buf: Vec = node - .send_and_receive_extended::>( - route![secure_channel, "version_info"], - Request::get("").to_vec()?, - MessageSendReceiveOptions::new().with_timeout(timeout), - ) - .await? - .body(); - let mut dec = Decoder::new(&buf); - - // Decode response - let hdr = dec.decode::()?; - if hdr.status() == Some(Status::Ok) { - Ok(dec.decode::()?) - } else { - Err(miette!("Failed to retrieve version information from node.").into()) - } + .get_identity_enrollments(enrollment_status) + .await?) } -fn print_output(opts: CommandGlobalOpts, cmd: StatusCommand, status: StatusData) -> Result<()> { - let plain = build_plain_output(&opts, &cmd, &status)?; +async fn print_output( + opts: CommandGlobalOpts, + cmd: StatusCommand, + status: StatusData, +) -> Result<()> { + let plain = build_plain_output(&cmd, &status).await?; let json = serde_json::to_string(&status)?; opts.terminal .stdout() @@ -186,21 +121,17 @@ fn print_output(opts: CommandGlobalOpts, cmd: StatusCommand, status: StatusData) Ok(()) } -fn build_plain_output( - opts: &CommandGlobalOpts, - cmd: &StatusCommand, - status: &StatusData, -) -> Result> { +async fn build_plain_output(cmd: &StatusCommand, status: &StatusData) -> Result> { let mut plain = Vec::new(); writeln!( &mut plain, "Controller version: {}", - status.orchestrator_version.controller_version + status.orchestrator_version.version() )?; writeln!( &mut plain, "Project version: {}", - status.orchestrator_version.project_version + status.orchestrator_version.project_version() )?; if status.identities.is_empty() { if cmd.all { @@ -214,16 +145,19 @@ fn build_plain_output( )?; } return Ok(plain); - } - let default_identity = opts.state.identities.default()?; + }; + for (i_idx, i) in status.identities.iter().enumerate() { writeln!(&mut plain, "Identity[{i_idx}]")?; - if default_identity.config().identifier() == i.identity.config().identifier() { + if i.is_default() { writeln!(&mut plain, "{:2}Default: yes", "")?; } - for line in i.identity.to_string().lines() { - writeln!(&mut plain, "{:2}{}", "", line)?; + if let Some(name) = i.name() { + writeln!(&mut plain, "{:2}{}", "Name", name)?; } + writeln!(&mut plain, "{:2}{}", "Identifier", i.identifier())?; + writeln!(&mut plain, "{:2}{}", "Enrolled", i.is_enrolled())?; + if !i.nodes.is_empty() { writeln!(&mut plain, "{:2}Linked Nodes:", "")?; for (n_idx, node) in i.nodes.iter().enumerate() { @@ -245,31 +179,24 @@ struct StatusData { impl StatusData { fn from_parts( - orchestrator_version: Result, - identities_details: Vec, + orchestrator_version: OrchestratorVersionInfo, + identities_details: Vec, mut nodes_details: Vec, ) -> Result { - let orchestrator_version = orchestrator_version - .map_err(|e| warn!(%e, "Failed to retrieve orchestrator version")) - .unwrap_or(OrchestratorVersionInfo { - controller_version: "N/A".to_string(), - project_version: "N/A".to_string(), - }); let mut identities = vec![]; for identity in identities_details.into_iter() { let mut identity_status = IdentityWithLinkedNodes { - identity, + identifier: identity.identifier(), + name: identity.name(), + is_default: identity.is_default(), + enrolled_at: identity + .enrolled_at() + .map(|o| TimestampInSeconds::from(o.unix_timestamp() as u64)), nodes: vec![], }; - nodes_details - .retain(|nd| nd.identifier == identity_status.identity.config().identifier()); + nodes_details.retain(|nd| nd.identifier == identity_status.identifier()); if !nodes_details.is_empty() { - for node in nodes_details.iter() { - identity_status.nodes.push(NodeStatus { - name: node.state.name().to_string(), - status: node.status.clone(), - }); - } + identity_status.nodes = nodes_details.clone(); } identities.push(identity_status); } @@ -282,31 +209,39 @@ impl StatusData { #[derive(serde::Serialize, serde::Deserialize)] struct IdentityWithLinkedNodes { - identity: IdentityState, - nodes: Vec, + identifier: Identifier, + name: Option, + is_default: bool, + enrolled_at: Option, + nodes: Vec, } -#[derive(serde::Serialize, serde::Deserialize)] -struct IdentityStatus {} +impl IdentityWithLinkedNodes { + fn identifier(&self) -> Identifier { + self.identifier.clone() + } -#[derive(serde::Serialize, serde::Deserialize)] -struct NodeStatus { - name: String, - status: String, + fn name(&self) -> Option { + self.name.clone() + } + + fn is_default(&self) -> bool { + self.is_default + } + + fn is_enrolled(&self) -> bool { + self.enrolled_at.is_some() + } + + #[allow(unused)] + fn nodes(&self) -> &Vec { + &self.nodes + } } -struct NodeDetails { +#[derive(serde::Serialize, serde::Deserialize, Clone)] +pub struct NodeDetails { identifier: Identifier, - state: NodeState, + name: String, status: String, } - -#[derive(Encode, Decode, Debug, serde::Serialize, serde::Deserialize)] -#[cfg_attr(test, derive(Clone))] -#[cbor(map)] -struct OrchestratorVersionInfo { - #[n(1)] - controller_version: String, - #[n(2)] - project_version: String, -} diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs index f763fb41b87..3eeefe32f84 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs @@ -1,13 +1,15 @@ use clap::Args; +use colorful::Colorful; use indoc::formatdoc; use miette::IntoDiagnostic; +use serde_json::json; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::BackgroundNode; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; +use crate::output::OutputFormat; +use crate::util::is_tty; use crate::{ docs, util::{api, node_rpc}, @@ -38,20 +40,67 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.from); node_rpc(run_impl, (opts, self)) } + + #[allow(unused)] + async fn print_output( + &self, + opts: &CommandGlobalOpts, + response: &TransportStatus, + ) -> miette::Result<()> { + // if output format is json, write json to stdout. + match opts.global_args.output_format { + OutputFormat::Plain => { + if !is_tty(std::io::stdout()) { + println!("{}", response.multiaddr().into_diagnostic()?); + return Ok(()); + } + let from = opts + .state + .get_node_name_or_default(&self.node_opts.from) + .await?; + let to = response.socket_addr().into_diagnostic()?; + if opts.global_args.no_color { + println!("\n TCP Connection:"); + println!(" From: /node/{from}"); + println!(" To: {} (/ip4/{}/tcp/{})", to, to.ip(), to.port()); + println!(" Address: {}", response.multiaddr().into_diagnostic()?); + } else { + println!("\n TCP Connection:"); + println!("{}", format!(" From: /node/{from}").light_magenta()); + println!( + "{}", + format!(" To: {} (/ip4/{}/tcp/{})", to, to.ip(), to.port()) + .light_magenta() + ); + println!( + "{}", + format!(" Address: {}", response.multiaddr().into_diagnostic()?) + .light_magenta() + ); + } + } + OutputFormat::Json => { + let json = json!([{"route": response.multiaddr().into_diagnostic()? }]); + println!("{json}"); + } + } + Ok(()) + } } async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let from = get_node_name(&opts.state, &cmd.node_opts.from); - let node_name = extract_address_value(&from)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.from).await?; let request = api::create_tcp_connection(&cmd); let transport_status: TransportStatus = node.ask(&ctx, request).await?; + let from = opts + .state + .get_node_name_or_default(&cmd.node_opts.from) + .await?; let to = transport_status.socket_addr().into_diagnostic()?; let plain = formatdoc! {r#" TCP Connection: @@ -59,7 +108,7 @@ async fn run_impl( To: {to} (/ip4/{}/tcp/{}) Address: {} "#, to.ip(), to.port(), transport_status.multiaddr().into_diagnostic()?}; - let json = serde_json::json!([{"route": transport_status.multiaddr().into_diagnostic()? }]); + let json = json!([{"route": transport_status.multiaddr().into_diagnostic()? }]); opts.terminal .stdout() .plain(plain) diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs index f61b067c8a8..3ef5389b71e 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs @@ -5,7 +5,6 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; @@ -28,7 +27,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -41,9 +39,8 @@ async fn run_impl( cmd.yes, "Are you sure you want to delete this TCP connection?", )? { + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let address = cmd.address; - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; let req = Request::delete("/node/tcp/connection") .body(models::transport::DeleteTransport::new(address.clone())); node.tell(&ctx, req).await?; diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs index 528dc4abfc0..19ff3714ff6 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs @@ -2,18 +2,15 @@ use std::fmt::Write; use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::transport::{TransportList, TransportStatus}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::terminal::OckamColor; use crate::util::node_rpc; @@ -34,7 +31,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -43,14 +39,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_transports = async { @@ -62,9 +51,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing TCP Connections on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -75,12 +62,10 @@ async fn run_impl( let list = opts.terminal.build_list( &transports.list, - &format!("TCP Connections on {}", node_name), + &format!("TCP Connections on {}", node.node_name()), &format!( "No TCP Connections found on {}", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) ), )?; diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs index 71a11c88580..88b1ad49912 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs @@ -1,12 +1,11 @@ use clap::Args; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -28,7 +27,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)); } } @@ -37,9 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let transport_status: TransportStatus = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs index cc386245f04..e28dccdc5c2 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs @@ -12,7 +12,6 @@ use tracing::log::trace; use ockam::identity::Identifier; use ockam::Context; - use ockam_api::nodes::models::portal::InletStatus; use ockam_api::nodes::service::portals::Inlets; use ockam_api::nodes::BackgroundNode; @@ -22,15 +21,11 @@ use ockam_core::Error; use ockam_multiaddr::proto::Project; use ockam_multiaddr::{MultiAddr, Protocol as _}; -use crate::node::{get_node_name, initialize_node_if_default}; - use crate::tcp::util::alias_parser; use crate::terminal::OckamColor; use crate::util::duration::duration_parser; use crate::util::parsers::socket_addr_parser; -use crate::util::{ - find_available_port, node_rpc, parse_node_name, port_is_free_guard, process_nodes_multiaddr, -}; +use crate::util::{find_available_port, node_rpc, port_is_free_guard, process_nodes_multiaddr}; use crate::{display_parse_logs, docs, fmt_log, fmt_ok, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -84,7 +79,6 @@ fn default_to_addr() -> MultiAddr { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(rpc, (opts, self)); } } @@ -101,12 +95,9 @@ async fn rpc( ))?; display_parse_logs(&opts); - cmd.to = process_nodes_multiaddr(&cmd.to, &opts.state)?; + cmd.to = process_nodes_multiaddr(&cmd.to, &opts.state).await?; - let node_name = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&node_name)?; - - let mut node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let mut node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; cmd.timeout.map(|t| node.set_timeout(t)); let is_finished: Mutex = Mutex::new(false); @@ -169,9 +160,7 @@ async fn rpc( let progress_messages = vec![ format!( "Creating TCP Inlet on {}...", - &node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + &node.node_name().color(OckamColor::PrimaryResource.color()) ), format!( "Hosting TCP Socket at {}...", @@ -200,9 +189,7 @@ async fn rpc( &cmd.from .to_string() .color(OckamColor::PrimaryResource.color()), - &node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + &node.node_name().color(OckamColor::PrimaryResource.color()) ) + &fmt_log!( "to the outlet at {}", &cmd.to diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs index def59da1cb4..39ba8e4c8ed 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs @@ -10,10 +10,10 @@ use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use crate::fmt_ok; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::tcp::util::alias_parser; use crate::terminal::tui::DeleteCommandTui; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -37,7 +37,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -62,11 +61,7 @@ impl DeleteTui { opts: CommandGlobalOpts, cmd: DeleteCommand, ) -> miette::Result<()> { - let node_name = { - let name = get_node_name(&opts.state, &cmd.node_opts.at_node); - parse_node_name(&name)? - }; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let tui = Self { ctx, opts, @@ -132,13 +127,13 @@ impl DeleteCommandTui for DeleteTui { plain.push_str(&fmt_ok!( "TCP inlet with alias {} on Node {} has been deleted\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } else { plain.push_str(&fmt_warn!( "Failed to delete TCP inlet with alias {} on Node {}\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs index 2e36fe2e397..c59e4cfe2ec 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs @@ -1,17 +1,15 @@ use clap::Args; use colorful::Colorful; -use miette::{miette, IntoDiagnostic}; +use miette::IntoDiagnostic; use tokio::sync::Mutex; use tokio::try_join; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::portal::InletList; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -31,7 +29,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node.at_node); node_rpc(run_impl, (opts, self)) } } @@ -40,14 +37,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node.at_node); - let node_name = extract_address_value(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_inlets = async { @@ -58,9 +48,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing TCP Inlets on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -72,7 +60,7 @@ async fn run_impl( let plain = opts.terminal.build_list( &inlets.list, "Inlets", - &format!("No TCP Inlets found on {node_name}"), + &format!("No TCP Inlets found on {}", node.node_name()), )?; let json = serde_json::to_string_pretty(&inlets.list).into_diagnostic()?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs index ae13ffe2617..5ba10af1608 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs @@ -8,11 +8,10 @@ use ockam_api::nodes::models::portal::InletStatus; use ockam_api::nodes::service::portals::Inlets; use ockam_api::nodes::BackgroundNode; -use crate::fmt_ok; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::tcp::util::alias_parser; -use crate::util::{node_rpc, parse_node_name}; -use crate::{docs, CommandGlobalOpts}; +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); @@ -34,7 +33,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -43,10 +41,7 @@ pub async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let inlet_status = node .show_inlet(&ctx, &cmd.alias) .await? diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs index 334fa9975ac..07a0f7afdbb 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs @@ -1,9 +1,3 @@ -use crate::util::{node_rpc, parse_node_name}; -use crate::{docs, fmt_log, CommandGlobalOpts}; -use crate::{ - fmt_ok, - node::{get_node_name, initialize_node_if_default}, -}; use clap::Args; use colorful::Colorful; use miette::IntoDiagnostic; @@ -14,6 +8,10 @@ use ockam_multiaddr::proto::{DnsAddr, Tcp}; use ockam_multiaddr::MultiAddr; use ockam_node::Context; +use crate::util::node_rpc; +use crate::{docs, CommandGlobalOpts}; +use crate::{fmt_log, fmt_ok}; + const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); /// Create a TCP listener @@ -30,7 +28,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(run_impl, (opts, self)) } } @@ -39,9 +36,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let transport_status: TransportStatus = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs index c4594b849e3..0dcac3b1484 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs @@ -8,9 +8,7 @@ use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::util::node_rpc; -use crate::util::parse_node_name; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -32,7 +30,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)); } } @@ -41,9 +38,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; // Check if there an TCP listener with the provided address exists let address = cmd.address; @@ -55,7 +50,8 @@ async fn run_impl( .found() .into_diagnostic()? .ok_or(miette!( - "TCP listener with address {address} was not found on Node {node_name}" + "TCP listener with address {address} was not found on Node {}", + node.node_name() ))?; // Proceed with the deletion @@ -70,9 +66,10 @@ async fn run_impl( opts.terminal .stdout() .plain(fmt_ok!( - "TCP listener with address {address} on Node {node_name} has been deleted" + "TCP listener with address {address} on Node {} has been deleted", + node.node_name() )) - .json(serde_json::json!({"node": node_name })) + .json(serde_json::json!({"node": node.node_name() })) .write_line() .unwrap(); } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs index c20713c32c7..d4670e20f4b 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs @@ -1,17 +1,15 @@ use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::transport::TransportList; use ockam_api::nodes::BackgroundNode; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::util::{api, node_rpc}; use crate::{docs, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -39,14 +36,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_transports = async { @@ -57,9 +47,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing TCP Listeners on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -70,12 +58,10 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let list = opts.terminal.build_list( &transports.list, - &format!("TCP Listeners on {}", node_name), + &format!("TCP Listeners on {}", node.node_name()), &format!( "No TCP Listeners found on {}", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) ), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs index 8a4b8b7b4c4..52aaeb70c9c 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs @@ -2,12 +2,11 @@ use clap::Args; use indoc::formatdoc; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -29,7 +28,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)); } } @@ -38,9 +36,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let transport_status: TransportStatus = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs index d691fb45f9d..79a853d7a8b 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs @@ -9,12 +9,10 @@ use tokio::try_join; use ockam::Context; use ockam_abac::Resource; use ockam_api::address::extract_address_value; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::nodes::models::portal::{CreateOutlet, OutletStatus}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::policy::{add_default_project_policy, has_policy}; use crate::tcp::util::alias_parser; use crate::terminal::OckamColor; @@ -48,7 +46,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(run_impl, (opts, self)) } } @@ -69,20 +66,12 @@ pub async fn run_impl( ))?; display_parse_logs(&opts); - let node_name = get_node_name(&opts.state, &cmd.at); - let node_name = extract_address_value(&node_name)?; - let project = opts - .state - .nodes - .get(&node_name)? - .config() - .setup() - .project - .to_owned(); + let node_name = opts.state.get_node_name_or_default(&cmd.at).await?; + let project = opts.state.get_node_project(&node_name).await.ok(); let resource = Resource::new("tcp-outlet"); if let Some(p) = project { if !has_policy(&node_name, &ctx, &opts, &resource).await? { - add_default_project_policy(&node_name, &ctx, &opts, p, &resource).await?; + add_default_project_policy(&node_name, &ctx, &opts, p.id, &resource).await?; } } @@ -110,9 +99,7 @@ pub async fn run_impl( "Setting up TCP outlet worker...".to_string(), format!( "Hosting outlet service at {}...", - &cmd.from - .to_string() - .color(OckamColor::PrimaryResource.color()) + cmd.from.clone().color(OckamColor::PrimaryResource.color()) ), ]; @@ -131,8 +118,7 @@ pub async fn run_impl( &node_name .to_string() .color(OckamColor::PrimaryResource.color()), - format!("/service/{}", extract_address_value(&cmd.from)?) - .color(OckamColor::PrimaryResource.color()), + &cmd.from.color(OckamColor::PrimaryResource.color()), &cmd.to .to_string() .color(OckamColor::PrimaryResource.color()) @@ -150,8 +136,7 @@ pub async fn send_request( payload: CreateOutlet, to_node: impl Into>, ) -> crate::Result { - let node_name = get_node_name(&opts.state, &to_node.into()); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &to_node.into()).await?; let req = Request::post("/node/outlet").body(payload); Ok(node.ask(ctx, req).await?) } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs index 96edfc27eb0..ab0d35bc255 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs @@ -8,9 +8,9 @@ use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use crate::fmt_ok; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::tcp::util::alias_parser; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -34,7 +34,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -43,9 +42,7 @@ pub async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; // Check if there an outlet with the provided alias/name exists let alias = cmd.alias; @@ -54,7 +51,8 @@ pub async fn run_impl( .found() .into_diagnostic()? .ok_or(miette!( - "TCP outlet with alias {alias} was not found on Node {node_name}" + "TCP outlet with alias {alias} was not found on Node {}", + node.node_name() ))?; // Proceed with the deletion @@ -68,10 +66,11 @@ pub async fn run_impl( opts.terminal .stdout() .plain(fmt_ok!( - "TCP outlet with alias {alias} on Node {node_name} has been deleted" + "TCP outlet with alias {alias} on Node {} has been deleted", + node.node_name() )) .machine(&alias) - .json(serde_json::json!({ "alias": alias, "node": node_name })) + .json(serde_json::json!({ "alias": alias, "node": node.node_name() })) .write_line() .unwrap(); } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs index 3122d8dc44f..ce15af7bc64 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs @@ -1,17 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::portal::OutletList; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -31,7 +28,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -40,26 +36,19 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let send_req = async { - let res = send_request(&ctx, &opts, node_name.clone()).await; + let res: OutletList = node.ask(&ctx, Request::get("/node/outlet")).await?; *is_finished.lock().await = true; - res + Ok(res) }; let output_messages = vec![format!( "Listing TCP Outlets on node {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -70,8 +59,8 @@ async fn run_impl( let list = opts.terminal.build_list( &outlets.list, - &format!("Outlets on Node {node_name}"), - &format!("No TCP Outlets found on node {node_name}."), + &format!("Outlets on Node {}", node.node_name()), + &format!("No TCP Outlets found on node {}.", node.node_name()), )?; let json: Vec<_> = outlets .list @@ -93,13 +82,3 @@ async fn run_impl( Ok(()) } - -pub async fn send_request( - ctx: &Context, - opts: &CommandGlobalOpts, - to_node: impl Into>, -) -> crate::Result { - let node_name = get_node_name(&opts.state, &to_node.into()); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; - Ok(node.ask(ctx, Request::get("/node/outlet")).await?) -} diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs index fa6712df3aa..b2f02436f1f 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs @@ -6,14 +6,13 @@ use miette::miette; use serde::Serialize; use ockam::{route, Context}; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::portal::OutletStatus; use ockam_api::nodes::BackgroundNode; use ockam_api::route_to_multiaddr; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::tcp::util::alias_parser; use crate::util::node_rpc; @@ -40,7 +39,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -67,10 +65,10 @@ pub async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; - let outlet_status: OutletStatus = node.ask(&ctx, make_api_request(cmd)?).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; + let outlet_status: OutletStatus = node + .ask(&ctx, Request::get(format!("/node/outlet/{}", cmd.alias))) + .await?; let info = OutletInformation { alias: outlet_status.alias, addr: route_to_multiaddr(&route![outlet_status.worker_addr.to_string()]) @@ -85,10 +83,3 @@ pub async fn run_impl( .write_line()?; Ok(()) } - -/// Construct a request to show a tcp outlet -fn make_api_request(cmd: ShowCommand) -> Result { - let alias = cmd.alias; - let request = Request::get(format!("/node/outlet/{alias}")); - Ok(request) -} diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/create.rs b/implementations/rust/ockam/ockam_command/src/trust_context/create.rs index 7a2ae2a13d1..7774e56595f 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/create.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/create.rs @@ -1,9 +1,15 @@ -use crate::util::local_cmd; -use crate::{docs, util::api::TrustContextOpts, CommandGlobalOpts}; use clap::Args; use indoc::formatdoc; -use miette::{miette, IntoDiagnostic}; -use ockam_api::cli_state::{random_name, StateDirTrait}; +use miette::IntoDiagnostic; + +use ockam::identity::Identity; +use ockam_api::cli_state::random_name; +use ockam_core::env::FromString; +use ockam_multiaddr::MultiAddr; +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -11,66 +17,81 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create a trust context #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = false, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = false, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct CreateCommand { /// The name of the trust context to create #[arg(default_value_t = random_name())] name: String, + /// The id of the trust context to create + #[arg(long)] + id: Option, + /// Create a trust context from a credential #[arg(long)] credential: Option, - #[command(flatten)] - trust_context_opts: TrustContextOpts, + /// Create a trust context from an authority + #[arg(long)] + authority_identity: Option, + + /// Create a trust context from an authority + #[arg(long)] + authority_route: Option, } impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: CreateCommand) -> miette::Result<()> { - let config = cmd - .trust_context_opts - .to_config(&opts.state)? - .with_credential_name(cmd.credential.as_ref()) - .use_default_trust_context(false) - .build(); +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, CreateCommand), +) -> miette::Result<()> { + let authority = match &cmd.authority_identity { + None => None, + Some(identity) => Some(Identity::create(identity).await.into_diagnostic()?), + }; + let authority_route = cmd + .authority_route + .map(|r| MultiAddr::from_string(&r).into_diagnostic()) + .transpose()?; - if let Some(c) = config { - opts.state.trust_contexts.create(&cmd.name, c.clone())?; + let trust_context = opts + .state + .create_trust_context( + Some(cmd.name.clone()), + cmd.id.clone(), + cmd.credential, + authority, + authority_route, + ) + .await?; - let auth = if let Ok(auth) = c.authority() { - auth.identity_str() - } else { - "None" - }; + let authority = trust_context + .authority_identity() + .await + .into_diagnostic()? + .map(|i| i.change_history().export_as_string().unwrap()) + .unwrap_or("None".to_string()); - let output = formatdoc!( - r#" + let output = formatdoc!( + r#" Trust Context: Name: {} ID: {} Authority: {} "#, - cmd.name, - c.id(), - auth - ); - - opts.terminal - .stdout() - .plain(output) - .json(serde_json::to_string_pretty(&c).into_diagnostic()?) - .write_line()?; - } else { - return Err(miette!("Unable to create trust context")); - } + cmd.name, + trust_context.trust_context_id(), + authority + ); + opts.terminal.stdout().plain(output).write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/default.rs b/implementations/rust/ockam/ockam_command/src/trust_context/default.rs index 2108917ebc7..4e30919edae 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/default.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/default.rs @@ -1,9 +1,9 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -22,21 +22,23 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { - let DefaultCommand { name } = cmd; - let state = opts.state.trust_contexts; - let tc = state.get(&name)?; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { + let name = cmd.name; + let default_trust_context = opts.state.get_default_trust_context().await?; // If it exists, warn the user and exit - if state.is_default(tc.name())? { + if default_trust_context.name() == name { Err(miette!("The trust context '{name}' is already the default")) } // Otherwise, set it as default else { - state.set_default(tc.name())?; + opts.state.set_default_trust_context(&name).await?; opts.terminal .stdout() .plain(fmt_ok!("The trust context '{name}' is now the default")) diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs b/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs index e293f6e6cd9..b100759d9d0 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs @@ -1,8 +1,9 @@ -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/delete/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -10,9 +11,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt" /// Delete a trust context #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = false, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = false, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct DeleteCommand { /// Name of the trust context @@ -25,25 +26,26 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DeleteCommand) -> miette::Result<()> { +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DeleteCommand), +) -> miette::Result<()> { if opts.terminal.confirmed_with_flag_or_prompt( cmd.yes, "Are you sure you want to delete this trust context?", )? { - let name = cmd.name; - let state = opts.state.trust_contexts; - state.get(&name)?; - state.delete(&name)?; + let name = &cmd.name; + opts.state.delete_trust_context(name).await?; opts.terminal .stdout() .plain(fmt_ok!( "The trust context with name '{name}' has been deleted" )) - .machine(&name) + .machine(name) .json(serde_json::json!({ "name": &name })) .write_line()?; } diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/list.rs b/implementations/rust/ockam/ockam_command/src/trust_context/list.rs index e6e85c4fc5b..aecf057b8bf 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/list.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/list.rs @@ -1,8 +1,8 @@ use clap::Args; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); @@ -12,26 +12,26 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List trust contexts #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand; impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts)); + node_rpc(run_impl, opts); } } -fn run_impl(opts: CommandGlobalOpts) -> miette::Result<()> { - let states = opts.state.trust_contexts.list()?; - if states.is_empty() { +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { + let trust_contexts = opts.state.get_trust_contexts().await?; + if trust_contexts.is_empty() { return Err(miette!("No trust contexts registered on this system!")); } let plain_output = { let mut output = String::new(); - for (idx, tc) in states.iter().enumerate() { + for (idx, tc) in trust_contexts.iter().enumerate() { output.push_str(&format!("Trust context[{idx}]:")); for line in tc.to_string().lines() { output.push_str(&format!("{:2}{}\n", "", line)); diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/show.rs b/implementations/rust/ockam/ockam_command/src/trust_context/show.rs index 570ebed49c1..ba567b0e730 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/show.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/show.rs @@ -1,7 +1,7 @@ use clap::Args; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/show/long_about.txt"); @@ -22,18 +22,18 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: ShowCommand) -> miette::Result<()> { - let name = cmd - .name - .unwrap_or(opts.state.trust_contexts.default()?.name().to_string()); - let state = opts.state.trust_contexts.get(name)?; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, ShowCommand), +) -> miette::Result<()> { + let trust_context = opts.state.get_trust_context_or_default(&cmd.name).await?; let plain_output = { let mut output = "Trust context:".to_string(); - for line in state.to_string().lines() { + for line in trust_context.to_string().lines() { output.push_str(&format!("{:2}{}\n", "", line)); } output diff --git a/implementations/rust/ockam/ockam_command/src/util/api.rs b/implementations/rust/ockam/ockam_command/src/util/api.rs index 381f907be5d..9658c4938ed 100644 --- a/implementations/rust/ockam/ockam_command/src/util/api.rs +++ b/implementations/rust/ockam/ockam_command/src/util/api.rs @@ -1,7 +1,4 @@ //! API shim to make it nicer to interact with the ockam messaging API - -use std::path::PathBuf; - use clap::Args; use miette::miette; // TODO: maybe we can remove this cross-dependency inside the CLI? @@ -9,14 +6,12 @@ use minicbor::Decoder; use regex::Regex; use ockam::identity::Identifier; -use ockam_api::cli_state::CliState; use ockam_api::nodes::models::flow_controls::AddConsumer; use ockam_api::nodes::models::services::{ StartAuthenticatedServiceRequest, StartAuthenticatorRequest, StartCredentialsService, StartHopServiceRequest, StartOktaIdentityProviderRequest, }; use ockam_api::nodes::*; -use ockam_api::trust_context::TrustContextConfigBuilder; use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_core::api::ResponseHeader; @@ -204,10 +199,6 @@ pub struct CloudOpts { #[derive(Clone, Debug, Args, Default)] pub struct TrustContextOpts { - /// Project config file - #[arg(global = true, long = "project-path", value_name = "PROJECT_JSON_PATH")] - pub project_path: Option, - /// Trust Context config file #[arg( global = true, @@ -217,24 +208,12 @@ pub struct TrustContextOpts { pub trust_context: Option, #[arg(global = true, long = "project", value_name = "PROJECT_NAME")] - pub project: Option, + pub project_name: Option, } impl TrustContextOpts { - pub fn to_config(&self, cli_state: &CliState) -> Result { - let trust_context = match &self.trust_context { - Some(tc) => Some(cli_state.trust_contexts.read_config_from_path(tc)?), - None => None, - }; - Ok(TrustContextConfigBuilder { - cli_state: cli_state.clone(), - project_path: self.project_path.clone(), - trust_context, - project: self.project.clone(), - authority_identity: None, - credential_name: None, - use_default_trust_context: true, - }) + pub fn project_name(&self) -> Option { + self.project_name.clone() } } diff --git a/implementations/rust/ockam/ockam_command/src/util/mod.rs b/implementations/rust/ockam/ockam_command/src/util/mod.rs index 6fbe1c3b3ed..8313099ef49 100644 --- a/implementations/rust/ockam/ockam_command/src/util/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/util/mod.rs @@ -1,7 +1,6 @@ use std::{ net::{SocketAddr, TcpListener}, path::Path, - str::FromStr, }; use miette::Context as _; @@ -9,15 +8,13 @@ use miette::{miette, IntoDiagnostic}; use tracing::error; use ockam::{Address, Context, NodeBuilder}; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; +use ockam_api::cli_state::CliState; use ockam_api::config::lookup::{InternetAddress, LookupMeta}; use ockam_core::DenyAll; use ockam_multiaddr::proto::{DnsAddr, Ip4, Ip6, Project, Space, Tcp}; -use ockam_multiaddr::{ - proto::{self, Node}, - MultiAddr, Protocol, -}; +use ockam_multiaddr::{proto::Node, MultiAddr, Protocol}; +use crate::error::Error; use crate::Result; pub mod api; @@ -152,43 +149,15 @@ pub fn print_path(p: &Path) -> String { p.to_str().unwrap_or("").to_string() } -/// Parses a node's input string for its name in case it's a `MultiAddr` string. -/// -/// Ensures that the node's name will be returned if the input string is a `MultiAddr` of the `node` type -/// Examples: `n1` or `/node/n1` returns `n1`; `/project/p1` or `/tcp/n2` returns an error message. -pub fn parse_node_name(input: &str) -> Result { - if input.is_empty() { - return Err(miette!("Empty address in node name argument").into()); - } - // Node name was passed as "n1", for example - if !input.contains('/') { - return Ok(input.to_string()); - } - // Input has "/", so we process it as a MultiAddr - let maddr = MultiAddr::from_str(input) - .into_diagnostic() - .wrap_err("Invalid format for node name argument")?; - let err_message = String::from("A node MultiAddr must follow the format /node/"); - if let Some(p) = maddr.iter().next() { - if p.code() == proto::Node::CODE { - let node_name = p - .cast::() - .ok_or(miette!("Failed to parse the 'node' protocol"))? - .to_string(); - if !node_name.is_empty() { - return Ok(node_name); - } - } - } - Err(miette!(err_message).into()) -} - /// Replace the node's name with its address or leave it if it's another type of address. /// /// Example: /// if n1 has address of 127.0.0.1:1234 /// `/node/n1` -> `/ip4/127.0.0.1/tcp/1234` -pub fn process_nodes_multiaddr(addr: &MultiAddr, cli_state: &CliState) -> crate::Result { +pub async fn process_nodes_multiaddr( + addr: &MultiAddr, + cli_state: &CliState, +) -> crate::Result { let mut processed_addr = MultiAddr::default(); for proto in addr.iter() { match proto.code() { @@ -196,9 +165,8 @@ pub fn process_nodes_multiaddr(addr: &MultiAddr, cli_state: &CliState) -> crate: let alias = proto .cast::() .ok_or_else(|| miette!("Invalid node address protocol"))?; - let node_state = cli_state.nodes.get(alias.to_string())?; - let node_setup = node_state.config().setup(); - let addr = node_setup.api_transport()?.maddr()?; + let node_info = cli_state.get_node(&alias).await?; + let addr = node_info.tcp_listener_multi_address()?; processed_addr.try_extend(&addr)? } _ => processed_addr.push_back_value(&proto)?, @@ -210,7 +178,7 @@ pub fn process_nodes_multiaddr(addr: &MultiAddr, cli_state: &CliState) -> crate: /// Go through a multiaddr and remove all instances of /// `/node/` out of it and replaces it with a fully /// qualified address to the target -pub fn clean_nodes_multiaddr( +pub async fn clean_nodes_multiaddr( input: &MultiAddr, cli_state: &CliState, ) -> Result<(MultiAddr, LookupMeta)> { @@ -221,10 +189,14 @@ pub fn clean_nodes_multiaddr( match p.code() { Node::CODE => { let alias = p.cast::().expect("Failed to parse node name"); - let node_state = cli_state.nodes.get(alias.to_string())?; - let node_setup = node_state.config().setup(); - let addr = &node_setup.api_transport()?.addr; - match addr { + let node_info = cli_state.get_node(&alias).await?; + let addr = node_info + .tcp_listener_address() + .ok_or(Error::new_internal_error( + "No transport API has been set on the node", + "", + ))?; + match &addr { InternetAddress::Dns(dns, _) => new_ma.push_back(DnsAddr::new(dns))?, InternetAddress::V4(v4) => new_ma.push_back(Ip4(*v4.ip()))?, InternetAddress::V6(v6) => new_ma.push_back(Ip6(*v6.ip()))?, @@ -269,137 +241,44 @@ pub fn is_tty(s: S) -> bool { s.is_terminal() } -pub fn is_enrolled_guard(cli_state: &CliState, identity_name: Option<&str>) -> miette::Result<()> { - if !cli_state - .identities - .get_or_default(identity_name) - .map(|s| s.is_enrolled()) - .unwrap_or(false) - { - return Err(miette!( - "Please enroll using 'ockam enroll' before using this command" - )); - } - Ok(()) -} - #[cfg(test)] mod tests { - use ockam_api::address::extract_address_value; - use ockam_api::cli_state; - use ockam_api::cli_state::identities::IdentityConfig; - use ockam_api::cli_state::traits::StateDirTrait; - use ockam_api::cli_state::{NodeConfig, VaultConfig}; - use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType}; + use std::str::FromStr; use super::*; - #[test] - fn test_parse_node_name() { - let test_cases = vec![ - ("", Err(())), - ("test", Ok("test")), - ("/test", Err(())), - ("test/", Err(())), - ("/node", Err(())), - ("/node/", Err(())), - ("/node/n1", Ok("n1")), - ("/service/s1", Err(())), - ("/project/p1", Err(())), - ("/randomprotocol/rp1", Err(())), - ("/node/n1/tcp", Err(())), - ("/node/n1/test", Err(())), - ("/node/n1/tcp/22", Ok("n1")), - ]; - for (input, expected) in test_cases { - if let Ok(addr) = expected { - assert_eq!(parse_node_name(input).unwrap(), addr); - } else { - assert!(parse_node_name(input).is_err()); - } - } - } - - #[test] - fn test_extract_address_value() { - let test_cases = vec![ - ("", Err(())), - ("test", Ok("test")), - ("/test", Err(())), - ("test/", Err(())), - ("/node", Err(())), - ("/node/", Err(())), - ("/node/n1", Ok("n1")), - ("/service/s1", Ok("s1")), - ("/project/p1", Ok("p1")), - ("/randomprotocol/rp1", Err(())), - ("/node/n1/tcp", Err(())), - ("/node/n1/test", Err(())), - ("/node/n1/tcp/22", Ok("n1")), - ]; - for (input, expected) in test_cases { - if let Ok(addr) = expected { - assert_eq!(extract_address_value(input).unwrap(), addr); - } else { - assert!(extract_address_value(input).is_err()); - } - } - } - #[ockam_macros::test(crate = "ockam")] async fn test_process_multi_addr(ctx: &mut Context) -> ockam::Result<()> { - let cli_state = CliState::test()?; + let cli_state = CliState::test().await?; - let v_name = cli_state::random_name(); - let v_config = VaultConfig::default(); - cli_state.vaults.create_async(&v_name, v_config).await?; - let v = cli_state.vaults.get(&v_name)?.get().await?; - let idt = cli_state - .get_identities(v) - .await - .unwrap() - .identities_creation() - .create_identity() - .await?; - let idt_config = IdentityConfig::new(&idt).await; - cli_state - .identities - .create(cli_state::random_name(), idt_config)?; + cli_state.create_node("n1").await?; - let n_state = cli_state - .nodes - .create("n1", NodeConfig::try_from(&cli_state)?)?; - n_state.set_setup(&n_state.config().setup_mut().set_api_transport( - CreateTransportJson::new(TransportType::Tcp, TransportMode::Listen, "127.0.0.0:4000")?, - ))?; + cli_state + .set_tcp_listener_address("n1", "127.0.0.0:4000".to_string()) + .await?; let test_cases = vec![ ( - MultiAddr::from_str("/node/n1").unwrap(), + MultiAddr::from_str("/node/n1")?, Ok("/ip4/127.0.0.0/tcp/4000"), ), + (MultiAddr::from_str("/project/p1")?, Ok("/project/p1")), + (MultiAddr::from_str("/service/s1")?, Ok("/service/s1")), ( - MultiAddr::from_str("/project/p1").unwrap(), - Ok("/project/p1"), - ), - ( - MultiAddr::from_str("/service/s1").unwrap(), - Ok("/service/s1"), - ), - ( - MultiAddr::from_str("/project/p1/node/n1/service/echo").unwrap(), + MultiAddr::from_str("/project/p1/node/n1/service/echo")?, Ok("/project/p1/ip4/127.0.0.0/tcp/4000/service/echo"), ), - (MultiAddr::from_str("/node/n2").unwrap(), Err(())), + (MultiAddr::from_str("/node/n2")?, Err(())), ]; for (ma, expected) in test_cases { if let Ok(addr) = expected { let result = process_nodes_multiaddr(&ma, &cli_state) + .await .unwrap() .to_string(); assert_eq!(result, addr); } else { - assert!(process_nodes_multiaddr(&ma, &cli_state).is_err()); + assert!(process_nodes_multiaddr(&ma, &cli_state).await.is_err()); } } diff --git a/implementations/rust/ockam/ockam_command/src/util/parsers.rs b/implementations/rust/ockam/ockam_command/src/util/parsers.rs index 0605d12c48a..ef8a6528aec 100644 --- a/implementations/rust/ockam/ockam_command/src/util/parsers.rs +++ b/implementations/rust/ockam/ockam_command/src/util/parsers.rs @@ -3,9 +3,12 @@ use std::str::FromStr; use miette::miette; -use ockam::identity::Identifier; +use ockam::identity::{Identifier, Identity}; +use ockam_api::config::lookup::InternetAddress; +use ockam_multiaddr::MultiAddr; use ockam_transport_tcp::resolve_peer; +use crate::util::api; use crate::Result; /// Helper function for parsing a socket from user input @@ -24,12 +27,45 @@ pub(crate) fn socket_addr_parser(input: &str) -> Result { .map_err(|e| miette!("cannot parse the address {address} as a socket address: {e}"))?) } -/// Helper fn for parsing an identity from user input by using +/// Helper fn for parsing an identifier from user input by using /// [`ockam_identity::Identifier::from_str()`] pub(crate) fn identity_identifier_parser(input: &str) -> Result { Identifier::from_str(input).map_err(|_| miette!("Invalid identity identifier: {input}").into()) } +/// Helper fn for parsing an identity from user input by using +/// [`ockam_identity::Identity::create()`] +pub(crate) fn identity_parser(input: &str) -> Result { + futures::executor::block_on(async { + Identity::create(input) + .await + .map_err(|_| miette!("Invalid identity: {input}").into()) + }) +} + +/// Helper fn for parsing a MultiAddr from user input by using +/// [`ockam_multiaddr::MultiAddr::from_str()`] +pub(crate) fn multiaddr_parser(input: &str) -> Result { + MultiAddr::from_str(input).map_err(|_| miette!("Invalid multiaddr: {input}").into()) +} + +/// Helper fn for parsing an InternetAddress from user input by using +/// [`InternetAddress::new()`] +pub(crate) fn internet_address_parser(input: &str) -> Result { + InternetAddress::new(input).ok_or_else(|| miette!("Invalid address: {input}").into()) +} + +pub(crate) fn validate_project_name(s: &str) -> Result { + match api::validate_cloud_resource_name(s) { + Ok(_) => Ok(s.to_string()), + Err(_e)=> Err(miette!( + "project name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ + Separators must occur between alphanumeric characters. This implies that separators can't \ + occur at the start or end of the name, nor they can occur in sequence.", + ).into()), + } +} + #[cfg(test)] mod tests { use std::net::Ipv6Addr; diff --git a/implementations/rust/ockam/ockam_command/src/vault/attach_key.rs b/implementations/rust/ockam/ockam_command/src/vault/attach_key.rs new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/vault/attach_key.rs @@ -0,0 +1 @@ + diff --git a/implementations/rust/ockam/ockam_command/src/vault/create.rs b/implementations/rust/ockam/ockam_command/src/vault/create.rs index 41140265882..46fca8ff69c 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/create.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/create.rs @@ -2,9 +2,7 @@ use clap::Args; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::traits::StateDirTrait; use crate::util::node_rpc; use crate::{docs, fmt_info, fmt_ok, CommandGlobalOpts}; @@ -15,8 +13,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create a vault #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct CreateCommand { #[arg(hide_default_value = true, default_value_t = random_name())] @@ -41,23 +39,22 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let CreateCommand { name, aws_kms, .. } = cmd; - let config = cli_state::VaultConfig::new(aws_kms)?; - if opts.state.vaults.is_empty()? { + if opts.state.get_vault_names().await?.is_empty() { opts.terminal.write_line(&fmt_info!( "This is the first vault to be created in this environment. It will be set as the default vault" ))?; } - opts.state - .vaults - .create_async(&name, config.clone()) - .await?; + if cmd.aws_kms { + opts.state.create_kms_vault(&cmd.name).await?; + } else { + opts.state.create_vault(&cmd.name).await?; + } opts.terminal .stdout() - .plain(fmt_ok!("Vault created with name '{name}'!")) - .machine(&name) - .json(serde_json::json!({ "name": &name })) + .plain(fmt_ok!("Vault created with name '{}'!", &cmd.name)) + .machine(&cmd.name) + .json(serde_json::json!({ "name": &cmd.name })) .write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/vault/default.rs b/implementations/rust/ockam/ockam_command/src/vault/default.rs index e67cffad88a..174ccec8ddb 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/default.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/default.rs @@ -1,9 +1,9 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -21,26 +21,26 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { - let DefaultCommand { name } = cmd; - let state = opts.state.vaults; - let v = state.get(&name)?; - // If it exists, warn the user and exit - if state.is_default(v.name())? { - Err(miette!("The vault '{}' is already the default", name)) +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { + // If the vault is already the default vault, warn the user and exit + if opts.state.is_default_vault(&cmd.name).await? { + Err(miette!("The vault '{}' is already the default", &cmd.name)) } // Otherwise, set it as default else { - state.set_default(v.name())?; + opts.state.set_default_vault(&cmd.name).await?; opts.terminal .stdout() - .plain(fmt_ok!("The vault '{name}' is now the default")) - .machine(&name) - .json(serde_json::json!({ "name": name })) + .plain(fmt_ok!("The vault '{}' is now the default", &cmd.name)) + .machine(&cmd.name) + .json(serde_json::json!({ "name": cmd.name })) .write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/vault/delete.rs b/implementations/rust/ockam/ockam_command/src/vault/delete.rs index 722e633cd9e..d6def7f42b9 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/delete.rs @@ -2,7 +2,6 @@ use clap::Args; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state::traits::StateDirTrait; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; @@ -45,8 +44,7 @@ async fn run_impl( .terminal .confirmed_with_flag_or_prompt(yes, "Are you sure you want to delete this vault?")? { - opts.state.vaults.get(&name)?; - opts.state.vaults.delete(&name)?; + opts.state.delete_vault(&name).await?; opts.terminal .stdout() .plain(fmt_ok!("Vault with name '{name}' has been deleted")) diff --git a/implementations/rust/ockam/ockam_command/src/vault/list.rs b/implementations/rust/ockam/ockam_command/src/vault/list.rs index 01885fd407f..7726b3368c9 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/list.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/list.rs @@ -1,9 +1,9 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::vault::util::VaultOutput; use crate::{docs, CommandGlobalOpts}; use clap::Args; use miette::IntoDiagnostic; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -20,17 +20,17 @@ pub struct ListCommand; impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts)); + node_rpc(run_impl, opts); } } -fn run_impl(opts: CommandGlobalOpts) -> miette::Result<()> { +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { let vaults = opts .state - .vaults - .list()? + .get_named_vaults() + .await? .into_iter() - .map(|v| VaultOutput::new(&v, opts.state.vaults.is_default(v.name()).unwrap_or(false))) + .map(|v| VaultOutput::new(&v)) .collect::>(); let plain = opts .terminal diff --git a/implementations/rust/ockam/ockam_command/src/vault/mod.rs b/implementations/rust/ockam/ockam_command/src/vault/mod.rs index a04813a619a..85ea75b8528 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/mod.rs @@ -13,17 +13,15 @@ use crate::vault::show::ShowCommand; use crate::{docs, CommandGlobalOpts}; use clap::{Args, Subcommand}; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::CliState; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); /// Manage Vaults #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - subcommand_required = true, - long_about = docs::about(LONG_ABOUT), +arg_required_else_help = true, +subcommand_required = true, +long_about = docs::about(LONG_ABOUT), )] pub struct VaultCommand { #[command(subcommand)] @@ -50,10 +48,3 @@ impl VaultCommand { } } } - -pub fn default_vault_name(cli_state: &CliState) -> String { - cli_state - .vaults - .default() - .map_or("default".to_string(), |v| v.name().to_string()) -} diff --git a/implementations/rust/ockam/ockam_command/src/vault/show.rs b/implementations/rust/ockam/ockam_command/src/vault/show.rs index 0c8c6f360c2..5dde4cf10d5 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/show.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/show.rs @@ -2,7 +2,6 @@ use clap::Args; use console::Term; use miette::IntoDiagnostic; -use ockam_api::cli_state::traits::StateDirTrait; use ockam_node::Context; use crate::output::Output; @@ -18,9 +17,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show the details of a vault #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { /// Name of the vault @@ -71,23 +70,27 @@ impl ShowCommandTui for ShowTui { Ok(self .vault_name .clone() - .unwrap_or(self.opts.state.vaults.default()?.name().to_string())) + .unwrap_or(self.opts.state.get_default_vault_name().await?)) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.vaults.list_items_names()?) + Ok(self + .opts + .state + .get_named_vaults() + .await? + .iter() + .map(|v| v.name()) + .collect()) } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let vault = VaultOutput::new( - &self.opts.state.vaults.get(item_name)?, - self.opts.state.vaults.is_default(item_name)?, - ); + let vault = VaultOutput::new(&self.opts.state.get_named_vault(item_name).await?); self.terminal() .stdout() .plain(vault.output()?) .json(serde_json::to_string(&vault).into_diagnostic()?) - .machine(&vault.name) + .machine(vault.name()) .write_line()?; Ok(()) } @@ -96,16 +99,11 @@ impl ShowCommandTui for ShowTui { let filtered = self .opts .state - .vaults - .list()? + .get_named_vaults() + .await? .into_iter() - .map(|v| { - VaultOutput::new( - &v, - self.opts.state.vaults.is_default(v.name()).unwrap_or(false), - ) - }) - .filter(|v| items_names.contains(&v.name)) + .map(|v| VaultOutput::new(&v)) + .filter(|v| items_names.contains(&v.name())) .collect::>(); let plain = self .terminal() diff --git a/implementations/rust/ockam/ockam_command/src/vault/util.rs b/implementations/rust/ockam/ockam_command/src/vault/util.rs index e9ee412fb1a..cf17c6039ff 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/util.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/util.rs @@ -2,24 +2,23 @@ use crate::output::Output; use crate::OckamColor; use colorful::Colorful; use indoc::formatdoc; -use ockam_api::cli_state::{StateItemTrait, VaultConfig, VaultState}; +use ockam_api::identity::NamedVault; #[derive(serde::Serialize)] pub struct VaultOutput { - pub(crate) name: String, - #[serde(flatten)] - config: VaultConfig, - is_default: bool, + vault: NamedVault, } impl VaultOutput { - pub fn new(state: &VaultState, is_default: bool) -> Self { + pub fn new(vault: &NamedVault) -> Self { Self { - name: state.name().to_string(), - config: state.config().clone(), - is_default, + vault: vault.clone(), } } + + pub fn name(&self) -> String { + self.vault.name().clone() + } } impl Output for VaultOutput { @@ -31,11 +30,16 @@ impl Output for VaultOutput { Type: {vault_type} "#, name = self - .name + .vault + .name() .to_string() .color(OckamColor::PrimaryResource.color()), - default = if self.is_default { "(default)" } else { "" }, - vault_type = match self.config.is_aws() { + default = if self.vault.is_default() { + "(default)" + } else { + "" + }, + vault_type = match self.vault.is_kms() { true => "AWS KMS", false => "OCKAM", } @@ -49,11 +53,16 @@ impl Output for VaultOutput { r#"Name: {name} {default} Type: {vault_type}"#, name = self - .name + .vault + .name() .to_string() .color(OckamColor::PrimaryResource.color()), - default = if self.is_default { "(default)" } else { "" }, - vault_type = match self.config.is_aws() { + default = if self.vault.is_default() { + "(default)" + } else { + "" + }, + vault_type = match self.vault.is_kms() { true => "AWS KMS", false => "OCKAM", } diff --git a/implementations/rust/ockam/ockam_command/src/worker/list.rs b/implementations/rust/ockam/ockam_command/src/worker/list.rs index 459e50db592..d1f58fd08d6 100644 --- a/implementations/rust/ockam/ockam_command/src/worker/list.rs +++ b/implementations/rust/ockam/ockam_command/src/worker/list.rs @@ -1,16 +1,12 @@ use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::workers::{WorkerList, WorkerStatus}; use ockam_api::nodes::BackgroundNode; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::output::Output; use crate::terminal::OckamColor; use crate::util::{api, node_rpc}; @@ -23,9 +19,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List workers on a node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { /// Node at which to lookup workers @@ -35,7 +31,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(run_impl, (opts, self)) } } @@ -44,14 +39,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = extract_address_value(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let is_finished: Mutex = Mutex::new(false); let get_workers = async { @@ -62,9 +50,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing Workers on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -75,8 +61,8 @@ async fn run_impl( let list = opts.terminal.build_list( &workers.list, - &format!("Workers on {node_name}"), - &format!("No workers found on {node_name}."), + &format!("Workers on {}", node.node_name()), + &format!("No workers found on {}.", node.node_name()), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/tests/bats/authority.bats b/implementations/rust/ockam/ockam_command/tests/bats/authority.bats index a140564834b..d182dbfb9ed 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/authority.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/authority.bats @@ -13,6 +13,16 @@ teardown() { } # ===== TESTS +@test "authority - an authority node must be shown as UP even if its tcp listener cannot be accessed" { + port="$(random_port)" + + run_success "$OCKAM" identity create authority + authority_identity_full=$($OCKAM identity show --full --encoding hex authority) + trusted="{}" + run_success "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" + run_success "$OCKAM" node show authority + assert_output --partial "\"is_up\": true" +} @test "authority - standalone authority, enrollers, members" { port="$(random_port)" @@ -36,50 +46,42 @@ teardown() { # Start the authority node. We pass a set of pre trusted-identities containing m1' identity identifier # For the first test we start the node with no direct authentication service nor token enrollment trusted="{\"$m1_identifier\": {\"sample_attr\": \"sample_val\", \"project_id\" : \"1\", \"trust_context_id\" : \"1\"}, \"$enroller_identifier\": {\"project_id\": \"1\", \"trust_context_id\": \"1\", \"ockam-role\": \"enroller\"}}" - run "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" --no-direct-authentication --no-token-enrollment - assert_success + run_success "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" --no-direct-authentication --no-token-enrollment sleep 1 # wait for authority to start TCP listener - PROJECT_JSON_PATH="$OCKAM_HOME/project-authority.json" PROJECT_NAME="default" - echo "{\"id\": \"1\", - \"name\" : \"$PROJECT_NAME\", - \"identity\" : \"I6c20e814b56579306f55c64e8747e6c1b4a53d9a\", - \"access_route\" : \"/dnsaddr/127.0.0.1/tcp/4000/service/api\", - \"authority_access_route\" : \"/dnsaddr/127.0.0.1/tcp/$port/service/api\", - \"authority_identity\" : \"$authority_identity_full\"}" >"$PROJECT_JSON_PATH" + run_success bash -c "$OCKAM project import \ + --project-name $PROJECT_NAME \ + --project-id 1 \ + --project-identifier I6c20e814b56579306f55c64e8747e6c1b4a53d9a \ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \ + --authority-identity $authority_identity_full \ + --authority-access-route /dnsaddr/127.0.0.1/tcp/$port/service/api" # m1 is a member (its on the set of pre-trusted identifiers) so it can get it's own credential - run "$OCKAM" project enroll --project-path "$PROJECT_JSON_PATH" --identity m1 - assert_success + run_success "$OCKAM" project enroll --project "$PROJECT_NAME" --identity m1 assert_output --partial "sample_val" echo "$trusted" >"$OCKAM_HOME/trusted-anchors.json" # Restart the authority node with a trusted identities file and check that m1 can still enroll - run "$OCKAM" node delete authority --yes - run "$OCKAM" authority create --tcp-listener-address=127.0.0.1:$port --project-identifier 1 --reload-from-trusted-identities-file "$OCKAM_HOME/trusted-anchors.json" - assert_success + run_success "$OCKAM" node delete authority --yes + run_success "$OCKAM" authority create --tcp-listener-address=127.0.0.1:$port --project-identifier 1 --reload-from-trusted-identities-file "$OCKAM_HOME/trusted-anchors.json" sleep 1 # wait for authority to start TCP listener - run "$OCKAM" project ticket --identity enroller --project "$PROJECT_NAME" --member $m2_identifier --attribute sample_attr=m2_member - assert_success + run_success "$OCKAM" project ticket --identity enroller --project "$PROJECT_NAME" --member $m2_identifier --attribute sample_attr=m2_member - run "$OCKAM" project enroll --force --project "$PROJECT_NAME" --identity m2 - assert_success + run_success "$OCKAM" project enroll --force --project "$PROJECT_NAME" --identity m2 assert_output --partial "m2_member" token1=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --attribute sample_attr=m3_member) - run "$OCKAM" project enroll --force $token1 --identity m3 - assert_success + run_success "$OCKAM" project enroll --force $token1 --identity m3 assert_output --partial "m3_member" token2=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --usage-count 2 --attribute sample_attr=members_group) - run "$OCKAM" project enroll --force $token2 --identity m4 - assert_success + run_success "$OCKAM" project enroll --force $token2 --identity m4 assert_output --partial "members_group" - run "$OCKAM" project enroll --force $token2 --identity m5 - assert_success + run_success "$OCKAM" project enroll --force $token2 --identity m5 assert_output --partial "members_group" run "$OCKAM" project enroll --force $token2 --identity m6 @@ -99,27 +101,26 @@ teardown() { # Start the authority node. trusted="{\"$enroller_identifier\": {\"project_id\": \"1\", \"trust_context_id\": \"1\", \"ockam-role\": \"enroller\"}}" - run "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" - assert_success + run_success "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" sleep 1 # wait for authority to start TCP listener - PROJECT_JSON_PATH="$OCKAM_HOME/project-authority.json" - echo "{\"id\": \"1\", - \"name\" : \"default\", - \"identity\" : \"I6c20e814b56579306f55c64e8747e6c1b4a53d9a\", - \"access_route\" : \"/dnsaddr/127.0.0.1/tcp/4000/service/api\", - \"authority_access_route\" : \"/dnsaddr/127.0.0.1/tcp/$port/service/api\", - \"authority_identity\" : \"$authority_identity_full\"}" >"$PROJECT_JSON_PATH" + PROJECT_NAME="default" + run_success bash -c "$OCKAM project import \ + --project-name $PROJECT_NAME \ + --project-id 1 \ + --project-identifier I6c20e814b56579306f55c64e8747e6c1b4a53d9a \ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \ + --authority-identity $authority_identity_full \ + --authority-access-route /dnsaddr/127.0.0.1/tcp/$port/service/api" # Enrollment ticket expired by the time it's used - token=$($OCKAM project ticket --identity enroller --project-path "$PROJECT_JSON_PATH" --attribute sample_attr=m3_member --expires-in 1s) + token=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --attribute sample_attr=m3_member --expires-in 1s) sleep 2 run "$OCKAM" project enroll $token --identity m3 assert_failure # Enrollment ticket with enough ttl - token=$($OCKAM project ticket --identity enroller --project-path "$PROJECT_JSON_PATH" --attribute sample_attr=m3_member --expires-in 30s) - run "$OCKAM" project enroll $token --identity m3 - assert_success + token=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --attribute sample_attr=m3_member --expires-in 30s) + run_success "$OCKAM" project enroll $token --identity m3 assert_output --partial "m3_member" } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats b/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats index 255eaa9a0bd..259fbe745b0 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats @@ -177,13 +177,11 @@ teardown() { @test "elastic encrypted relays" { setup_orchestrator_test - "$OCKAM" project information --output json >/${BATS_TEST_TMPDIR}/project.json - a="$(random_str)" b="$(random_str)" - run_success "$OCKAM" node create "$a" --project-path /${BATS_TEST_TMPDIR}/project.json - run_success "$OCKAM" node create "$b" --project-path /${BATS_TEST_TMPDIR}/project.json + run_success "$OCKAM" node create "$a" --project $PROJECT_NAME + run_success "$OCKAM" node create "$b" --project $PROJECT_NAME run_success "$OCKAM" relay create "$b" --at /project/default --to "/node/$a" output=$("$OCKAM" secure-channel create --from "$a" --to "/project/default/service/forward_to_$b/service/api" | @@ -288,13 +286,11 @@ teardown() { @test "managed authorities" { setup_orchestrator_test - "$OCKAM" project information --output json >/${BATS_TEST_TMPDIR}/project.json - a="$(random_str)" b="$(random_str)" - run_success "$OCKAM" node create "$a" --project-path /${BATS_TEST_TMPDIR}/project.json - run_success "$OCKAM" node create "$b" --project-path /${BATS_TEST_TMPDIR}/project.json + run_success "$OCKAM" node create "$a" --project $PROJECT_NAME + run_success "$OCKAM" node create "$b" --project $PROJECT_NAME run_success "$OCKAM" relay create "$b" --at /project/default --to "/node/$a/service/forward_to_$b" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash b/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash index 9f14f70407f..c8ce8d06af2 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash +++ b/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash @@ -84,7 +84,7 @@ teardown_home_dir() { # Copy the CLI directory to $HOME/.bats-tests so it can be inspected. # For some reason, if the directory is moved, the teardown function gets stuck. echo "Failed test dir: $OCKAM_HOME" >&3 - cp -a "$OCKAM_HOME" "$HOME/.bats-tests" + cp -r "$OCKAM_HOME/." "$HOME/.bats-tests" fi run $OCKAM node delete --all --force --yes run $OCKAM reset -y diff --git a/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash b/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash index 38371f3341e..21168e0a828 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash +++ b/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash @@ -16,34 +16,7 @@ function skip_if_orchestrator_tests_not_enabled() { function copy_local_orchestrator_data() { if [ ! -z "${ORCHESTRATOR_TESTS}" ]; then - cp -a $OCKAM_HOME_BASE $OCKAM_HOME - export PROJECT_JSON_PATH="$OCKAM_HOME/project.json" + cp -r $OCKAM_HOME_BASE/. $OCKAM_HOME export PROJECT_NAME="default" - cp $OCKAM_HOME/projects/default.json $PROJECT_JSON_PATH - fi -} - -function fetch_orchestrator_data() { - copy_local_orchestrator_data - max_retries=5 - i=0 - while [[ $i -lt $max_retries ]]; do - run bash -c "$OCKAM project information --output json >$PROJECT_JSON_PATH" - # if status is not 0, retry - if [ $status -ne 0 ]; then - sleep 5 - ((i++)) - continue - fi - # if file is empty, exit with error - if [ ! -s "$PROJECT_JSON_PATH" ]; then - echo "Project information is empty" >&3 - exit 1 - fi - break - done - if [ $i -eq $max_retries ]; then - echo "Failed to fetch project information" >&3 - exit 1 fi } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats b/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats index bab60178c3c..cc0e5a7a80a 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats @@ -18,7 +18,7 @@ force_kill_node() { max_retries=5 i=0 while [[ $i -lt $max_retries ]]; do - pid="$(cat $OCKAM_HOME/nodes/$1/pid)" + pid="$(ps | grep -e 'ockam.*node create' | awk '{print $1}' | head -1)" run kill -9 $pid # Killing a node created without `-f` leaves the # process in a defunct state when running within Docker. diff --git a/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats b/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats index 626da01002c..bcae8be500a 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats @@ -24,13 +24,13 @@ teardown() { @test "portals - create an inlet/outlet pair, a relay in an orchestrator project and move tcp traffic through it" { port="$(random_port)" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 fwd="$(random_str)" run_success "$OCKAM" relay create "$fwd" --to /node/blue - run_success "$OCKAM" node create green --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create green --project "$PROJECT_NAME" run_success bash -c "$OCKAM secure-channel create --from /node/green --to /project/default/service/forward_to_$fwd/service/api \ | $OCKAM tcp-inlet create --at /node/green --from 127.0.0.1:$port --to -/service/outlet" @@ -38,10 +38,9 @@ teardown() { } @test "portals - create an inlet using only default arguments, an outlet, a relay in an orchestrator project and move tcp traffic through it" { - port="$(random_port)" - - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create blue run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 + run_success "$OCKAM" relay create --to /node/blue addr=$($OCKAM tcp-inlet create) @@ -51,13 +50,13 @@ teardown() { @test "portals - create an inlet (with implicit secure channel creation), an outlet, a relay in an orchestrator project and move tcp traffic through it" { port="$(random_port)" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 fwd="$(random_str)" run_success "$OCKAM" relay create "$fwd" --to /node/blue - run_success "$OCKAM" node create green --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create green --project "$PROJECT_NAME" run_success "$OCKAM" tcp-inlet create --at /node/green --from "127.0.0.1:$port" --to "/project/default/service/forward_to_$fwd/secure/api/service/outlet" run_success curl --fail --head --max-time 10 "127.0.0.1:$port" @@ -70,14 +69,15 @@ teardown() { # Setup nodes from a non-enrolled environment setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue green_identifier=$($OCKAM identity show green) blue_identifier=$($OCKAM identity show blue) - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue # Green isn't enrolled as project member OCKAM_HOME=$ENROLLED_OCKAM_HOME @@ -90,7 +90,7 @@ teardown() { run_success "$OCKAM" relay create "$fwd" --to /node/blue assert_output --partial "forward_to_$fwd" - run_success bash -c "$OCKAM secure-channel create --from /node/green --to /project/default/service/forward_to_$fwd/service/api \ + run_success bash -c "$OCKAM secure-channel create --from /node/green --identity green --to /project/default/service/forward_to_$fwd/service/api \ | $OCKAM tcp-inlet create --at /node/green --from 127.0.0.1:$port --to -/service/outlet" # Green can't establish secure channel with blue, because it didn't exchange credential with it. @@ -102,14 +102,15 @@ teardown() { ENROLLED_OCKAM_HOME=$OCKAM_HOME setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue green_identifier=$($OCKAM identity show green) blue_identifier=$($OCKAM identity show blue) - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue # Green isn't enrolled as project member OCKAM_HOME=$ENROLLED_OCKAM_HOME @@ -131,14 +132,15 @@ teardown() { ENROLLED_OCKAM_HOME=$OCKAM_HOME setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue green_identifier=$($OCKAM identity show green) blue_identifier=$($OCKAM identity show blue) - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue OCKAM_HOME=$ENROLLED_OCKAM_HOME run_success "$OCKAM" project ticket --member "$blue_identifier" --attribute role=member @@ -166,16 +168,17 @@ teardown() { setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue run_success "$OCKAM" project enroll $green_token --identity green - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green run_success "$OCKAM" policy create --at green --resource tcp-inlet --expression '(= subject.app "app1")' run_success "$OCKAM" project enroll $blue_token --identity blue - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue run_success "$OCKAM" policy create --at blue --resource tcp-outlet --expression '(= subject.app "app1")' run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 @@ -209,21 +212,22 @@ teardown() { run_success curl --head --retry-connrefused --retry 20 --retry-max-time 20 --max-time 1 "127.0.0.1:$port" } -@test "portals - local inlet and outlet passing trhough a relay, removing and re-creating the outlet" { +@test "portals - local inlet and outlet passing through a relay, removing and re-creating the outlet" { port="$(random_port)" node_port="$(random_port)" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" --tcp-listener-address "127.0.0.1:$node_port" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --tcp-listener-address "127.0.0.1:$node_port" run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 + run_success "$OCKAM" relay create --to /node/blue - run_success "$OCKAM" node create green --project "$PROJECT_JSON_PATH" - run_success "$OCKAM" tcp-inlet create --at /node/green --from "127.0.0.1:$port" --to /project/default/service/forward_to_default/secure/api/service/outlet + run_success "$OCKAM" node create green --project "$PROJECT_NAME" + run_success "$OCKAM" tcp-inlet create --at /node/green --from "127.0.0.1:$port" --to "/project/default/service/forward_to_default/secure/api/service/outlet" run_success curl --fail --head --max-time 10 "127.0.0.1:$port" $OCKAM node delete blue --yes run_failure curl --fail --head --max-time 2 "127.0.0.1:$port" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" --tcp-listener-address "127.0.0.1:$node_port" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --tcp-listener-address "127.0.0.1:$node_port" run_success "$OCKAM" relay create --to /node/blue run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 run_success curl --head --retry-connrefused --retry 50 --max-time 1 "127.0.0.1:$port" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/projects.bats b/implementations/rust/ockam/ockam_command/tests/bats/projects.bats new file mode 100644 index 00000000000..d2fa722de64 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/tests/bats/projects.bats @@ -0,0 +1,27 @@ +#!/bin/bash + +# ===== SETUP + +setup() { + load load/base.bash + load_bats_ext + setup_home_dir +} + +teardown() { + teardown_home_dir +} + +# ===== TESTS + +@test "projects - a project can be imported" { + run_success bash -c "$OCKAM project import \ + --project-name awesome \ + --project-id 1 \ + --project-identifier I6c20e814b56579306f55c64e8747e6c1b4a53d9a \ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \ + --authority-identity 81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805 \ + --authority-access-route /dnsaddr/127.0.0.1/tcp/5000/service/api" + + assert_output --partial "Successfully imported project awesome" +} diff --git a/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats b/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats index 07556e2df33..b7ce84b96a9 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats @@ -39,8 +39,10 @@ teardown() { @test "projects - enrollment" { ENROLLED_OCKAM_HOME=$OCKAM_HOME + # Change to a new home directory where there are no enrolled identities setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green green_identifier=$($OCKAM identity show green) @@ -49,7 +51,7 @@ teardown() { blue_identifier=$($OCKAM identity show blue) # They haven't been added by enroller yet - run_failure "$OCKAM" project enroll --identity green --project-path "$PROJECT_JSON_PATH" + run_failure "$OCKAM" project enroll --identity green OCKAM_HOME=$ENROLLED_OCKAM_HOME $OCKAM project ticket --member "$green_identifier" --attribute role=member @@ -57,7 +59,7 @@ teardown() { OCKAM_HOME=$NON_ENROLLED_OCKAM_HOME # Green' identity was added by enroller - run_success "$OCKAM" project enroll --identity green --project-path "$PROJECT_JSON_PATH" + run_success "$OCKAM" project enroll --identity green assert_output --partial "$green_identifier" # For blue, we use an enrollment token generated by enroller @@ -72,17 +74,18 @@ teardown() { # Change to a new home directory where there are no enrolled identities setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME # Create a named default identity run_success "$OCKAM" identity create green green_identifier=$($OCKAM identity show green) # Create node for the non-enrolled identity using the exported project information - run_success "$OCKAM" node create green --project-path "$ENROLLED_OCKAM_HOME/project.json" + run_success "$OCKAM" node create green --project $PROJECT_NAME # Node can't create relay as it isn't a member fwd=$(random_str) - run_failure "$OCKAM" relay create "$fwd" + run_failure "$OCKAM" relay create --identity green "$fwd" # Add node as a member OCKAM_HOME=$ENROLLED_OCKAM_HOME @@ -99,9 +102,9 @@ teardown() { # than the admin?. If we pass project' address directly (instead of /project/ thing), would # it present credential? would read authority info from project.json? - run_success "$OCKAM" project information --output json >/tmp/project.json - + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam + run_success "$OCKAM" identity create m2 run_success "$OCKAM" identity create m1 m1_identifier=$($OCKAM identity show m1) @@ -111,14 +114,14 @@ teardown() { export OCKAM_HOME=/tmp/ockam # m1' identity was added by enroller - run_success $OCKAM project enroll --identity m1 --project-path "$PROJECT_JSON_PATH" + run_success $OCKAM project enroll --identity m1 --project $PROJECT_NAME # m1 is a member, must be able to contact the project' service - run_success $OCKAM message send --timeout 5 --identity m1 --project-path "$PROJECT_JSON_PATH" --to /project/default/service/echo hello + run_success $OCKAM message send --timeout 5 --identity m1 --project $PROJECT_NAME --to /project/default/service/echo hello assert_output "hello" # m2 is not a member, must not be able to contact the project' service - run_failure $OCKAM message send --timeout 5 --identity m2 --project-path "$PROJECT_JSON_PATH" --to /project/default/service/echo hello + run_failure $OCKAM message send --timeout 5 --identity m2 --project $PROJECT_NAME --to /project/default/service/echo hello } @test "projects - list addons" { @@ -158,9 +161,9 @@ teardown() { sleep 30 #FIXME workaround, project not yet ready after configuring addon - run_success "$OCKAM" project information default --output json >/tmp/project.json - + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam + run_success "$OCKAM" identity create m1 run_success "$OCKAM" identity create m2 run_success "$OCKAM" identity create m3 @@ -172,21 +175,22 @@ teardown() { run_success "$OCKAM" project ticket --member $m1_identifier --attribute service=sensor run_success "$OCKAM" project ticket --member $m2_identifier --attribute service=web + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam # m1 and m2 identity was added by enroller - run_success "$OCKAM" project enroll --identity m1 --project-path "$PROJECT_JSON_PATH" + run_success "$OCKAM" project enroll --identity m1 --project $PROJECT_NAME assert_output --partial $green_identifier - run_success "$OCKAM" project enroll --identity m2 --project-path "$PROJECT_JSON_PATH" + run_success "$OCKAM" project enroll --identity m2 --project $PROJECT_NAME assert_output --partial $green_identifier # m1 and m2 can use the lease manager - run_success "$OCKAM" lease --identity m1 --project-path "$PROJECT_JSON_PATH" create - run_success "$OCKAM" lease --identity m2 --project-path "$PROJECT_JSON_PATH" create + run_success "$OCKAM" lease --identity m1 --project $PROJECT_NAME create + run_success "$OCKAM" lease --identity m2 --project $PROJECT_NAME create # m3 can't - run_success "$OCKAM" lease --identity m3 --project-path "$PROJECT_JSON_PATH" create + run_success "$OCKAM" lease --identity m3 --project $PROJECT_NAME create assert_failure unset OCKAM_HOME @@ -194,10 +198,12 @@ teardown() { sleep 30 #FIXME workaround, project not yet ready after configuring addon + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam + # m1 can use the lease manager (it has a service=sensor attribute attested by authority) - run_success "$OCKAM" lease --identity m1 --project-path "$PROJECT_JSON_PATH" create + run_success "$OCKAM" lease --identity m1 --project $PROJECT_NAME create # m2 can't use the lease manager now (it doesn't have a service=sensor attribute attested by authority) - run_failure "$OCKAM" lease --identity m2 --project-path "$PROJECT_JSON_PATH" create + run_failure "$OCKAM" lease --identity m2 --project $PROJECT_NAME create } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats b/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats index 5bd5848b7c1..3c6e31eb74f 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats @@ -54,15 +54,12 @@ teardown() { @test "trust context - trust context with an id only; ABAC rules are applied" { run_success "$OCKAM" identity create m1 - echo "{ - \"id\": \"1\" - }" >"$OCKAM_HOME/trust_context.json" - m1_identifier=$(run_success "$OCKAM" identity show m1) trusted="{\"$m1_identifier\": {\"sample_attr\": \"sample_val\", \"project_id\" : \"1\", \"trust_context_id\" : \"1\"}}" run_success "$OCKAM" node create n1 --identity m1 - run_success "$OCKAM" node create n2 --trust-context "$OCKAM_HOME/trust_context.json" --trusted-identities "$trusted" + run_success "$OCKAM" trust-context create default --id 1 + run_success "$OCKAM" node create n2 --trust-context default --trusted-identities "$trusted" run_success bash -c "$OCKAM secure-channel create --from /node/n1 --to /node/n2/service/api \ | $OCKAM message send hello --from /node/n1 --to -/service/echo" run_failure "$OCKAM" message send hello --timeout 2 --from /node/n1 --to /node/n2/service/echo @@ -113,8 +110,9 @@ teardown() { assert_output $msg run_success "$OCKAM" node delete alice --yes - echo "{\"id\": \"$authority_id\"}" >"$OCKAM_HOME/alice-trust-context.json" - run_success "$OCKAM" node create alice --tcp-listener-address 127.0.0.1:$port --identity alice --trust-context "$OCKAM_HOME/alice-trust-context.json" + run_success "$OCKAM" trust-context create alice-trust-context --id "$authority_id" + + run_success "$OCKAM" node create alice --tcp-listener-address 127.0.0.1:$port --identity alice --trust-context alice-trust-context run_failure "$OCKAM" message send --timeout 2 --identity bob --to /dnsaddr/127.0.0.1/tcp/$port/secure/api/service/echo --trust-context bob-trust-context $msg } @@ -135,26 +133,20 @@ teardown() { assert_success sleep 1 - echo "{\"id\": \"test-context\", - \"authority\" : { - \"identity\" : \"$authority_identity\", - \"own_credential\" :{ - \"FromCredentialIssuer\" : { - \"identity\": \"$authority_identity\", - \"multiaddr\" : \"/dnsaddr/127.0.0.1/tcp/$auth_port/service/api\" }}}}" >"$OCKAM_HOME/trust_context.json" - - run_success "$OCKAM" node create --identity alice --tcp-listener-address 127.0.0.1:$node_port --trust-context "$OCKAM_HOME/trust_context.json" + authority_route="/dnsaddr/127.0.0.1/tcp/$auth_port/service/api" + run_success "$OCKAM" trust-context create test-context --id test-context --authority-identity $authority_identity --authority-route $authority_route + run_success "$OCKAM" node create --identity alice --tcp-listener-address 127.0.0.1:$node_port --trust-context test-context sleep 1 # send a message to alice using the trust context msg=$(random_str) - run_success "$OCKAM" message send --identity bob --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context "$OCKAM_HOME/trust_context.json" $msg + run_success "$OCKAM" message send --timeout 2 --identity bob --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context test-context $msg assert_output "$msg" # send a message to authority node echo service to make sure we can use it as a healthcheck endpoint run_success "$OCKAM" message send --timeout 2 --identity bob --to "/dnsaddr/127.0.0.1/tcp/$auth_port/secure/api/service/echo" $msg assert_output "$msg" - run_failure "$OCKAM" message send --timeout 2 --identity attacker --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context "$OCKAM_HOME/trust_context.json" $msg + run_failure "$OCKAM" message send --timeout 2 --identity attacker --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context test-context $msg run_failure "$OCKAM" message send --timeout 2 --identity attacker --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context $msg } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats b/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats index 575d31120f3..2959a67b5cd 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats @@ -67,40 +67,46 @@ teardown() { @test "use-case - abac" { skip_if_orchestrator_tests_not_enabled copy_local_orchestrator_data + export OCKAM_BASE_HOME=$OCKAM_HOME port_1=9002 port_2=9003 # Administrator - ADMIN_OCKAM_HOME=$OCKAM_HOME + setup_home_dir + cp -r $OCKAM_BASE_HOME/. $OCKAM_HOME + cp1_token=$($OCKAM project ticket --attribute component=control) ep1_token=$($OCKAM project ticket --attribute component=edge) x_token=$($OCKAM project ticket --attribute component=x) # Control plane setup_home_dir - CONTROL_OCKAM_HOME=$OCKAM_HOME + cp -r $OCKAM_BASE_HOME/. $OCKAM_HOME + fwd=$(random_str) $OCKAM identity create control_identity - $OCKAM project enroll $cp1_token --project-path "$PROJECT_JSON_PATH" --identity control_identity - $OCKAM node create control_plane1 --project-path "$PROJECT_JSON_PATH" --identity control_identity + $OCKAM project enroll $cp1_token --project "$PROJECT_NAME" --identity control_identity + $OCKAM node create control_plane1 --project "$PROJECT_NAME" --identity control_identity $OCKAM policy create --at control_plane1 --resource tcp-outlet --expression '(= subject.component "edge")' $OCKAM tcp-outlet create --at /node/control_plane1 --to 127.0.0.1:5000 run_success "$OCKAM" relay create "$fwd" --to /node/control_plane1 # Edge plane setup_home_dir + cp -r $OCKAM_BASE_HOME/. $OCKAM_HOME + $OCKAM identity create edge_identity - $OCKAM project enroll $ep1_token --project-path "$PROJECT_JSON_PATH" --identity edge_identity - $OCKAM node create edge_plane1 --project-path "$PROJECT_JSON_PATH" --identity edge_identity + $OCKAM project enroll $ep1_token --project "$PROJECT_NAME" --identity edge_identity + $OCKAM node create edge_plane1 --project "$PROJECT_NAME" --identity edge_identity $OCKAM policy create --at edge_plane1 --resource tcp-inlet --expression '(= subject.component "control")' $OCKAM tcp-inlet create --at /node/edge_plane1 --from "127.0.0.1:$port_1" --to "/project/default/service/forward_to_$fwd/secure/api/service/outlet" run_success curl --fail --head --max-time 5 "127.0.0.1:$port_1" ## The following is denied $OCKAM identity create x_identity - $OCKAM project enroll $x_token --project-path "$PROJECT_JSON_PATH" --identity x_identity - $OCKAM node create x --project-path "$PROJECT_JSON_PATH" --identity x_identity + $OCKAM project enroll $x_token --project "$PROJECT_NAME" --identity x_identity + $OCKAM node create x --project "$PROJECT_NAME" --identity x_identity $OCKAM policy create --at x --resource tcp-inlet --expression '(= subject.component "control")' $OCKAM tcp-inlet create --at /node/x --from "127.0.0.1:$port_2" --to "/project/default/service/forward_to_$fwd/secure/api/service/outlet" run curl --fail --head --max-time 5 "127.0.0.1:$port_2" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/vault.bats b/implementations/rust/ockam/ockam_command/tests/bats/vault.bats index a44dc8c69d4..657ca071740 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/vault.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/vault.bats @@ -20,7 +20,7 @@ teardown() { run_success "$OCKAM" vault show "${v1}" --output json assert_output --partial "\"name\":\"${v1}\"" - assert_output --partial "\"aws_kms\":false" + assert_output --partial "\"is_kms\":false" run_success "$OCKAM" vault list --output json assert_output --partial "\"is_default\":true" @@ -30,12 +30,12 @@ teardown() { run_success "$OCKAM" vault show "${v2}" --output json assert_output --partial "\"name\":\"${v2}\"" - assert_output --partial "\"aws_kms\":false" + assert_output --partial "\"is_kms\":false" run_success "$OCKAM" vault list --output json assert_output --partial "\"name\":\"${v1}\"" assert_output --partial "\"name\":\"${v2}\"" - assert_output --partial "\"aws_kms\":false" + assert_output --partial "\"is_kms\":false" assert_output --partial "\"is_default\":true" assert_output --partial "\"is_default\":false" } diff --git a/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket b/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket index 8933d082b2d..bce70aa5eee 100644 --- a/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket +++ b/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket @@ -1 +1 @@ -7b226f6e655f74696d655f636f6465223a2263306633656163373862383238633665346564316263393935626261626431653432373436653332343137373332373564303230313165326137326263623133222c2270726f6a656374223a7b226e6f64655f726f757465223a222f646e73616464722f6b38732d6875626465762d6e67696e78696e672d383536373330343661622d363630656539343138303064346131312e656c622e75732d776573742d312e616d617a6f6e6177732e636f6d2f7463702f343031312f736572766963652f617069222c226964223a2232646163363536342d656537382d346261622d623938322d643432306238646261643966222c226e616d65223a2264656661756c74222c226964656e746974795f6964223a224939393535313030383963336632343831326265633337356438333065303130373631666539666432222c22617574686f72697479223a7b226964223a224936656439666465326439613333646539336166366132643666616265653765383865353630323537222c2261646472657373223a222f646e73616464722f6b38732d6875626465762d6e67696e78696e672d383536373330343661622d363630656539343138303064346131312e656c622e75732d776573742d312e616d617a6f6e6177732e636f6d2f7463702f343031322f736572766963652f617069222c226964656e74697479223a2238316132303135383362613230313031303235383335613430323832303138313538323062633633666661316337343834636533386464333430373933626166613861333831626637633736646534386138363766643832623266353562393962383061303366343034316136346462383839643035316137376137386239643032383230313831353834306131336332386539366563393732343539383738343666663032363639313464643339383739383234643234363532393631646131366633363930663963303736306139643439303836633130373730343664643139393138393062656130653935373764613436643865323633316238636263316466383762356164373061227d2c226f6b7461223a6e756c6c7d2c2274727573745f636f6e74657874223a6e756c6c7d +7b226f6e655f74696d655f636f6465223a2266393964376533386430393736643665323733643039663034383934653030306262383733626261666538356233633462386434303435333536313564616633222c2270726f6a656374223a7b226964223a2234386465333566332d343465622d343264312d393561372d643034323966313065633164222c226e616d65223a2264656661756c74222c2273706163655f6e616d65223a22222c226163636573735f726f757465223a222f646e73616464722f6b38732d6875622d6e67696e78696e672d613631306264343233622d633135313863323965623936633463312e656c622e75732d776573742d312e616d617a6f6e6177732e636f6d2f7463702f343031352f736572766963652f617069222c227573657273223a5b5d2c2273706163655f6964223a22222c226964656e74697479223a224932396530356139383863363233336237303432643239396562343632626537626237333134343633222c22617574686f726974795f6163636573735f726f757465223a222f646e73616464722f6b38732d6875622d6e67696e78696e672d613631306264343233622d633135313863323965623936633463312e656c622e75732d776573742d312e616d617a6f6e6177732e636f6d2f7463702f343031362f736572766963652f617069222c22617574686f726974795f6964656e74697479223a2238316132303135383362613230313031303235383335613430323832303138313538323038373063346337366161393838323231383931653536323935613035353131613338323665306637353166646234353239373336353332323364623930326161303366343034316136353464376330653035316137383139376630653032383230313831353834306165636138653538393635396366316164373533336461396538363366633761653765643236323131333763343662303131366534396633383865363838343362323236313534366334316464363963636533393563343535643263353664393263306433366130353535376163353161666266303239373038666263363038222c2276657273696f6e223a6e756c6c2c2272756e6e696e67223a6e756c6c2c226f7065726174696f6e5f6964223a6e756c6c2c22757365725f726f6c6573223a5b5d7d7d diff --git a/implementations/rust/ockam/ockam_identity/Cargo.toml b/implementations/rust/ockam/ockam_identity/Cargo.toml index 11224f3c6d2..7bc537c6ca1 100644 --- a/implementations/rust/ockam/ockam_identity/Cargo.toml +++ b/implementations/rust/ockam/ockam_identity/Cargo.toml @@ -36,6 +36,7 @@ OCKAM_XX_25519_ChaChaPolyBLAKE2s = [ # be available on a standard platform. std = [ "alloc", + "chrono/std", "ockam_core/std", "ockam_macros/std", "ockam_node/std", @@ -44,11 +45,9 @@ std = [ "serde_bare/std", "minicbor/std", "time/std", - "lmdb", + "storage", ] -lmdb = ["tokio-retry", "lmdb-rkv"] - debugger = ["ockam_core/debugger"] # Feature: "no_std" enables functionality required for platforms @@ -70,30 +69,29 @@ alloc = [ "serde_bare/alloc", ] -# Feature: "sqlite" enables functionality to use sqlite for identity and policy storage -sqlite = ["rusqlite"] +storage = ["ockam_vault/storage", "sqlx", "tokio-retry"] [dependencies] arrayref = "0.3" async-trait = "0.1.74" cfg-if = "1.0.0" +chrono = { version = "0.4.31", default-features = false } delegate = "0.10.0" group = { version = "0.13.0", default-features = false } heapless = "0.7" hex = { version = "0.4", default-features = false } -lmdb-rkv = { version = "0.14.0", optional = true } minicbor = { version = "0.20.0", features = ["alloc", "derive"] } ockam_core = { path = "../ockam_core", version = "^0.93.0", default-features = false } ockam_macros = { path = "../ockam_macros", version = "^0.32.0", default-features = false } ockam_node = { path = "../ockam_node", version = "^0.98.0", default-features = false } ockam_vault = { path = "../ockam_vault", version = "^0.91.0", default-features = false, optional = true } rand = { version = "0.8", default-features = false } -rusqlite = { version = "0.29.0", optional = true } serde = { version = "1.0", default-features = false, features = ["derive"] } serde-big-array = "0.5" serde_bare = { version = "0.5.0", default-features = false, features = ["alloc"] } serde_json = { version = "1.0", optional = true } sha2 = { version = "0.10", default-features = false } +sqlx = { version = "0.7", optional = true } subtle = { version = "2.4.1", default-features = false } time = { version = "0.3.30", features = ["macros", "formatting", "std"], optional = true } tokio-retry = { version = "0.3.0", default-features = false, optional = true } diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs b/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs index 663af865637..babb516c4b5 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs @@ -65,6 +65,7 @@ impl AuthorityService { let credential = retriever.retrieve(ctx, subject).await?; debug!("retrieved a credential for subject {}", subject); + debug!("verifying the credential for authority {}", self.identifier); let credential_data = self .credentials .credentials_verification() diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs index d5b30050e02..aa6f422b6f8 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs @@ -1,9 +1,12 @@ -use crate::models::{CredentialData, PurposeKeyAttestationData}; -use crate::{CredentialsCreation, CredentialsVerification, IdentitiesRepository, PurposeKeys}; - use ockam_core::compat::sync::Arc; use ockam_vault::{VaultForSigning, VaultForVerifyingSignatures}; +use crate::models::{CredentialData, PurposeKeyAttestationData}; +use crate::{ + CredentialsCreation, CredentialsVerification, IdentitiesCreation, IdentityAttributesRepository, + PurposeKeys, +}; + /// Structure with both [`CredentialData`] and [`PurposeKeyAttestationData`] that we get /// after parsing and verifying corresponding [`Credential`] and [`super::super::models::PurposeKeyAttestation`] #[derive(Clone, Debug, PartialEq, Eq)] @@ -19,7 +22,8 @@ pub struct Credentials { credential_vault: Arc, verifying_vault: Arc, purpose_keys: Arc, - identities_repository: Arc, + identities_creation: Arc, + identity_attributes_repository: Arc, } impl Credentials { @@ -28,13 +32,15 @@ impl Credentials { credential_vault: Arc, verifying_vault: Arc, purpose_keys: Arc, - identities_repository: Arc, + identities_creation: Arc, + identity_attributes_repository: Arc, ) -> Self { Self { credential_vault, verifying_vault, purpose_keys, - identities_repository, + identities_creation, + identity_attributes_repository, } } @@ -43,18 +49,13 @@ impl Credentials { self.purpose_keys.clone() } - /// [`IdentitiesRepository`] - pub fn identities_repository(&self) -> Arc { - self.identities_repository.clone() - } - /// Return [`CredentialsCreation`] pub fn credentials_creation(&self) -> Arc { Arc::new(CredentialsCreation::new( self.purpose_keys.purpose_keys_creation(), self.credential_vault.clone(), self.verifying_vault.clone(), - self.identities_repository.clone(), + self.identities_creation.clone(), )) } @@ -63,20 +64,23 @@ impl Credentials { Arc::new(CredentialsVerification::new( self.purpose_keys.purpose_keys_verification(), self.verifying_vault.clone(), - self.identities_repository.clone(), + self.identity_attributes_repository.clone(), )) } } #[cfg(test)] mod tests { - use crate::identities::identities; - use crate::models::CredentialSchemaIdentifier; - use crate::Attributes; + use std::time::Duration; + use minicbor::bytes::ByteVec; + use ockam_core::compat::collections::BTreeMap; use ockam_core::Result; - use std::time::Duration; + + use crate::identities::identities; + use crate::models::CredentialSchemaIdentifier; + use crate::Attributes; #[tokio::test] async fn test_issue_credential() -> Result<()> { diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs index 2096cf31d77..b561c2ab9b6 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs @@ -1,20 +1,21 @@ -use crate::models::{ - Attributes, Credential, CredentialAndPurposeKey, CredentialData, Identifier, VersionedData, -}; -use crate::utils::{add_seconds, now}; -use crate::{IdentitiesRepository, Identity, PurposeKeyCreation}; - use core::time::Duration; + use ockam_core::compat::sync::Arc; use ockam_core::Result; use ockam_vault::{VaultForSigning, VaultForVerifyingSignatures}; +use crate::models::{ + Attributes, Credential, CredentialAndPurposeKey, CredentialData, Identifier, VersionedData, +}; +use crate::utils::{add_seconds, now}; +use crate::{IdentitiesCreation, PurposeKeyCreation}; + /// Service for managing [`Credential`]s pub struct CredentialsCreation { purpose_keys_creation: Arc, credential_vault: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_creation: Arc, } impl CredentialsCreation { @@ -23,20 +24,15 @@ impl CredentialsCreation { purpose_keys_creation: Arc, credential_vault: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_creation: Arc, ) -> Self { Self { purpose_keys_creation, verifying_vault, credential_vault, - identities_repository, + identities_creation, } } - - /// [`IdentitiesRepository`] - pub fn identities_repository(&self) -> Arc { - self.identities_repository.clone() - } } impl CredentialsCreation { @@ -54,13 +50,7 @@ impl CredentialsCreation { .get_or_create_credential_purpose_key(issuer) .await?; - let subject_change_history = self.identities_repository.get_identity(subject).await?; - let subject_identity = Identity::import_from_change_history( - Some(subject), - subject_change_history, - self.verifying_vault.clone(), - ) - .await?; + let subject_identity = self.identities_creation.get_identity(subject).await?; let created_at = now()?; let expires_at = add_seconds(&created_at, ttl.as_secs()); diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs index 22cb65dab9c..d3ff24e3e64 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs @@ -1,6 +1,7 @@ -use crate::models::{Attributes, CredentialAndPurposeKey, CredentialSchemaIdentifier, Identifier}; -use crate::utils::AttributesBuilder; -use crate::{Credentials, IdentitiesRepository, IdentitySecureChannelLocalInfo}; +use core::time::Duration; + +use minicbor::Decoder; +use tracing::trace; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::boxed::Box; @@ -11,9 +12,9 @@ use ockam_core::compat::vec::Vec; use ockam_core::{Result, Routed, Worker}; use ockam_node::Context; -use core::time::Duration; -use minicbor::Decoder; -use tracing::trace; +use crate::models::{Attributes, CredentialAndPurposeKey, CredentialSchemaIdentifier, Identifier}; +use crate::utils::AttributesBuilder; +use crate::{Credentials, IdentityAttributesRepository, IdentitySecureChannelLocalInfo}; /// Name of the attribute identifying the trust context for that attribute, meaning /// from which set of trusted authorities the attribute comes from @@ -30,7 +31,7 @@ pub const MAX_CREDENTIAL_VALIDITY: Duration = Duration::from_secs(30 * 24 * 3600 /// This struct runs as a Worker to issue credentials based on a request/response protocol pub struct CredentialsIssuer { - identities_repository: Arc, + identity_attributes_repository: Arc, credentials: Arc, issuer: Identifier, subject_attributes: Attributes, @@ -39,7 +40,7 @@ pub struct CredentialsIssuer { impl CredentialsIssuer { /// Create a new credentials issuer pub fn new( - identities_repository: Arc, + identity_attributes_repository: Arc, credentials: Arc, issuer: &Identifier, trust_context: String, @@ -49,7 +50,7 @@ impl CredentialsIssuer { .build(); Self { - identities_repository, + identity_attributes_repository, credentials, issuer: issuer.clone(), subject_attributes, @@ -61,8 +62,7 @@ impl CredentialsIssuer { subject: &Identifier, ) -> Result> { let entry = match self - .identities_repository - .as_attributes_reader() + .identity_attributes_repository .get_attributes(subject) .await? { diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs index 78a2d796754..ac3b81ad1d1 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs @@ -78,7 +78,7 @@ impl CredentialsServerWorker { .credentials_verification() .receive_presented_credential( &sender, - self.trust_context.authorities().await?.as_slice(), + &self.trust_context.authorities(), &credential_and_purpose_key, ) .await; @@ -110,7 +110,7 @@ impl CredentialsServerWorker { .credentials_verification() .receive_presented_credential( &sender, - self.trust_context.authorities().await?.as_slice(), + &self.trust_context.authorities(), &credential_and_purpose_key, ) .await; @@ -128,13 +128,12 @@ impl CredentialsServerWorker { ); let credential = self .trust_context - .authority()? - .credential(ctx, &self.identifier) - .await; + .get_credential(ctx, &self.identifier) + .await?; match credential.as_ref() { - Ok(p) if self.present_back => { + Some(c) if self.present_back => { info!("Mutual credential presentation request processed successfully with {}. Responding with own credential...", sender); - Response::ok(req).body(p).to_vec()? + Response::ok(req).body(c).to_vec()? } _ => { info!("Mutual credential presentation request processed successfully with {}. No credential to respond!", sender); diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs index 17d8f07c6d3..d4d7189aa6a 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs @@ -1,10 +1,4 @@ -use crate::identities::AttributesEntry; -use crate::models::{CredentialAndPurposeKey, CredentialData, Identifier, PurposePublicKey}; -use crate::utils::now; -use crate::{ - CredentialAndPurposeKeyData, IdentitiesRepository, IdentityError, PurposeKeyVerification, - TimestampInSeconds, -}; +use tracing::{debug, warn}; use ockam_core::compat::collections::BTreeMap; use ockam_core::compat::sync::Arc; @@ -12,6 +6,14 @@ use ockam_core::compat::vec::Vec; use ockam_core::Result; use ockam_vault::VaultForVerifyingSignatures; +use crate::identities::AttributesEntry; +use crate::models::{CredentialAndPurposeKey, CredentialData, Identifier, PurposePublicKey}; +use crate::utils::now; +use crate::{ + CredentialAndPurposeKeyData, IdentityAttributesRepository, IdentityError, + PurposeKeyVerification, TimestampInSeconds, +}; + /// We allow Credentials to be created in the future related to this machine's time due to /// possible time dyssynchronization const MAX_ALLOWED_TIME_DRIFT: TimestampInSeconds = TimestampInSeconds(5); @@ -20,7 +22,7 @@ const MAX_ALLOWED_TIME_DRIFT: TimestampInSeconds = TimestampInSeconds(5); pub struct CredentialsVerification { purpose_keys_verification: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_attributes_repository: Arc, } impl CredentialsVerification { @@ -28,19 +30,14 @@ impl CredentialsVerification { pub fn new( purpose_keys_verification: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_attributes_repository: Arc, ) -> Self { Self { purpose_keys_verification, verifying_vault, - identities_repository, + identities_attributes_repository, } } - - /// [`IdentitiesRepository`] - pub fn identities_repository(&self) -> Arc { - self.identities_repository.clone() - } } impl CredentialsVerification { @@ -52,6 +49,7 @@ impl CredentialsVerification { authorities: &[Identifier], credential_and_purpose_key: &CredentialAndPurposeKey, ) -> Result { + debug!("verify purpose key attestation"); let purpose_key_data = self .purpose_keys_verification .verify_purpose_key_attestation( @@ -60,20 +58,26 @@ impl CredentialsVerification { ) .await?; + debug!("verify issuer"); if !authorities.contains(&purpose_key_data.subject) { + warn!( + "unknown authority on a credential: {}. Accepted authorities: {:?}", + purpose_key_data.subject, authorities + ); return Err(IdentityError::UnknownAuthority.into()); } + debug!("verify purpose key type"); let public_key = match purpose_key_data.public_key.clone() { PurposePublicKey::SecureChannelStatic(_) => { - return Err(IdentityError::InvalidKeyType.into()) + return Err(IdentityError::InvalidKeyType.into()); } PurposePublicKey::CredentialSigning(public_key) => public_key, }; + debug!("verify signature"); let public_key = public_key.into(); - let versioned_data_hash = self .verifying_vault .sha256(&credential_and_purpose_key.credential.data) @@ -93,6 +97,7 @@ impl CredentialsVerification { return Err(IdentityError::CredentialVerificationFailed.into()); } + debug!("verify version"); let versioned_data = credential_and_purpose_key.credential.get_versioned_data()?; if versioned_data.version != 1 { return Err(IdentityError::UnknownCredentialVersion.into()); @@ -100,6 +105,10 @@ impl CredentialsVerification { let credential_data = CredentialData::get_data(&versioned_data)?; + debug!( + "verify subject {:?}. Expected {:?}", + credential_data.subject, expected_subject + ); if credential_data.subject.is_none() { // Currently unsupported return Err(IdentityError::CredentialVerificationFailed.into()); @@ -116,6 +125,7 @@ impl CredentialsVerification { return Err(IdentityError::CredentialVerificationFailed.into()); } + debug!("verify dates"); if credential_data.created_at < purpose_key_data.created_at { // Credential validity time range should be inside the purpose key validity time range return Err(IdentityError::CredentialVerificationFailed.into()); @@ -184,7 +194,7 @@ impl CredentialsVerification { .map(|(k, v)| (Vec::::from(k), Vec::::from(v))) .collect(); - self.identities_repository + self.identities_attributes_repository .put_attributes( subject, AttributesEntry::new( diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs b/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs index 08c99053d88..c7747f92566 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs @@ -1,11 +1,11 @@ -use ockam_core::compat::string::{String, ToString}; +use ockam_core::compat::string::String; use ockam_core::compat::vec::Vec; -use ockam_core::Result; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; use ockam_node::Context; -use tracing::{debug, error}; use crate::models::{CredentialAndPurposeKey, Identifier}; -use crate::{AuthorityService, IdentityError}; +use crate::AuthorityService; /// A trust context defines which authorities are trusted to attest to which attributes, within a context. /// Our first implementation assumes that there is only one authority and it is trusted to attest to all attributes within this context. @@ -13,14 +13,18 @@ use crate::{AuthorityService, IdentityError}; pub struct TrustContext { /// This is the ID of the trust context; which is primarily used for ABAC policies id: String, - /// Authority capable of retrieving credentials - authority: Option, + + /// Authority service + authority_service: Option, } impl TrustContext { /// Create a new Trust Context - pub fn new(id: String, authority: Option) -> Self { - Self { id, authority } + pub fn new(id: String, authority_service: Option) -> Self { + Self { + id, + authority_service, + } } /// Return the ID of the Trust Context @@ -28,16 +32,20 @@ impl TrustContext { &self.id } - /// Return the Authority of the Trust Context - pub fn authority(&self) -> Result<&AuthorityService> { - self.authority - .as_ref() - .ok_or_else(|| IdentityError::UnknownAuthority.into()) + /// Return the authority identities attached to this trust context + /// There is only the possibility to have 1 at the moment + pub fn authorities(&self) -> Vec { + match self.authority_identifier() { + Some(identifier) => vec![identifier], + None => vec![], + } } - /// Return the authority identities attached to this trust context - pub async fn authorities(&self) -> Result> { - Ok(vec![self.authority()?.identifier().clone()]) + /// Return the authority identifier + pub fn authority_identifier(&self) -> Option { + self.authority_service() + .ok() + .map(|a| a.identifier().clone()) } /// Return the credential for a given identity if an Authority has been defined @@ -46,27 +54,26 @@ impl TrustContext { &self, ctx: &Context, identifier: &Identifier, - ) -> Option { - match self.authority().ok() { - Some(authority) => match authority.credential(ctx, identifier).await { - Ok(credential) => { - debug!("retrieved a credential using the trust context authority"); - Some(credential) - } - Err(e) => { - error!( - "no credential could be retrieved {}, authority {}, subject {}", - e.to_string(), - authority.identifier(), - identifier - ); - None - } - }, - None => { - debug!("no authority is defined on the trust context"); - None + ) -> Result> { + match self.authority_service().ok() { + Some(authority_service) => { + Ok(Some(authority_service.credential(ctx, identifier).await?)) } + None => Ok(None), } } + + /// Return the authority service + fn authority_service(&self) -> Result { + self.authority_service.clone().ok_or_else(|| { + Error::new( + Origin::Identity, + Kind::Internal, + format!( + "no authority service has been defined for the trust context {}", + self.id + ), + ) + }) + } } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/identities.rs b/implementations/rust/ockam/ockam_identity/src/identities/identities.rs index bb9579924e7..2ad9843a98b 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/identities.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/identities.rs @@ -1,19 +1,29 @@ -use crate::identities::{IdentitiesKeys, IdentitiesRepository}; -use crate::purpose_keys::storage::{PurposeKeysRepository, PurposeKeysStorage}; -use crate::{ - Credentials, CredentialsServer, CredentialsServerModule, Identifier, IdentitiesBuilder, - IdentitiesCreation, IdentitiesReader, IdentitiesStorage, Identity, PurposeKeys, Vault, -}; - use ockam_core::compat::sync::Arc; use ockam_core::compat::vec::Vec; use ockam_core::Result; +#[cfg(feature = "storage")] +use crate::identities::storage::ChangeHistorySqlxDatabase; +#[cfg(feature = "storage")] +use crate::identities::storage::IdentityAttributesSqlxDatabase; +use crate::identities::{ChangeHistoryRepository, IdentitiesKeys}; +use crate::models::ChangeHistory; +use crate::purpose_keys::storage::PurposeKeysRepository; +#[cfg(feature = "storage")] +use crate::purpose_keys::storage::PurposeKeysSqlxDatabase; +#[cfg(feature = "storage")] +use crate::IdentitiesBuilder; +use crate::{ + Credentials, CredentialsServer, CredentialsServerModule, Identifier, IdentitiesCreation, + Identity, IdentityAttributesRepository, PurposeKeys, Vault, +}; + /// This struct supports all the services related to identities #[derive(Clone)] pub struct Identities { vault: Vault, - identities_repository: Arc, + change_history_repository: Arc, + identity_attributes_repository: Arc, purpose_keys_repository: Arc, } @@ -24,8 +34,13 @@ impl Identities { } /// Return the identities repository - pub fn repository(&self) -> Arc { - self.identities_repository.clone() + pub fn change_history_repository(&self) -> Arc { + self.change_history_repository.clone() + } + + /// Return the identity attributes repository + pub fn identity_attributes_repository(&self) -> Arc { + self.identity_attributes_repository.clone() } /// Return the purpose keys repository @@ -35,13 +50,14 @@ impl Identities { /// Get an [`Identity`] from the repository pub async fn get_identity(&self, identifier: &Identifier) -> Result { - let change_history = self.identities_repository.get_identity(identifier).await?; - Identity::import_from_change_history( - Some(identifier), - change_history, - self.vault.verifying_vault.clone(), - ) - .await + self.identities_creation().get_identity(identifier).await + } + + /// Return the change history of a persisted identity + pub async fn get_change_history(&self, identifier: &Identifier) -> Result { + self.identities_creation() + .get_change_history(identifier) + .await } /// Export an [`Identity`] from the repository @@ -53,7 +69,7 @@ impl Identities { pub fn purpose_keys(&self) -> Arc { Arc::new(PurposeKeys::new( self.vault.clone(), - self.identities_repository.as_identities_reader(), + self.identities_creation().clone(), self.identities_keys(), self.purpose_keys_repository.clone(), )) @@ -70,24 +86,20 @@ impl Identities { /// Return the identities creation service pub fn identities_creation(&self) -> Arc { Arc::new(IdentitiesCreation::new( - self.repository(), + self.change_history_repository(), self.vault.identity_vault.clone(), self.vault.verifying_vault.clone(), )) } - /// Return the identities reader - pub fn identities_reader(&self) -> Arc { - self.repository().as_identities_reader() - } - /// Return the identities credentials service pub fn credentials(&self) -> Arc { Arc::new(Credentials::new( self.vault.credential_vault.clone(), self.vault.verifying_vault.clone(), self.purpose_keys(), - self.identities_repository.clone(), + self.identities_creation().clone(), + self.identity_attributes_repository.clone(), )) } @@ -101,22 +113,26 @@ impl Identities { /// Create a new identities module pub(crate) fn new( vault: Vault, - identities_repository: Arc, + change_history_repository: Arc, + identity_attributes_repository: Arc, purpose_keys_repository: Arc, ) -> Identities { Identities { vault, - identities_repository, + change_history_repository, + identity_attributes_repository, purpose_keys_repository, } } /// Return a default builder for identities + #[cfg(feature = "storage")] pub fn builder() -> IdentitiesBuilder { IdentitiesBuilder { vault: Vault::create(), - repository: IdentitiesStorage::create(), - purpose_keys_repository: PurposeKeysStorage::create(), + change_history_repository: ChangeHistorySqlxDatabase::create(), + identity_attributes_repository: IdentityAttributesSqlxDatabase::create(), + purpose_keys_repository: PurposeKeysSqlxDatabase::create(), } } } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs b/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs index 4cc6e9252a6..106ac6f68bb 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs @@ -1,27 +1,29 @@ -use crate::identities::{Identities, IdentitiesRepository, IdentitiesStorage}; -use crate::purpose_keys::storage::{PurposeKeysRepository, PurposeKeysStorage}; -use crate::storage::Storage; -use crate::{Vault, VaultStorage}; - use ockam_core::compat::sync::Arc; +use ockam_vault::storage::SecretsRepository; + +use crate::identities::{ChangeHistoryRepository, Identities}; +use crate::purpose_keys::storage::PurposeKeysRepository; +use crate::{IdentityAttributesRepository, Vault}; /// Builder for Identities services #[derive(Clone)] pub struct IdentitiesBuilder { pub(crate) vault: Vault, - pub(crate) repository: Arc, + pub(crate) change_history_repository: Arc, + pub(crate) identity_attributes_repository: Arc, pub(crate) purpose_keys_repository: Arc, } /// Return a default identities +#[cfg(feature = "storage")] pub fn identities() -> Arc { Identities::builder().build() } impl IdentitiesBuilder { - /// With Software Vault with given Storage - pub fn with_vault_storage(mut self, storage: VaultStorage) -> Self { - self.vault = Vault::create_with_persistent_storage(storage); + /// With Software Vault with given secrets repository + pub fn with_secrets_repository(mut self, repository: Arc) -> Self { + self.vault = Vault::create_with_secrets_repository(repository); self } @@ -31,20 +33,22 @@ impl IdentitiesBuilder { self } - /// Set a specific storage for identities - pub fn with_identities_storage(self, storage: Arc) -> Self { - self.with_identities_repository(Arc::new(IdentitiesStorage::new(storage))) - } - /// Set a specific repository for identities - pub fn with_identities_repository(mut self, repository: Arc) -> Self { - self.repository = repository; + pub fn with_change_history_repository( + mut self, + repository: Arc, + ) -> Self { + self.change_history_repository = repository; self } - /// Set a specific storage for Purpose Keys - pub fn with_purpose_keys_storage(self, storage: Arc) -> Self { - self.with_purpose_keys_repository(Arc::new(PurposeKeysStorage::new(storage))) + /// Set a specific repository for identity attributes + pub fn with_identity_attributes_repository( + mut self, + repository: Arc, + ) -> Self { + self.identity_attributes_repository = repository; + self } /// Set a specific repository for Purpose Keys @@ -60,7 +64,8 @@ impl IdentitiesBuilder { pub fn build(self) -> Arc { Arc::new(Identities::new( self.vault, - self.repository, + self.change_history_repository, + self.identity_attributes_repository, self.purpose_keys_repository, )) } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs b/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs index cbc12f10673..ec666cdbb49 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs @@ -1,15 +1,16 @@ use ockam_core::compat::sync::Arc; -use ockam_core::Result; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; use ockam_vault::{SigningSecretKeyHandle, VaultForSigning, VaultForVerifyingSignatures}; use crate::identities::identity_builder::IdentityBuilder; use crate::models::{ChangeHistory, Identifier}; -use crate::{IdentitiesKeys, IdentitiesRepository, Identity, IdentityError}; +use crate::{ChangeHistoryRepository, IdentitiesKeys, Identity, IdentityError}; use crate::{IdentityHistoryComparison, IdentityOptions}; /// This struct supports functions for the creation and import of identities using an IdentityVault pub struct IdentitiesCreation { - pub(super) repository: Arc, + pub(super) repository: Arc, pub(super) identity_vault: Arc, pub(super) verifying_vault: Arc, } @@ -17,7 +18,7 @@ pub struct IdentitiesCreation { impl IdentitiesCreation { /// Create a new identities import module pub fn new( - repository: Arc, + repository: Arc, identity_vault: Arc, verifying_vault: Arc, ) -> Self { @@ -37,18 +38,13 @@ impl IdentitiesCreation { } /// Import and verify identity from its binary format - /// This action persists the Identity in the storage, use `Identity::import` to avoid that pub async fn import( &self, expected_identifier: Option<&Identifier>, data: &[u8], ) -> Result { - let identity = - Identity::import(expected_identifier, data, self.verifying_vault.clone()).await?; - - self.update_identity(&identity).await?; - - Ok(identity.identifier().clone()) + self.import_from_change_history(expected_identifier, ChangeHistory::import(data)?) + .await } /// Import and verify identity from its Change History @@ -66,7 +62,6 @@ impl IdentitiesCreation { .await?; self.update_identity(&identity).await?; - Ok(identity.identifier().clone()) } @@ -91,9 +86,7 @@ impl IdentitiesCreation { options: IdentityOptions, ) -> Result { let identity = self.identities_keys().create_initial_key(options).await?; - self.repository - .update_identity(identity.identifier(), identity.change_history()) - .await?; + self.repository.store_change_history(&identity).await?; Ok(identity.identifier().clone()) } @@ -111,23 +104,13 @@ impl IdentitiesCreation { identifier: &Identifier, options: IdentityOptions, ) -> Result<()> { - let change_history = self.repository.get_identity(identifier).await?; - - let identity = Identity::import_from_change_history( - Some(identifier), - change_history, - self.verifying_vault.clone(), - ) - .await?; - + let identity = self.get_identity(identifier).await?; let identity = self .identities_keys() .rotate_key_with_options(identity, options) .await?; - self.repository - .update_identity(identity.identifier(), identity.change_history()) - .await?; + self.repository.store_change_history(&identity).await?; Ok(()) } @@ -159,6 +142,7 @@ impl IdentitiesCreation { return Err(IdentityError::WrongSecretKey.into()); } + self.repository.store_change_history(&identity).await?; Ok(identity.identifier().clone()) } @@ -171,6 +155,37 @@ impl IdentitiesCreation { pub fn verifying_vault(&self) -> Arc { self.verifying_vault.clone() } + + /// Return the change history of a persisted identity + pub async fn get_identity(&self, identifier: &Identifier) -> Result { + match self.repository.get_change_history(identifier).await? { + Some(change_history) => { + let identity = Identity::import_from_change_history( + Some(identifier), + change_history, + self.verifying_vault.clone(), + ) + .await?; + Ok(identity) + } + None => Err(Error::new( + Origin::Core, + Kind::NotFound, + format!("identity not found for identifier {}", identifier), + )), + } + } + /// Return the change history of a persisted identity + pub async fn get_change_history(&self, identifier: &Identifier) -> Result { + match self.repository.get_change_history(identifier).await? { + Some(change_history) => Ok(change_history), + None => Err(Error::new( + Origin::Core, + Kind::NotFound, + format!("identity not found for identifier {}", identifier), + )), + } + } } impl IdentitiesCreation { @@ -182,7 +197,7 @@ impl IdentitiesCreation { pub async fn update_identity(&self, identity: &Identity) -> Result<()> { if let Some(known_identity) = self .repository - .retrieve_identity(identity.identifier()) + .get_change_history(identity.identifier()) .await? { let known_identity = Identity::import_from_change_history( @@ -197,16 +212,12 @@ impl IdentitiesCreation { return Err(IdentityError::ConsistencyError.into()); } IdentityHistoryComparison::Newer => { - self.repository - .update_identity(identity.identifier(), identity.change_history()) - .await?; + self.repository.store_change_history(identity).await?; } IdentityHistoryComparison::Equal => {} } } else { - self.repository - .update_identity(identity.identifier(), identity.change_history()) - .await?; + self.repository.store_change_history(identity).await?; } Ok(()) diff --git a/implementations/rust/ockam/ockam_identity/src/identities/mod.rs b/implementations/rust/ockam/ockam_identity/src/identities/mod.rs index 469b141a565..c582ab0041d 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/mod.rs @@ -5,9 +5,7 @@ mod identities_creation; mod identity_builder; mod identity_keys; mod identity_options; - -/// Identities storage functions -pub mod storage; +mod storage; pub use identities::*; pub use identities_builder::*; diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository.rs new file mode 100644 index 00000000000..e7c40e6dc59 --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository.rs @@ -0,0 +1,24 @@ +use ockam_core::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; + +use crate::models::{ChangeHistory, Identifier}; +use crate::Identity; + +/// This repository stores identity change histories +#[async_trait] +pub trait ChangeHistoryRepository: Send + Sync + 'static { + /// Store changes if there are new key changes associated to that identity + /// We take an Identity to make sure that the stored changes have been verified + async fn store_change_history(&self, identity: &Identity) -> Result<()>; + + /// Delete a change history given its identifier + async fn delete_change_history(&self, identifier: &Identifier) -> Result<()>; + + /// Return the change history of a persisted identity + async fn get_change_history(&self, identifier: &Identifier) -> Result>; + + /// Return all the change histories + async fn get_change_histories(&self) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs new file mode 100644 index 00000000000..6244216b00d --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs @@ -0,0 +1,168 @@ +use core::str::FromStr; + +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::models::{ChangeHistory, Identifier}; +use crate::{ChangeHistoryRepository, Identity, TimestampInSeconds}; + +/// Implementation of `IdentitiesRepository` trait based on an underlying database +/// using sqlx as its API, and Sqlite as its driver +#[derive(Clone)] +pub struct ChangeHistorySqlxDatabase { + database: Arc, +} + +impl ChangeHistorySqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for change history"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory( + "change history", + )))) + } +} + +#[async_trait] +impl ChangeHistoryRepository for ChangeHistorySqlxDatabase { + async fn store_change_history(&self, identity: &Identity) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity VALUES (?, ?)") + .bind(identity.identifier().to_sql()) + .bind(identity.change_history().to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_change_history(&self, identifier: &Identifier) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + let query1 = query("DELETE FROM identity where identifier=?").bind(identifier.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + let query2 = + query("DELETE FROM identity_attributes where identifier=?").bind(identifier.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void()?; + Ok(()) + } + + async fn get_change_history(&self, identifier: &Identifier) -> Result> { + let query = + query_as("SELECT * FROM identity WHERE identifier=$1").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.change_history()).transpose() + } + + async fn get_change_histories(&self) -> Result> { + let query = query_as("SELECT * FROM identity"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.iter().map(|r| r.change_history()).collect() + } +} + +impl ToSqlxType for Identifier { + fn to_sql(&self) -> SqlxType { + self.to_string().to_sql() + } +} + +impl ToSqlxType for TimestampInSeconds { + fn to_sql(&self) -> SqlxType { + self.0.to_sql() + } +} + +impl ToSqlxType for ChangeHistory { + fn to_sql(&self) -> SqlxType { + self.export_as_string().unwrap().to_sql() + } +} + +#[derive(sqlx::FromRow)] +pub(crate) struct ChangeHistoryRow { + identifier: String, + change_history: String, +} + +impl ChangeHistoryRow { + #[allow(dead_code)] + pub(crate) fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + pub(crate) fn change_history(&self) -> Result { + ChangeHistory::import_from_string(&self.change_history) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use tempfile::NamedTempFile; + + use crate::Identity; + + use super::*; + + #[tokio::test] + async fn test_identities_repository() -> Result<()> { + let identity1 = create_identity1().await?; + let identity2 = create_identity2().await?; + let db_file = NamedTempFile::new().unwrap(); + let repository = create_repository(db_file.path()).await?; + + // store and retrieve or get an identity + repository.store_change_history(&identity1).await?; + + // the change history can be retrieved as an Option + let result = repository + .get_change_history(identity1.identifier()) + .await?; + assert_eq!(result, Some(identity1.change_history().clone())); + + // trying to retrieve a missing identity returns None + let result = repository + .get_change_history(identity2.identifier()) + .await?; + assert_eq!(result, None); + + // a change history can also be deleted from the repository + repository.store_change_history(&identity2).await?; + repository + .delete_change_history(identity2.identifier()) + .await?; + let result = repository + .get_change_history(identity2.identifier()) + .await?; + assert_eq!(result, None); + Ok(()) + } + + /// HELPERS + async fn create_identity1() -> Result { + Identity::create("81a201583ba20101025835a4028201815820530d1c2e9822433b679a66a60b9c2ed47c370cd0ce51cbe1a7ad847b5835a96303f4041a64dd4060051a77a94360028201815840042fff8f6c80603fb1cec4a3cf1ff169ee36889d3ed76184fe1dfbd4b692b02892df9525c61c2f1286b829586d13d5abf7d18973141f734d71c1840520d40a0e") + .await + } + + async fn create_identity2() -> Result { + Identity::create("81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805") + .await + } + + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(ChangeHistorySqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_impl.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_impl.rs deleted file mode 100644 index f2c84584181..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_impl.rs +++ /dev/null @@ -1,170 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::string::ToString; -use ockam_core::compat::sync::Arc; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; - -use crate::identity::IdentityConstants; -use crate::models::{ChangeHistory, Identifier}; -use crate::storage::{InMemoryStorage, Storage}; -use crate::utils::now; -use crate::{ - AttributesEntry, IdentitiesReader, IdentitiesRepository, IdentitiesWriter, - IdentityAttributesReader, IdentityAttributesWriter, -}; - -/// Implementation of `IdentityAttributes` trait based on an underlying `Storage` -#[derive(Clone)] -pub struct IdentitiesStorage { - storage: Arc, -} - -#[async_trait] -impl IdentitiesRepository for IdentitiesStorage { - fn as_attributes_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_attributes_writer(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_writer(&self) -> Arc { - Arc::new(self.clone()) - } -} - -impl IdentitiesStorage { - /// Create a new storage for attributes - pub fn new(storage: Arc) -> Self { - Self { storage } - } - - /// Create a new storage for attributes - pub fn create() -> Arc { - Arc::new(Self::new(InMemoryStorage::create())) - } -} - -#[async_trait] -impl IdentityAttributesReader for IdentitiesStorage { - async fn get_attributes(&self, identity_id: &Identifier) -> Result> { - let id = identity_id.to_string(); - let entry = match self - .storage - .get(&id, IdentityConstants::ATTRIBUTES_KEY) - .await? - { - Some(e) => e, - None => return Ok(None), - }; - - let entry: AttributesEntry = minicbor::decode(&entry)?; - - let now = now()?; - match entry.expires() { - Some(exp) if exp <= now => { - self.storage - .del(&id, IdentityConstants::ATTRIBUTES_KEY) - .await?; - Ok(None) - } - _ => Ok(Some(entry)), - } - } - - async fn list(&self) -> Result> { - let mut l = Vec::new(); - for id in self.storage.keys(IdentityConstants::ATTRIBUTES_KEY).await? { - let identity_identifier = Identifier::try_from(id)?; - if let Some(attrs) = self.get_attributes(&identity_identifier).await? { - l.push((identity_identifier, attrs)) - } - } - Ok(l) - } -} - -#[async_trait] -impl IdentityAttributesWriter for IdentitiesStorage { - async fn put_attributes(&self, sender: &Identifier, entry: AttributesEntry) -> Result<()> { - // TODO: Implement expiration mechanism in Storage - let entry = minicbor::to_vec(&entry)?; - - self.storage - .set( - &sender.to_string(), - IdentityConstants::ATTRIBUTES_KEY.to_string(), - entry, - ) - .await?; - - Ok(()) - } - - /// Store an attribute name/value pair for a given identity - async fn put_attribute_value( - &self, - subject: &Identifier, - attribute_name: Vec, - attribute_value: Vec, - ) -> Result<()> { - let mut attributes = match self.get_attributes(subject).await? { - Some(entry) => (*entry.attrs()).clone(), - None => BTreeMap::new(), - }; - attributes.insert(attribute_name, attribute_value); - let entry = AttributesEntry::new(attributes, now()?, None, Some(subject.clone())); - self.put_attributes(subject, entry).await - } - - async fn delete(&self, identity: &Identifier) -> Result<()> { - self.storage - .del( - identity.to_string().as_str(), - IdentityConstants::ATTRIBUTES_KEY, - ) - .await - } -} - -#[async_trait] -impl IdentitiesWriter for IdentitiesStorage { - async fn update_identity( - &self, - identifier: &Identifier, - change_history: &ChangeHistory, - ) -> Result<()> { - self.storage - .set( - &identifier.to_string(), - IdentityConstants::CHANGE_HISTORY_KEY.to_string(), - minicbor::to_vec(change_history)?, - ) - .await - } -} - -#[async_trait] -impl IdentitiesReader for IdentitiesStorage { - async fn retrieve_identity(&self, identifier: &Identifier) -> Result> { - if let Some(data) = self - .storage - .get( - &identifier.to_string(), - IdentityConstants::CHANGE_HISTORY_KEY, - ) - .await? - { - Ok(Some(minicbor::decode(&data)?)) - } else { - Ok(None) - } - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_trait.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_trait.rs deleted file mode 100644 index 0d305d25859..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_trait.rs +++ /dev/null @@ -1,86 +0,0 @@ -use ockam_core::compat::boxed::Box; -use ockam_core::compat::sync::Arc; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::Result; -use ockam_core::{async_trait, Error}; - -use crate::models::{ChangeHistory, Identifier}; -use crate::AttributesEntry; - -/// Repository for data related to identities: key changes and attributes -#[async_trait] -pub trait IdentitiesRepository: - IdentityAttributesReader + IdentityAttributesWriter + IdentitiesReader + IdentitiesWriter -{ - /// Restrict this repository as a reader for attributes - fn as_attributes_reader(&self) -> Arc; - - /// Restrict this repository as a writer for attributes - fn as_attributes_writer(&self) -> Arc; - - /// Restrict this repository as a reader for identities - fn as_identities_reader(&self) -> Arc; - - /// Restrict this repository as a writer for identities - fn as_identities_writer(&self) -> Arc; -} - -/// Trait implementing read access to attributes -#[async_trait] -pub trait IdentityAttributesReader: Send + Sync + 'static { - /// Get the attributes associated with the given identity identifier - async fn get_attributes(&self, identity: &Identifier) -> Result>; - - /// List all identities with their attributes - async fn list(&self) -> Result>; -} - -/// Trait implementing write access to attributes -#[async_trait] -pub trait IdentityAttributesWriter: Send + Sync + 'static { - /// Set the attributes associated with the given identity identifier. - /// Previous values gets overridden. - async fn put_attributes(&self, identity: &Identifier, entry: AttributesEntry) -> Result<()>; - - /// Store an attribute name/value pair for a given identity - async fn put_attribute_value( - &self, - subject: &Identifier, - attribute_name: Vec, - attribute_value: Vec, - ) -> Result<()>; - - /// Remove all attributes for a given identity identifier - async fn delete(&self, identity: &Identifier) -> Result<()>; -} - -/// Trait implementing write access to identities -#[async_trait] -pub trait IdentitiesWriter: Send + Sync + 'static { - /// Store changes if there are new key changes associated to that identity - async fn update_identity( - &self, - identifier: &Identifier, - change_history: &ChangeHistory, - ) -> Result<()>; -} - -/// Trait implementing read access to identiets -#[async_trait] -pub trait IdentitiesReader: Send + Sync + 'static { - /// Return a persisted identity - async fn retrieve_identity(&self, identifier: &Identifier) -> Result>; - - /// Return a persisted identity that is expected to be present and return and Error if this is not the case - async fn get_identity(&self, identifier: &Identifier) -> Result { - match self.retrieve_identity(identifier).await? { - Some(change_history) => Ok(change_history), - None => Err(Error::new( - Origin::Core, - Kind::NotFound, - format!("identity not found for identifier {}", identifier), - )), - } - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository.rs new file mode 100644 index 00000000000..316d770655e --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository.rs @@ -0,0 +1,30 @@ +use crate::{AttributesEntry, Identifier}; +use async_trait::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; + +/// Trait implementing read access to attributes +#[async_trait] +pub trait IdentityAttributesRepository: Send + Sync + 'static { + /// Get the attributes associated with the given identity identifier + async fn get_attributes(&self, subject: &Identifier) -> Result>; + + /// List all identities with their attributes + async fn list(&self) -> Result>; + + /// Set the attributes associated with the given identity identifier. + /// Previous values gets overridden. + async fn put_attributes(&self, subject: &Identifier, entry: AttributesEntry) -> Result<()>; + + /// Store an attribute name/value pair for a given identity + async fn put_attribute_value( + &self, + subject: &Identifier, + attribute_name: Vec, + attribute_value: Vec, + ) -> Result<()>; + + /// Remove all attributes for a given identity identifier + async fn delete(&self, identity: &Identifier) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository_sql.rs new file mode 100644 index 00000000000..0362f07d3ce --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository_sql.rs @@ -0,0 +1,238 @@ +use core::str::FromStr; +use std::collections::BTreeMap; + +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::models::Identifier; +use crate::utils::now; +use crate::{AttributesEntry, IdentityAttributesRepository, TimestampInSeconds}; + +/// Implementation of `IdentitiesRepository` trait based on an underlying database +/// using sqlx as its API, and Sqlite as its driver +#[derive(Clone)] +pub struct IdentityAttributesSqlxDatabase { + database: Arc, +} + +impl IdentityAttributesSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for identity attributes"); + Self { database } + } + + /// Create a new in-memory database + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory( + "identity attributes", + )))) + } +} + +#[async_trait] +impl IdentityAttributesRepository for IdentityAttributesSqlxDatabase { + async fn get_attributes(&self, identity: &Identifier) -> Result> { + let query = query_as("SELECT * FROM identity_attributes WHERE identifier=$1") + .bind(identity.to_sql()); + let identity_attributes: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(identity_attributes.map(|r| r.attributes()).transpose()?) + } + + async fn list(&self) -> Result> { + let query = query_as("SELECT * FROM identity_attributes"); + let result: Vec = + query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identifier().and_then(|i| r.attributes().map(|a| (i, a)))) + .collect::>>() + } + + async fn put_attributes(&self, subject: &Identifier, entry: AttributesEntry) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity_attributes VALUES (?, ?, ?, ?, ?)") + .bind(subject.to_sql()) + .bind(minicbor::to_vec(entry.attrs())?.to_sql()) + .bind(entry.added().to_sql()) + .bind(entry.expires().map(|e| e.to_sql())) + .bind(entry.attested_by().map(|e| e.to_sql())); + query.execute(&self.database.pool).await.void() + } + + /// Store an attribute name/value pair for a given identity + async fn put_attribute_value( + &self, + subject: &Identifier, + attribute_name: Vec, + attribute_value: Vec, + ) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + + let mut attributes = match self.get_attributes(subject).await? { + Some(entry) => (*entry.attrs()).clone(), + None => BTreeMap::new(), + }; + attributes.insert(attribute_name, attribute_value); + let entry = AttributesEntry::new(attributes, now()?, None, Some(subject.clone())); + self.put_attributes(subject, entry).await?; + + transaction.commit().await.void() + } + + async fn delete(&self, identity: &Identifier) -> Result<()> { + let query = + query("DELETE FROM identity_attributes WHERE identifier = ?").bind(identity.to_sql()); + query.execute(&self.database.pool).await.void() + } +} + +#[derive(FromRow)] +struct IdentityAttributesRow { + identifier: String, + attributes: Vec, + added: i64, + expires: Option, + attested_by: Option, +} + +impl IdentityAttributesRow { + fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + fn attributes(&self) -> Result { + let attributes = + minicbor::decode(self.attributes.as_slice()).map_err(SqlxDatabase::map_decode_err)?; + let added = TimestampInSeconds(self.added as u64); + let expires = self.expires.map(|v| TimestampInSeconds(v as u64)); + let attested_by = self + .attested_by + .clone() + .map(|v| Identifier::from_str(&v)) + .transpose()?; + + Ok(AttributesEntry::new( + attributes, + added, + expires, + attested_by, + )) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + use std::time::Duration; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_identities_attributes_repository() -> Result<()> { + let identifier1 = + Identifier::from_str("Ie92f183eb4c324804ef4d62962dea94cf095a265").unwrap(); + + let attributes = create_attributes_entry().await?; + let db_file = NamedTempFile::new().unwrap(); + let repository = create_repository(db_file.path()).await?; + + // store and retrieve attributes by identity + repository + .put_attributes(&identifier1, attributes.clone()) + .await?; + + let result = repository.list().await?; + assert_eq!(result, vec![(identifier1.clone(), attributes.clone())]); + + let result = repository.get_attributes(&identifier1).await?; + assert_eq!(result, Some(attributes)); + + // delete attributes + repository.delete(&identifier1).await?; + let result = repository.get_attributes(&identifier1).await?; + assert_eq!(result, None); + + // store just one attribute name / value + let before_adding = now()?; + repository + .put_attribute_value( + &identifier1, + "name".as_bytes().to_vec(), + "value".as_bytes().to_vec(), + ) + .await?; + + let result = repository.get_attributes(&identifier1).await?.unwrap(); + // the name/value pair is present + assert_eq!( + result.attrs().get("name".as_bytes()), + Some(&"value".as_bytes().to_vec()) + ); + // there is a timestamp showing when the attributes have been added + assert!(result.added() >= before_adding); + + // the attributes are self-attested + assert_eq!(result.attested_by(), Some(identifier1.clone())); + + // store one more attribute name / value + // Let time pass for bit to observe a timestamp update + // We need to wait at least one second since this is the granularity of the + // timestamp for tracking attributes + tokio::time::sleep(Duration::from_millis(1100)).await; + repository + .put_attribute_value( + &identifier1, + "name2".as_bytes().to_vec(), + "value2".as_bytes().to_vec(), + ) + .await?; + + let result2 = repository.get_attributes(&identifier1).await?.unwrap(); + + // both the new and the old name/value pairs are present + assert_eq!( + result2.attrs().get("name".as_bytes()), + Some(&"value".as_bytes().to_vec()) + ); + assert_eq!( + result2.attrs().get("name2".as_bytes()), + Some(&"value2".as_bytes().to_vec()) + ); + // The original timestamp has been updated + assert!(result2.added() > result.added()); + + // the attributes are still self-attested + assert_eq!(result2.attested_by(), Some(identifier1.clone())); + Ok(()) + } + + /// HELPERS + async fn create_attributes_entry() -> Result { + let identifier1 = + Identifier::from_str("Ie92f183eb4c324804ef4d62962dea94cf095a265").unwrap(); + Ok(AttributesEntry::new( + BTreeMap::from([ + ("name".as_bytes().to_vec(), "alice".as_bytes().to_vec()), + ("age".as_bytes().to_vec(), "20".as_bytes().to_vec()), + ]), + TimestampInSeconds(1000), + Some(TimestampInSeconds(2000)), + Some(identifier1.clone()), + )) + } + + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(IdentityAttributesSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs index 5ac04e522a0..ba1a56d7732 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs @@ -1,7 +1,16 @@ +pub use attributes_entry::*; +pub use change_history_repository::*; +#[cfg(feature = "storage")] +pub use change_history_repository_sql::*; +pub use identity_attributes_repository::*; +#[cfg(feature = "storage")] +pub use identity_attributes_repository_sql::*; + mod attributes_entry; -mod identities_repository_impl; -mod identities_repository_trait; +mod change_history_repository; +mod identity_attributes_repository; -pub use attributes_entry::*; -pub use identities_repository_impl::*; -pub use identities_repository_trait::*; +#[cfg(feature = "storage")] +mod change_history_repository_sql; +#[cfg(feature = "storage")] +mod identity_attributes_repository_sql; diff --git a/implementations/rust/ockam/ockam_identity/src/identity/identity.rs b/implementations/rust/ockam/ockam_identity/src/identity/identity.rs index 96c30427b43..92f82745181 100644 --- a/implementations/rust/ockam/ockam_identity/src/identity/identity.rs +++ b/implementations/rust/ockam/ockam_identity/src/identity/identity.rs @@ -1,11 +1,12 @@ use crate::models::{Change, ChangeHash, ChangeHistory, Identifier}; use crate::verified_change::VerifiedChange; -use crate::IdentityError; use crate::IdentityHistoryComparison; +use crate::{IdentityError, Vault}; use core::cmp::Ordering; use core::fmt; use core::fmt::{Display, Formatter}; +use ockam_core::compat::string::String; use ockam_core::compat::sync::Arc; use ockam_core::compat::vec::Vec; use ockam_core::Result; @@ -76,6 +77,36 @@ impl Identity { self.change_history.export() } + /// Export an `Identity` to a hex-encoded string + pub fn export_as_string(&self) -> Result { + self.change_history.export_as_string() + } + + /// Import and verify Identity from the ChangeHistory as a string + pub async fn import_from_string( + expected_identifier: Option<&Identifier>, + change_history: &str, + vault: Arc, + ) -> Result { + let change_history = ChangeHistory::import_from_string(change_history)?; + Self::import_from_change_history(expected_identifier, change_history, vault).await + } + + /// Create an identity for its change history as a hex-encoded string + pub async fn create(change_history: &str) -> Result { + Self::import_from_string(None, change_history, Vault::create_verifying_vault()).await + } + + /// Create an identity for its change history + pub async fn create_from_change_history(change_history: &ChangeHistory) -> Result { + Self::import_from_change_history( + None, + change_history.clone(), + Vault::create_verifying_vault(), + ) + .await + } + /// Import and verify Identity from the ChangeHistory pub async fn import_from_change_history( expected_identifier: Option<&Identifier>, @@ -168,6 +199,12 @@ impl Display for Identity { let identifier = self.identifier(); writeln!(f, "Identifier: {identifier}")?; + self.change_history.fmt(f) + } +} + +impl Display for ChangeHistory { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let history = hex::encode(self.export().map_err(|_| fmt::Error)?); writeln!(f, "Change history: {history}") } diff --git a/implementations/rust/ockam/ockam_identity/src/lib.rs b/implementations/rust/ockam/ockam_identity/src/lib.rs index 49151948155..087b78cab76 100644 --- a/implementations/rust/ockam/ockam_identity/src/lib.rs +++ b/implementations/rust/ockam/ockam_identity/src/lib.rs @@ -25,12 +25,25 @@ unused_import_braces, unused_qualifications )] #![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] -extern crate core; - #[cfg(feature = "alloc")] #[macro_use] extern crate alloc; +#[cfg(feature = "std")] +extern crate core; + +/// +/// Exports +/// +pub use credentials::*; +pub use error::*; +pub use identities::*; +pub use identity::*; +pub use models::{Attributes, Credential, Identifier, TimestampInSeconds}; +pub use purpose_key::*; +pub use purpose_keys::*; +pub use secure_channel::*; +pub use secure_channels::*; +pub use vault::*; /// Utilities pub mod utils; @@ -62,23 +75,5 @@ pub mod secure_channel; /// Service supporting the creation of secure channel listener and connection to a listener pub mod secure_channels; -/// Storage functions -pub mod storage; - /// Vault pub mod vault; - -/// -/// Exports -/// -pub use credentials::*; -pub use error::*; -pub use identities::*; -pub use identity::*; -pub use purpose_key::*; -pub use purpose_keys::*; -pub use secure_channel::*; -pub use secure_channels::*; -pub use vault::*; - -pub use models::{Attributes, Credential, Identifier, TimestampInSeconds}; diff --git a/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs b/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs index c2af6b9cc82..3e62a36bfb7 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs @@ -1,6 +1,13 @@ -use crate::models::{Credential, PurposeKeyAttestation}; use minicbor::{Decode, Encode}; +use ockam_core::compat::string::String; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; + +use crate::alloc::string::ToString; +use crate::models::{Credential, CredentialData, PurposeKeyAttestation}; + /// [`Credential`] and the corresponding [`PurposeKeyAttestation`] that was used to issue that /// [`Credential`] and will be used to verify it #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] @@ -13,3 +20,82 @@ pub struct CredentialAndPurposeKey { /// [`Credential`] and will be used to verify it #[n(2)] pub purpose_key_attestation: PurposeKeyAttestation, } + +impl CredentialAndPurposeKey { + /// Encode the credential as a hex String + pub fn encode_as_string(&self) -> Result { + Ok(hex::encode(self.encode_as_bytes()?)) + } + + /// Encode the credential as a CBOR bytes + pub fn encode_as_bytes(&self) -> Result> { + Ok(minicbor::to_vec(self)?) + } + + /// Decode the credential from bytes + pub fn decode_from_bytes(bytes: &[u8]) -> Result { + Ok(minicbor::decode(bytes)?) + } + + /// Decode the credential from an hex string + pub fn decode_from_string(as_hex: &str) -> Result { + let hex_decoded = hex::decode(as_hex.as_bytes()) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + Self::decode_from_bytes(&hex_decoded) + } + + /// Return the encoded credential data + pub fn get_credential_data(&self) -> Result { + self.credential.get_credential_data() + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::identities; + use crate::models::CredentialSchemaIdentifier; + use crate::utils::AttributesBuilder; + + use super::*; + + #[tokio::test] + async fn test_encode_decode_as_bytes() -> Result<()> { + let credential = create_credential().await?; + let decoded = + CredentialAndPurposeKey::decode_from_bytes(&credential.encode_as_bytes().unwrap()); + assert!(decoded.is_ok()); + assert_eq!(decoded.unwrap(), credential); + + Ok(()) + } + + #[tokio::test] + async fn test_encode_decode_as_string() -> Result<()> { + let credential = create_credential().await?; + let decoded = + CredentialAndPurposeKey::decode_from_string(&credential.encode_as_string().unwrap()); + assert!(decoded.is_ok()); + assert_eq!(decoded.unwrap(), credential); + + Ok(()) + } + + /// HELPERS + async fn create_credential() -> Result { + let identities = identities(); + let issuer = identities.identities_creation().create_identity().await?; + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + identities + .credentials() + .credentials_creation() + .issue_credential(&issuer, &subject, attributes, Duration::from_secs(1)) + .await + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs b/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs index 69962d6608e..4c17bcb9b31 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs @@ -1,5 +1,9 @@ +use core::fmt::{Debug, Formatter}; + use minicbor::{Decode, Encode}; +use crate::alloc::string::ToString; + /// Identifier length pub const IDENTIFIER_LEN: usize = 20; @@ -9,10 +13,16 @@ pub const CHANGE_HASH_LEN: usize = 20; /// Unique identifier for an [`super::super::identity::Identity`] /// Equals to the [`ChangeHash`] of the first [`super::Change`] in the [`super::ChangeHistory`] /// Computed as truncated SHA256 of the first [`super::ChangeData`] CBOR binary -#[derive(Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq, Encode, Decode)] +#[derive(Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Encode, Decode)] #[cbor(transparent)] pub struct Identifier(#[cbor(n(0), with = "minicbor::bytes")] pub [u8; IDENTIFIER_LEN]); +impl Debug for Identifier { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.to_string()) + } +} + /// Unique identifier for a [`super::Change`] /// Computed as truncated SHA256 of the corresponding [`super::ChangeData`] CBOR binary #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode)] diff --git a/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs b/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs index e713a920ef8..4435ed228c2 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs @@ -1,12 +1,15 @@ +use ockam_core::compat::string::String; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; +use ockam_vault::{Signature, VerifyingPublicKey}; + +use crate::alloc::string::ToString; use crate::models::utils::get_versioned_data; use crate::models::{ Change, ChangeData, ChangeHistory, ChangeSignature, PrimaryPublicKey, VersionedData, }; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; -use ockam_vault::{Signature, VerifyingPublicKey}; - impl Change { /// Extract [`VersionedData`] pub fn get_versioned_data(&self) -> Result { @@ -27,10 +30,23 @@ impl ChangeHistory { Ok(minicbor::to_vec(self)?) } + /// Export [`ChangeHistory`] to a hex encoded string + pub fn export_as_string(&self) -> Result { + Ok(hex::encode(self.export()?)) + } + /// Import [`ChangeHistory`] from a binary format using CBOR pub fn import(data: &[u8]) -> Result { Ok(minicbor::decode(data)?) } + + /// Import [`ChangeHistory`] from a hex-encoded string + pub fn import_from_string(data: &str) -> Result { + Self::import( + &hex::decode(data) + .map_err(|e| Error::new(Origin::Identity, Kind::Serialization, e.to_string()))?, + ) + } } impl From for VerifyingPublicKey { diff --git a/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs b/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs index 5c4fe9bd9d2..0c42c0bf826 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs @@ -10,6 +10,11 @@ impl Credential { pub fn get_versioned_data(&self) -> Result { get_versioned_data(&self.data) } + + /// Extract [`CredentialData`] + pub fn get_credential_data(&self) -> Result { + CredentialData::get_data(&get_versioned_data(&self.data)?) + } } impl CredentialData { diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs index 5e641324b4e..582ab94bd5e 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs @@ -6,7 +6,7 @@ use crate::models::{ }; use crate::purpose_keys::storage::PurposeKeysRepository; use crate::{ - CredentialPurposeKey, CredentialPurposeKeyBuilder, IdentitiesKeys, IdentitiesReader, Identity, + CredentialPurposeKey, CredentialPurposeKeyBuilder, IdentitiesCreation, IdentitiesKeys, IdentityError, Purpose, PurposeKeyVerification, SecureChannelPurposeKey, SecureChannelPurposeKeyBuilder, TimestampInSeconds, Vault, }; @@ -15,7 +15,7 @@ use crate::{ #[derive(Clone)] pub struct PurposeKeyCreation { vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, } @@ -24,13 +24,13 @@ impl PurposeKeyCreation { /// Constructor. pub(crate) fn new( vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, ) -> Self { Self { vault, - identities_reader, + identities_creation, identity_keys, repository, } @@ -45,7 +45,7 @@ impl PurposeKeyCreation { pub fn purpose_keys_verification(&self) -> Arc { Arc::new(PurposeKeyVerification::new( self.vault.verifying_vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), )) } @@ -57,7 +57,7 @@ impl PurposeKeyCreation { SecureChannelPurposeKeyBuilder::new( Arc::new(Self::new( self.vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), self.identity_keys.clone(), self.repository.clone(), )), @@ -73,7 +73,7 @@ impl PurposeKeyCreation { CredentialPurposeKeyBuilder::new( Arc::new(Self::new( self.vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), self.identity_keys.clone(), self.repository.clone(), )), @@ -114,13 +114,7 @@ impl PurposeKeyCreation { created_at: TimestampInSeconds, expires_at: TimestampInSeconds, ) -> Result<(PurposeKeyAttestation, PurposeKeyAttestationData)> { - let identity_change_history = self.identities_reader.get_identity(&identifier).await?; - let identity = Identity::import_from_change_history( - Some(&identifier), - identity_change_history, - self.vault.verifying_vault.clone(), - ) - .await?; + let identity = self.identities_creation.get_identity(&identifier).await?; let attestation_data = PurposeKeyAttestationData { subject: identifier, diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs index 404f589f7fe..97380f549dc 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs @@ -4,7 +4,7 @@ use ockam_vault::VaultForVerifyingSignatures; use crate::models::{Identifier, PurposeKeyAttestation, PurposeKeyAttestationData}; use crate::utils::now; -use crate::{IdentitiesReader, Identity, IdentityError, TimestampInSeconds}; +use crate::{IdentitiesCreation, IdentityError, TimestampInSeconds}; /// We allow purpose keys to be created in the future related to this machine's time due to /// possible time dyssynchronization @@ -14,18 +14,18 @@ const MAX_ALLOWED_TIME_DRIFT: TimestampInSeconds = TimestampInSeconds(5); #[derive(Clone)] pub struct PurposeKeyVerification { verifying_vault: Arc, - identities_reader: Arc, + identities_creation: Arc, } impl PurposeKeyVerification { /// Create a new identities module pub(crate) fn new( verifying_vault: Arc, - identities_reader: Arc, + identities_creation: Arc, ) -> Self { Self { verifying_vault, - identities_reader, + identities_creation, } } } @@ -53,18 +53,10 @@ impl PurposeKeyVerification { return Err(IdentityError::PurposeKeyAttestationVerificationFailed.into()); } } - - let change_history = self - .identities_reader + let identity = self + .identities_creation .get_identity(&purpose_key_data.subject) .await?; - let identity = Identity::import_from_change_history( - Some(&purpose_key_data.subject), - change_history, - self.verifying_vault.clone(), - ) - .await?; - let latest_change = identity.get_latest_change()?; // TODO: We should inspect purpose_key_data.subject_latest_change_hash, the possibilities are: diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs index daab3d402e1..84630cd708a 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs @@ -1,13 +1,15 @@ use ockam_core::compat::sync::Arc; use crate::purpose_keys::storage::PurposeKeysRepository; -use crate::{IdentitiesKeys, IdentitiesReader, PurposeKeyCreation, PurposeKeyVerification, Vault}; +use crate::{ + IdentitiesCreation, IdentitiesKeys, PurposeKeyCreation, PurposeKeyVerification, Vault, +}; /// This struct supports all the services related to identities #[derive(Clone)] pub struct PurposeKeys { vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, } @@ -16,13 +18,13 @@ impl PurposeKeys { /// Create a new identities module pub fn new( vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, ) -> Self { Self { vault, - identities_reader, + identities_creation, identity_keys, repository, } @@ -37,7 +39,7 @@ impl PurposeKeys { pub fn purpose_keys_creation(&self) -> Arc { Arc::new(PurposeKeyCreation::new( self.vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), self.identity_keys.clone(), self.repository.clone(), )) @@ -47,16 +49,17 @@ impl PurposeKeys { pub fn purpose_keys_verification(&self) -> Arc { Arc::new(PurposeKeyVerification::new( self.vault.verifying_vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), )) } } #[cfg(test)] mod tests { - use crate::{identities, Purpose}; use ockam_core::Result; + use crate::{identities, Purpose}; + #[tokio::test] async fn create_purpose_keys() -> Result<()> { let identities = identities(); diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs index df4359d5cd4..3adfc63a4a3 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs @@ -1,5 +1,8 @@ -mod purpose_keys_repository_impl; -mod purpose_keys_repository_trait; +pub use purpose_keys_repository::*; +#[cfg(feature = "storage")] +pub use purpose_keys_repository_sql::*; -pub use purpose_keys_repository_impl::*; -pub use purpose_keys_repository_trait::*; +mod purpose_keys_repository; + +#[cfg(feature = "storage")] +mod purpose_keys_repository_sql; diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_trait.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository.rs similarity index 100% rename from implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_trait.rs rename to implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository.rs diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_impl.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_impl.rs deleted file mode 100644 index 3cbd77f20bf..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_impl.rs +++ /dev/null @@ -1,91 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::string::{String, ToString}; -use ockam_core::compat::sync::Arc; -use ockam_core::Result; - -use crate::identity::IdentityConstants; -use crate::models::{Identifier, PurposeKeyAttestation}; -use crate::purpose_keys::storage::{PurposeKeysReader, PurposeKeysRepository, PurposeKeysWriter}; -use crate::storage::{InMemoryStorage, Storage}; -use crate::Purpose; - -/// Storage for own [`super::super::super::purpose_key::PurposeKey`]s -#[derive(Clone)] -pub struct PurposeKeysStorage { - storage: Arc, -} - -#[async_trait] -impl PurposeKeysRepository for PurposeKeysStorage { - fn as_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_writer(&self) -> Arc { - Arc::new(self.clone()) - } -} - -impl PurposeKeysStorage { - /// Create a new Storage - pub fn new(storage: Arc) -> Self { - Self { storage } - } - - /// Create a new in-memory Storage - pub fn create() -> Arc { - Arc::new(Self::new(InMemoryStorage::create())) - } - - fn key(purpose: Purpose) -> String { - let key = match purpose { - Purpose::SecureChannel => IdentityConstants::SECURE_CHANNEL_PURPOSE_KEY, - Purpose::Credentials => IdentityConstants::CREDENTIALS_PURPOSE_KEY, - }; - - key.to_string() - } -} - -#[async_trait] -impl PurposeKeysWriter for PurposeKeysStorage { - async fn set_purpose_key( - &self, - subject: &Identifier, - purpose: Purpose, - purpose_key_attestation: &PurposeKeyAttestation, - ) -> Result<()> { - let key = Self::key(purpose); - self.storage - .set( - &subject.to_string(), - key.to_string(), - minicbor::to_vec(purpose_key_attestation)?, - ) - .await - } - - async fn delete_purpose_key(&self, subject: &Identifier, purpose: Purpose) -> Result<()> { - let key = Self::key(purpose); - self.storage - .del(&subject.to_string(), &key.to_string()) - .await - } -} - -#[async_trait] -impl PurposeKeysReader for PurposeKeysStorage { - async fn retrieve_purpose_key( - &self, - identifier: &Identifier, - purpose: Purpose, - ) -> Result> { - let key = Self::key(purpose); - if let Some(data) = self.storage.get(&identifier.to_string(), &key).await? { - Ok(Some(minicbor::decode(&data)?)) - } else { - Ok(None) - } - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_sql.rs new file mode 100644 index 00000000000..e631eaead8d --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_sql.rs @@ -0,0 +1,196 @@ +use core::str::FromStr; + +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::string::{String, ToString}; +use ockam_core::compat::sync::Arc; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::identity::IdentityConstants; +use crate::models::{Identifier, PurposeKeyAttestation}; +use crate::purpose_keys::storage::{PurposeKeysReader, PurposeKeysRepository, PurposeKeysWriter}; +use crate::Purpose; + +/// Storage for own [`super::super::super::purpose_key::PurposeKey`]s +#[derive(Clone)] +pub struct PurposeKeysSqlxDatabase { + database: Arc, +} + +#[async_trait] +impl PurposeKeysRepository for PurposeKeysSqlxDatabase { + fn as_reader(&self) -> Arc { + Arc::new(self.clone()) + } + + fn as_writer(&self) -> Arc { + Arc::new(self.clone()) + } +} + +impl PurposeKeysSqlxDatabase { + /// Create a new database for purpose keys + pub fn new(database: Arc) -> Self { + debug!("create a repository for purpose keys"); + Self { database } + } + + /// Create a new in-memory database for purpose keys + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("purpose keys")))) + } +} + +#[async_trait] +impl PurposeKeysWriter for PurposeKeysSqlxDatabase { + async fn set_purpose_key( + &self, + subject: &Identifier, + purpose: Purpose, + purpose_key_attestation: &PurposeKeyAttestation, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO purpose_key VALUES (?, ?, ?)") + .bind(subject.to_sql()) + .bind(purpose.to_sql()) + .bind(minicbor::to_vec(purpose_key_attestation)?.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_purpose_key(&self, subject: &Identifier, purpose: Purpose) -> Result<()> { + let query = query("DELETE FROM purpose_key WHERE identifier = ? and purpose = ?") + .bind(subject.to_sql()) + .bind(purpose.to_sql()); + query.execute(&self.database.pool).await.void() + } +} + +#[async_trait] +impl PurposeKeysReader for PurposeKeysSqlxDatabase { + async fn retrieve_purpose_key( + &self, + identifier: &Identifier, + purpose: Purpose, + ) -> Result> { + let query = query_as("SELECT * FROM purpose_key WHERE identifier=$1 and purpose=$2") + .bind(identifier.to_sql()) + .bind(purpose.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.purpose_key_attestation()).transpose()?) + } +} + +#[derive(FromRow)] +pub(crate) struct PurposeKeyRow { + // The identifier who is using this key + identifier: String, + // Purpose of the key (signing, encrypting, etc...) + purpose: String, + // Attestation that this key is valid + purpose_key_attestation: Vec, +} + +impl PurposeKeyRow { + #[allow(dead_code)] + pub(crate) fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + #[allow(dead_code)] + pub(crate) fn purpose(&self) -> Result { + match self.purpose.as_str() { + IdentityConstants::SECURE_CHANNEL_PURPOSE_KEY => Ok(Purpose::SecureChannel), + IdentityConstants::CREDENTIALS_PURPOSE_KEY => Ok(Purpose::Credentials), + _ => Err(ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + format!("unknown purpose {}", self.purpose), + )), + } + } + + pub(crate) fn purpose_key_attestation(&self) -> Result { + Ok(minicbor::decode(self.purpose_key_attestation.as_slice())?) + } +} + +impl ToSqlxType for Purpose { + fn to_sql(&self) -> SqlxType { + match self { + Purpose::SecureChannel => { + SqlxType::Text(IdentityConstants::SECURE_CHANNEL_PURPOSE_KEY.to_string()) + } + Purpose::Credentials => { + SqlxType::Text(IdentityConstants::CREDENTIALS_PURPOSE_KEY.to_string()) + } + } + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use tempfile::NamedTempFile; + + use ockam_vault::ECDSASHA256CurveP256Signature; + + use crate::models::PurposeKeyAttestationSignature; + + use super::*; + + #[tokio::test] + async fn test_purpose_keys_repository() -> Result<()> { + let db_file = NamedTempFile::new().unwrap(); + let repository = create_repository(db_file.path()).await?; + + let identity1 = Identifier::try_from("Ie86be15e83d1c93e24dd1967010b01b6df491b45").unwrap(); + + // A purpose key can be stored and retrieved + let attestation1 = PurposeKeyAttestation { + data: vec![1, 2, 3], + signature: PurposeKeyAttestationSignature::ECDSASHA256CurveP256( + ECDSASHA256CurveP256Signature([1; 64]), + ), + }; + repository + .set_purpose_key(&identity1, Purpose::Credentials, &attestation1) + .await?; + + let result = repository + .get_purpose_key(&identity1, Purpose::Credentials) + .await?; + assert_eq!(result, attestation1); + + // the attestation can be updated + let attestation2 = PurposeKeyAttestation { + data: vec![4, 5, 6], + signature: PurposeKeyAttestationSignature::ECDSASHA256CurveP256( + ECDSASHA256CurveP256Signature([1; 64]), + ), + }; + repository + .set_purpose_key(&identity1, Purpose::Credentials, &attestation2) + .await?; + + let result = repository + .get_purpose_key(&identity1, Purpose::Credentials) + .await?; + assert_eq!(result, attestation2); + + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(PurposeKeysSqlxDatabase::new(Arc::new(db)))) + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs index 8b532ba83fa..908eb1c0d00 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs @@ -4,25 +4,25 @@ use ockam_core::compat::{boxed::Box, sync::Arc, vec::Vec}; use ockam_core::Result; use ockam_core::{async_trait, RelayMessage}; -use crate::identities::IdentitiesRepository; use crate::secure_channel::local_info::IdentitySecureChannelLocalInfo; +use crate::IdentityAttributesRepository; /// Access control checking that message senders have a specific set of attributes #[derive(Clone)] pub struct CredentialAccessControl { required_attributes: Vec<(Vec, Vec)>, - storage: Arc, + identity_attributes_repository: Arc, } impl CredentialAccessControl { /// Create a new credential access control pub fn new( required_attributes: &[(Vec, Vec)], - storage: Arc, + identity_attributes_repository: Arc, ) -> Self { Self { required_attributes: required_attributes.to_vec(), - storage, + identity_attributes_repository, } } } @@ -44,7 +44,7 @@ impl IncomingAccessControl for CredentialAccessControl { IdentitySecureChannelLocalInfo::find_info(relay_message.local_message()) { let attributes = match self - .storage + .identity_attributes_repository .get_attributes(&msg_identity_id.their_identity_id()) .await? { diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs index 735760b1015..bd040d5ef2e 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs @@ -600,13 +600,13 @@ mod tests { use super::*; use hex::decode; use ockam_core::Result; - use ockam_node::InMemoryKeyValueStorage; + use ockam_vault::storage::SecretsSqlxDatabase; use ockam_vault::{SoftwareVaultForSecureChannels, X25519SecretKey}; #[tokio::test] async fn test_initialization() -> Result<()> { let vault = Arc::new(SoftwareVaultForSecureChannels::new( - InMemoryKeyValueStorage::create(), + SecretsSqlxDatabase::create(), )); let static_key = vault.generate_static_x25519_secret_key().await?; diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs index 179e349641d..0a341335251 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs @@ -1,17 +1,16 @@ use minicbor::{Decode, Encode}; +use tracing::{debug, warn}; + use ockam_core::compat::string::ToString; use ockam_core::compat::sync::Arc; use ockam_core::compat::{boxed::Box, vec::Vec}; use ockam_core::{async_trait, Result}; use ockam_vault::{AeadSecretKeyHandle, X25519PublicKey}; -use tracing::{debug, warn}; use crate::models::{ ChangeHistory, CredentialAndPurposeKey, Identifier, PurposeKeyAttestation, PurposePublicKey, }; -use crate::{ - Identities, Identity, IdentityError, SecureChannelTrustInfo, TrustContext, TrustPolicy, -}; +use crate::{Identities, IdentityError, SecureChannelTrustInfo, TrustContext, TrustPolicy}; /// Interface for a state machine in a key exchange protocol #[async_trait] @@ -100,11 +99,7 @@ impl CommonStateMachine { /// pub(super) async fn make_identity_payload(&self) -> Result> { // prepare the payload that will be sent either in message 2 or message 3 - let change_history = self - .identities - .repository() - .get_identity(&self.identifier) - .await?; + let change_history = self.identities.get_change_history(&self.identifier).await?; let payload = IdentityAndCredentials { change_history, purpose_key_attestation: self.purpose_key_attestation.clone(), @@ -121,26 +116,17 @@ impl CommonStateMachine { peer: IdentityAndCredentials, peer_public_key: &X25519PublicKey, ) -> Result<()> { - let identity = Identity::import_from_change_history( - None, - peer.change_history.clone(), - self.identities.vault().verifying_vault, - ) - .await?; - - self.identities + let identifier = self + .identities .identities_creation() - .update_identity(&identity) + .import_from_change_history(None, peer.change_history.clone()) .await?; let purpose_key = self .identities .purpose_keys() .purpose_keys_verification() - .verify_purpose_key_attestation( - Some(identity.identifier()), - &peer.purpose_key_attestation, - ) + .verify_purpose_key_attestation(Some(&identifier), &peer.purpose_key_attestation) .await?; match &purpose_key.public_key { @@ -150,13 +136,13 @@ impl CommonStateMachine { } } PurposePublicKey::CredentialSigning(_) => { - return Err(IdentityError::InvalidKeyType.into()) + return Err(IdentityError::InvalidKeyType.into()); } } - self.verify_credentials(identity.identifier(), peer.credentials) + self.verify_credentials(&identifier, peer.credentials) .await?; - self.their_identifier = Some(identity.identifier().clone()); + self.their_identifier = Some(identifier.clone()); Ok(()) } @@ -171,6 +157,10 @@ impl CommonStateMachine { let trust_info = SecureChannelTrustInfo::new(their_identifier.clone()); let trusted = self.trust_policy.check(&trust_info).await?; if !trusted { + warn!( + "the trust policy checking the identifier {} failed", + their_identifier + ); // TODO: Shutdown? Communicate error? return Err(IdentityError::SecureChannelTrustCheckFailed.into()); } @@ -191,7 +181,7 @@ impl CommonStateMachine { .credentials_verification() .receive_presented_credential( their_identifier, - &[trust_context.authority()?.identifier().clone()], + &trust_context.authorities(), credential, ) .await; diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs index 41a4868f54c..bc7cf55f2d5 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs @@ -49,21 +49,20 @@ impl IdentityChannelListener { /// If credentials are not provided via list in options /// get them from the trust context async fn get_credentials(&self, ctx: &mut Context) -> Result> { - let credentials = if self.options.credentials.is_empty() { + let credential = if self.options.credentials.is_empty() { if let Some(trust_context) = &self.options.trust_context { - vec![ - trust_context - .authority()? - .credential(ctx, &self.identifier) - .await?, - ] + trust_context + .get_credential(ctx, &self.identifier) + .await? + .into_iter() + .collect::>() } else { vec![] } } else { self.options.credentials.clone() }; - Ok(credentials) + Ok(credential) } } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs index 3d023617518..335b5d5b3d7 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs @@ -10,7 +10,9 @@ use crate::secure_channel::{ Addresses, IdentityChannelListener, Role, SecureChannelListenerOptions, SecureChannelOptions, SecureChannelRegistry, }; -use crate::{SecureChannel, SecureChannelListener, SecureChannelsBuilder, Vault}; +#[cfg(feature = "storage")] +use crate::SecureChannelsBuilder; +use crate::{SecureChannel, SecureChannelListener, Vault}; /// Identity implementation #[derive(Clone)] @@ -47,6 +49,7 @@ impl SecureChannels { } /// Create a builder for secure channels + #[cfg(feature = "storage")] pub fn builder() -> SecureChannelsBuilder { SecureChannelsBuilder { identities_builder: Identities::builder(), diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs index 19b5509e5d7..e140700f8c5 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs @@ -1,10 +1,11 @@ use ockam_core::compat::sync::Arc; +use ockam_vault::storage::SecretsRepository; -use crate::identities::{Identities, IdentitiesRepository}; +use crate::identities::{ChangeHistoryRepository, Identities}; use crate::secure_channel::SecureChannelRegistry; use crate::secure_channels::SecureChannels; -use crate::storage::Storage; -use crate::{IdentitiesBuilder, Vault, VaultStorage}; +use crate::storage::PurposeKeysRepository; +use crate::{IdentitiesBuilder, IdentityAttributesRepository, Vault}; /// This struct supports all the services related to secure channels #[derive(Clone)] @@ -15,14 +16,15 @@ pub struct SecureChannelsBuilder { } /// Create default, in-memory, secure channels (mostly for examples and testing) +#[cfg(feature = "storage")] pub fn secure_channels() -> Arc { SecureChannels::builder().build() } impl SecureChannelsBuilder { - /// With Software Vault with given Storage - pub fn with_vault_storage(mut self, storage: VaultStorage) -> Self { - self.identities_builder = self.identities_builder.with_vault_storage(storage); + /// With Software Vault with given secrets repository + pub fn with_secrets_repository(mut self, repository: Arc) -> Self { + self.identities_builder = self.identities_builder.with_secrets_repository(repository); self } @@ -32,17 +34,36 @@ impl SecureChannelsBuilder { self } - /// Set a specific storage for the identities repository - pub fn with_identities_storage(mut self, storage: Arc) -> Self { - self.identities_builder = self.identities_builder.with_identities_storage(storage); + /// Set a specific identities repository + pub fn with_change_history_repository( + mut self, + repository: Arc, + ) -> Self { + self.identities_builder = self + .identities_builder + .with_change_history_repository(repository); self } - /// Set a specific identities repository - pub fn with_identities_repository(mut self, repository: Arc) -> Self { + /// Set a specific identity attributes repository + pub fn with_identity_attributes_repository( + mut self, + repository: Arc, + ) -> Self { + self.identities_builder = self + .identities_builder + .with_identity_attributes_repository(repository); + self + } + + /// Set a specific purpose keys repository + pub fn with_purpose_keys_repository( + mut self, + repository: Arc, + ) -> Self { self.identities_builder = self .identities_builder - .with_identities_repository(repository); + .with_purpose_keys_repository(repository); self } @@ -50,7 +71,8 @@ impl SecureChannelsBuilder { pub fn with_identities(mut self, identities: Arc) -> Self { self.identities_builder = self .identities_builder - .with_identities_repository(identities.repository()) + .with_change_history_repository(identities.change_history_repository()) + .with_identity_attributes_repository(identities.identity_attributes_repository()) .with_vault(identities.vault()) .with_purpose_keys_repository(identities.purpose_keys_repository()); self diff --git a/implementations/rust/ockam/ockam_identity/src/storage/lmdb_storage.rs b/implementations/rust/ockam/ockam_identity/src/storage/lmdb_storage.rs deleted file mode 100644 index c6fb0507589..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/lmdb_storage.rs +++ /dev/null @@ -1,146 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::string::String; -use ockam_core::compat::sync::Arc; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_node::tokio::task::{self, JoinError}; - -use crate::storage::Storage; - -use core::str; -use lmdb::{Cursor, Database, Environment, Transaction}; -use std::fmt; -use std::path::Path; -use tokio_retry::strategy::{jitter, FixedInterval}; -use tokio_retry::Retry; -use tracing::debug; - -/// Storage using the LMDB database -#[derive(Clone)] -pub struct LmdbStorage { - /// lmdb da - pub env: Arc, - /// lmdb database file - pub map: Database, -} - -impl fmt::Debug for LmdbStorage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Store") - } -} - -impl LmdbStorage { - /// Constructor - pub async fn new>(p: P) -> Result { - // creating a new database might be failing a few times - // if the files are currently being held by another pod which is shutting down. - // In that case we retry a few times, between 1 and 10 seconds. - let retry_strategy = FixedInterval::from_millis(1000) - .map(jitter) // add jitter to delays - .take(10); // limit to 10 retries - - let path: &Path = p.as_ref(); - Retry::spawn(retry_strategy, || async { Self::make(path).await }).await - } - - async fn make(p: &Path) -> Result { - debug!("create the LMDB database"); - std::fs::create_dir_all(p.parent().unwrap()) - .map_err(|e| Error::new(Origin::Node, Kind::Io, e))?; - let p = p.to_path_buf(); - let env = Environment::new() - .set_flags(lmdb::EnvironmentFlags::NO_SUB_DIR | lmdb::EnvironmentFlags::NO_TLS) - .set_max_dbs(1) - .open(p.as_ref()) - .map_err(map_lmdb_err)?; - let map = env - .create_db(Some("map"), lmdb::DatabaseFlags::empty()) - .map_err(map_lmdb_err)?; - Ok(LmdbStorage { - env: Arc::new(env), - map, - }) - } - - /// Write a new binary value for a given key in the database - pub async fn write(&self, k: String, v: Vec) -> Result<()> { - let d = self.clone(); - let t = move || { - let mut w = d.env.begin_rw_txn().map_err(map_lmdb_err)?; - w.put(d.map, &k, &v, lmdb::WriteFlags::empty()) - .map_err(map_lmdb_err)?; - w.commit().map_err(map_lmdb_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - /// Delete a database entry - pub async fn delete(&self, k: String) -> Result<()> { - let d = self.clone(); - let t = move || { - let mut w = d.env.begin_rw_txn().map_err(map_lmdb_err)?; - match w.del(d.map, &k, None) { - Ok(()) | Err(lmdb::Error::NotFound) => {} - Err(e) => return Err(map_lmdb_err(e)), - } - w.commit().map_err(map_lmdb_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } -} - -#[async_trait] -impl Storage for LmdbStorage { - async fn get(&self, id: &str, key: &str) -> Result>> { - let d = self.clone(); - let k = format!("{id}:{key}"); - let t = move || { - let r = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - match r.get(d.map, &k) { - Ok(value) => Ok(Some(Vec::from(value))), - Err(lmdb::Error::NotFound) => Ok(None), - Err(e) => Err(map_lmdb_err(e)), - } - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set(&self, id: &str, key: String, val: Vec) -> Result<()> { - self.write(format!("{id}:{key}"), val).await - } - - async fn del(&self, id: &str, key: &str) -> Result<()> { - self.delete(format!("{id}:{key}")).await - } - - async fn keys(&self, namespace: &str) -> Result> { - let d = self.clone(); - let suffix = format!(":{}", namespace); - let t = move || { - let r = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - let mut cursor = r.open_ro_cursor(d.map).map_err(map_lmdb_err)?; - Ok(cursor - .iter() - .filter_map(|r| { - let (k, _) = r.unwrap(); - let key = str::from_utf8(k).unwrap(); - key.rsplit_once(&suffix).map(|(k, _)| k.to_string()) - }) - .collect()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_lmdb_err(err: lmdb::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} diff --git a/implementations/rust/ockam/ockam_identity/src/storage/memory.rs b/implementations/rust/ockam/ockam_identity/src/storage/memory.rs deleted file mode 100644 index fb0822837b8..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/memory.rs +++ /dev/null @@ -1,76 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::{ - boxed::Box, - collections::BTreeMap, - string::{String, ToString}, - sync::{Arc, RwLock}, - vec::Vec, -}; -use ockam_core::Result; - -use crate::storage::Storage; - -/// Non-persistent table stored in RAM -#[derive(Clone, Default)] -pub struct InMemoryStorage { - map: Arc>>, -} - -type Attributes = BTreeMap>; - -impl InMemoryStorage { - /// Constructor - pub fn new() -> Self { - Default::default() - } - - /// Constructor - pub fn create() -> Arc { - Arc::new(Self::new()) - } -} - -#[async_trait] -impl Storage for InMemoryStorage { - async fn get(&self, id: &str, namespace: &str) -> Result>> { - let m = self.map.read().unwrap(); - if let Some(a) = m.get(namespace) { - return Ok(a.get(id).cloned()); - } - Ok(None) - } - - async fn set(&self, id: &str, namespace: String, val: Vec) -> Result<()> { - let mut m = self.map.write().unwrap(); - match m.get_mut(&namespace) { - Some(a) => { - a.insert(id.to_string(), val); - } - None => { - m.insert(namespace, BTreeMap::from([(id.to_string(), val)])); - } - } - Ok(()) - } - - async fn del(&self, id: &str, namespace: &str) -> Result<()> { - let mut m = self.map.write().unwrap(); - if let Some(a) = m.get_mut(namespace) { - a.remove(id); - if a.is_empty() { - m.remove(namespace); - } - } - Ok(()) - } - - async fn keys(&self, namespace: &str) -> Result> { - Ok(self - .map - .read() - .unwrap() - .get(namespace) - .map(|m| m.keys().cloned().collect()) - .unwrap_or_default()) - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/storage/mod.rs b/implementations/rust/ockam/ockam_identity/src/storage/mod.rs deleted file mode 100644 index 4fd5b5a704c..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[allow(clippy::module_inception)] -mod storage; - -mod memory; - -/// LMDB implementation of the Storage trait -#[cfg(feature = "std")] -pub mod lmdb_storage; -/// Sqlite implementation of the Storage trait -#[cfg(feature = "sqlite")] -pub mod sqlite_storage; - -pub use memory::*; -pub use storage::*; - -#[cfg(feature = "std")] -pub use lmdb_storage::*; - -#[cfg(feature = "sqlite")] -pub use sqlite_storage::*; diff --git a/implementations/rust/ockam/ockam_identity/src/storage/sqlite_storage.rs b/implementations/rust/ockam/ockam_identity/src/storage/sqlite_storage.rs deleted file mode 100644 index 5da866126d8..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/sqlite_storage.rs +++ /dev/null @@ -1,192 +0,0 @@ -use core::str; -use ockam_core::async_trait; -use ockam_core::compat::sync::{Arc, Mutex}; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_node::tokio::task::{self, JoinError}; -use rusqlite::{params, Connection}; -use std::fmt; -use std::path::Path; -use tokio_retry::strategy::{jitter, FixedInterval}; -use tokio_retry::Retry; -use tracing::debug; - -use Storage; - -/// Storage using the Sqlite database -#[derive(Clone)] -pub struct SqliteStorage { - /// Sqlite Connection - conn: Arc>, -} - -impl fmt::Debug for SqliteStorage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("SqliteStore") - } -} - -impl SqliteStorage { - const CREATE_IDENTITY_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS identity ( - id INTEGER PRIMARY KEY, - identity_id TEXT NOT NULL, - key TEXT NOT NULL, - value BLOB - );"; - const CREATE_IDENTITY_INDEX_SQL: &str = - "CREATE UNIQUE INDEX IF NOT EXISTS idx_identity_id_key ON identity (identity_id, key);"; - - const CREATE_POLICY_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS policy ( - id INTEGER PRIMARY KEY, - resource TEXT NOT NULL, - action TEXT NOT NULL, - value BLOB - );"; - const CREATE_POLICY_INDEX_SQL: &str = "CREATE UNIQUE INDEX IF NOT EXISTS idx_policy_resource_action ON policy (resource, action);"; - - /// Constructor - pub async fn new>(p: P) -> Result { - // Not sure we need this - // creating a new database might be failing a few times - // if the files are currently being held by another pod which is shutting down. - // In that case we retry a few times, between 1 and 10 seconds. - let retry_strategy = FixedInterval::from_millis(1000) - .map(jitter) // add jitter to delays - .take(10); // limit to 10 retries - - let path: &Path = p.as_ref(); - Retry::spawn(retry_strategy, || async { Self::make(path).await }).await - } - - async fn make(p: &Path) -> Result { - debug!("create the Sqlite database"); - let p = p.to_path_buf(); - // Creates database file if it doesn't exist - let conn = Connection::open(p).map_err(map_sqlite_err)?; - let _ = conn - .execute_batch( - &("PRAGMA encoding = 'UTF-8';".to_owned() - + SqliteStorage::CREATE_IDENTITY_TABLE_SQL - + SqliteStorage::CREATE_IDENTITY_INDEX_SQL - + SqliteStorage::CREATE_POLICY_TABLE_SQL - + SqliteStorage::CREATE_POLICY_INDEX_SQL), - ) - .map_err(map_sqlite_err)?; - Ok(SqliteStorage { - conn: Arc::new(Mutex::new(conn)), - }) - } - - /// Getter for Sqlite Connection - pub fn conn(&self) -> Arc> { - Arc::clone(&self.conn) - } -} - -#[async_trait] -impl Storage for SqliteStorage { - async fn get(&self, id: &str, key: &str) -> Result>> { - let conn = self.conn(); - let id = String::from(id); - let key = String::from(key); - - let t = move || { - let conn = conn.lock().unwrap(); - let result = conn - .query_row::, _, _>( - "SELECT value FROM identity WHERE identity_id = ?1 AND key = ?2;", - params![id, key], - |row| row.get(0), - ) - .map_err(map_sqlite_err)?; - Ok(Some(result)) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set(&self, id: &str, key: String, val: Vec) -> Result<()> { - let conn = self.conn(); - let id = String::from(id); - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "INSERT OR REPLACE INTO identity (identity_id, key, value) VALUES (?1, ?2, ?3)", - params![id, key, val], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn del(&self, id: &str, key: &str) -> Result<()> { - let conn = self.conn(); - let id = String::from(id); - let key = String::from(key); - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "DELETE FROM identity WHERE identity_id = ?1 AND key = ?2;", - params![id, key], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn keys(&self, namespace: &str) -> Result> { - let conn = self.conn(); - let namespace = String::from(namespace); - let t = move || { - let conn = conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT identity_id FROM identity WHERE key = ?1;") - .map_err(map_sqlite_err)?; - let result: Result> = stmt - .query_map(params![namespace], |row| row.get(0)) - .map_err(map_sqlite_err)? - .map(|value| value.map_err(map_sqlite_err)) - .collect(); - result - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_sqlite_err(err: rusqlite::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -#[cfg(test)] -mod test { - use super::*; - use tempfile::NamedTempFile; - - #[tokio::test] - async fn test_basic_functionality() -> Result<()> { - let temp_path = NamedTempFile::new().unwrap().into_temp_path(); - let db = SqliteStorage::new(temp_path.to_path_buf()).await?; - - db.set("1", String::from("2"), vec![1, 2, 3, 4]).await?; - assert_eq!( - db.get("1", "2").await?, - Some(vec![1, 2, 3, 4]), - "Verify set and get" - ); - assert_eq!(db.keys("2").await?.len(), 1, "Verify keys"); - - db.set("2", String::from("2"), vec![1, 2, 3, 4]).await?; - assert_eq!(db.keys("2").await?.len(), 2, "Verify multiple keys"); - - db.del("2", "2").await?; - assert_eq!(db.keys("2").await?.len(), 1, "Verify delete"); - - Ok(()) - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/storage/storage.rs b/implementations/rust/ockam/ockam_identity/src/storage/storage.rs deleted file mode 100644 index 01b664fe7c3..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/storage.rs +++ /dev/null @@ -1,20 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::{boxed::Box, string::String, vec::Vec}; -use ockam_core::Result; - -/// Storage for Authenticated data -#[async_trait] -pub trait Storage: Send + Sync + 'static { - /// Get entry - async fn get(&self, id: &str, key: &str) -> Result>>; - - /// Set entry - async fn set(&self, id: &str, key: String, val: Vec) -> Result<()>; - - /// Delete entry - async fn del(&self, id: &str, key: &str) -> Result<()>; - - /// List all keys of a given "type". TODO: we shouldn't store different things on a single - /// store. - async fn keys(&self, namespace: &str) -> Result>; -} diff --git a/implementations/rust/ockam/ockam_identity/src/vault.rs b/implementations/rust/ockam/ockam_identity/src/vault.rs index 53ccbaa8895..0a6ed075311 100644 --- a/implementations/rust/ockam/ockam_identity/src/vault.rs +++ b/implementations/rust/ockam/ockam_identity/src/vault.rs @@ -1,14 +1,14 @@ use ockam_core::compat::sync::Arc; -use ockam_node::{InMemoryKeyValueStorage, KeyValueStorage}; -use ockam_vault::legacy::{KeyId, StoredSecret}; +#[cfg(feature = "storage")] +use ockam_node::database::SqlxDatabase; +use ockam_vault::storage::SecretsRepository; +#[cfg(feature = "storage")] +use ockam_vault::storage::SecretsSqlxDatabase; use ockam_vault::{ SoftwareVaultForSecureChannels, SoftwareVaultForSigning, SoftwareVaultForVerifyingSignatures, VaultForSecureChannels, VaultForSigning, VaultForVerifyingSignatures, }; -/// Storage for Vault persistent values -pub type VaultStorage = Arc>; - /// Vault #[derive(Clone)] pub struct Vault { @@ -38,7 +38,8 @@ impl Vault { } } - /// Create Software implementation Vault with [`InMemoryKeyVaultStorage`] + /// Create Software implementation Vault with an in-memory storage + #[cfg(feature = "storage")] pub fn create() -> Self { Self::new( Self::create_identity_vault(), @@ -48,25 +49,24 @@ impl Vault { ) } - /// Create [`SoftwareVaultForSigning`] with [`InMemoryKeyVaultStorage`] + /// Create [`SoftwareVaultForSigning`] with an in-memory storage + #[cfg(feature = "storage")] pub fn create_identity_vault() -> Arc { - Arc::new(SoftwareVaultForSigning::new( - InMemoryKeyValueStorage::create(), - )) + Arc::new(SoftwareVaultForSigning::new(SecretsSqlxDatabase::create())) } - /// Create [`SoftwareSecureChannelVault`] with [`InMemoryKeyVaultStorage`] + /// Create [`SoftwareSecureChannelVault`] with an in-memory storage + #[cfg(feature = "storage")] pub fn create_secure_channel_vault() -> Arc { Arc::new(SoftwareVaultForSecureChannels::new( - InMemoryKeyValueStorage::create(), + SecretsSqlxDatabase::create(), )) } - /// Create [`SoftwareVaultForSigning`] with [`InMemoryKeyVaultStorage`] + /// Create [`SoftwareVaultForSigning`] with an in-memory storage + #[cfg(feature = "storage")] pub fn create_credential_vault() -> Arc { - Arc::new(SoftwareVaultForSigning::new( - InMemoryKeyValueStorage::create(), - )) + Arc::new(SoftwareVaultForSigning::new(SecretsSqlxDatabase::create())) } /// Create [`SoftwareVaultForVerifyingSignatures`] @@ -76,21 +76,28 @@ impl Vault { } impl Vault { - /// Create Software Vaults with [`PersistentStorage`] with a given path - #[cfg(feature = "std")] + /// Create Software Vaults and persist them to a given path + #[cfg(feature = "storage")] pub async fn create_with_persistent_storage_path( path: &std::path::Path, ) -> ockam_core::Result { - let storage = ockam_vault::storage::PersistentStorage::create(path).await?; - Ok(Self::create_with_persistent_storage(storage)) + Ok(Self::create_with_database(Arc::new( + SqlxDatabase::create(path).await?, + ))) + } + + /// Create Software Vaults and persist them to a sql database + #[cfg(feature = "storage")] + pub fn create_with_database(database: Arc) -> Vault { + Self::create_with_secrets_repository(Arc::new(SecretsSqlxDatabase::new(database))) } - /// Create Software Vaults with a given [`VaultStorage`]r - pub fn create_with_persistent_storage(storage: VaultStorage) -> Vault { + /// Create Software Vaults with a given secrets repository + pub fn create_with_secrets_repository(repository: Arc) -> Vault { Self::new( - Arc::new(SoftwareVaultForSigning::new(storage.clone())), - Arc::new(SoftwareVaultForSecureChannels::new(storage.clone())), - Arc::new(SoftwareVaultForSigning::new(storage)), + Arc::new(SoftwareVaultForSigning::new(repository.clone())), + Arc::new(SoftwareVaultForSecureChannels::new(repository.clone())), + Arc::new(SoftwareVaultForSigning::new(repository.clone())), Arc::new(SoftwareVaultForVerifyingSignatures {}), ) } diff --git a/implementations/rust/ockam/ockam_identity/tests/channel.rs b/implementations/rust/ockam/ockam_identity/tests/channel.rs index 0759c293a23..fc4821abb6c 100644 --- a/implementations/rust/ockam/ockam_identity/tests/channel.rs +++ b/implementations/rust/ockam/ockam_identity/tests/channel.rs @@ -182,7 +182,7 @@ async fn test_channel_send_credentials(context: &mut Context) -> Result<()> { let alice_attributes = secure_channels .identities() - .repository() + .identity_attributes_repository() .get_attributes(&alice) .await? .unwrap(); @@ -201,7 +201,7 @@ async fn test_channel_send_credentials(context: &mut Context) -> Result<()> { let bob_attributes = secure_channels .identities() - .repository() + .identity_attributes_repository() .get_attributes(&bob) .await? .unwrap(); diff --git a/implementations/rust/ockam/ockam_identity/tests/credentials.rs b/implementations/rust/ockam/ockam_identity/tests/credentials.rs index 47bcb17623b..39d7a7ea54f 100644 --- a/implementations/rust/ockam/ockam_identity/tests/credentials.rs +++ b/implementations/rust/ockam/ockam_identity/tests/credentials.rs @@ -18,7 +18,7 @@ async fn full_flow_oneway(ctx: &mut Context) -> Result<()> { let secure_channels = secure_channels(); let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); - let identities_repository = identities.repository(); + let identity_attributes_repository = identities.identity_attributes_repository(); let credentials = identities.credentials(); let credentials_service = identities.credentials_server(); @@ -82,7 +82,7 @@ async fn full_flow_oneway(ctx: &mut Context) -> Result<()> { .present_credential(ctx, route![channel, "credential_exchange"], credential) .await?; - let attrs = identities_repository + let attrs = identity_attributes_repository .get_attributes(&client) .await? .unwrap(); @@ -99,7 +99,7 @@ async fn full_flow_twoway(ctx: &mut Context) -> Result<()> { let secure_channels = secure_channels(); let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); - let identities_repository = identities.repository(); + let identity_attributes_repository = identities.identity_attributes_repository(); let credentials = identities.credentials(); let credentials_service = identities.credentials_server(); @@ -173,12 +173,12 @@ async fn full_flow_twoway(ctx: &mut Context) -> Result<()> { .present_credential_mutual( ctx, route![channel, "credential_exchange"], - trust_context.authorities().await?.as_slice(), + &trust_context.authorities(), credential, ) .await?; - let attrs1 = identities_repository + let attrs1 = identity_attributes_repository .get_attributes(&client1) .await? .unwrap(); @@ -192,7 +192,7 @@ async fn full_flow_twoway(ctx: &mut Context) -> Result<()> { b"true" ); - let attrs2 = identities_repository + let attrs2 = identity_attributes_repository .get_attributes(&client2) .await? .unwrap(); @@ -210,7 +210,7 @@ async fn access_control(ctx: &mut Context) -> Result<()> { let secure_channels = secure_channels(); let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); - let identities_repository = identities.repository(); + let identity_attributes_repository = identities.identity_attributes_repository(); let credentials = identities.credentials(); let credentials_service = identities.credentials_server(); @@ -275,7 +275,7 @@ async fn access_control(ctx: &mut Context) -> Result<()> { let required_attributes = vec![(b"is_superuser".to_vec(), b"true".to_vec())]; let access_control = - CredentialAccessControl::new(&required_attributes, identities_repository.clone()); + CredentialAccessControl::new(&required_attributes, identity_attributes_repository.clone()); ctx.flow_controls() .add_consumer("counter", listener.flow_control_id()); diff --git a/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs b/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs index 5803df1c8c0..001495ffc68 100644 --- a/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs +++ b/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs @@ -1,50 +1,37 @@ use core::str::FromStr; + use ockam_core::Result; +use ockam_identity::identities; use ockam_identity::models::Identifier; -use ockam_identity::{identities, Identity}; use ockam_vault::SigningKeyType; #[tokio::test] async fn create_and_retrieve() -> Result<()> { let identities = identities(); let identities_creation = identities.identities_creation(); - let repository = identities.repository(); let identities_keys = identities.identities_keys(); let identifier = identities_creation.create_identity().await?; - let actual = repository.get_identity(&identifier).await?; + let actual = identities_creation.get_identity(&identifier).await?; - let actual = Identity::import_from_change_history( - Some(&identifier), - actual, - identities.vault().verifying_vault, - ) - .await?; assert_eq!( actual.identifier(), &identifier, "the identity can be retrieved from the repository" ); - let actual = repository.retrieve_identity(&identifier).await?; - assert!(actual.is_some()); - let actual = Identity::import_from_change_history( - Some(&identifier), - actual.unwrap(), - identities.vault().verifying_vault, - ) - .await?; - assert_eq!( - actual.identifier(), - &identifier, - "the identity can be retrieved from the repository as an Option" - ); + let actual = identities_creation.get_change_history(&identifier).await; + assert!(actual.is_ok()); let another_identifier = Identifier::from_str("Ie92f183eb4c324804ef4d62962dea94cf095a265")?; - let missing = repository.retrieve_identity(&another_identifier).await?; - assert_eq!(missing, None, "a missing identity returns None"); - - let root_key = identities_keys.get_secret_key(&actual).await; + let missing = identities_creation + .get_identity(&another_identifier) + .await + .ok(); + assert_eq!(missing, None, "a missing identity returns an error"); + + let identity = identities.get_identity(&identifier).await?; + let root_key = identities_keys.get_secret_key(&identity).await; assert!(root_key.is_ok(), "there is a key for the created identity"); Ok(()) @@ -54,7 +41,6 @@ async fn create_and_retrieve() -> Result<()> { async fn create_p256() -> Result<()> { let identities = identities(); let identities_creation = identities.identities_creation(); - let repository = identities.repository(); let identities_keys = identities.identities_keys(); let identifier = identities_creation @@ -62,14 +48,8 @@ async fn create_p256() -> Result<()> { .with_random_key(SigningKeyType::ECDSASHA256CurveP256) .build() .await?; - let actual = repository.get_identity(&identifier).await?; + let actual = identities_creation.get_identity(&identifier).await?; - let actual = Identity::import_from_change_history( - Some(&identifier), - actual, - identities.vault().verifying_vault, - ) - .await?; assert_eq!( actual.identifier(), &identifier, diff --git a/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs b/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs index 4fa1d5d3ee9..8ff89bd7557 100644 --- a/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs +++ b/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs @@ -73,8 +73,8 @@ async fn test_eject_signatures() -> Result<()> { identities_creation.rotate_identity(&identifier).await?; } - let identity = identities.repository().get_identity(&identifier).await?; - let change_history = eject_random_signature(&identity)?; + let change_history = identities.get_change_history(&identifier).await?; + let change_history = eject_random_signature(&change_history)?; let res = check_change_history(Some(&identifier), change_history).await; assert!(res.is_err()); diff --git a/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs b/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs index d960fc841a7..79eaff15bfe 100644 --- a/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs +++ b/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs @@ -1,11 +1,13 @@ -use crate::common::message_flow_auth::{ - message_should_not_pass, message_should_not_pass_with_ctx, message_should_pass_with_ctx, -}; +use std::time::Duration; + use ockam_core::{route, AllowAll, Result}; use ockam_identity::{secure_channels, SecureChannelListenerOptions, SecureChannelOptions}; use ockam_node::Context; use ockam_transport_tcp::{TcpConnectionOptions, TcpListenerOptions, TcpTransport}; -use std::time::Duration; + +use crate::common::message_flow_auth::{ + message_should_not_pass, message_should_not_pass_with_ctx, message_should_pass_with_ctx, +}; mod common; @@ -113,7 +115,7 @@ async fn test2(ctx: &mut Context) -> Result<()> { ) .await?; - ctx.sleep(Duration::from_millis(50)).await; // Wait for workers to add themselves to the registry + ctx.sleep(Duration::from_millis(500)).await; // Wait for workers to add themselves to the registry let channels = bob_secure_channels .secure_channel_registry() diff --git a/implementations/rust/ockam/ockam_node/Cargo.toml b/implementations/rust/ockam/ockam_node/Cargo.toml index 84328652e48..063c668c67a 100644 --- a/implementations/rust/ockam/ockam_node/Cargo.toml +++ b/implementations/rust/ockam/ockam_node/Cargo.toml @@ -65,7 +65,7 @@ metrics = [] # message flows within Ockam apps. debugger = ["ockam_core/debugger"] -storage = ["std", "serde_json"] +storage = ["std", "time", "serde_json", "sqlx", "tokio-retry", "futures/executor"] [dependencies] cfg-if = "1.0.0" @@ -80,7 +80,10 @@ ockam_transport_core = { path = "../ockam_transport_core", version = "^0.66.0", serde = { version = "1.0", default-features = false, features = ["derive"] } serde_bare = { version = "0.5.0", default-features = false } serde_json = { version = "1", optional = true } +sqlx = { version = "0.7.2", optional = true, features = ["sqlite", "migrate", "runtime-tokio"] } +time = { version = "0.3.30", default-features = false, optional = true } tokio = { version = "1.33", default-features = false, optional = true, features = ["sync", "time", "rt", "rt-multi-thread", "macros"] } +tokio-retry = { version = "0.3", optional = true } tracing = { version = "0.1", default_features = false } tracing-error = { version = "0.2", optional = true } tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"], optional = true } diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/20231006100000_create_database.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/20231006100000_create_database.sql new file mode 100644 index 00000000000..7c48d707348 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/20231006100000_create_database.sql @@ -0,0 +1,187 @@ +CREATE TABLE identity +( + identifier TEXT NOT NULL UNIQUE, + change_history TEXT NOT NULL +); + +CREATE TABLE identity_enrollment +( + identifier TEXT NOT NULL UNIQUE, + enrolled_at INTEGER NOT NULL +); + +CREATE TABLE identity_attributes +( + identifier TEXT PRIMARY KEY, + attributes BLOB NOT NULL, + added INTEGER NOT NULL, + expires INTEGER, + attested_by TEXT +); + +CREATE TABLE purpose_key +( + identifier TEXT NOT NULL, + purpose TEXT NOT NULL, + purpose_key_attestation BLOB NOT NULL +); + +CREATE UNIQUE INDEX purpose_key_index ON purpose_key (identifier, purpose); + +CREATE TABLE policy +( + resource TEXT NOT NULL, + action TEXT NOT NULL, + expression BLOB NOT NULL +); + +CREATE TABLE named_identity +( + identifier TEXT NOT NULL UNIQUE, + name TEXT UNIQUE, + vault_name TEXT NOT NULL, + is_default INTEGER DEFAULT 0 +); + +CREATE TABLE node +( + name TEXT PRIMARY KEY, + identifier TEXT NOT NULL, + verbosity INTEGER NOT NULL, + is_default INTEGER NOT NULL, + is_authority INTEGER NOT NULL, + tcp_listener_address TEXT, + pid INTEGER +); + +CREATE TABLE node_project +( + node_name TEXT PRIMARY KEY, + project_name TEXT NOT NULL +); + +CREATE TABLE okta_config +( + project_id TEXT NOT NULL, + tenant_base_url TEXT NOT NULL, + client_id TEXT NOT NULL, + certificate TEXT NOT NULL, + attributes TEXT +); + +CREATE TABLE confluent_config +( + project_id TEXT NOT NULL, + bootstrap_server TEXT NOT NULL +); + +CREATE TABLE vault +( + name TEXT PRIMARY KEY, + path TEXT NOT NULL, + is_default INTEGER, + is_kms INTEGER +); + +CREATE TABLE signing_secret +( + handle BLOB PRIMARY KEY, + secret_type TEXT NOT NULL, + secret BLOB NOT NULL +); + +CREATE TABLE x25519_secret +( + handle BLOB PRIMARY KEY, + secret BLOB NOT NULL +); + +CREATE TABLE project +( + project_id TEXT PRIMARY KEY, + project_name TEXT NOT NULL, + is_default INTEGER NOT NULL, + space_id TEXT NOT NULL, + space_name TEXT NOT NULL, + identifier TEXT, + access_route TEXT NOT NULL, + authority_identity TEXT, + authority_access_route TEXT, + version TEXT, + running INTEGER, + operation_id TEXT +); + +CREATE TABLE user_project +( + user_email TEXT NOT NULL, + project_id TEXT NOT NULL +); + + +CREATE TABLE space +( + space_id TEXT PRIMARY KEY, + space_name TEXT NOT NULL, + is_default INTEGER NOT NULL +); + +CREATE TABLE user_space +( + user_email TEXT NOT NULL, + space_id TEXT NOT NULL +); + +CREATE TABLE user_role +( + user_id INTEGER NOT NULL, + project_id TEXT NOT NULL, + user_email TEXT NOT NULL, + role TEXT NOT NULL, + scope TEXT NOT NULL +); + +CREATE TABLE user +( + email TEXT PRIMARY KEY, + sub TEXT NOT NULL, + nickname TEXT NOT NULL, + name TEXT NOT NULL, + picture TEXT NOT NULL, + updated_at TEXT NOT NULL, + email_verified INTEGER NOT NULL, + is_default INTEGER NOT NULL +); + +CREATE TABLE credential +( + name TEXT PRIMARY KEY, + issuer_identifier TEXT NOT NULL, + issuer_change_history TEXT NOT NULL, + credential TEXT NOT NULL +); + +CREATE TABLE trust_context +( + name TEXT PRIMARY KEY, + trust_context_id TEXT NOT NULL, + is_default INTEGER NOT NULL, + credential TEXT, + authority_change_history TEXT, + authority_route TEXT +); + +CREATE TABLE tcp_outlet_status +( + alias TEXT PRIMARY KEY, + socket_addr TEXT NOT NULL, + worker_addr TEXT NOT NULL, + payload TEXT +); + +CREATE TABLE incoming_service +( + invitation_id TEXT PRIMARY KEY, + enabled INTEGER NOT NULL, + name TEXT NULL +); diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/mod.rs b/implementations/rust/ockam/ockam_node/src/storage/database/mod.rs new file mode 100644 index 00000000000..956af323eb3 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/mod.rs @@ -0,0 +1,5 @@ +mod sqlx_database; +mod sqlx_types; + +pub use sqlx_database::*; +pub use sqlx_types::*; diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs new file mode 100644 index 00000000000..f62a4a263d4 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs @@ -0,0 +1,223 @@ +use core::fmt::{Debug, Formatter}; +use std::ops::Deref; +use std::path::Path; + +use futures::executor; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::{ConnectOptions, SqlitePool}; +use tokio_retry::strategy::{jitter, FixedInterval}; +use tokio_retry::Retry; +use tracing::debug; +use tracing::log::LevelFilter; + +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; + +/// We use sqlx as our primary interface for interacting with the database +/// The database driver is currently Sqlite +pub struct SqlxDatabase { + /// Pool of connections to the database + pub pool: SqlitePool, +} + +impl Debug for SqlxDatabase { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str(format!("database options {:?}", self.pool.connect_options()).as_str()) + } +} + +impl Deref for SqlxDatabase { + type Target = SqlitePool; + + fn deref(&self) -> &Self::Target { + &self.pool + } +} + +impl SqlxDatabase { + /// Constructor for a database persisted on disk + pub async fn create>(path: P) -> Result { + path.as_ref() + .parent() + .map(std::fs::create_dir_all) + .transpose() + .map_err(|e| Error::new(Origin::Api, Kind::Io, e.to_string()))?; + + // creating a new database might be failing a few times + // if the files are currently being held by another pod which is shutting down. + // In that case we retry a few times, between 1 and 10 seconds. + let retry_strategy = FixedInterval::from_millis(1000) + .map(jitter) // add jitter to delays + .take(10); // limit to 10 retries + + let db = Retry::spawn(retry_strategy, || async { + Self::create_and_migrate(path.as_ref()).await + }) + .await?; + Ok(db) + } + + /// Constructor for an in-memory database + /// The implementation blocks during the creation of the database + /// so that we don't have to propagate async in all the code base when using an + /// in-memory database, especially when writing examples + pub fn in_memory(usage: &str) -> Self { + executor::block_on(async { + debug!("create an in memory database for {usage}"); + let pool = Self::create_in_memory_connection_pool() + .await + .expect("creating an in-memory connection should work"); + let db = SqlxDatabase { pool }; + db.migrate().await.expect("the migration should work"); + db + }) + } + + async fn create_and_migrate(path: &Path) -> Result { + // Creates database file if it doesn't exist + let pool = Self::create_connection_pool(path).await?; + let db = SqlxDatabase { pool }; + db.migrate().await?; + Ok(db) + } + + async fn create_connection_pool(path: &Path) -> Result { + let options = SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .log_statements(LevelFilter::Debug); + let pool = SqlitePool::connect_with(options) + .await + .map_err(Self::map_sql_err)?; + Ok(pool) + } + + async fn create_in_memory_connection_pool() -> Result { + let pool = SqlitePool::connect("sqlite::memory:") + .await + .map_err(Self::map_sql_err)?; + Ok(pool) + } + + async fn migrate(&self) -> Result<()> { + sqlx::migrate!("./src/storage/database/migrations") + .run(&self.pool) + .await + .map_err(Self::map_migrate_err) + } + + /// Map a sqlx error into an ockam error + pub fn map_sql_err(err: sqlx::Error) -> Error { + Error::new(Origin::Application, Kind::Io, err) + } + + /// Map a sqlx migration error into an ockam error + pub fn map_migrate_err(err: sqlx::migrate::MigrateError) -> Error { + Error::new( + Origin::Application, + Kind::Io, + format!("migration error {err}"), + ) + } + + /// Map a minicbor decode error into an ockam error + pub fn map_decode_err(err: minicbor::decode::Error) -> Error { + Error::new(Origin::Application, Kind::Io, err) + } +} + +/// This trait provides some syntax for transforming sqlx errors into ockam errors +pub trait FromSqlxError { + /// Make an ockam core Error + fn into_core(self) -> Result; +} + +impl FromSqlxError for core::result::Result { + fn into_core(self) -> Result { + self.map_err(|e| Error::new(Origin::Api, Kind::Internal, e.to_string())) + } +} + +/// This trait provides some syntax to shorten queries execution returning () +pub trait ToVoid { + /// Return a () value + fn void(self) -> Result<()>; +} + +impl ToVoid for core::result::Result { + fn void(self) -> Result<()> { + self.map(|_| ()).into_core() + } +} + +#[cfg(test)] +mod tests { + use sqlx::sqlite::SqliteQueryResult; + use sqlx::FromRow; + use tempfile::NamedTempFile; + + use crate::database::ToSqlxType; + + use super::*; + + /// This is a sanity check to test that the database can be created with a file path + /// and that migrations are running ok, at least for one table + #[tokio::test] + async fn test_create_identity_table() -> Result<()> { + // let db = create_database().await?; + // let inserted = insert_identity(&db).await.unwrap(); + let db_file = NamedTempFile::new().unwrap(); + let db = SqlxDatabase::create(db_file.path()).await?; + + let inserted = insert_identity(&db).await.unwrap(); + + assert_eq!(inserted.rows_affected(), 1); + Ok(()) + } + + /// This test checks that we can run a query and return an entity + #[tokio::test] + async fn test_query() -> Result<()> { + let db_file = NamedTempFile::new().unwrap(); + let db = SqlxDatabase::create(db_file.path()).await?; + + insert_identity(&db).await.unwrap(); + + // successful query + let result: Option = + sqlx::query_as("SELECT identifier FROM identity WHERE identifier=?1") + .bind("Ifa804b7fca12a19eed206ae180b5b576860ae651") + .fetch_optional(&db.pool) + .await + .unwrap(); + assert_eq!( + result, + Some(IdentifierRow( + "Ifa804b7fca12a19eed206ae180b5b576860ae651".into() + )) + ); + + // failed query + let result: Option = + sqlx::query_as("SELECT identifier FROM identity WHERE identifier=?1") + .bind("x") + .fetch_optional(&db.pool) + .await + .unwrap(); + assert_eq!(result, None); + Ok(()) + } + + /// HELPERS + async fn insert_identity(db: &SqlxDatabase) -> Result { + sqlx::query("INSERT INTO identity VALUES (?1, ?2)") + .bind("Ifa804b7fca12a19eed206ae180b5b576860ae651") + .bind("123".to_sql()) + .execute(&db.pool) + .await + .into_core() + } + + #[derive(FromRow, PartialEq, Eq, Debug)] + struct IdentifierRow(String); +} diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_types.rs b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_types.rs new file mode 100644 index 00000000000..4861c361076 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_types.rs @@ -0,0 +1,200 @@ +use std::net::SocketAddr; +use std::path::PathBuf; + +use ockam_core::Address; +use sqlx::database::HasArguments; +use sqlx::encode::IsNull; +use sqlx::{Database, Encode, Sqlite, Type}; +use time::OffsetDateTime; + +/// This enum represents the set of types that we currently support in our database +/// Since we support only Sqlite at the moment, those types are close to what is supported by Sqlite: +/// https://www.sqlite.org/datatype3.html +/// +/// The purpose of this type is to ease the serialization of data types in Ockam into data types in +/// our database. For example, if we describe how to translate an `Identifier` into some `Text` then +/// we can use the `Text` as a parameter in a sqlx query. +/// +/// Note: see the `ToSqlxType` trait and its instances for how the conversion is done +/// +pub enum SqlxType { + /// This type represents text in the database + Text(String), + /// This type represents arbitrary bytes in the database + Blob(Vec), + /// This type represents ints, signed or unsigned + Integer(i64), + /// This type represents floats + #[allow(unused)] + Real(f64), +} + +/// The SqlType implements the Type trait from sqlx to allow its values to be serialized +/// to an Sqlite database +impl Type for SqlxType { + fn type_info() -> ::TypeInfo { + as Type>::type_info() + } +} + +/// The SqlType implements the Encode trait from sqlx to allow its values to be serialized +/// to an Sqlite database. There is a 1 to 1 mapping with the database native types +impl Encode<'_, Sqlite> for SqlxType { + fn encode_by_ref(&self, buf: &mut ::ArgumentBuffer) -> IsNull { + match self { + SqlxType::Text(v) => >::encode_by_ref(v, buf), + SqlxType::Blob(v) => as Encode<'_, Sqlite>>::encode_by_ref(v, buf), + SqlxType::Integer(v) => >::encode_by_ref(v, buf), + SqlxType::Real(v) => >::encode_by_ref(v, buf), + } + } + + fn produces(&self) -> Option<::TypeInfo> { + Some(match self { + SqlxType::Text(_) => >::type_info(), + SqlxType::Blob(_) => as Type>::type_info(), + SqlxType::Integer(_) => >::type_info(), + SqlxType::Real(_) => >::type_info(), + }) + } +} + +/// This trait can be implemented by any type that can be converted to a database type +/// Typically an `Identifier` (to a `Text`), a `TimestampInSeconds` (to an `Integer`) etc... +/// +/// This allows a value to be used as a bind parameters in a sqlx query for example: +/// +/// use std::str::FromStr; +/// use sqlx::query_as; +/// use ockam_node::database::{SqlxType, ToSqlxType}; +/// +/// // newtype for a UNIX-like timestamp +/// struct TimestampInSeconds(u64); +/// +/// // this implementation maps the TimestampInSecond type to one of the types that Sqlx +/// // can serialize for sqlite +/// impl ToSqlxType for TimestampInSeconds { +/// fn to_sql(&self) -> SqlxType { +/// self.0.to_sql() +/// } +/// } +/// +/// let timestamp = TimestampInSeconds(10000000); +/// let query = query_as("SELECT * FROM identity WHERE created_at >= $1").bind(timestamp.as_sql()); +/// +/// +pub trait ToSqlxType { + /// Return the appropriate sql type + fn to_sql(&self) -> SqlxType; +} + +impl ToSqlxType for String { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.clone()) + } +} + +impl ToSqlxType for &str { + fn to_sql(&self) -> SqlxType { + self.to_string().to_sql() + } +} + +impl ToSqlxType for bool { + fn to_sql(&self) -> SqlxType { + if *self { + 1.to_sql() + } else { + 0.to_sql() + } + } +} + +impl ToSqlxType for u64 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for u32 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for u16 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for u8 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for usize { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for i32 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for i16 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for i8 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for OffsetDateTime { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(self.unix_timestamp()) + } +} + +impl ToSqlxType for Vec { + fn to_sql(&self) -> SqlxType { + SqlxType::Blob(self.clone()) + } +} + +impl ToSqlxType for &[u8; 32] { + fn to_sql(&self) -> SqlxType { + SqlxType::Blob(self.to_vec().clone()) + } +} + +impl ToSqlxType for SocketAddr { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.to_string()) + } +} + +impl ToSqlxType for Address { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.to_string()) + } +} + +impl ToSqlxType for PathBuf { + fn to_sql(&self) -> SqlxType { + SqlxType::Text( + self.as_path() + .to_str() + .unwrap_or("a path should be a valid string") + .into(), + ) + } +} diff --git a/implementations/rust/ockam/ockam_node/src/storage/mod.rs b/implementations/rust/ockam/ockam_node/src/storage/mod.rs index f1897523362..180efbf10bb 100644 --- a/implementations/rust/ockam/ockam_node/src/storage/mod.rs +++ b/implementations/rust/ockam/ockam_node/src/storage/mod.rs @@ -1,3 +1,13 @@ +#[cfg(feature = "std")] +pub use file_key_value_storage::*; +#[cfg(feature = "std")] +pub use file_value_storage::*; +pub use in_memory_key_value_storage::*; +pub use in_memory_value_storage::*; +pub use key_value_storage::*; +pub use to_string_key::*; +pub use value_storage::*; + /// File implementation of a key value storage #[cfg(feature = "std")] mod file_key_value_storage; @@ -21,12 +31,6 @@ mod to_string_key; /// Trait defining the functions for a value storage mod value_storage; +/// Database support #[cfg(feature = "std")] -pub use file_key_value_storage::*; -#[cfg(feature = "std")] -pub use file_value_storage::*; -pub use in_memory_key_value_storage::*; -pub use in_memory_value_storage::*; -pub use key_value_storage::*; -pub use to_string_key::*; -pub use value_storage::*; +pub mod database; diff --git a/implementations/rust/ockam/ockam_vault/Cargo.toml b/implementations/rust/ockam/ockam_vault/Cargo.toml index 43702c47521..bb571ed0026 100644 --- a/implementations/rust/ockam/ockam_vault/Cargo.toml +++ b/implementations/rust/ockam/ockam_vault/Cargo.toml @@ -44,6 +44,7 @@ std = [ "tracing/std", "alloc", "p256/std", + "storage", ] # Feature: "no_std" enables functionality required for platforms @@ -68,7 +69,7 @@ alloc = [ "p256/pem", ] -storage = ["ockam_node", "ockam_node/storage", "std", "serde_cbor"] +storage = ["ockam_node/storage", "sqlx"] [dependencies] aes-gcm = { version = "0.9", default-features = false, features = ["aes"] } @@ -88,6 +89,7 @@ rand_pcg = { version = "0.3.1", default-features = false, optional = true } serde = { version = "1", default-features = false, features = ["derive"] } serde_cbor = { version = "0.11.2", optional = true } sha2 = { version = "0.10", default-features = false } +sqlx = { version = "0.7", optional = true } static_assertions = "1.1.0" thiserror = { version = "1.0.50", optional = true } tracing = { version = "0.1", default-features = false, features = ["attributes"] } diff --git a/implementations/rust/ockam/ockam_vault/src/lib.rs b/implementations/rust/ockam/ockam_vault/src/lib.rs index e605c85b64b..63f19ddb909 100644 --- a/implementations/rust/ockam/ockam_vault/src/lib.rs +++ b/implementations/rust/ockam/ockam_vault/src/lib.rs @@ -30,7 +30,6 @@ extern crate core; extern crate alloc; /// Storage -#[cfg(feature = "storage")] pub mod storage; /// Errors diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs index 97123ba2ffc..fab4de58fe5 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs @@ -1,4 +1,16 @@ -use super::aes::make_aes; +use sha2::{Digest, Sha256}; + +use ockam_core::compat::boxed::Box; +use ockam_core::compat::collections::BTreeMap; +use ockam_core::compat::rand::{thread_rng, RngCore}; +use ockam_core::compat::sync::{Arc, RwLock}; +use ockam_core::compat::vec::{vec, Vec}; +use ockam_core::{async_trait, Result}; + +use crate::storage::SecretsRepository; + +#[cfg(feature = "storage")] +use crate::storage::SecretsSqlxDatabase; use crate::{ AeadSecret, AeadSecretKeyHandle, BufferSecret, HKDFNumberOfOutputs, HandleToSecret, HashOutput, @@ -7,39 +19,31 @@ use crate::{ AEAD_SECRET_LENGTH, }; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::rand::{thread_rng, RngCore}; -use ockam_core::compat::sync::{Arc, RwLock}; -use ockam_core::compat::vec::{vec, Vec}; -use ockam_core::{async_trait, compat::boxed::Box, Result}; -use ockam_node::{InMemoryKeyValueStorage, KeyValueStorage}; - -use crate::legacy::{KeyId, StoredSecret}; -use sha2::{Digest, Sha256}; +use super::aes::make_aes; /// [`SecureChannelVault`] implementation using software pub struct SoftwareVaultForSecureChannels { ephemeral_buffer_secrets: Arc>>, ephemeral_aead_secrets: Arc>>, ephemeral_x25519_secrets: Arc>>, - // Use String as a key for backwards compatibility - static_x25519_secrets: Arc>, + static_x25519_secrets: Arc, } impl SoftwareVaultForSecureChannels { /// Constructor - pub fn new(storage: Arc>) -> Self { + pub fn new(repository: Arc) -> Self { Self { ephemeral_buffer_secrets: Default::default(), ephemeral_aead_secrets: Default::default(), ephemeral_x25519_secrets: Default::default(), - static_x25519_secrets: storage, + static_x25519_secrets: repository, } } - /// Create Software implementation Vault with [`InMemoryKeyVaultStorage`] + /// Create Software implementation Vault with an in-memory implementation to store secrets + #[cfg(feature = "storage")] pub fn create() -> Arc { - Arc::new(Self::new(InMemoryKeyValueStorage::create())) + Arc::new(Self::new(SecretsSqlxDatabase::create())) } } @@ -53,7 +57,7 @@ impl SoftwareVaultForSecureChannels { let handle = Self::compute_handle_for_public_key(&public_key); self.static_x25519_secrets - .put(hex::encode(handle.0.value()), secret.into()) + .store_x25519_secret(&handle, secret) .await?; Ok(handle) @@ -83,7 +87,11 @@ impl SoftwareVaultForSecureChannels { /// Return the total number of static x25519 secrets present in the Vault pub async fn number_of_static_x25519_secrets(&self) -> Result { - Ok(self.static_x25519_secrets.keys().await?.len()) + Ok(self + .static_x25519_secrets + .get_x25519_secret_handles() + .await? + .len()) } /// Return the total number of ephemeral x25519 secrets present in the Vault @@ -176,15 +184,10 @@ impl SoftwareVaultForSecureChannels { return Ok(secret.clone()); } - if let Some(stored_secret) = self - .static_x25519_secrets - .get(&hex::encode(handle.0.value())) + self.static_x25519_secrets + .get_x25519_secret(handle) .await? - { - return stored_secret.try_into(); - } - - Err(VaultError::KeyNotFound.into()) + .ok_or(VaultError::KeyNotFound.into()) } async fn get_buffer_secret(&self, handle: &SecretBufferHandle) -> Result { @@ -306,7 +309,7 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { ) -> Result { Ok(self .static_x25519_secrets - .delete(&hex::encode(secret_key_handle.0.value())) + .delete_x25519_secret(&secret_key_handle) .await? .is_some()) } diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs index 96b684b317a..886638f73bd 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs @@ -9,33 +9,32 @@ use crate::{ EDDSA_CURVE25519_SECRET_KEY_LENGTH, }; +use crate::storage::SecretsRepository; +#[cfg(feature = "storage")] +use crate::storage::SecretsSqlxDatabase; +use arrayref::array_ref; use ockam_core::compat::rand::thread_rng; use ockam_core::compat::sync::Arc; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{async_trait, compat::boxed::Box, Error, Result}; -use ockam_node::{InMemoryKeyValueStorage, KeyValueStorage}; - -use crate::legacy::KeyId; -use crate::software::legacy::StoredSecret; -use arrayref::array_ref; use sha2::{Digest, Sha256}; /// [`SigningVault`] implementation using software #[derive(Clone)] pub struct SoftwareVaultForSigning { - // Use String as a key for backwards compatibility - secrets: Arc>, + secrets: Arc, } impl SoftwareVaultForSigning { /// Constructor - pub fn new(secrets: Arc>) -> Self { + pub fn new(secrets: Arc) -> Self { Self { secrets } } - /// Create Software implementation Vault with [`InMemoryKeyVaultStorage`] + /// Create an in-memory Software implementation Vault + #[cfg(feature = "storage")] pub fn create() -> Arc { - Arc::new(Self::new(InMemoryKeyValueStorage::create())) + Arc::new(Self::new(SecretsSqlxDatabase::create())) } /// Import a key from a binary @@ -43,16 +42,14 @@ impl SoftwareVaultForSigning { let public_key = Self::compute_public_key_from_secret(&key)?; let handle = Self::compute_handle_for_public_key(&public_key)?; - self.secrets - .put(hex::encode(handle.handle().value()), key.into()) - .await?; + self.secrets.store_signing_secret(&handle, key).await?; Ok(handle) } /// Return the total number of keys pub async fn number_of_keys(&self) -> Result { - Ok(self.secrets.keys().await?.len()) + Ok(self.secrets.get_signing_secret_handles().await?.len()) } } @@ -139,9 +136,9 @@ impl VaultForSigning for SoftwareVaultForSigning { signing_secret_key_handle: SigningSecretKeyHandle, ) -> Result { self.secrets - .delete(&hex::encode(signing_secret_key_handle.handle().value())) + .delete_signing_secret(&signing_secret_key_handle) .await - .map(|r| r.is_some()) + .map(|s| s.is_some()) } } @@ -219,17 +216,12 @@ impl SoftwareVaultForSigning { &self, signing_secret_key_handle: &SigningSecretKeyHandle, ) -> Result { - let handle = match signing_secret_key_handle { - SigningSecretKeyHandle::EdDSACurve25519(handle) => handle, - SigningSecretKeyHandle::ECDSASHA256CurveP256(handle) => handle, - }; - let stored_secret = self .secrets - .get(&hex::encode(handle.value())) + .get_signing_secret(signing_secret_key_handle) .await? .ok_or(VaultError::KeyNotFound)?; - stored_secret.try_into() + Ok(stored_secret) } } diff --git a/implementations/rust/ockam/ockam_vault/src/storage/mod.rs b/implementations/rust/ockam/ockam_vault/src/storage/mod.rs index f33260668b8..43e0a4f7aac 100644 --- a/implementations/rust/ockam/ockam_vault/src/storage/mod.rs +++ b/implementations/rust/ockam/ockam_vault/src/storage/mod.rs @@ -1,4 +1,14 @@ /// Storage of secrets to a file +#[cfg(feature = "storage")] mod persistent_storage; +mod secrets_repository; +#[cfg(feature = "storage")] +mod secrets_repository_sql; + +#[cfg(feature = "storage")] pub use persistent_storage::*; +pub use secrets_repository::*; + +#[cfg(feature = "storage")] +pub use secrets_repository_sql::*; diff --git a/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository.rs b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository.rs new file mode 100644 index 00000000000..19a6c08d4c6 --- /dev/null +++ b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository.rs @@ -0,0 +1,53 @@ +use crate::{SigningSecret, SigningSecretKeyHandle, X25519SecretKey, X25519SecretKeyHandle}; +use ockam_core::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; + +/// A secrets repository supports the persistence of signing and X25519 secrets +#[async_trait] +pub trait SecretsRepository: Send + Sync + 'static { + /// Store a signing secret + async fn store_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + secret: SigningSecret, + ) -> Result<()>; + + /// Delete a signing secret + async fn delete_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result>; + + /// Get a signing secret + async fn get_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result>; + + /// Get the list of all signing secret handles + async fn get_signing_secret_handles(&self) -> Result>; + + /// Get a X25519 secret + async fn store_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + secret: X25519SecretKey, + ) -> Result<()>; + + /// Get a X25519 secret + async fn delete_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result>; + + /// Get a X25519 secret + async fn get_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result>; + + /// Get the list of all X25519 secret handles + async fn get_x25519_secret_handles(&self) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs new file mode 100644 index 00000000000..1467fa987b9 --- /dev/null +++ b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs @@ -0,0 +1,324 @@ +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::storage::secrets_repository::SecretsRepository; + +use crate::{ + ECDSASHA256CurveP256SecretKey, EdDSACurve25519SecretKey, HandleToSecret, SigningSecret, + SigningSecretKeyHandle, X25519SecretKey, X25519SecretKeyHandle, +}; + +/// Implementation of a secrets repository using a SQL database +#[derive(Clone)] +pub struct SecretsSqlxDatabase { + database: Arc, +} + +impl SecretsSqlxDatabase { + /// Create a new database for policies keys + pub fn new(database: Arc) -> Self { + debug!("create a repository for secrets"); + Self { database } + } + + /// Create a new in-memory database for policies + pub fn create() -> Arc { + Arc::new(Self::new(Arc::new(SqlxDatabase::in_memory("secrets")))) + } +} + +#[async_trait] +impl SecretsRepository for SecretsSqlxDatabase { + async fn store_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + secret: SigningSecret, + ) -> Result<()> { + let secret_type: String = match handle { + SigningSecretKeyHandle::EdDSACurve25519(_) => "EdDSACurve25519".into(), + SigningSecretKeyHandle::ECDSASHA256CurveP256(_) => "ECDSASHA256CurveP256".into(), + }; + + let query = query("INSERT OR REPLACE INTO signing_secret VALUES (?, ?, ?)") + .bind(handle.to_sql()) + .bind(secret_type.to_sql()) + .bind(secret.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result> { + if let Some(secret) = self.get_signing_secret(handle).await? { + let query = query("DELETE FROM signing_secret WHERE handle = ?").bind(handle.to_sql()); + query.execute(&self.database.pool).await.void()?; + Ok(Some(secret)) + } else { + Ok(None) + } + } + + async fn get_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result> { + let query = query_as("SELECT * FROM signing_secret WHERE handle=?").bind(handle.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.signing_secret()).transpose()?) + } + + async fn get_signing_secret_handles(&self) -> Result> { + let query = query_as("SELECT * FROM signing_secret"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + Ok(rows + .iter() + .map(|r| r.handle()) + .collect::>>()?) + } + + async fn store_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + secret: X25519SecretKey, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO x25519_secret VALUES (?, ?)") + .bind(handle.to_sql()) + .bind(secret.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result> { + if let Some(secret) = self.get_x25519_secret(handle).await? { + let query = query("DELETE FROM x25519_secret WHERE handle = ?").bind(handle.to_sql()); + query.execute(&self.database.pool).await.void()?; + Ok(Some(secret)) + } else { + Ok(None) + } + } + + async fn get_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result> { + let query = query_as("SELECT * FROM x25519_secret WHERE handle=?").bind(handle.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.x25519_secret()).transpose()?) + } + + async fn get_x25519_secret_handles(&self) -> Result> { + let query = query_as("SELECT * FROM x25519_secret"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + Ok(rows + .iter() + .map(|r| r.handle()) + .collect::>>()?) + } +} + +impl ToSqlxType for SigningSecret { + fn to_sql(&self) -> SqlxType { + match self { + SigningSecret::EdDSACurve25519(k) => k.key().to_sql(), + SigningSecret::ECDSASHA256CurveP256(k) => k.key().to_sql(), + } + } +} + +impl ToSqlxType for SigningSecretKeyHandle { + fn to_sql(&self) -> SqlxType { + match self { + SigningSecretKeyHandle::EdDSACurve25519(h) => h.to_sql(), + SigningSecretKeyHandle::ECDSASHA256CurveP256(h) => h.to_sql(), + } + } +} + +impl ToSqlxType for X25519SecretKeyHandle { + fn to_sql(&self) -> SqlxType { + self.0.value().to_sql() + } +} + +impl ToSqlxType for HandleToSecret { + fn to_sql(&self) -> SqlxType { + self.value().to_sql() + } +} + +impl ToSqlxType for X25519SecretKey { + fn to_sql(&self) -> SqlxType { + self.key().to_sql() + } +} + +#[derive(FromRow)] +struct SigningSecretRow { + handle: Vec, + secret_type: String, + secret: Vec, +} + +impl SigningSecretRow { + fn signing_secret(&self) -> Result { + let secret: [u8; 32] = self.secret.clone().try_into().map_err(|_| { + ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot convert a signing secret to [u8; 32]", + ) + })?; + match self.secret_type.as_str() { + "EdDSACurve25519" => Ok(SigningSecret::EdDSACurve25519( + EdDSACurve25519SecretKey::new(secret), + )), + "ECDSASHA256CurveP256" => Ok(SigningSecret::ECDSASHA256CurveP256( + ECDSASHA256CurveP256SecretKey::new(secret), + )), + _ => Err(ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot deserialize a signing secret", + )), + } + } + + fn handle(&self) -> Result { + match self.secret_type.as_str() { + "EdDSACurve25519" => Ok(SigningSecretKeyHandle::EdDSACurve25519( + HandleToSecret::new(self.handle.clone()), + )), + "ECDSASHA256CurveP256" => Ok(SigningSecretKeyHandle::ECDSASHA256CurveP256( + HandleToSecret::new(self.handle.clone()), + )), + _ => Err(ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot deserialize a signing secret handle", + )), + } + } +} + +#[derive(FromRow)] +struct X25519SecretRow { + handle: Vec, + secret: Vec, +} + +impl X25519SecretRow { + fn x25519_secret(&self) -> Result { + let secret: [u8; 32] = self.secret.clone().try_into().map_err(|_| { + ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot convert a X25519 secret to [u8; 32]", + ) + })?; + Ok(X25519SecretKey::new(secret)) + } + + fn handle(&self) -> Result { + Ok(X25519SecretKeyHandle(HandleToSecret::new( + self.handle.clone(), + ))) + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use tempfile::NamedTempFile; + + use super::*; + + #[tokio::test] + async fn test_signing_secrets_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + + let handle1 = + SigningSecretKeyHandle::ECDSASHA256CurveP256(HandleToSecret::new(vec![1, 2, 3])); + let secret1 = + SigningSecret::ECDSASHA256CurveP256(ECDSASHA256CurveP256SecretKey::new([1; 32])); + + let handle2 = SigningSecretKeyHandle::EdDSACurve25519(HandleToSecret::new(vec![4, 5, 6])); + let secret2 = SigningSecret::EdDSACurve25519(EdDSACurve25519SecretKey::new([1; 32])); + + repository + .store_signing_secret(&handle1, secret1.clone()) + .await?; + repository + .store_signing_secret(&handle2, secret2.clone()) + .await?; + + let result = repository.get_signing_secret(&handle1).await?; + assert!(result == Some(secret1)); + + let result = repository.get_signing_secret_handles().await?; + assert_eq!(result, vec![handle1.clone(), handle2]); + + repository.delete_signing_secret(&handle1).await?; + + let result = repository.get_signing_secret(&handle1).await?; + assert!(result.is_none()); + + Ok(()) + } + + #[tokio::test] + async fn test_x25519_secrets_repository() -> Result<()> { + let file = NamedTempFile::new().unwrap(); + let repository = create_repository(file.path()).await?; + + let handle1 = X25519SecretKeyHandle(HandleToSecret::new(vec![1, 2, 3])); + let secret1 = X25519SecretKey::new([1; 32]); + + let handle2 = X25519SecretKeyHandle(HandleToSecret::new(vec![4, 5, 6])); + let secret2 = X25519SecretKey::new([1; 32]); + + repository + .store_x25519_secret(&handle1, secret1.clone()) + .await?; + repository + .store_x25519_secret(&handle2, secret2.clone()) + .await?; + + let result = repository.get_x25519_secret(&handle1).await?; + assert!(result == Some(secret1)); + + let result = repository.get_x25519_secret_handles().await?; + assert_eq!(result, vec![handle1.clone(), handle2]); + + repository.delete_x25519_secret(&handle1).await?; + + let result = repository.get_x25519_secret(&handle1).await?; + assert!(result.is_none()); + + Ok(()) + } + + /// HELPERS + async fn create_repository(path: &Path) -> Result> { + let db = SqlxDatabase::create(path).await?; + Ok(Arc::new(SecretsSqlxDatabase::new(Arc::new(db)))) + } +}