Can we increase the re-usability of this key-oriented access-protection pattern?
Asked Answered
P

3

30

Can we increase the re-usability for this key-oriented access-protection pattern:

class SomeKey { 
    friend class Foo;
    // more friends... ?
    SomeKey() {} 
    // possibly non-copyable too
};

class Bar {
public:
    void protectedMethod(SomeKey); // only friends of SomeKey have access
};

To avoid continued misunderstandings, this pattern is different from the Attorney-Client idiom:

  • It can be more concise than Attorney-Client (as it doesn't involve proxying through a third class)
  • It can allow delegation of access rights
  • ... but its also more intrusive on the original class (one dummy parameter per method)

(A side-discussion developed in this question, thus i'm opening this question.)

Pongee answered 24/7, 2010 at 11:27 Comment(4)
Hypothetical friend memberships aside and without going into a nonsensical foo-bar example - can you provide a "practical" example where usage of this pattern is superior to some other simpler technique, furthermore what would say the C# or Java equivalents of this be?Bouncing
@Beh: Whenever you have to restrict access to resources, but don't want to grant privileged clients full access (which is rarely needed) to retain encapsulation. The linked attorney-client article goes into more detail. As a practical example take e.g. a case like this - the wrapper class isn't for public use, its supposed to be an opaque helper. The free function that uses it has full access, although it only needs to access the wrappers get_function_pointer().Pongee
I'm trying to use this with a template class (one friend of the key is a method from the template class) and can't quite figure out how to manage dependencies since I can't separate the template declaration and definition. Am I right in concluding that I can't use this to give a template class method key access?Tunable
I decided to move ahead on this issue by creating a class that wraps the template class in a reference. I can then convert the template class into a non template class, split declaration and definition, and proceed as normal without introducing hardly any additional complexity or extra code.Tunable
E
28

I like this idiom, and it has the potential to become much cleaner and more expressive.

In standard C++03, I think the following way is the easiest to use and most generic. (Not too much of an improvement, though. Mostly saves on repeating yourself.) Because template parameters cannot be friends, we have to use a macro to define passkey's:

// define passkey groups
#define EXPAND(pX) pX

#define PASSKEY_1(pKeyname, pFriend1)                             \
        class EXPAND(pKeyname)                                    \
        {                                                         \
        private:                                                  \
            friend EXPAND(pFriend1);                              \
            EXPAND(pKeyname)() {}                                 \
                                                                  \
            EXPAND(pKeyname)(const EXPAND(pKeyname)&);            \
            EXPAND(pKeyname)& operator=(const EXPAND(pKeyname)&); \
        }

#define PASSKEY_2(pKeyname, pFriend1, pFriend2)                   \
        class EXPAND(pKeyname)                                    \
        {                                                         \
        private:                                                  \
            friend EXPAND(pFriend1);                              \
            friend EXPAND(pFriend2);                              \
            EXPAND(pKeyname)() {}                                 \
                                                                  \
            EXPAND(pKeyname)(const EXPAND(pKeyname)&);            \
            EXPAND(pKeyname)& operator=(const EXPAND(pKeyname)&); \
        }
// and so on to some N

//////////////////////////////////////////////////////////
// test!
//////////////////////////////////////////////////////////
struct bar;
struct baz;
struct qux;
void quux(int, double);

struct foo
{
    PASSKEY_1(restricted1_key, struct bar);
    PASSKEY_2(restricted2_key, struct bar, struct baz);
    PASSKEY_1(restricted3_key, void quux(int, double));

    void restricted1(restricted1_key) {}
    void restricted2(restricted2_key) {}
    void restricted3(restricted3_key) {}
} f;

struct bar
{
    void run(void)
    {
        // passkey works
        f.restricted1(foo::restricted1_key());
        f.restricted2(foo::restricted2_key());
    }
};

struct baz
{
    void run(void)
    {
        // cannot create passkey
        /* f.restricted1(foo::restricted1_key()); */

        // passkey works
        f.restricted2(foo::restricted2_key());
    }
};

struct qux
{
    void run(void)
    {
        // cannot create any required passkeys
        /* f.restricted1(foo::restricted1_key()); */
        /* f.restricted2(foo::restricted2_key()); */
    }
};

void quux(int, double)
{
    // passkey words
    f.restricted3(foo::restricted3_key());
}

void corge(void)
{
    // cannot use quux's passkey
    /* f.restricted3(foo::restricted3_key()); */
}

int main(){}

This method has two drawbacks: 1) the caller has to know the specific passkey it needs to create. While a simple naming scheme (function_key) basically eliminates it, it could still be one abstraction cleaner (and easier). 2) While it's not very difficult to use the macro can be seen as a bit ugly, requiring a block of passkey-definitions. However, improvements to these drawbacks cannot be made in C++03.


In C++0x, the idiom can reach its simplest and most expressive form. This is due to both variadic templates and allowing template parameters to be friends. (Note that MSVC pre-2010 allows template friend specifiers as an extension; therefore one can simulate this solution):

// each class has its own unique key only it can create
// (it will try to get friendship by "showing" its passkey)
template <typename T>
class passkey
{
private:
    friend T; // C++0x, MSVC allows as extension
    passkey() {}

    // noncopyable
    passkey(const passkey&) = delete;
    passkey& operator=(const passkey&) = delete;
};

// functions still require a macro. this
// is because a friend function requires
// the entire declaration, which is not
// just a type, but a name as well. we do 
// this by creating a tag and specializing 
// the passkey for it, friending the function
#define EXPAND(pX) pX

// we use variadic macro parameters to allow
// functions with commas, it all gets pasted
// back together again when we friend it
#define PASSKEY_FUNCTION(pTag, pFunc, ...)               \
        struct EXPAND(pTag);                             \
                                                         \
        template <>                                      \
        class passkey<EXPAND(pTag)>                      \
        {                                                \
        private:                                         \
            friend pFunc __VA_ARGS__;                    \
            passkey() {}                                 \
                                                         \
            passkey(const passkey&) = delete;            \
            passkey& operator=(const passkey&) = delete; \
        }

// meta function determines if a type 
// is contained in a parameter pack
template<typename T, typename... List>
struct is_contained : std::false_type {};

template<typename T, typename... List>
struct is_contained<T, T, List...> : std::true_type {};

template<typename T, typename Head, typename... List>
struct is_contained<T, Head, List...> : is_contained<T, List...> {};

// this class can only be created with allowed passkeys
template <typename... Keys>
class allow
{
public:
    // check if passkey is allowed
    template <typename Key>
    allow(const passkey<Key>&)
    {
        static_assert(is_contained<Key, Keys>::value, 
                        "Passkey is not allowed.");
    }

private:
    // noncopyable
    allow(const allow&) = delete;
    allow& operator=(const allow&) = delete;
};

//////////////////////////////////////////////////////////
// test!
//////////////////////////////////////////////////////////
struct bar;
struct baz;
struct qux;
void quux(int, double);

// make a passkey for quux function
PASSKEY_FUNCTION(quux_tag, void quux(int, double));

struct foo
{    
    void restricted1(allow<bar>) {}
    void restricted2(allow<bar, baz>) {}
    void restricted3(allow<quux_tag>) {}
} f;

struct bar
{
    void run(void)
    {
        // passkey works
        f.restricted1(passkey<bar>());
        f.restricted2(passkey<bar>());
    }
};

struct baz
{
    void run(void)
    {
        // passkey does not work
        /* f.restricted1(passkey<baz>()); */

        // passkey works
        f.restricted2(passkey<baz>());
    }
};

struct qux
{
    void run(void)
    {
        // own passkey does not work,
        // cannot create any required passkeys
        /* f.restricted1(passkey<qux>()); */
        /* f.restricted2(passkey<qux>()); */
        /* f.restricted1(passkey<bar>()); */
        /* f.restricted2(passkey<baz>()); */
    }
};

void quux(int, double)
{
    // passkey words
    f.restricted3(passkey<quux_tag>());
}

void corge(void)
{
    // cannot use quux's passkey
    /* f.restricted3(passkey<quux_tag>()); */
}

int main(){}

Note with just the boilerplate code, in most cases (all non-function cases!) nothing more ever needs to be specially defined. This code generically and simply implements the idiom for any combination of classes and functions.

The caller doesn't need to try to create or remember a passkey specific to the function. Rather, each class now has its own unique passkey and the function simply chooses which passkey's it will allow in the template parameters of the passkey parameter (no extra definitions required); this eliminates both drawbacks. The caller just creates its own passkey and calls with that, and doesn't need to worry about anything else.

Evy answered 24/7, 2010 at 11:56 Comment(6)
I like where you are going, but (of course a but ;)) now we are back to making a key for every type (for the moment i can't yet go for C++0x features)? Also, while your approach has other advantages, i like the simplicity of the former version. It doesn't need a supporting structure and probably has fewer problems of getting through reviews.Pongee
@Georg: Indeed, I think in C++03 the best way is to accept you have to manually (well, made easier with macros) make passkeys per collection of friends, and go with it. I'm not sure what you mean by reviews, but I find the C++03 much easier, just throw the utility stuff in some passkey.hpp header and never look at it again. :) The macro is much cleaner than doing it by hand. I really like the C++0x version though; the mere fact the last parameter can literally read "allow this, this, and that", and that types simply say "here's my key, let me in" is a dream.Evy
True, the readability on the locked C++0x methods is nice :) With reviews i mean more conservative guide-lines or code-reviewers - if we could code everything just like we want it it would be a different matter (mainly addressing the macros here).Pongee
@Georg: Oh, I always get to make up my own guide-lines. :)Evy
Then enjoy it while it lasts ;) I definitely like the improvement on readability with being able to directly pass them to passkey for class-types.Pongee
The allow structure is very nice, I didn't know about the template friend goodie of C++0x (I'm still coding in C++03 mostly...) and it does the trick very nicely!Portmanteau
I
3

I've read a lot of comments about non-copyability. Many people thought it should not be non copyable because then we cannot pass it as an argument to the function that needs the key. And some were even surprised it was working. Well it really should not and is apparently related to some Visual C++ compilers, as I had the same weirdness before but not with Visual C++12 (Studio 2013) anymore.

But here's the thing, we can enhance the security with "basic" non-copyability. Boost version is too much as it completely prevents use of the copy constructor and thus is a bit too much for what we need. What we need is actually making the copy constructor private but not without an implementation. Of course the implementation will be empty, but it must exists. I've recently asked who was calling the copy-ctor in such a case (in this case who calls the copy constructor of SomeKey when calling ProtectedMethod). The answer was that apparently the standard ensure it is the method caller that calls the -ctor which honestly looks quite logical. So by making the copy-ctor private we allow friends function (the protected Bar and the granted Foo) to call it, thus allowing Foo to call the ProtectedMethod because it uses value argument passing, but it also prevents anyone out of Foo's scope.

By doing this, even if a fellow developer tries to play smart with the code, he will actually have to make Foo do the job, another class won't be able to get the key, and chances are he will realize his mistakes almost 100% of the time this way (hopefully, otherwise he's too much of a beginner to use this pattern or he should stop development :P ).

Iridosmine answered 14/7, 2014 at 17:13 Comment(6)
This is not an answer and should therefore not be posted as one.Witted
Ok so what do I do? Randomly go through posts on StackOverflow to hope to get enough answers to get my rep up so I can comment? You didn't read the first part where I apologized for not being able to comment I guess ;) I have to have 50 rep to comment and can't, and that's plain stupid, if people must not be able to do one of two things from the start, it's answering, not commenting =/Iridosmine
Doing something wrong is not magically going to correct itself if you apologise for it. As you said you could try to answer some questions until you hit the 50 rep mark. 50 rep is not that much so you should be able to achieve those relatively quick ;)Witted
Tried to make it as much an answer as I could. Just to point it out, I really don't have that much time to actually produce content on StackOverflow to increase my rep, I think that as requested by many there should be a minor change in comments handling. People can, like me in this case, want to bring in something new, something that is not an answer but partly an answer, in this way they want to contribute to the StackExchange growth, but they can't ! I hope some good idea will be found some dayIridosmine
I really hope because the issue (I am not concerned by this I could still get 50 rep with a bit of time) is that with time, questions get more complicated, and thus answers as well, to the point that someday only a niche of people will ask questions or answer them, preventing newcomers to actually get any rep. Still these newcomers (that's how humankind evolve) can have one small but brilliant and fresh idea (I've seen that sometimes) that will enhance something known greatlyIridosmine
Indeed. The time is approaching when StackOverflow achieves completion. At that point no more questions will need to be asked. (But new questions will be allowed so that we have something to downvote and closevote.)Camorra
M
1

Great answer from @GManNickG. Learnt a lot. In trying to get it to work, found a couple of typos. Full example repeated for clarity. My example borrows "contains Key in Keys..." function from Check if C++0x parameter pack contains a type posted by @snk_kid.

#include<type_traits>
#include<iostream>

// identify if type is in a parameter pack or not
template < typename Tp, typename... List >
struct contains : std::false_type {};

template < typename Tp, typename Head, typename... Rest >
struct contains<Tp, Head, Rest...> :
  std::conditional< std::is_same<Tp, Head>::value,
  std::true_type,
  contains<Tp, Rest...>
  >::type{};

template < typename Tp >
struct contains<Tp> : std::false_type{};


// everything is private!
template <typename T>
class passkey {
private:
  friend T;
  passkey() {}

  // noncopyable
  passkey(const passkey&) = delete;
  passkey& operator=(const passkey&) = delete;
};


// what keys are allowed
template <typename... Keys>
class allow {
public:
  template <typename Key>
  allow(const passkey<Key>&) {
    static_assert(contains<Key, Keys...>::value, "Pass key is not allowed");
  }

private:
  // noncopyable
  allow(const allow&) = delete;
  allow& operator=(const allow&) = delete;
};


struct for1;
struct for2;

struct foo {
  void restrict1(allow<for1>) {}
  void restrict2(allow<for1, for2>){}
} foo1;
struct for1 {
  void myFnc() {
    foo1.restrict1(passkey<for1>());
  }
};
struct for2 {
  void myFnc() {
    foo1.restrict2(passkey<for2>());
   // foo1.restrict1(passkey<for2>()); // no passkey
  }
};


void main() {
  std::cout << contains<int, int>::value << std::endl;
  std::cout << contains<int>::value << std::endl;
  std::cout << contains<int, double, bool, unsigned int>::value << std::endl;
  std::cout << contains<int, double>::value << std::endl;
}
Methoxychlor answered 18/4, 2016 at 5:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.