Home > Software engineering >  Implementing trait generics in map struct fields
Implementing trait generics in map struct fields

Time:01-06

So for a rocket project, I decided to setup a security system that will handle grants based of Security voters (a naive version of Symfony ones).

I decided to implement as Security struct to contain a HashMap with HashMap<String, T> where the string is the subject of the voter (like "books", "authors", etc. Some domain stuff) and the T would be a struct implementing the trait SecurityVoter.

The goal is to store all the security voters in an instance of the Security struct, then store the security in a rocket State.

Here is my code :

pub trait SecurityVoter {
    /// Gets the "subject" supported by the voter
    fn supports(&self) -> String;
    /// Takes a given right and a user, and checks if the user has_access to the action represented by the right.
    fn has_access(&self, right: &str, user: User) -> bool;
}
#[derive(Default)]
pub struct Security<T>
where
    T: SecurityVoter,
{
    voters: HashMap<String, T>,
}
impl<T> Security<T>
where
    T: SecurityVoter,
{
    pub fn add_handler(&mut self, handler: T) -> &mut Self {
        self.voters.insert(handler.supports(), handler);

        self
    }

    pub fn has_access(&self, subject: &str, right: &str, user: &User) -> bool {
        if self.voters.contains_key(subject) {
            return self.voters.get(subject).unwrap().has_access(right, user);
        }

        false
    }
}

But when I try to test it with a little unit test, I cannot build because of errors out of my comprehension.

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Default)]
    struct TestSecurityHandler {}

    impl SecurityVoter for TestSecurityHandler {
        fn supports(&self) -> String {
            "test".to_string()
        }

        fn has_access(&self, right: &str, _user: &User) -> bool {
            if right == "ACCEPT_ACCESS" {
                return true;
            }

            false
        }
    }

    #[test]
    fn test_custom_security_handler() {
        let mut security = Security::<dyn SecurityVoter>::default(); // <- line with the error

        security.add_handler(TestSecurityHandler::default());

        let mut user = User::default();

        assert!(security.has_access("test", "ACCEPT_ACCESS", &user));
    }
}

Here is the error :

error[E0599]: the function or associated item `default` exists for struct `core::security::Security<dyn core::security::SecurityVoter>`, but its trait bounds were not satisfied
  --> src/core/security.rs:68:59
   |
6  | / pub struct Security<T>
7  | | where
8  | |     T: SecurityVoter,
9  | | {
10 | |     voters: HashMap<String, T>,
11 | | }
   | | -
   | | |
   | |_function or associated item `default` not found for this
   |   doesn't satisfy `_: std::default::Default`
...
38 |   pub trait SecurityVoter {
   |   -----------------------
   |   |
   |   doesn't satisfy `_: std::default::Default`
   |   doesn't satisfy `_: std::marker::Sized`
...
68 |           let mut security = Security::<dyn SecurityVoter>::default(); // <- line with the error
   |                                                             ^^^^^^^ function or associated item cannot be called on `core::security::Security<dyn core::security::SecurityVoter>` due to unsatisfied trait bounds
   |
note: the following trait bounds were not satisfied:
      `dyn core::security::SecurityVoter: std::default::Default`
      `dyn core::security::SecurityVoter: std::marker::Sized`
  --> src/core/security.rs:5:10
   |
5  | #[derive(Default)]
   |          ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro
   = note: the following trait bounds were not satisfied:
           `dyn core::security::SecurityVoter: std::marker::Sized`
           which is required by `core::security::Security<dyn core::security::SecurityVoter>: std::default::Default`
           `dyn core::security::SecurityVoter: std::default::Default`
           which is required by `core::security::Security<dyn core::security::SecurityVoter>: std::default::Default`


I tried some modification using Sized and Default traits, and tried to use Box<> in my Hashmap but nothing helped.

Can you provide some help to solve this issue ? Thanks :)


Update : I think I fixed my issue modifying my code as this :

  • Added a ?Sized requirement for generics of Security
  • Used Box<> to store my voters inside the hashmap
  • Removed any mention of the Default trait
pub struct Security<T: ?Sized> {
    voters: HashMap<String, Box<T>>,
}

impl<T> Security<T>
where
    T: SecurityVoter   ?Sized,
{
    pub fn new() -> Self {
        Self {
            voters: HashMap::new(),
        }
    }

    pub fn add_handler(&mut self, handler: Box<T>) -> &mut Self {
        self.voters.insert(handler.supports(), handler);

        self
    }

    pub fn is_granted(user: &User, roles: Vec<&str>) -> bool {
        roles
            .iter()
            .all(|item| user.roles.contains(&item.to_string()))
    }

    pub fn has_access(&self, subject: &str, right: &str, user: &User) -> bool {
        if self.voters.contains_key(subject) {
            return self.voters.get(subject).unwrap().has_access(right, user);
        }

        false
    }
}

// ...


#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Default)]
    struct TestSecurityHandler {}

    impl SecurityVoter for TestSecurityHandler {
        fn supports(&self) -> String {
            "test".to_string()
        }

        fn has_access(&self, right: &str, _user: &User) -> bool {
            if right == "ACCEPT_ACCESS" {
                return true;
            }

            false
        }
    }

    #[derive(Default)]
    struct STestSecurityHandler {}

    impl SecurityVoter for STestSecurityHandler {
        fn supports(&self) -> String {
            "test".to_string()
        }

        fn has_access(&self, right: &str, _user: &User) -> bool {
            if right == "ACCEPT_ACCESS" {
                return true;
            }

            false
        }
    }

    #[test]
    fn test_custom_security_handler() {
        let mut security = Security::<dyn SecurityVoter>::new(); // <- line with the error

        security.add_handler(Box::new(TestSecurityHandler::default()));
        security.add_handler(Box::new(STestSecurityHandler::default()));

        let user = User::default();

        assert!(security.has_access("test", "ACCEPT_ACCESS", &user));
    }
}

But I don't really know if this is a good solution.

If you have any advice on how to properly implement this, feel free to comment.

CodePudding user response:

You almost got this correctly. The only thing I would change is to implement Default. The problem with #[derive(Default)] is that it generates an incorrect T: Default bound (the wish for a #[derive] that won't generate incorrect bounds is called perfect derive, but it is not implemented). Instead, you need to implement Default manually:

impl<T: ?Sized> Default for Security<T> {
    fn default() -> Self {
        Self { voters: HashMap::default() }
    }
}

Another way to implement this, and better if you also use the struct with concrete, not dyn SecurityVoter (because this saves a Box for this case), is to do require T: Sized but use Box<dyn SecurityVoter> instead of bare dyn SecurityVoter in the test, and implement SecurityVoter for Box<dyn SecurityVoter>. You still need to implement Default manually, though:

pub struct Security<T> {
    // I removed the `T: SecurityVoter` bound, see https://stackoverflow.com/q/49229332/7884305.
    voters: HashMap<String, T>,
}

impl<T> Default for Security<T> {
    fn default() -> Self {
        Self { voters: HashMap::default() }
    }
}

impl<T: ?Sized   SecurityVoter> SecurityVoter for Box<T> {
    fn supports(&self) -> String {
        T::supports(&**self)
    }
    fn has_access(&self, right: &str, user: &User) -> bool {
        T::has_access(&**self, right, user)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Default)]
    struct TestSecurityHandler {}

    impl SecurityVoter for TestSecurityHandler {
        fn supports(&self) -> String {
            "test".to_string()
        }

        fn has_access(&self, right: &str, _user: &User) -> bool {
            if right == "ACCEPT_ACCESS" {
                return true;
            }

            false
        }
    }

    #[test]
    fn test_custom_security_handler() {
        let mut security = Security::<Box<dyn SecurityVoter>>::default();

        security.add_handler(Box::<TestSecurityHandler>::default());

        let mut user = User::default();

        assert!(security.has_access("test", "ACCEPT_ACCESS", &user));
    }
}
  • Related