Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6f42406
Add building blocks for Acl plugin
mooori Sep 10, 2022
40be1d3
make trait method acl_storage_prefix static
mooori Sep 16, 2022
7e66d31
implement TryFrom<&str> instead of FromStr
mooori Sep 16, 2022
2793779
Remove `_bitflag` suffix from trait method names
mooori Sep 19, 2022
e8dfbac
Omit optional storage_prefix arg in test contract
mooori Sep 19, 2022
aca932f
Generate bitflags dynamically
mooori Sep 19, 2022
d2352c7
document mapping between roles and bitflags
mooori Sep 20, 2022
29adf81
cleanup
mooori Sep 20, 2022
f0032c5
Rename variants of Role enum
mooori Sep 20, 2022
2100b14
cleanup
mooori Sep 20, 2022
41fa725
Add `fn new_bitflags_type_ident`
mooori Sep 20, 2022
1802bd5
Make some methods in trait impl #[private]
mooori Sep 20, 2022
bdcc45f
Add event RoleGranted
mooori Sep 20, 2022
b578c25
test: add helpers via AccessControllableContract
mooori Sep 21, 2022
21570e9
test: add Setup to avoid repetition
mooori Sep 21, 2022
5b6bdc4
test: add tests for acl_grant_role_unchecked
mooori Sep 22, 2022
8ab61d3
Remove method stub
mooori Sep 22, 2022
bf886a6
Verify role type satisfies trait bounds
mooori Sep 23, 2022
e3d88f2
Extend `is_near_bindgen_wrapped_or_marshall`
mooori Sep 26, 2022
91d496f
Implement `#[access_control_any(roles(...))]`
mooori Sep 27, 2022
521e1ae
test: add `test_attribute_access_control_any`
mooori Sep 27, 2022
5eca45c
chore: rename `want` -> `expected`
mooori Sep 28, 2022
ca5f014
pr review
mooori Sep 28, 2022
f60faa8
pr review
mooori Sep 28, 2022
df4b6bd
Validate input `permission` in `add_bearer()`
mooori Sep 28, 2022
790824f
Document heuristic to detect *Ext impl
mooori Sep 28, 2022
1c3a05a
refactor: avoid a Vec allocation
mooori Sep 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/target
Cargo.lock
near-plugins/tests/contracts/*/target

# Ignore IDE data
.vscode/
Expand Down
214 changes: 214 additions & 0 deletions near-plugins-derive/src/access_control_role.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//! Implements `AccessControlRole` for an enum.
//!
//! The conversion of enum variants to bitflags representable by `u128` is the
//! key part of this implementation. Assume the trait is derived on the
//! following enum:
//!
//! ```ignore
//! #[derive(AccessControlRole)]
//! pub enum Role {
//! LevelA,
//! LevelB,
//! LevelC,
//! }
//! ```
//!
//! This results in the following bitflags:
//! ```ignore
//! bitflags! {
//! struct RoleFlags: u128 {
//! const __SUPER_ADMIN = 1u128 << 0;
//! const LEVELA = 1u128 << 1;
//! const LEVELA_ADMIN = 1u128 << 2;
//! const LEVELB = 1u128 << 3;
//! const LEVELB_ADMIN = 1u128 << 4;
//! const LEVELC = 1u128 << 5;
//! const LEVELC_ADMIN = 1u128 << 6;
//! }
//! }
//! ```
//!
//! The mapping between enum variants and bitflag has these properties:
//!
//! - Each flag has exactly one bit with value 1.
//! - A bitflag `1u128 << x` with odd `x` represents a role permission.
//! - A bitflag `1u128 << x` with even `x` represents an admin permission.
//! - Shifting a role's 1-bit to the left by one position yields the
//! corresponding admin permission.
//!
//! The last property aims to facilitate migrations which add or remove enum
//! variants.

use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use std::convert::TryFrom;
use syn::{parse_macro_input, ItemEnum};

const DEFAULT_SUPER_ADMIN_NAME: &str = "__SUPER_ADMIN";
const DEFAULT_BITFLAGS_TYPE_NAME: &str = "RoleFlags";
const DEFAULT_BOUNDCHECKER_TYPE_NAME: &str = "__AclBoundchecker";

pub fn derive_access_control_role(input: TokenStream) -> TokenStream {
// This derive doesn't take attributes, so no need to use `darling`.
let input: ItemEnum = parse_macro_input!(input);
let ItemEnum {
ident, variants, ..
} = input;

let variants = variants.into_iter().collect::<Vec<_>>();
let variant_idxs: Vec<_> =
(0..u8::try_from(variants.len()).expect("Too many enum variants")).collect();
let variant_names: Vec<_> = variants.iter().map(|v| format!("{}", v.ident)).collect();

let boundchecker_type = Ident::new(DEFAULT_BOUNDCHECKER_TYPE_NAME, ident.span());
let bitflags_type_ident = new_bitflags_type_ident(Span::call_site());
let bitflags_idents = bitflags_idents(variant_names.as_ref(), bitflags_type_ident.span());
let bitflags_idxs: Vec<_> =
(0..u8::try_from(bitflags_idents.len()).expect("Too many bitflags")).collect();

let output = quote! {
// Ensure #ident satisfies bounds required for acl. This is done
// explicitly to provide a clear error message to developers whose
// enum doesn't satisfy the required bounds.
//
// Without this explicit check, compilation would still fail if a bound
// is not satisfied. Though with less a clear error message.
struct #boundchecker_type<T: Copy + Clone> {
_marker: ::std::marker::PhantomData<T>,
}
impl<T: Copy + Clone> #boundchecker_type<T> {
fn new() -> Self {
Self { _marker: Default::default() }
}
}
impl #ident {
fn check_bounds() {
// Compilation will fail if #ident doesn't satisfy above bounds.
let _x = #boundchecker_type::<#ident>::new();
}
}

impl From<#ident> for u8 {
fn from(value: #ident) -> Self {
match value {
#(
#ident::#variants => #variant_idxs,
)*
}
}
}

impl ::std::convert::TryFrom<u8> for #ident {
type Error = &'static str;

fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
#(
#variant_idxs => Ok(#ident::#variants),
)*
_ => Err("Value does not correspond to a variant"),
}
}
}

impl From<#ident> for &'static str {
fn from(value: #ident) -> Self {
match value {
#(
#ident::#variants => #variant_names,
)*
}
}
}

impl From<#ident> for String {
fn from(value: #ident) -> Self {
match value {
#(
#ident::#variants => #variant_names.to_string(),
)*
}
}
}

impl ::std::convert::TryFrom<&str> for #ident {
type Error = &'static str;

fn try_from(value: &str) -> Result<#ident, Self::Error> {
match value {
#(
#variant_names => Ok(#ident::#variants),
)*
_ => Err("Value does not correspond to a variant"),
}
}
}

/// Panics if `n` is too large.
fn safe_leftshift(value: u128, n: u8) -> u128 {
value
.checked_shl(n.into())
.expect("Too many enum variants to be represented by bitflags")
}

impl AccessControlRole for #ident {
fn acl_super_admin_permission() -> u128 {
// See module documentation.
1 // corresponds to safe_leftshift(1, 0)
}

fn acl_permission(self) -> u128 {
// Shift 1u128 left by an odd number, see module documentation.
let n = (u8::from(self) + 1)
.checked_mul(2)
.expect("Too many enum variants") - 1;
safe_leftshift(1, n)
}

fn acl_admin_permission(self) -> u128 {
// Shift 1u128 left by an even number, see module documentation.
let n = (u8::from(self) + 1)
.checked_mul(2)
.expect("Too many enum variants");
safe_leftshift(1, n)
}
}

::bitflags::bitflags! {
/// Encodes permissions for roles and admins.
#[derive(BorshDeserialize, BorshSerialize, Default)]
struct #bitflags_type_ident: u128 {
#(
const #bitflags_idents = 1u128 << #bitflags_idxs;
)*
}
}
};

output.into()
}

pub fn new_bitflags_type_ident(span: Span) -> Ident {
Ident::new(DEFAULT_BITFLAGS_TYPE_NAME, span)
}

fn bitflags_idents(names: &[String], span: Span) -> Vec<Ident> {
// Assuming enum variant names are in camel case, simply converting them
// to uppercase is not ideal. However, bitflag identifiers aren't exposed,
// so let's not bother with converting camel to screaming-snake case.
let names = names
.iter()
.map(|name| name.to_uppercase())
.collect::<Vec<_>>();
let admin_names = names
.iter()
.map(|name| format!("{}_ADMIN", name))
.collect::<Vec<_>>();
let mut idents = vec![Ident::new(DEFAULT_SUPER_ADMIN_NAME, span.clone())];
for (name, admin_name) in names.iter().zip(admin_names) {
idents.push(Ident::new(name.as_ref(), span.clone()));
idents.push(Ident::new(admin_name.as_ref(), span.clone()));
}
idents
}
Loading