Explorar el Código

Merge pull request #8 from Joystream/development

Rome
Mokhtar Naamani hace 5 años
padre
commit
983c0e53b5
Se han modificado 10 ficheros con 1924 adiciones y 0 borrados
  1. 15 0
      .gitignore
  2. 15 0
      .travis.yml
  3. 45 0
      Cargo.toml
  4. 28 0
      src/constraint.rs
  5. 57 0
      src/credentials.rs
  6. 646 0
      src/lib.rs
  7. 164 0
      src/mock.rs
  8. 135 0
      src/operations.rs
  9. 154 0
      src/permissions.rs
  10. 665 0
      src/tests.rs

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+# Generated by Cargo
+# will have compiled files and executables
+**/target/
+
+# Cargo lock file for native runtime - only used for cargo test
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# JetBrains IDEs
+.idea
+
+# Vim
+.*.sw*

+ 15 - 0
.travis.yml

@@ -0,0 +1,15 @@
+language: rust
+
+rust:
+  - 1.41.1
+
+cache:
+  - cargo
+
+before_script:
+  - rustup component add rustfmt
+
+script:
+  - cargo fmt --all -- --check
+  - cargo test --verbose --all
+

+ 45 - 0
Cargo.toml

@@ -0,0 +1,45 @@
+[package]
+name = 'substrate-versioned-store-permissions-module'
+version = '1.0.0'
+authors = ['Mokhtar Naamani <mokhtar.naamani@gmail.com>']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+timestamp = { package = 'srml-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+#https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '=1.0.2'
+
+[dependencies.versioned-store]
+default_features = false
+package ='substrate-versioned-store'
+git = 'https://github.com/joystream/substrate-versioned-store-module'
+tag = "v1.0.0"
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'timestamp/std',
+	'versioned-store/std',
+]

+ 28 - 0
src/constraint.rs

@@ -0,0 +1,28 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+
+/// Reference to a specific property of a specific class.
+#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
+pub struct PropertyOfClass<ClassId, PropertyIndex> {
+    pub class_id: ClassId,
+    pub property_index: PropertyIndex,
+}
+
+/// The type of constraint imposed on referencing a class via class property of type "Internal".
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ReferenceConstraint<ClassId: Ord, PropertyIndex: Ord> {
+    /// No property can reference the class.
+    NoReferencingAllowed,
+
+    /// Any property of any class may reference the class.
+    NoConstraint,
+
+    /// Only a set of properties of specific classes can reference the class.
+    Restricted(BTreeSet<PropertyOfClass<ClassId, PropertyIndex>>),
+}
+
+impl<ClassId: Ord, PropertyIndex: Ord> Default for ReferenceConstraint<ClassId, PropertyIndex> {
+    fn default() -> Self {
+        ReferenceConstraint::NoReferencingAllowed
+    }
+}

+ 57 - 0
src/credentials.rs

@@ -0,0 +1,57 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+use rstd::prelude::*;
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct CredentialSet<Credential>(BTreeSet<Credential>);
+
+impl<Credential> From<Vec<Credential>> for CredentialSet<Credential>
+where
+    Credential: Ord,
+{
+    fn from(v: Vec<Credential>) -> CredentialSet<Credential> {
+        let mut set = CredentialSet(BTreeSet::new());
+        for credential in v.into_iter() {
+            set.insert(credential);
+        }
+        set
+    }
+}
+
+/// Default CredentialSet set is just an empty set.
+impl<Credential: Ord> Default for CredentialSet<Credential> {
+    fn default() -> Self {
+        CredentialSet(BTreeSet::new())
+    }
+}
+
+impl<Credential: Ord> CredentialSet<Credential> {
+    pub fn new() -> Self {
+        Self(BTreeSet::new())
+    }
+
+    pub fn insert(&mut self, value: Credential) -> bool {
+        self.0.insert(value)
+    }
+
+    pub fn contains(&self, value: &Credential) -> bool {
+        self.0.contains(value)
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+/// Type, derived from dispatchable call, identifies the caller
+#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
+pub enum AccessLevel<Credential> {
+    /// ROOT origin
+    System,
+    /// Caller identified as the entity maintainer
+    EntityMaintainer, // Maybe enclose EntityId?
+    /// Verified Credential
+    Credential(Credential),
+    /// In cases where a signed extrinsic doesn't provide a Credential
+    Unspecified,
+}

+ 646 - 0
src/lib.rs

@@ -0,0 +1,646 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::Codec;
+use rstd::collections::btree_map::BTreeMap;
+use rstd::prelude::*;
+use runtime_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
+use srml_support::{decl_module, decl_storage, dispatch, ensure, Parameter};
+use system;
+
+// EntityId, ClassId -> should be configured on versioned_store::Trait
+pub use versioned_store::{ClassId, ClassPropertyValue, EntityId, Property, PropertyValue};
+
+mod constraint;
+mod credentials;
+mod mock;
+mod operations;
+mod permissions;
+mod tests;
+
+pub use constraint::*;
+pub use credentials::*;
+pub use operations::*;
+pub use permissions::*;
+
+/// Trait for checking if an account has specified Credential
+pub trait CredentialChecker<T: Trait> {
+    fn account_has_credential(account: &T::AccountId, credential: T::Credential) -> bool;
+}
+
+/// An implementation where no account has any credential. Effectively
+/// only the system will be able to perform any action on the versioned store.
+impl<T: Trait> CredentialChecker<T> for () {
+    fn account_has_credential(_account: &T::AccountId, _credential: T::Credential) -> bool {
+        false
+    }
+}
+
+/// An implementation that calls into multiple checkers. This allows for multiple modules
+/// to maintain AccountId to Credential mappings.
+impl<T: Trait, X: CredentialChecker<T>, Y: CredentialChecker<T>> CredentialChecker<T> for (X, Y) {
+    fn account_has_credential(account: &T::AccountId, group: T::Credential) -> bool {
+        X::account_has_credential(account, group) || Y::account_has_credential(account, group)
+    }
+}
+
+/// Trait for externally checking if an account can create new classes in the versioned store.
+pub trait CreateClassPermissionsChecker<T: Trait> {
+    fn account_can_create_class_permissions(account: &T::AccountId) -> bool;
+}
+
+/// An implementation that does not permit any account to create classes. Effectively
+/// only the system can create classes.
+impl<T: Trait> CreateClassPermissionsChecker<T> for () {
+    fn account_can_create_class_permissions(_account: &T::AccountId) -> bool {
+        false
+    }
+}
+
+pub type ClassPermissionsType<T> =
+    ClassPermissions<ClassId, <T as Trait>::Credential, u16, <T as system::Trait>::BlockNumber>;
+
+pub trait Trait: system::Trait + versioned_store::Trait {
+    // type Event: ...
+    // Do we need Events?
+
+    /// Type that represents an actor or group of actors in the system.
+    type Credential: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + Clone
+        + MaybeSerialize
+        + Eq
+        + PartialEq
+        + Ord;
+
+    /// External type for checking if an account has specified credential.
+    type CredentialChecker: CredentialChecker<Self>;
+
+    /// External type used to check if an account has permission to create new Classes.
+    type CreateClassPermissionsChecker: CreateClassPermissionsChecker<Self>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as VersionedStorePermissions {
+      /// ClassPermissions of corresponding Classes in the versioned store
+      pub ClassPermissionsByClassId get(class_permissions_by_class_id): linked_map ClassId => ClassPermissionsType<T>;
+
+      /// Owner of an entity in the versioned store. If it is None then it is owned by the system.
+      pub EntityMaintainerByEntityId get(entity_maintainer_by_entity_id): linked_map EntityId => Option<T::Credential>;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        /// Sets the admins for a class
+        fn set_class_admins(
+            origin,
+            class_id: ClassId,
+            admins: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                None,
+                Self::is_system, // root origin
+                class_id,
+                |class_permissions| {
+                    class_permissions.admins = admins;
+                    Ok(())
+                }
+            )
+        }
+
+        // Methods for updating concrete permissions
+
+        fn set_class_entity_permissions(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            entity_permissions: EntityPermissions<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.entity_permissions = entity_permissions;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_entities_can_be_created(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            can_be_created: bool
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.entities_can_be_created = can_be_created;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_add_schemas_set(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            credential_set: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.add_schemas = credential_set;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_create_entities_set(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            credential_set: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.create_entities = credential_set;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_reference_constraint(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            constraint: ReferenceConstraint<ClassId, u16>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.reference_constraint = constraint;
+                    Ok(())
+                }
+            )
+        }
+
+        // Setting a new maintainer for an entity may require having additional constraints.
+        // So for now it is disabled.
+        // pub fn set_entity_maintainer(
+        //     origin,
+        //     entity_id: EntityId,
+        //     new_maintainer: Option<T::Credential>
+        // ) -> dispatch::Result {
+        //     ensure_root(origin)?;
+
+        //     // ensure entity exists in the versioned store
+        //     let _ = Self::get_class_id_by_entity_id(entity_id)?;
+
+        //     <EntityMaintainerByEntityId<T>>::mutate(entity_id, |maintainer| {
+        //         *maintainer = new_maintainer;
+        //     });
+
+        //     Ok(())
+        // }
+
+        // Permissioned proxy calls to versioned store
+
+        pub fn create_class(
+            origin,
+            name: Vec<u8>,
+            description: Vec<u8>,
+            class_permissions: ClassPermissionsType<T>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            let can_create_class = match raw_origin {
+                system::RawOrigin::Root => true,
+                system::RawOrigin::Signed(sender) => {
+                    T::CreateClassPermissionsChecker::account_can_create_class_permissions(&sender)
+                },
+                _ => false
+            };
+
+            if can_create_class {
+                let class_id = <versioned_store::Module<T>>::create_class(name, description)?;
+
+                // is there a need to assert class_id is unique?
+
+                <ClassPermissionsByClassId<T>>::insert(&class_id, class_permissions);
+
+                Ok(())
+            } else {
+                Err("NotPermittedToCreateClass")
+            }
+        }
+
+        pub fn create_class_with_default_permissions(
+            origin,
+            name: Vec<u8>,
+            description: Vec<u8>
+        ) -> dispatch::Result {
+            Self::create_class(origin, name, description, ClassPermissions::default())
+        }
+
+        pub fn add_class_schema(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            existing_properties: Vec<u16>,
+            new_properties: Vec<Property>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::if_class_permissions_satisfied(
+                &raw_origin,
+                with_credential,
+                None,
+                ClassPermissions::can_add_class_schema,
+                class_id,
+                |_class_permissions, _access_level| {
+                    // If a new property points at another class,
+                    // at this point we don't enforce anything about reference constraints
+                    // because of the chicken and egg problem. Instead enforcement is done
+                    // at the time of creating an entity.
+                    let _schema_index = <versioned_store::Module<T>>::add_class_schema(class_id, existing_properties, new_properties)?;
+                    Ok(())
+                }
+            )
+        }
+
+        /// Creates a new entity of type class_id. The maintainer is set to be either None if the origin is root, or the provided credential
+        /// associated with signer.
+        pub fn create_entity(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            let _entity_id = Self::do_create_entity(&raw_origin, with_credential, class_id)?;
+            Ok(())
+        }
+
+        pub fn add_schema_support_to_entity(
+            origin,
+            with_credential: Option<T::Credential>,
+            as_entity_maintainer: bool,
+            entity_id: EntityId,
+            schema_id: u16, // Do not type alias u16!! - u16,
+            property_values: Vec<ClassPropertyValue>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            Self::do_add_schema_support_to_entity(&raw_origin, with_credential, as_entity_maintainer, entity_id, schema_id, property_values)
+        }
+
+        pub fn update_entity_property_values(
+            origin,
+            with_credential: Option<T::Credential>,
+            as_entity_maintainer: bool,
+            entity_id: EntityId,
+            property_values: Vec<ClassPropertyValue>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            Self::do_update_entity_property_values(&raw_origin, with_credential, as_entity_maintainer, entity_id, property_values)
+        }
+
+        pub fn transaction(origin, operations: Vec<Operation<T::Credential>>) -> dispatch::Result {
+            // This map holds the EntityId of the entity created as a result of executing a CreateEntity Operation
+            // keyed by the indexed of the operation, in the operations vector.
+            let mut entity_created_in_operation: BTreeMap<usize, EntityId> = BTreeMap::new();
+
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            for (op_index, operation) in operations.into_iter().enumerate() {
+                match operation.operation_type {
+                    OperationType::CreateEntity(create_entity_operation) => {
+                        let entity_id = Self::do_create_entity(&raw_origin, operation.with_credential, create_entity_operation.class_id)?;
+                        entity_created_in_operation.insert(op_index, entity_id);
+                    },
+                    OperationType::UpdatePropertyValues(update_property_values_operation) => {
+                        let entity_id = operations::parametrized_entity_to_entity_id(&entity_created_in_operation, update_property_values_operation.entity_id)?;
+                        let property_values = operations::parametrized_property_values_to_property_values(&entity_created_in_operation, update_property_values_operation.new_parametrized_property_values)?;
+                        Self::do_update_entity_property_values(&raw_origin, operation.with_credential, operation.as_entity_maintainer, entity_id, property_values)?;
+                    },
+                    OperationType::AddSchemaSupportToEntity(add_schema_support_to_entity_operation) => {
+                        let entity_id = operations::parametrized_entity_to_entity_id(&entity_created_in_operation, add_schema_support_to_entity_operation.entity_id)?;
+                        let schema_id = add_schema_support_to_entity_operation.schema_id;
+                        let property_values = operations::parametrized_property_values_to_property_values(&entity_created_in_operation, add_schema_support_to_entity_operation.parametrized_property_values)?;
+                        Self::do_add_schema_support_to_entity(&raw_origin, operation.with_credential, operation.as_entity_maintainer, entity_id, schema_id, property_values)?;
+                    }
+                }
+            }
+
+            Ok(())
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_root_or_signed(
+        origin: T::Origin,
+    ) -> Result<system::RawOrigin<T::AccountId>, &'static str> {
+        match origin.into() {
+            Ok(system::RawOrigin::Root) => Ok(system::RawOrigin::Root),
+            Ok(system::RawOrigin::Signed(account_id)) => Ok(system::RawOrigin::Signed(account_id)),
+            _ => Err("BadOrigin:ExpectedRootOrSigned"),
+        }
+    }
+
+    fn do_create_entity(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        class_id: ClassId,
+    ) -> Result<EntityId, &'static str> {
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            None,
+            ClassPermissions::can_create_entity,
+            class_id,
+            |_class_permissions, access_level| {
+                let entity_id = <versioned_store::Module<T>>::create_entity(class_id)?;
+
+                // Note: mutating value to None is equivalient to removing the value from storage map
+                <EntityMaintainerByEntityId<T>>::mutate(
+                    entity_id,
+                    |maintainer| match access_level {
+                        AccessLevel::System => *maintainer = None,
+                        AccessLevel::Credential(credential) => *maintainer = Some(*credential),
+                        _ => *maintainer = None,
+                    },
+                );
+
+                Ok(entity_id)
+            },
+        )
+    }
+
+    fn do_update_entity_property_values(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: bool,
+        entity_id: EntityId,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        let class_id = Self::get_class_id_by_entity_id(entity_id)?;
+
+        Self::ensure_internal_property_values_permitted(class_id, &property_values)?;
+
+        let as_entity_maintainer = if as_entity_maintainer {
+            Some(entity_id)
+        } else {
+            None
+        };
+
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            as_entity_maintainer,
+            ClassPermissions::can_update_entity,
+            class_id,
+            |_class_permissions, _access_level| {
+                <versioned_store::Module<T>>::update_entity_property_values(
+                    entity_id,
+                    property_values,
+                )
+            },
+        )
+    }
+
+    fn do_add_schema_support_to_entity(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: bool,
+        entity_id: EntityId,
+        schema_id: u16,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        // class id of the entity being updated
+        let class_id = Self::get_class_id_by_entity_id(entity_id)?;
+
+        Self::ensure_internal_property_values_permitted(class_id, &property_values)?;
+
+        let as_entity_maintainer = if as_entity_maintainer {
+            Some(entity_id)
+        } else {
+            None
+        };
+
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            as_entity_maintainer,
+            ClassPermissions::can_update_entity,
+            class_id,
+            |_class_permissions, _access_level| {
+                <versioned_store::Module<T>>::add_schema_support_to_entity(
+                    entity_id,
+                    schema_id,
+                    property_values,
+                )
+            },
+        )
+    }
+
+    /// Derives the AccessLevel the caller is attempting to act with.
+    /// It expects only signed or root origin.
+    fn derive_access_level(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: Option<EntityId>,
+    ) -> Result<AccessLevel<T::Credential>, &'static str> {
+        match raw_origin {
+            system::RawOrigin::Root => Ok(AccessLevel::System),
+            system::RawOrigin::Signed(account_id) => {
+                if let Some(credential) = with_credential {
+                    if T::CredentialChecker::account_has_credential(&account_id, credential) {
+                        if let Some(entity_id) = as_entity_maintainer {
+                            // is entity maintained by system
+                            ensure!(
+                                <EntityMaintainerByEntityId<T>>::exists(entity_id),
+                                "NotEnityMaintainer"
+                            );
+                            // ensure entity maintainer matches
+                            match Self::entity_maintainer_by_entity_id(entity_id) {
+                                Some(maintainer_credential)
+                                    if credential == maintainer_credential =>
+                                {
+                                    Ok(AccessLevel::EntityMaintainer)
+                                }
+                                _ => Err("NotEnityMaintainer"),
+                            }
+                        } else {
+                            Ok(AccessLevel::Credential(credential))
+                        }
+                    } else {
+                        Err("OriginCannotActWithRequestedCredential")
+                    }
+                } else {
+                    Ok(AccessLevel::Unspecified)
+                }
+            }
+            _ => Err("BadOrigin:ExpectedRootOrSigned"),
+        }
+    }
+
+    /// Returns the stored class permissions if exist, error otherwise.
+    fn ensure_class_permissions(
+        class_id: ClassId,
+    ) -> Result<ClassPermissionsType<T>, &'static str> {
+        ensure!(
+            <ClassPermissionsByClassId<T>>::exists(class_id),
+            "ClassPermissionsNotFoundByClassId"
+        );
+        Ok(Self::class_permissions_by_class_id(class_id))
+    }
+
+    /// Derives the access level of the caller.
+    /// If the predicate passes, the mutate method is invoked.
+    fn mutate_class_permissions<Predicate, Mutate>(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        // predicate to test
+        predicate: Predicate,
+        // class permissions to perform mutation on if it exists
+        class_id: ClassId,
+        // actual mutation to apply.
+        mutate: Mutate,
+    ) -> dispatch::Result
+    where
+        Predicate:
+            FnOnce(&ClassPermissionsType<T>, &AccessLevel<T::Credential>) -> dispatch::Result,
+        Mutate: FnOnce(&mut ClassPermissionsType<T>) -> dispatch::Result,
+    {
+        let access_level = Self::derive_access_level(raw_origin, with_credential, None)?;
+        let mut class_permissions = Self::ensure_class_permissions(class_id)?;
+
+        predicate(&class_permissions, &access_level)?;
+        mutate(&mut class_permissions)?;
+        class_permissions.last_permissions_update = <system::Module<T>>::block_number();
+        <ClassPermissionsByClassId<T>>::insert(class_id, class_permissions);
+        Ok(())
+    }
+
+    fn is_system(
+        _: &ClassPermissionsType<T>,
+        access_level: &AccessLevel<T::Credential>,
+    ) -> dispatch::Result {
+        if *access_level == AccessLevel::System {
+            Ok(())
+        } else {
+            Err("NotRootOrigin")
+        }
+    }
+
+    /// Derives the access level of the caller.
+    /// If the peridcate passes the callback is invoked. Returns result of the callback
+    /// or error from failed predicate.
+    fn if_class_permissions_satisfied<Predicate, Callback, R>(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: Option<EntityId>,
+        // predicate to test
+        predicate: Predicate,
+        // class permissions to test
+        class_id: ClassId,
+        // callback to invoke if predicate passes
+        callback: Callback,
+    ) -> Result<R, &'static str>
+    where
+        Predicate:
+            FnOnce(&ClassPermissionsType<T>, &AccessLevel<T::Credential>) -> dispatch::Result,
+        Callback: FnOnce(
+            &ClassPermissionsType<T>,
+            &AccessLevel<T::Credential>,
+        ) -> Result<R, &'static str>,
+    {
+        let access_level =
+            Self::derive_access_level(raw_origin, with_credential, as_entity_maintainer)?;
+        let class_permissions = Self::ensure_class_permissions(class_id)?;
+
+        predicate(&class_permissions, &access_level)?;
+        callback(&class_permissions, &access_level)
+    }
+
+    fn get_class_id_by_entity_id(entity_id: EntityId) -> Result<ClassId, &'static str> {
+        // use a utility method on versioned_store module
+        ensure!(
+            versioned_store::EntityById::exists(entity_id),
+            "EntityNotFound"
+        );
+        let entity = <versioned_store::Module<T>>::entity_by_id(entity_id);
+        Ok(entity.class_id)
+    }
+
+    // Ensures property_values of type Internal that point to a class,
+    // the target entity and class exists and constraint allows it.
+    fn ensure_internal_property_values_permitted(
+        source_class_id: ClassId,
+        property_values: &[ClassPropertyValue],
+    ) -> dispatch::Result {
+        for property_value in property_values.iter() {
+            if let PropertyValue::Internal(ref target_entity_id) = property_value.value {
+                // get the class permissions for target class
+                let target_class_id = Self::get_class_id_by_entity_id(*target_entity_id)?;
+                // assert class permissions exists for target class
+                let class_permissions = Self::class_permissions_by_class_id(target_class_id);
+
+                // ensure internal reference is permitted
+                match class_permissions.reference_constraint {
+                    ReferenceConstraint::NoConstraint => Ok(()),
+                    ReferenceConstraint::NoReferencingAllowed => {
+                        Err("EntityCannotReferenceTargetEntity")
+                    }
+                    ReferenceConstraint::Restricted(permitted_properties) => {
+                        if permitted_properties.contains(&PropertyOfClass {
+                            class_id: source_class_id,
+                            property_index: property_value.in_class_index,
+                        }) {
+                            Ok(())
+                        } else {
+                            Err("EntityCannotReferenceTargetEntity")
+                        }
+                    }
+                }?;
+            }
+        }
+
+        // if we reach here all Internal properties have passed the constraint check
+        Ok(())
+    }
+}

+ 164 - 0
src/mock.rs

@@ -0,0 +1,164 @@
+#![cfg(test)]
+
+use crate::*;
+use crate::{Module, Trait};
+
+use primitives::H256;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+use versioned_store::InputValidationLengthConstraint;
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Runtime;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Runtime {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl versioned_store::Trait for Runtime {
+    type Event = ();
+}
+
+impl Trait for Runtime {
+    type Credential = u64;
+    type CredentialChecker = MockCredentialChecker;
+    type CreateClassPermissionsChecker = MockCreateClassPermissionsChecker;
+}
+
+pub const MEMBER_ONE_WITH_CREDENTIAL_ZERO: u64 = 100;
+pub const MEMBER_TWO_WITH_CREDENTIAL_ZERO: u64 = 101;
+pub const MEMBER_ONE_WITH_CREDENTIAL_ONE: u64 = 102;
+pub const MEMBER_TWO_WITH_CREDENTIAL_ONE: u64 = 103;
+
+pub const PRINCIPAL_GROUP_MEMBERS: [[u64; 2]; 2] = [
+    [
+        MEMBER_ONE_WITH_CREDENTIAL_ZERO,
+        MEMBER_TWO_WITH_CREDENTIAL_ZERO,
+    ],
+    [
+        MEMBER_ONE_WITH_CREDENTIAL_ONE,
+        MEMBER_TWO_WITH_CREDENTIAL_ONE,
+    ],
+];
+
+pub struct MockCredentialChecker {}
+
+impl CredentialChecker<Runtime> for MockCredentialChecker {
+    fn account_has_credential(
+        account_id: &<Runtime as system::Trait>::AccountId,
+        credential_id: <Runtime as Trait>::Credential,
+    ) -> bool {
+        if (credential_id as usize) < PRINCIPAL_GROUP_MEMBERS.len() {
+            PRINCIPAL_GROUP_MEMBERS[credential_id as usize]
+                .iter()
+                .any(|id| *id == *account_id)
+        } else {
+            false
+        }
+    }
+}
+
+pub const CLASS_PERMISSIONS_CREATOR1: u64 = 200;
+pub const CLASS_PERMISSIONS_CREATOR2: u64 = 300;
+pub const UNAUTHORIZED_CLASS_PERMISSIONS_CREATOR: u64 = 50;
+
+const CLASS_PERMISSIONS_CREATORS: [u64; 2] =
+    [CLASS_PERMISSIONS_CREATOR1, CLASS_PERMISSIONS_CREATOR2];
+
+pub struct MockCreateClassPermissionsChecker {}
+
+impl CreateClassPermissionsChecker<Runtime> for MockCreateClassPermissionsChecker {
+    fn account_can_create_class_permissions(
+        account_id: &<Runtime as system::Trait>::AccountId,
+    ) -> bool {
+        CLASS_PERMISSIONS_CREATORS
+            .iter()
+            .any(|id| *id == *account_id)
+    }
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+
+fn default_versioned_store_genesis_config() -> versioned_store::GenesisConfig {
+    versioned_store::GenesisConfig {
+        class_by_id: vec![],
+        entity_by_id: vec![],
+        next_class_id: 1,
+        next_entity_id: 1,
+        property_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        property_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+        class_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        class_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+    }
+}
+
+fn build_test_externalities(
+    config: versioned_store::GenesisConfig,
+) -> runtime_io::TestExternalities {
+    let mut t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    config.assimilate_storage(&mut t).unwrap();
+
+    t.into()
+}
+
+pub fn with_test_externalities<R, F: FnOnce() -> R>(f: F) -> R {
+    let versioned_store_config = default_versioned_store_genesis_config();
+    build_test_externalities(versioned_store_config).execute_with(f)
+}
+
+// pub type System = system::Module;
+
+/// Export module on a test runtime
+pub type Permissions = Module<Runtime>;

+ 135 - 0
src/operations.rs

@@ -0,0 +1,135 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_map::BTreeMap;
+use rstd::prelude::*;
+use versioned_store::{ClassId, ClassPropertyValue, EntityId, PropertyValue};
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ParametrizedPropertyValue {
+    /// Same fields as normal PropertyValue
+    PropertyValue(PropertyValue),
+
+    /// This is the index of an operation creating an entity in the transaction/batch operations
+    InternalEntityJustAdded(u32), // should really be usize but it doesn't have Encode/Decode support
+
+    /// Vector of mix of Entities already existing and just added in a recent operation
+    InternalEntityVec(Vec<ParameterizedEntity>),
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ParameterizedEntity {
+    InternalEntityJustAdded(u32),
+    ExistingEntity(EntityId),
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct ParametrizedClassPropertyValue {
+    /// Index is into properties vector of class.
+    pub in_class_index: u16,
+
+    /// Value of property with index `in_class_index` in a given class.
+    pub value: ParametrizedPropertyValue,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct CreateEntityOperation {
+    pub class_id: ClassId,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct UpdatePropertyValuesOperation {
+    pub entity_id: ParameterizedEntity,
+    pub new_parametrized_property_values: Vec<ParametrizedClassPropertyValue>,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct AddSchemaSupportToEntityOperation {
+    pub entity_id: ParameterizedEntity,
+    pub schema_id: u16,
+    pub parametrized_property_values: Vec<ParametrizedClassPropertyValue>,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum OperationType {
+    CreateEntity(CreateEntityOperation),
+    UpdatePropertyValues(UpdatePropertyValuesOperation),
+    AddSchemaSupportToEntity(AddSchemaSupportToEntityOperation),
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct Operation<Credential> {
+    pub with_credential: Option<Credential>,
+    pub as_entity_maintainer: bool,
+    pub operation_type: OperationType,
+}
+
+pub fn parametrized_entity_to_entity_id(
+    created_entities: &BTreeMap<usize, EntityId>,
+    entity: ParameterizedEntity,
+) -> Result<EntityId, &'static str> {
+    match entity {
+        ParameterizedEntity::ExistingEntity(entity_id) => Ok(entity_id),
+        ParameterizedEntity::InternalEntityJustAdded(op_index_u32) => {
+            let op_index = op_index_u32 as usize;
+            if created_entities.contains_key(&op_index) {
+                let entity_id = created_entities.get(&op_index).unwrap();
+                Ok(*entity_id)
+            } else {
+                Err("EntityNotCreatedByOperation")
+            }
+        }
+    }
+}
+
+pub fn parametrized_property_values_to_property_values(
+    created_entities: &BTreeMap<usize, EntityId>,
+    parametrized_property_values: Vec<ParametrizedClassPropertyValue>,
+) -> Result<Vec<ClassPropertyValue>, &'static str> {
+    let mut class_property_values: Vec<ClassPropertyValue> = vec![];
+
+    for parametrized_class_property_value in parametrized_property_values.into_iter() {
+        let property_value = match parametrized_class_property_value.value {
+            ParametrizedPropertyValue::PropertyValue(value) => value,
+            ParametrizedPropertyValue::InternalEntityJustAdded(
+                entity_created_in_operation_index,
+            ) => {
+                // Verify that referenced entity was indeed created created
+                let op_index = entity_created_in_operation_index as usize;
+                if created_entities.contains_key(&op_index) {
+                    let entity_id = created_entities.get(&op_index).unwrap();
+                    PropertyValue::Internal(*entity_id)
+                } else {
+                    return Err("EntityNotCreatedByOperation");
+                }
+            }
+            ParametrizedPropertyValue::InternalEntityVec(parametrized_entities) => {
+                let mut entities: Vec<EntityId> = vec![];
+
+                for parametrized_entity in parametrized_entities.into_iter() {
+                    match parametrized_entity {
+                        ParameterizedEntity::ExistingEntity(id) => entities.push(id),
+                        ParameterizedEntity::InternalEntityJustAdded(
+                            entity_created_in_operation_index,
+                        ) => {
+                            let op_index = entity_created_in_operation_index as usize;
+                            if created_entities.contains_key(&op_index) {
+                                let entity_id = created_entities.get(&op_index).unwrap();
+                                entities.push(*entity_id);
+                            } else {
+                                return Err("EntityNotCreatedByOperation");
+                            }
+                        }
+                    }
+                }
+
+                PropertyValue::InternalVec(entities)
+            }
+        };
+
+        class_property_values.push(ClassPropertyValue {
+            in_class_index: parametrized_class_property_value.in_class_index,
+            value: property_value,
+        });
+    }
+
+    Ok(class_property_values)
+}

+ 154 - 0
src/permissions.rs

@@ -0,0 +1,154 @@
+use codec::{Decode, Encode};
+use srml_support::dispatch;
+
+use crate::constraint::*;
+use crate::credentials::*;
+
+/// Permissions for an instance of a Class in the versioned store.
+#[derive(Encode, Decode, Default, Eq, PartialEq, Clone, Debug)]
+pub struct ClassPermissions<ClassId, Credential, PropertyIndex, BlockNumber>
+where
+    ClassId: Ord,
+    Credential: Ord + Clone,
+    PropertyIndex: Ord,
+{
+    // concrete permissions
+    /// Permissions that are applied to entities of this class, define who in addition to
+    /// root origin can update entities of this class.
+    pub entity_permissions: EntityPermissions<Credential>,
+
+    /// Wether new entities of this class be created or not. Is not enforced for root origin.
+    pub entities_can_be_created: bool,
+
+    /// Who can add new schemas in the versioned store for this class
+    pub add_schemas: CredentialSet<Credential>,
+
+    /// Who can create new entities in the versioned store of this class
+    pub create_entities: CredentialSet<Credential>,
+
+    /// The type of constraint on referencing the class from other entities.
+    pub reference_constraint: ReferenceConstraint<ClassId, PropertyIndex>,
+
+    /// Who (in addition to root origin) can update all concrete permissions.
+    /// The admins can only be set by the root origin, "System".
+    pub admins: CredentialSet<Credential>,
+
+    // Block where permissions were changed
+    pub last_permissions_update: BlockNumber,
+}
+
+impl<ClassId, Credential, PropertyIndex, BlockNumber>
+    ClassPermissions<ClassId, Credential, PropertyIndex, BlockNumber>
+where
+    ClassId: Ord,
+    Credential: Ord + Clone,
+    PropertyIndex: Ord,
+{
+    /// Returns Ok if access_level is root origin or credential is in admins set, Err otherwise
+    pub fn is_admin(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if class_permissions.admins.contains(credential) {
+                    Ok(())
+                } else {
+                    Err("NotInAdminsSet")
+                }
+            }
+            AccessLevel::Unspecified => Err("UnspecifiedActor"),
+            AccessLevel::EntityMaintainer => Err("AccessLevel::EntityMaintainer-UsedOutOfPlace"),
+        }
+    }
+
+    pub fn can_add_class_schema(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if class_permissions.add_schemas.contains(credential) {
+                    Ok(())
+                } else {
+                    Err("NotInAddSchemasSet")
+                }
+            }
+            AccessLevel::Unspecified => Err("UnspecifiedActor"),
+            AccessLevel::EntityMaintainer => Err("AccessLevel::EntityMaintainer-UsedOutOfPlace"),
+        }
+    }
+
+    pub fn can_create_entity(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if !class_permissions.entities_can_be_created {
+                    Err("EntitiesCannotBeCreated")
+                } else if class_permissions.create_entities.contains(credential) {
+                    Ok(())
+                } else {
+                    Err("NotInCreateEntitiesSet")
+                }
+            }
+            AccessLevel::Unspecified => Err("UnspecifiedActor"),
+            AccessLevel::EntityMaintainer => Err("AccessLevel::EntityMaintainer-UsedOutOfPlace"),
+        }
+    }
+
+    pub fn can_update_entity(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if class_permissions
+                    .entity_permissions
+                    .update
+                    .contains(credential)
+                {
+                    Ok(())
+                } else {
+                    Err("CredentialNotInEntityPermissionsUpdateSet")
+                }
+            }
+            AccessLevel::EntityMaintainer => {
+                if class_permissions
+                    .entity_permissions
+                    .maintainer_has_all_permissions
+                {
+                    Ok(())
+                } else {
+                    Err("MaintainerNotGivenAllPermissions")
+                }
+            }
+            _ => Err("UnknownActor"),
+        }
+    }
+}
+
+#[derive(Encode, Decode, Clone, Debug, Eq, PartialEq)]
+pub struct EntityPermissions<Credential>
+where
+    Credential: Ord,
+{
+    // Principals permitted to update any entity of the class which this permission is associated with.
+    pub update: CredentialSet<Credential>,
+    /// Wether the designated maintainer (if set) of an entity has permission to update it.
+    pub maintainer_has_all_permissions: bool,
+}
+
+impl<Credential: Ord> Default for EntityPermissions<Credential> {
+    fn default() -> Self {
+        EntityPermissions {
+            maintainer_has_all_permissions: true,
+            update: CredentialSet::new(),
+        }
+    }
+}

+ 665 - 0
src/tests.rs

@@ -0,0 +1,665 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+use rstd::collections::btree_set::BTreeSet;
+use versioned_store::PropertyType;
+
+use srml_support::{assert_err, assert_ok};
+
+fn simple_test_schema() -> Vec<Property> {
+    vec![Property {
+        prop_type: PropertyType::Int64,
+        required: false,
+        name: b"field1".to_vec(),
+        description: b"Description field1".to_vec(),
+    }]
+}
+
+fn simple_test_entity_property_values() -> Vec<ClassPropertyValue> {
+    vec![ClassPropertyValue {
+        in_class_index: 0,
+        value: PropertyValue::Int64(1337),
+    }]
+}
+
+fn create_simple_class(permissions: ClassPermissionsType<Runtime>) -> ClassId {
+    let class_id = <versioned_store::Module<Runtime>>::next_class_id();
+    assert_ok!(Permissions::create_class(
+        Origin::signed(CLASS_PERMISSIONS_CREATOR1),
+        b"class_name_1".to_vec(),
+        b"class_description_1".to_vec(),
+        permissions
+    ));
+    class_id
+}
+
+fn create_simple_class_with_default_permissions() -> ClassId {
+    create_simple_class(Default::default())
+}
+
+fn class_permissions_minimal() -> ClassPermissionsType<Runtime> {
+    ClassPermissions {
+        // remove special permissions for entity maintainers
+        entity_permissions: EntityPermissions {
+            maintainer_has_all_permissions: false,
+            ..Default::default()
+        },
+        ..Default::default()
+    }
+}
+
+fn class_permissions_minimal_with_admins(
+    admins: Vec<<Runtime as Trait>::Credential>,
+) -> ClassPermissionsType<Runtime> {
+    ClassPermissions {
+        admins: admins.into(),
+        ..class_permissions_minimal()
+    }
+}
+
+fn next_entity_id() -> EntityId {
+    <versioned_store::Module<Runtime>>::next_entity_id()
+}
+
+#[test]
+fn create_class_then_entity_with_default_class_permissions() {
+    with_test_externalities(|| {
+        // Only authorized accounts can create classes
+        assert_err!(
+            Permissions::create_class_with_default_permissions(
+                Origin::signed(UNAUTHORIZED_CLASS_PERMISSIONS_CREATOR),
+                b"class_name".to_vec(),
+                b"class_description".to_vec(),
+            ),
+            "NotPermittedToCreateClass"
+        );
+
+        let class_id = create_simple_class_with_default_permissions();
+
+        assert!(<ClassPermissionsByClassId<Runtime>>::exists(class_id));
+
+        // default class permissions have empty add_schema acl
+        assert_err!(
+            Permissions::add_class_schema(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ZERO),
+                Some(0),
+                class_id,
+                vec![],
+                simple_test_schema()
+            ),
+            "NotInAddSchemasSet"
+        );
+
+        // give members of GROUP_ZERO permission to add schemas
+        let add_schema_set = CredentialSet::from(vec![0]);
+        assert_ok!(Permissions::set_class_add_schemas_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            add_schema_set
+        ));
+
+        // successfully add a new schema
+        assert_ok!(Permissions::add_class_schema(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ZERO),
+            Some(0),
+            class_id,
+            vec![],
+            simple_test_schema()
+        ));
+
+        // System can always create entities (provided class exists) bypassing any permissions
+        let entity_id_1 = next_entity_id();
+        assert_ok!(Permissions::create_entity(Origin::ROOT, None, class_id,));
+        // entities created by system are "un-owned"
+        assert!(!<EntityMaintainerByEntityId<Runtime>>::exists(entity_id_1));
+        assert_eq!(
+            Permissions::entity_maintainer_by_entity_id(entity_id_1),
+            None
+        );
+
+        // default permissions have empty create_entities set and by default no entities can be created
+        assert_err!(
+            Permissions::create_entity(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+            ),
+            "EntitiesCannotBeCreated"
+        );
+
+        assert_ok!(Permissions::set_class_entities_can_be_created(
+            Origin::ROOT,
+            None,
+            class_id,
+            true
+        ));
+
+        assert_err!(
+            Permissions::create_entity(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+            ),
+            "NotInCreateEntitiesSet"
+        );
+
+        // give members of GROUP_ONE permission to create entities
+        let create_entities_set = CredentialSet::from(vec![1]);
+        assert_ok!(Permissions::set_class_create_entities_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            create_entities_set
+        ));
+
+        let entity_id_2 = next_entity_id();
+        assert_ok!(Permissions::create_entity(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            Some(1),
+            class_id,
+        ));
+        assert!(<EntityMaintainerByEntityId<Runtime>>::exists(entity_id_2));
+        assert_eq!(
+            Permissions::entity_maintainer_by_entity_id(entity_id_2),
+            Some(1)
+        );
+
+        // Updating entity must be authorized
+        assert_err!(
+            Permissions::add_schema_support_to_entity(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ZERO),
+                Some(0),
+                false, // not claiming to be entity maintainer
+                entity_id_2,
+                0, // first schema created
+                simple_test_entity_property_values()
+            ),
+            "CredentialNotInEntityPermissionsUpdateSet"
+        );
+
+        // default permissions give entity maintainer permission to update and delete
+        assert_ok!(Permissions::add_schema_support_to_entity(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            Some(1),
+            true, // we are claiming to be the entity maintainer
+            entity_id_2,
+            0,
+            simple_test_entity_property_values()
+        ));
+        assert_ok!(Permissions::update_entity_property_values(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            Some(1),
+            true, // we are claiming to be the entity maintainer
+            entity_id_2,
+            simple_test_entity_property_values()
+        ));
+    })
+}
+
+#[test]
+fn class_permissions_set_admins() {
+    with_test_externalities(|| {
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal());
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.admins.is_empty());
+
+        let credential_set = CredentialSet::from(vec![1]);
+
+        // only root should be able to set admins
+        assert_err!(
+            Permissions::set_class_admins(Origin::signed(1), class_id, credential_set.clone()),
+            "NotRootOrigin"
+        );
+        assert_err!(
+            Permissions::set_class_admins(
+                Origin::NONE, //unsigned inherent?
+                class_id,
+                credential_set.clone()
+            ),
+            "BadOrigin:ExpectedRootOrSigned"
+        );
+
+        // root origin can set admins
+        assert_ok!(Permissions::set_class_admins(
+            Origin::ROOT,
+            class_id,
+            credential_set.clone()
+        ));
+
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.admins, credential_set);
+    })
+}
+
+#[test]
+fn class_permissions_set_add_schemas_set() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.add_schemas.is_empty());
+
+        let credential_set1 = CredentialSet::from(vec![1, 2]);
+        let credential_set2 = CredentialSet::from(vec![3, 4]);
+
+        // root
+        assert_ok!(Permissions::set_class_add_schemas_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            credential_set1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.add_schemas, credential_set1);
+
+        // admins
+        assert_ok!(Permissions::set_class_add_schemas_set(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            credential_set2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.add_schemas, credential_set2);
+
+        // non-admins
+        assert_err!(
+            Permissions::set_class_add_schemas_set(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                credential_set2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_create_entities_set() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.create_entities.is_empty());
+
+        let credential_set1 = CredentialSet::from(vec![1, 2]);
+        let credential_set2 = CredentialSet::from(vec![3, 4]);
+
+        // root
+        assert_ok!(Permissions::set_class_create_entities_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            credential_set1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.create_entities, credential_set1);
+
+        // admins
+        assert_ok!(Permissions::set_class_create_entities_set(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            credential_set2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.create_entities, credential_set2);
+
+        // non-admins
+        assert_err!(
+            Permissions::set_class_create_entities_set(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                credential_set2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_entities_can_be_created() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert_eq!(class_permissions.entities_can_be_created, false);
+
+        // root
+        assert_ok!(Permissions::set_class_entities_can_be_created(
+            Origin::ROOT,
+            None,
+            class_id,
+            true
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entities_can_be_created, true);
+
+        // admins
+        assert_ok!(Permissions::set_class_entities_can_be_created(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            false
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entities_can_be_created, false);
+
+        // non-admins
+        assert_err!(
+            Permissions::set_class_entities_can_be_created(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                true
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_entity_permissions() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.entity_permissions.update.is_empty());
+
+        let entity_permissions1 = EntityPermissions {
+            update: CredentialSet::from(vec![1]),
+            maintainer_has_all_permissions: true,
+        };
+
+        //root
+        assert_ok!(Permissions::set_class_entity_permissions(
+            Origin::ROOT,
+            None,
+            class_id,
+            entity_permissions1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entity_permissions, entity_permissions1);
+
+        let entity_permissions2 = EntityPermissions {
+            update: CredentialSet::from(vec![4]),
+            maintainer_has_all_permissions: true,
+        };
+        //admins
+        assert_ok!(Permissions::set_class_entity_permissions(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            entity_permissions2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entity_permissions, entity_permissions2);
+
+        // non admins
+        assert_err!(
+            Permissions::set_class_entity_permissions(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                entity_permissions2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_reference_constraint() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert_eq!(class_permissions.reference_constraint, Default::default());
+
+        let mut constraints_set = BTreeSet::new();
+        constraints_set.insert(PropertyOfClass {
+            class_id: 1,
+            property_index: 0,
+        });
+        let reference_constraint1 = ReferenceConstraint::Restricted(constraints_set);
+
+        //root
+        assert_ok!(Permissions::set_class_reference_constraint(
+            Origin::ROOT,
+            None,
+            class_id,
+            reference_constraint1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(
+            class_permissions.reference_constraint,
+            reference_constraint1
+        );
+
+        let mut constraints_set = BTreeSet::new();
+        constraints_set.insert(PropertyOfClass {
+            class_id: 2,
+            property_index: 2,
+        });
+        let reference_constraint2 = ReferenceConstraint::Restricted(constraints_set);
+
+        //admins
+        assert_ok!(Permissions::set_class_reference_constraint(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            reference_constraint2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(
+            class_permissions.reference_constraint,
+            reference_constraint2
+        );
+
+        // non admins
+        assert_err!(
+            Permissions::set_class_reference_constraint(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                reference_constraint2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn batch_transaction_simple() {
+    with_test_externalities(|| {
+        const CREDENTIAL_ONE: u64 = 1;
+
+        let new_class_id = create_simple_class(ClassPermissions {
+            entities_can_be_created: true,
+            create_entities: vec![CREDENTIAL_ONE].into(),
+            reference_constraint: ReferenceConstraint::NoConstraint,
+            ..Default::default()
+        });
+
+        let new_properties = vec![Property {
+            prop_type: PropertyType::Internal(new_class_id),
+            required: true,
+            name: b"entity".to_vec(),
+            description: b"another entity of same class".to_vec(),
+        }];
+
+        assert_ok!(Permissions::add_class_schema(
+            Origin::ROOT,
+            None,
+            new_class_id,
+            vec![],
+            new_properties
+        ));
+
+        let operations = vec![
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: true, // in prior operation CREDENTIAL_ONE became the maintainer
+                operation_type: OperationType::AddSchemaSupportToEntity(
+                    AddSchemaSupportToEntityOperation {
+                        entity_id: ParameterizedEntity::InternalEntityJustAdded(0), // index 0 (prior operation)
+                        schema_id: 0,
+                        parametrized_property_values: vec![ParametrizedClassPropertyValue {
+                            in_class_index: 0,
+                            value: ParametrizedPropertyValue::InternalEntityJustAdded(0),
+                        }],
+                    },
+                ),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: true, // in prior operation CREDENTIAL_ONE became the maintainer
+                operation_type: OperationType::UpdatePropertyValues(
+                    UpdatePropertyValuesOperation {
+                        entity_id: ParameterizedEntity::InternalEntityJustAdded(0), // index 0 (prior operation)
+                        new_parametrized_property_values: vec![ParametrizedClassPropertyValue {
+                            in_class_index: 0,
+                            value: ParametrizedPropertyValue::InternalEntityJustAdded(2),
+                        }],
+                    },
+                ),
+            },
+        ];
+
+        let entity_id = next_entity_id();
+
+        assert_ok!(Permissions::transaction(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            operations
+        ));
+
+        // two entities created
+        assert!(versioned_store::EntityById::exists(entity_id));
+        assert!(versioned_store::EntityById::exists(entity_id + 1));
+    })
+}
+
+#[test]
+fn batch_transaction_vector_of_entities() {
+    with_test_externalities(|| {
+        const CREDENTIAL_ONE: u64 = 1;
+
+        let new_class_id = create_simple_class(ClassPermissions {
+            entities_can_be_created: true,
+            create_entities: vec![CREDENTIAL_ONE].into(),
+            reference_constraint: ReferenceConstraint::NoConstraint,
+            ..Default::default()
+        });
+
+        let new_properties = vec![Property {
+            prop_type: PropertyType::InternalVec(10, new_class_id),
+            required: true,
+            name: b"entities".to_vec(),
+            description: b"vector of entities of same class".to_vec(),
+        }];
+
+        assert_ok!(Permissions::add_class_schema(
+            Origin::ROOT,
+            None,
+            new_class_id,
+            vec![],
+            new_properties
+        ));
+
+        let operations = vec![
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: true, // in prior operation CREDENTIAL_ONE became the maintainer
+                operation_type: OperationType::AddSchemaSupportToEntity(
+                    AddSchemaSupportToEntityOperation {
+                        entity_id: ParameterizedEntity::InternalEntityJustAdded(0),
+                        schema_id: 0,
+                        parametrized_property_values: vec![ParametrizedClassPropertyValue {
+                            in_class_index: 0,
+                            value: ParametrizedPropertyValue::InternalEntityVec(vec![
+                                ParameterizedEntity::InternalEntityJustAdded(1),
+                                ParameterizedEntity::InternalEntityJustAdded(2),
+                            ]),
+                        }],
+                    },
+                ),
+            },
+        ];
+
+        let entity_id = next_entity_id();
+
+        assert_ok!(Permissions::transaction(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            operations
+        ));
+
+        // three entities created
+        assert!(versioned_store::EntityById::exists(entity_id));
+        assert!(versioned_store::EntityById::exists(entity_id + 1));
+        assert!(versioned_store::EntityById::exists(entity_id + 2));
+
+        assert_eq!(
+            versioned_store::EntityById::get(entity_id),
+            versioned_store::Entity {
+                class_id: new_class_id,
+                id: entity_id,
+                in_class_schema_indexes: vec![0],
+                values: vec![ClassPropertyValue {
+                    in_class_index: 0,
+                    value: PropertyValue::InternalVec(vec![entity_id + 1, entity_id + 2,])
+                }]
+            }
+        );
+    })
+}