Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rules for Roles #286

Merged
merged 72 commits into from
Dec 6, 2024
Merged

Conversation

CyonAlexRDX
Copy link
Contributor

@CyonAlexRDX CyonAlexRDX commented Nov 30, 2024

Important

The UniFFI exported SecurityShieldBuilder is a reference type which is stateful and hosts
SHOULD not keep any state relating to factors in its ViewMode/Reducer.State, hosts should instead
hold a builder: SecurityShieldBuilder and use that and only that for state!
For Swift we might need wrap that reference type in a value type to play well with Reducer.State?
Maybe using @shared state!

Note

I've changed from mozilla/UniFFI to Sajjon/UniFFI (my fork) awaiting merge of JNA bug fix
into Mozilla: mozilla/uniffi-rs#2344

Description

Impl of rules for roles

This PR implements a MatrixOfFactorSourceIdsBuilder - which uses a new RoleWithFactorSourceIdsBuilder - one for each role:

  • Primary Role
  • Recovery Role
  • Confirmation Role

Each of these roles builders validates any mutation done to it and can answer two basic queries:

  • "Given your current state - do you support addition of a FactorSource(ID) of this (FactorSource)Kind?"
  • "Given your current state - FOR EACH FactorSource(ID) what would happen if I were to try to add it?"

Furthermore, we can call a validate method on MatrixOfFactorSourceIdsBuilder, which aparts from validating each role builder in isolation; also validates the roles in combination with each other (and also validates the number_of_days_until_auto_confirm).

Finally the MatrixOfFactorSourceIdsBuilder of course has a build method, which calls validate and if valid returns a built MatrixOfFactorSourceIds.

Important

Building happens on FactorSourceID level - since it is what we are going to store into Profile anyway, and it is trivial to map from FactorSource -> FactorSourceID (just call .factor_source_id() on the factor source). We note UI in hosts works on FactorSource level, but the builder works with FactorSourceIDs. We have throwing conversion from a built MatrixOfFactorSourceIds -> MatrixOfFactorSources which we can use for screen where we wanna display a Security Shield already saved in Profile.

API

See unit test in Sargon-Uniffi to get a feeling of the API, but will paste parts of it here:

API EXAMPLE HERE (Click me ‼️)
#[test]
fn example() {
  // Create builder (UniFFI exported)
  let builder = SecurityShieldBuilder::new(); 

  // It starts with a generic name
  assert_eq!(builder.get_name(), "My Shield");

  // update name
  builder.set_name("S.H.I.E.L.D.".to_owned());

  // Number of days until auto confirm starts with a valid value (>0)
  assert_eq!(builder.get_number_of_days_until_auto_confirm(), 14);

  // Update days
  builder.set_number_of_days_until_auto_confirm(u16::MAX).unwrap();
  assert_eq!(builder.get_number_of_days_until_auto_confirm(), u16::MAX); // works!

  // =====================
  // *** PRIMARY ROLE  ***
  // =====================

  // Add Device to `threshold`
  builder.add_factor_source_to_primary_threshold(
    FactorSourceID::sample_device(),
  )
  .unwrap();

  // Set threshold
  builder.set_threshold(1);
  // Get threshold
  assert_eq!(builder.get_primary_threshold(), 1);

  // Add Arculus to `override`
  builder.add_factor_source_to_primary_override(
    FactorSourceID::sample_arculus(),
  )
  .unwrap();

  // Add another Arculus to `override`
  builder.add_factor_source_to_primary_override(
    FactorSourceID::sample_arculus_other(),
  )
  .unwrap();

  // Get `override` factors
  assert_eq!(
    builder.get_primary_override_factors(),
    vec![
      FactorSourceID::sample_arculus(),
      FactorSourceID::sample_arculus_other()
    ]
  );

  // =====================
  // *** RECOVERY ROLE  ***
  // =====================

  // Add Ledger
  builder.add_factor_source_to_recovery_override(
    FactorSourceID::sample_ledger(),
  )
  .unwrap();

  // Add another Ledger
  builder.add_factor_source_to_recovery_override(
    FactorSourceID::sample_ledger_other(),
  )
  .unwrap();

  // Get factors - Recovery does not have any `threshold` factors
  assert_eq!(
    builder.get_recovery_factors(),
    vec![
      FactorSourceID::sample_ledger(),
      FactorSourceID::sample_ledger_other()
    ]
  );

  // ==========================
  // *** CONFIRMATION ROLE  ***
  // ==========================

  // Add Device
  builder.add_factor_source_to_confirmation_override(
    FactorSourceID::sample_device(),
  )
  .unwrap();

  // Get factors - Confirmation does not have any `threshold` factors
  assert_eq!(
    builder.get_confirmation_factors(),
    vec![FactorSourceID::sample_device(),]
  );

  // ======================
  // *** REMOVAL WORKS  ***
  // ======================
  builder.remove_factor(FactorSourceID::sample_arculus_other())
    .unwrap();
  builder.remove_factor(FactorSourceID::sample_ledger_other())
    .unwrap();


  // =========================
  // *** VALIDATE & BUILD  ***
  // =========================
  assert_eq!(builder.validate(), Ok(()));

  let shield = builder.build().unwrap(); // type is: `SecurityStructureOfFactorSourceIds`

  // ===============================
  // *** ASSERT VALUES ON BUILD  ***
  // ===============================
  assert_eq!(shield.wrapped.metadata.display_name.value, "S.H.I.E.L.D.");
  assert_eq!(
    shield
      .wrapped
      .matrix_of_factors
      .primary()
      .get_override_factors(),
    &vec![FactorSourceID::sample_arculus().into()]
  );
  assert_eq!(
    shield
      .wrapped
      .matrix_of_factors
      .recovery()
      .get_override_factors(),
    &vec![FactorSourceID::sample_ledger().into()]
  );
  assert_eq!(
    shield
      .wrapped
      .matrix_of_factors
      .confirmation()
      .get_override_factors(),
    &vec![FactorSourceID::sample_device().into()]
  );
}

Implementation

All past implementations of SecurityStructureOf*, MatrixOf and RoleWith have been re-implemented/replaced (they were declared by overly-complex macros in Sargon which was copy pasted over to sargon-uniffi crate. In fact I started this work by trying to implement all rules inside the macro, but it quickly became much too complex on something which already was too complex.).

The current implementation uses some macros inside the sargon-uniffi crate for conversion between Sargon and Sargon-UniFFI types - but these very much more well written than the old ones, and in fact quite "easy" to understand. They are much more well structured, much shorter and also documented. Here is the macro for role_conversion and here is the macro for matrix_conversion. Why are those macros needed? Well I did not want to let UniFFI crate dictate how Sargon implements its types - which the InternalConversion proc-macro does! It dictates that the fields in Sargon and Sargon-UniFFI crates map 1:1 and also requires all fields in Sargon to be pub - that is not how I've implemented the types in Sargon!

The implementation of the types in Sargon uses one single type for ALL role types (except GeneralRoleWithHierarchicalDeterministicFactorInstances):

pub struct AbstractRoleBuilderOrBuilt<const R: u8, F, T> {
    #[serde(skip)]
    #[doc(hidden)]
    built: PhantomData<T>,

    /// How many threshold factors that must be used to perform some function with
    /// this role.
    threshold: u8,

    /// Factors which are used in combination with other factors, amounting to at
    /// least `threshold` many factors to perform some function with this role.
    threshold_factors: Vec<F>,

    /// Overriding / Super admin / "sudo" / God / factors, **ANY**
    /// single of these factor which can perform the function of this role,
    /// disregarding of `threshold`.
    override_factors: Vec<F>,
}

The type is named "BuilderOrBuilt" because it is used by both the (role)builders and the "built" roles!

All role builder types are then declared by using the:

pub(crate) type RoleBuilder<const R: u8> =
    AbstractRoleBuilderOrBuilt<R, FactorSourceID, Built>;

like so:

pub type PrimaryRoleBuilder = RoleBuilder<{ ROLE_PRIMARY }>;
pub type RecoveryRoleBuilder = RoleBuilder<{ ROLE_RECOVERY }>;
pub type ConfirmationRoleBuilder = RoleBuilder<{ ROLE_CONFIRMATION }>;

Note the const R: u8 is 🪄 Rust magic ✨🔮 it is a "Value Generics"! Not a generic type, but a generic value! This allows me to create distinct RoleBuilders, as if they were different types, using a typealias. But I dont have to declare distinct "TagTypes" (like Swift Tagged), instead I can just use three const pub const ROLE_PRIMARY: u8 = 0; which is neat! Furthermore, since I declare ROLE_PRIMARY = 0 I can do this:

impl<const R: u8> RoleBuilder<R>
where
    Assert<{ R > ROLE_PRIMARY }>: IsTrue,
{
    /// If Ok => self is mutated
    /// If Err(NotYetValid) => self is mutated
    /// If Err(ForeverInvalid) => self is not mutated
    pub(crate) fn add_factor_source(
        &mut self,
        factor_source_id: FactorSourceID,
    ) -> RoleBuilderMutateResult {
        self.add_factor_source_to_override(factor_source_id)
    }
}

Assert<{ R > ROLE_PRIMARY }>: IsTrue, This is soo cool! We can put shared behaviour between only the RecoveryRoleBuilder and ConfirmationRoleBuilder builder! Very convenient!

(
which does require us to put:

#![allow(incomplete_features)]
#![feature(generic_const_exprs)]

but feels OK, not a huge pain to remove it if the feature generic_const_exprs wont be stabilized....
)

Rules

I've implemented the rules without checking the const value, rather relying on runtime checks instead of compile time checks, like so match self.role() and doing validation. I think it is an easier and more flexible implementation, and I have 100% code coverage for all rules. See e.g. validation_add

Templates

I've implemented all example Security Shield configs listed in table (except the last two which required "Custodian" FS). This is cool! I've implemented the vision I had 1,5 years ago! Like so:

pub type MatrixTemplate = AbstractMatrixBuilt<FactorSourceTemplate>;
...

/// A "template" FactorSourceID/FactorSource to be used in a RoleTemplate is
/// FactorSourceKind with some placeholder ID, to distinguish between two different
/// FactorSourceIDs of some kind, e.g. `FactorSourceID::sample()` and `FactorSourceID::sample_other()`.
/// but exactly which FactorSourceID values are not yet known, since this is a template.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FactorSourceTemplate {
    /// The kind of FactorSource, e.g. Device, LedgerHQHardwareWallet, Password, etc.
    pub kind: FactorSourceKind,

    /// Some placeholder ID to distinguish between two different FactorSourceIDs
    /// to be concretely defined later.
    pub id: u8,
}

This might in fact be a valid implementation of Automatic Shield Construction (see below under Future Work). This allows us to create a MatrixOfFactorSourceIds without using a builder - instead using MatrixTemplate::materialize(template, factors_in_profile) and it will always produce a valid Matrix! We can in fact in the future build UI where user can select any of these predefined templates! But I did them mostly to be able to "reuse matrices" between tests - and not have to build them all the time. These templates are useful for tests and for can be really useful for end user!

Never construct MatrixOfFactor ourselves!

We never construct any MatrixOfFactor*** ourselves (apart from using sample()/sample_other() ofc...)

MatrixOfFactorSourceIds

These are two ways by which we can construct MatrixOfFactorSourceIds*:

  • Either using the MatrixBuilder (FactorSourceID level)
  • or use (MatrixTemplate + FactorSourceIds collection, e.g. those in Profile or for tests using ALL_FACTOR_SOURCE_ID_SAMPLES_INC_NON_HD)

MatrixOFFactorSources

Use MatrixOfFactorSourceIds and try_from with a iter of FactorSources, there is a new ctor which is throwing (of the referenced FactorSourceID was not found among the provided FactorSources).

MatrixOfFactorInstances

This is the process of using the FactorInstancesProvider (FIP) or actually one of its "specialized adopters" the SecurifyEntityFactorInstancesProvider, see gated behind cfg(test) method __OFFLINE_ONLY_securify_accounts on SargonOS. Which derives FactorInstances from a SecurityStructureOfFactorSources.

Note

We should probably update FactorInstancesProvider / SecurifyEntityFactorInstancesProvider and also KeysCollector to work on FactorSourceId level instead of FactorSource!
But this depends a bit on @micbakos-rdx work with Interactors. We can use FactorSourceIDs everywhere and then we can let the interactor on the Host side be responsible for doing the lookup FactorSourceID -> FactorSource if it needs to show more info about the factor source (such as "last used" / "label" etc).

Future Work

TransactionManifest building support

I did not carry over the Into<ScryptoAccessRule> which for a *RoleWithFactorInstances had on main branch. I will do that in a coming PR - also with support for MatrixOfFactorInstances!

Automatic Shield Construction

In Automatic Security Shield Construction section of the Confluence page, Matt lays out a heuristics for trying to build a shield solely based on the FactorSources user has in Profile. That has not yet been done.

Tip

However, since I did implement the Configs one could possible very easily implement automatic shield construction by MatrixTemplate::all().iter(|template| template.materialize(profile.factors).first()!

@CyonAlexRDX CyonAlexRDX marked this pull request as draft November 30, 2024 12:28
@CyonAlexRDX CyonAlexRDX changed the title [no ci] WIP Rules for Roles Rules for Roles Nov 30, 2024
Copy link

codecov bot commented Dec 1, 2024

Codecov Report

Attention: Patch coverage is 95.74468% with 58 lines in your changes missing coverage. Please review.

Project coverage is 93.2%. Comparing base (8d10a02) to head (e2476b1).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...mfa/security_structures/security_shield_builder.rs 93.8% 10 Missing ⚠️
...ructures/security_shield_builder_invalid_reason.rs 82.1% 10 Missing ⚠️
...security_structures/roles/builder/roles_builder.rs 95.4% 9 Missing ⚠️
...actor_instance_level/role_with_factor_instances.rs 75.0% 6 Missing ⚠️
...structures/matrices/matrix_of_factor_source_ids.rs 89.1% 5 Missing ⚠️
...s/factor_source_level/roles_with_factor_sources.rs 83.3% 5 Missing ⚠️
...structures/roles/abstract_role_builder_or_built.rs 94.5% 3 Missing ⚠️
...profile/logic/account/query_security_structures.rs 88.2% 2 Missing ⚠️
...ity_structures/matrices/builder/matrix_template.rs 98.8% 2 Missing ⚠️
...factors/security_structure_of_factor_source_ids.rs 86.6% 2 Missing ⚠️
... and 3 more
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##            main    #286     +/-   ##
=======================================
+ Coverage   93.1%   93.2%   +0.1%     
=======================================
  Files       1077    1084      +7     
  Lines      21805   22740    +935     
  Branches      77      77             
=======================================
+ Hits       20304   21214    +910     
- Misses      1487    1512     +25     
  Partials      14      14             
Flag Coverage Δ
kotlin 97.9% <ø> (-0.1%) ⬇️
rust 92.6% <95.6%> (+0.2%) ⬆️
swift 95.0% <100.0%> (+<0.1%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...thods/RET/TransactionManifest+Wrap+Functions.swift 100.0% <100.0%> (ø)
.../Profile/MFA/SecurityShieldBuilder+Swifified.swift 100.0% <100.0%> (ø)
...pters/securify_entity_factor_instances_provider.rs 92.0% <ø> (-4.0%) ⬇️
...archical_deterministic/cap26/paths/account_path.rs 100.0% <ø> (ø)
...rchical_deterministic/cap26/paths/identity_path.rs 94.1% <ø> (ø)
...tures/matrices/abstract_matrix_builder_or_built.rs 100.0% <100.0%> (ø)
.../mfa/security_structures/matrices/builder/error.rs 100.0% <100.0%> (ø)
...rity_structures/matrices/builder/matrix_builder.rs 100.0% <100.0%> (ø)
...ty_structures/matrices/factor_source_id_samples.rs 100.0% <100.0%> (ø)
..._structures/matrices/matrix_of_factor_instances.rs 100.0% <100.0%> (ø)
... and 34 more

... and 3 files with indirect coverage changes

@CyonAlexRDX CyonAlexRDX marked this pull request as ready for review December 1, 2024 21:48
Copy link
Contributor

@GhenadieVP GhenadieVP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments, need to review further.

Copy link

github-actions bot commented Dec 5, 2024

Phylum Report Link

…einterpret` crate instead of `paste` crate.
@CyonAlexRDX CyonAlexRDX merged commit 5fdde66 into main Dec 6, 2024
13 checks passed
@CyonAlexRDX CyonAlexRDX deleted the ac/security_structures_matrices_roles_with_rules branch December 6, 2024 13:38
@CyonAlexRDX CyonAlexRDX restored the ac/security_structures_matrices_roles_with_rules branch December 6, 2024 13:41
@CyonAlexRDX CyonAlexRDX deleted the ac/security_structures_matrices_roles_with_rules branch December 18, 2024 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants