Skip to content

Delegating and restricting authority

Any interesting program will need to interact with the outside world, like accessing the network or the file system, or by creating and communicating with other programs. We call ambient authority all those rights implicitly granted to the program to make these things possible, and because Pony makes this concept explicit, we need to take the time to talk about what it means and how it works. In other languages like for example C, you can always attempt to do an operation like I/O, and it will usually succeed save some runtime checks (your disk being full might make an operation fail, for example). But in Pony, any piece of code interacting with the outside world needs the authority to do so.

The operating system essentially knows nothing about the structure of any program it runs, in particular it is ignorant of any Pony specific concepts. In order to impose that some code is authorized for an operation like writing to disk, Pony requires a special argument to be passed, a capability bearing the authority required for the task at hand. The type system guarantees that inadequate capabilities for a task fail at compile time.

Recall the definition of capability from the Object Capabilities section:

A capability is an unforgeable token that (a) designates an object and (b) gives the program the authority to perform a specific set of actions on that object.

In Pony, the Main actor is created with an Env object, which holds the unforgeable AmbientAuth token in its root field. This value is the capability that represents the ambient authority given to us by the system.

Here is a program that connects to example.com via TCP on port 80 and quits:

use "net"

class MyTCPConnectionNotify is TCPConnectionNotify
  let _out: OutStream

  new iso create(out: OutStream) =>
    _out = out

  fun ref connected(conn: TCPConnection ref) =>
    _out.print("connected")
    conn.close()

  fun ref connect_failed(conn: TCPConnection ref) =>
    _out.print("connect_failed")

actor Connect
  new create(out: OutStream, auth: TCPConnectAuth) =>
    TCPConnection(auth, MyTCPConnectionNotify(out), "example.com", "80")

actor Main
  new create(env: Env) =>
    Connect(env.out, TCPConnectAuth(env.root))

The Main actor authorizes the Connect actor by passing a TCPConnectAuth token created from the ambient authority token in env.root. The ambient authority token is unforgeable since the AmbientAuth constructor is private and the only existing instance is provided by the runtime itself.

The Connect actor uses this derived authority when it creates a TCP connection:

TCPConnection(auth, MyTCPConnectionNotify(out), "example.com", "80")

The TCPConnection requires an authority as first parameter, and since the compiler checks that the correct type was passed, this guarantees that a TCPConnection can only be created by an actor holding the required authorization.

The implementation of the TCPConnection constructor does not even use the authorization parameter at run time, all it does is require it to be of the right type. The type checking done by the compiler is sufficient for this guarantee.

Restrict, then delegate your authority

In order to handle our own code and that of others more safely, and also to understand our code better, we want to split up the authority, and only grant the particular authority a piece of code actually requires.

The first parameter of the TCPConnection constructor has the type TCPConnectAuth. This is what we call “the most specific authority”. All classes in the standard library that require an authority token only accept a single type of token; the token of “most specific authority”. In the case of TCPConnection, this is TCPConnectAuth.

Now imagine we don’t trust the Connect actor, so we don’t want to provide it with more authority than needed. For example, there is no point in granting it filesystem access, since it is supposed to do network things (specifically, TCP), not access the filesystem. Instead of passing the entire AmbientAuth (the root of all authority), we “downgrade” that to a TCPConnectAuth (the most restrictive authority in net), pass it to the Connect actor, and have that pass it to the TCPConnection constructor:

actor Connect
  new create(out: OutStream, auth: TCPConnectAuth) =>
    TCPConnection(auth, MyTCPConnectionNotify(out), "example.com", "80")

actor Main
  new create(env: Env) =>
    try Connect(env.out, TCPConnectAuth(env.root)) end

Now we are sure it cannot access the filesystem or listen on a TCP or UDP port. Pay close mind to the authority that code you are calling is asking for. Never give AmbientAuth to any code you do not trust completely both now and in the future. You should always create the most specific authority and give the library that authority. If the library is asking for more authority than it needs, do not use the library.

Authorization-friendly interface

Consider the above example again, but this time let’s think of the Connect actor being part of a 3rd party package that we are building. Our goal is to write the actor in such a way that users of our package can grant it only the authority necessary for it to function.

As the package author, it is then our responsibility to realize that the minimal authority possible is the TCPConnectAuth. We should only request TCPConnectAuth from our users. Our current implementation already satisfies this requirement. Rather than requesting a less specific authority like AmbientAuth from our users and creating the TCPConnectAuth in our library, we only ask for the TCPConnetAuth that is required.

Authority hierarchies

Let’s have a look at the authorizations available in the standard library’s net package.

primitive NetAuth
  new create(from: AmbientAuth) =>
    None

primitive DNSAuth
  new create(from: (AmbientAuth | NetAuth)) =>
    None

primitive UDPAuth
  new create(from: (AmbientAuth | NetAuth)) =>
    None

primitive TCPAuth
  new create(from: (AmbientAuth | NetAuth)) =>
    None

primitive TCPListenAuth
  new create(from: (AmbientAuth | NetAuth | TCPAuth)) =>
    None

primitive TCPConnectAuth
  new create(from: (AmbientAuth | NetAuth | TCPAuth)) =>
    None

Look at the constructor for TCPConnectAuth:

new create(from: (AmbientAuth | NetAuth | TCPAuth))

you might notice that this looks like a hierarchy of authorities:

AmbientAuth >> NetAuth >> TCPAuth >> TCPConnectAuth

where in this paragraph, “>>” means “grants at least as much authority as”. In fact, the AmbientAuth encompasses all ambient authority and is a strictly larger authority than NetAuth, which grants access to the network, which is more powerful than TCPAuth which is restricted to the TCP protocol. Finally, TCPConnectAuth is good only for creating a TCPConnection.

This hierarchy is established by means of the constructor of the weaker authority accepting one of the stronger authorities, for example:

primitive TCPAuth
  new create(from: (AmbientAuth | NetAuth)) =>
    None

Where TCPAuth grants less authority than NetAuth. NetAuth can be used to create any of the derived authorities DNSAuth, UDPAuth, TCPAuth, TCPListenAuth, TCPConnectAuth whereas TCPAuth can only be used to derive TCPListenAuth and TCPConnectAuth.