From 170d4df51c8a1e022fa2f48328bc06453bf099ee Mon Sep 17 00:00:00 2001 From: Nadja Reitzenstein Date: Sat, 2 Oct 2021 12:02:38 +0200 Subject: [PATCH] More changes because breaking existing code is fun --- general.capnp | 28 ------ resource.capnp | 261 ++++++++++++++++++++++++++++++++---------------- resources.capnp | 1 - space.capnp | 16 --- user.capnp | 40 ++++---- users.capnp | 1 - utils.capnp | 17 ++++ 7 files changed, 212 insertions(+), 152 deletions(-) delete mode 100644 general.capnp delete mode 100644 space.capnp diff --git a/general.capnp b/general.capnp deleted file mode 100644 index 8a0d470..0000000 --- a/general.capnp +++ /dev/null @@ -1,28 +0,0 @@ -@0xff5b4a767d98592a; - -using Rust = import "programming_language/rust.capnp"; -$Rust.parentModule("schema"); - -using CSharp = import "programming_language/csharp.capnp"; -$CSharp.namespace("FabAccessAPI.Schema"); - -struct UUID { - # UUID type used to identify machines. - # Since the exact value has no meaning the encoding rules are not too relevant, but it is - # paramount that you are consistent when encoding and decoding this type. - # - # Consider using this algorithm for assembling the 128-bit integer: - # (assuming ISO9899:2018 shifting & casting rules) - # uint128_t num = (uuid1 << 64) + uuid0; - # And then respectively this code for deconstructing it: - # uint64_t uuid0 = (uint64_t) num; - # uint64_t uuid1 = (uint64_t) (num >> 64); - - uuid0 @0 :UInt64; - uuid1 @1 :UInt64; -} - -struct KeyValuePair { - key @0 :Text; - value @1 :Text; -} \ No newline at end of file diff --git a/resource.capnp b/resource.capnp index badf634..dabfaa4 100644 --- a/resource.capnp +++ b/resource.capnp @@ -9,142 +9,211 @@ $CSharp.namespace("FabAccessAPI.Schema"); using Persistent = import "/capnp/persistent.capnp".Persistent; using Value = import "/capnp/schema.capnp".Value; -using General = import "general.capnp"; using User = import "user.capnp".User; -using Space = import "space.capnp".Space; +using L10NString = import "utils.capnp".L10NString; +using UUID = import "utils.capnp".UUID; -struct Node { - # A node in the state tree. If it's the root note this struct "contains" the whole tree. +struct Resource { + # BFFH's smallest unit of a physical or abstract "thing". + # A resource can be as simple and physical as a table, as complex as a PCB production line or as + # abstract as "people with specific know-how are present". - # TODO: I'm not happy with this representation. While it's about as generic as we can get it's - # unhandly because all clients and servers have to always manually check every leaf of the - # state tree, relying on convention instead of static type checking. But I'm not sure how else - # to represent the state extensibly in a way that lets us evolve the protocol by stabilizing - # extensions. One option could be to use OID or UUID as "tag bits" and "stabilize" them by - # defining those as `const` values, but that wouldn't give us proper type checking either. + uuid @0 :UUID; + # An stable, globally unique descriptor for a resource. Two resources with the same UUID are + # (almost¹) guaranteed to be the same instance, and the UUID of a resource will survive through + # server restarts, renaming, reconfiguration etc. + # + # [¹]: UUID are 128-bit integer. A collision is *possible*, just *very* unlikely. If you + # generate 1 billion UUID every second for the next 100 years you have a chance for a collision + # of about 50%. - part @0 :Text; - # Name of the node, making up a path to this node (e.g. "set/colour/red") - union { - # Content of a node. A node has either children *or* a value, not both. + id @1 :Text; + # Every resource in BFFH has a human-readable "name" that is locally unique, but not persistent. + # That is a resource called "hello" today may be called "bye" tomorrow and a resource called + # "hi~~" may not be the same resource as the resource called "hi~~" yesterday. This name is + # canonical and thus identifying. There is exactly *one* valid representation of this name at + # any given point in time. Thus this name can also not be translated. - children @1 :List(Node); - # Node is not a leaf node ⇒ it has a list of children + name @2 :L10NString; + # A resource may also have a human-meaningful name that is designed to be shown to users. This + # name does not have to be unique or identifiable or canonical, its main use is to be + # human-meaningful. For example a "name" could be the translations: + # - (en, "Prusa SL1 SLA-Printer") + # - (de, "Prusa SL1 SLA-Drucker") + # - (es, "Impresora 3D de SLA Prusa SL1") - value @2 :Value; - # Node is a leaf node ⇒ it contains a (typed) Cap'n Proto value. - # The type `Value` comes from the Cap'n Proto schema definition file (usually - # /usr/include/capnp/schema.capnp) and can be any basic capnp type, including lists and - # structs (as :AnyPointer which a client has to cast) - } + description @3 :L10NString; + # A resource may have a description attached to tell an user some more information on a resource + # in a free-form format. + # Similar to the human-meaningful name this description can be translated. + + notify @4 :Notify; + # Readonly access to the state of a resource. + # A resource can have "state". State are values attached to a resource that describe a specific + # state that users or administrators want this resource to be in. Usually this state consists of + # a number of primitive values encoding for example "turned on" or "turned off". + # Users with the required permissions may change this state. + + claimable @5 :Claimable; + # Writeable access to the state of a resource. + # Resources are semaphores. They allow writeable access for n ∈ ℕ\{0} clients, depending on the + # exact resource in question. In some cases n ≔ 1, and the only write access is exclusive. + # "Claims" model this by requiring a client to first assert a claim, thus reserving a semaphore + # slot or failing if no more are available, and then using this claim to write to a resources + # state. + + interestable @6 :Interestable; + # Sometimes clients are not just interested in the state of a resource but rather want a + # resource to stay in a specific state. e.g. somebody working in a makerspace wants the space to + # stay open, even though they themselves may not have permission to keep the makerspace open. + # "Interest" represents this. Specifically right now it tells BFFH that the client wants at + # least one `Claim` to remain. } -struct Applied { - # Encodes if a specific actor has applied/verified a state change +struct Map(Key, Value) { + # A serialized key-value map represented as a list of (k,v) tuples. - name @0 :Text; - # Name of the actor + entries @0 : List(Entry); - state @1 :State; - # State of the state change in the actor - enum State { - unapplied @0; - applied @1; - verified @2; + struct Entry { + key @0 :Key; + val @1 :Value; } } +using State = Map(Text, Value); +# Update state provided to a resource via a claim is represented as a Map of human-readable +# identifiers to Cap'n Proto Values. These Values can be either primitive types such as Uint8, +# Float64 or more complex types such as structs, lists, or enums. +# The resulting state of a resource, which is the output of whatever internal logic the resource +# implements, is also represented in this form, but the keys and also values may be different. +# +# Later on very common cases (use, register, return, etc.) can get shortcut functions in the Claim +# interface that pre-emptively check permissions and ability (so you get the respective cap iff the +# resource supports that update and if you're allowed to do that) but these functions only serve to +# make the update more efficient than calling `update` with the string identifier and dynamic typed +# value but do the exact same serverside as an `update` call would. This way we can make future +# versions of the API more efficient and easier to use while not breaking compatibility with old +# clients. +# +# TODO: This has the potential problem that a newer client can not distinguish between a server +# using an old version of the API and a client simply not being allowed to call a specific shortcut +# method because in both cases that cap will be a nullptr. Could be solved by making `Claim` a +# struct and indicating which shortcut methods it knows of. +# Not sure if this is a big problem, we optimize for old clients and up-to-date servers. +# +# TODO: We should provide a number of sensible implementations for common complex `Value` types such +# as "colour", "temperature", etc. and define identifiers for common values. interface Access { - # Allow syncronous read access to a resource's state. You're not given this capability directly - # but instead Notify, Interest and Claim all extend it, allowing you to call these methods from - # any of those. + # Allow syncronous read access to a resource's output state. You're not given this capability + # directly but instead Notify, Interest and Claim all extend it, allowing you to call these + # methods on any of those. - readState @0 Node; - readApplied @1 Applied; + readOutput @0 State; # TODO: There should probably be a more efficient approach for reading state than "read *all* # state". } interface Notify extends(Access) { - # The Notify interface allows clients to be informed about state changes asyncronously. - # It is mainly designed around the `register` function which allows a client to register a - # Callback on the client that is called every time state changes happen to the resource in - # question. - # Notify are ephermal. If the connection to the server is lost all `Notify` are unregistered + # The Notify interface allows clients to be informed about state changes asyncronously. It is + # mainly designed around the `register` function which allows a client to register a callback + # that is called every time state changes happen to the resource in question. + # + # Notify are ephermal. If the connection to the server is lost all `Notify` from that client are + # unregistered. register @0 ( cb: Callback ); # Register a given callback to be called on every state update. If this client already has a # callback registered for this resource the old callback is replaced. - # The two fields `state` and `applied` indicate interest for `state` and `applied`. If they are - # unset the respective method on the callback will not be called. unregister @1 (); - # Unregister the current callback, if any. + # Unregister this callback interface Callback { - # This callback interface is implemented on the client + # This callback interface needs to be implemented on the client - newState @0 Node; - # A server will call newState() with the updated set state tree if `state` was set to `true` - # in `register`, however unless the last call to newState() didn't complete yet, as to not - # overload a client. + newState @0 State; + # A server will call newState() with the updated output state. However a server will only + # allow one in-flight call, so as long as the previous call to newState() hasn't completed + # the server will drop intermediary updates as to not overload a client. + # Specifically, example timeline: + # 1. Update A + # 2. Server calls newState(A) + # 3. Update B + # 4. Update C + # 5. Call to newState(A) completes + # 6. Server calls newState(C) + # So Update B was never sent to the client but the client will eventually always end up with + # the latest state. # TODO: There should probably be a more efficient approach here too, something along the # lines of server-side filtering. - - # TODO: Add newApplied? } } interface Interestable { - interest @0 () -> ( interest: Interest ); -} + # "Interest" right now it tells BFFH that the client wants at least one `Claim` to remain. -interface Interest extends(Access) { - register @0 ( cb: Callback ); - unregister @1 (); + register @0 ( cb: Callback ) -> ( handle :Handle ); + # Register a callback that BFFH will use to send notifications back to the client asyncronously. + # This creates an "Interest" on this resource. Setting the callback to a `nullptr` will still + # register an interest but the server will not be able to inform a client about an impeding + # claim drop. - blocking @2 (); + blocking @1 (); # As an alternative to the `register`/`Callback` system you can also call `blocking` which will - # — as the name suggests — block until the last claim was dropped. + # — as the name suggests — block until the last claim is being dropped. This will register an + # ephermal Interest that can not survive a disconnect. interface Callback { drop @0 (); - # The last claim on the resource this Interest is registered on was dropped, invalidating + # The last claim on the resource this Interest is registered is being dropped, invalidating # the Interest. } + + interface Handle { + # A Handle back to the server side Interest registered. Destroying this capability will also + # inform the server and remove the Interest again. + # TODO: `extends (Persistance)` so that clients can `save` this capability and thus make the + # Interest survive disconnects. + + } } + interface Claimable { # Having this capability set (i.e. not be a `nullptr`) means the user has at least writeable # access to a resource and the resource is claimable (n > 0). claim @0 () -> ClaimResponse; # Assert a claim on a resource. -} -interface Lockable { - # Having this capability set means the user has managerial access to a resource. + struct ClaimResponse { + enum Error { + # Error describing why a claim failed. - lock @0 () -> ( lock: Claim ); -} + exhausted @0; + # There are no more free Claim slots -struct ClaimResponse { - enum Error { - # Error describing why a claim failed. + locked @1; + # The resource was locked - exhausted @0; - # There are no more free Claim slots + precondition @2; + # Some precondition was not met - locked @1; - # The resource was locked - } + dependencies @3; + # Resource failed to secure dependencies + } - union { - error @0 :Error; - success @1 :Claim; + union { + failed :group { + error @0 :Error; + reason @1 :L10NString; + } + success @2 :Claim; + } } } @@ -157,17 +226,37 @@ interface Claim extends(Access) { # In this case the `Restorer` service could be `Claimable` / `Interestable` providing a # `restore( ref: SturdyRef )` method. - update @0 Node; + readInput @0 () -> State; + # Get the current *input* state. This is not the output state that `Notify` or Actors get access + # to but instead the currently stored input state of a resource. + + update @1 State -> UpdateResult; # Update the State of the claimed resource with the given one -} -struct Resource { - name @0 :Text; - description @1 :Text; - typeid @2 :Text; + struct UpdateResult { + enum Error { + # Reason why the update failed - notify @3 :Notify; - interest @4 :Interest; - claimable @5 :Claimable; - lockable @6 :Lockable; + denied @0; + # Update was denied beause user is missing an required permission + + precondition @1; + # Some other precondition failed, e.g. because a required field is not set + + invalid @2; + # The update is invalid, e.g. because an unknown field was set. + + typeError @3; + # A field in the update has a known identifier but a bad type for that identifier + } + + union { + failed :group { + error @0 :Error; + field @1 :Text; + reason @2 :L10NString; + } + success @3 :Void; + } + } } diff --git a/resources.capnp b/resources.capnp index 99ecc7b..f30fa0c 100644 --- a/resources.capnp +++ b/resources.capnp @@ -6,7 +6,6 @@ $Rust.parentModule("schema"); using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); -using General = import "general.capnp"; using Resource = import "resource.capnp".Resource; interface Resources diff --git a/space.capnp b/space.capnp deleted file mode 100644 index 6dd0aab..0000000 --- a/space.capnp +++ /dev/null @@ -1,16 +0,0 @@ -@0xbacaff4190ac7d80; - -using Rust = import "programming_language/rust.capnp"; -$Rust.parentModule("schema"); - -using CSharp = import "programming_language/csharp.capnp"; -$CSharp.namespace("FabAccessAPI.Schema"); - -using General = import "general.capnp"; - -struct Space -{ - id @0 :General.UUID; - name @1 :Text; - info @2 :Text; -} \ No newline at end of file diff --git a/user.capnp b/user.capnp index 180efee..719af16 100644 --- a/user.capnp +++ b/user.capnp @@ -6,39 +6,39 @@ $Rust.parentModule("schema"); using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); -using General = import "general.capnp"; -using Space = import "space.capnp".Space; +using UUID = import "utils.capnp".UUID; using Role = import "role.capnp".Role; -struct User -{ - id @0 :General.UUID; - username @1 :Text; - space @2 :Space; +struct User { + # Intergalactic lifeform that wants to use BFFH - struct UserInfoExtended - { - id @0 :General.UUID; - name @1 :Text; + id @0 :UUID; + # The UUID of an user is a globally unique, persistent identifier for this user. + + username @1 :Text; + # username. Locally unique so identifying, but not persistent. + + info @2 :Info; + interface Info $CSharp.name("InfoInterface") { + listRoles @0 () -> ( roles :List(Role) ); + # lists explicit roles for this user. A session may have a number of additional, implicit, + # roles set by their choice of authentication or other context. } - info @3 :Info; - interface Info $CSharp.name("InfoInterface") { - getUserInfoExtended @0 () -> ( userInfoExtended :UserInfoExtended ); - listRoles @1 () -> ( roles :List(Role) ); + passwd @3 :Passwd; + interface Passwd { + changepw @0 ( old: Text, new: Text ); } manage @4 :Manage; interface Manage $CSharp.name("ManageInterface") { - pwd @0 ( old_pwd :Text, new_pwd :Text ) -> (); + addRole @0 ( role :Role ); + removeRole @1 ( role :Role ); } admin @5 :Admin; interface Admin $CSharp.name("AdminInterface") { - addRole @0 ( role :Role ) -> (); - removeRole @1 ( role :Role ) -> (); - - pwd @2 ( new_pwd :Text ) -> (); + setpw @0 ( new :Text ); } cardDESFireEV2 @6 :CardDESFireEV2; diff --git a/users.capnp b/users.capnp index 96e3793..c5bc2ff 100644 --- a/users.capnp +++ b/users.capnp @@ -6,7 +6,6 @@ $Rust.parentModule("schema"); using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); -using General = import "general.capnp"; using User = import "user.capnp".User; interface Users diff --git a/utils.capnp b/utils.capnp index 880c4f9..eb45c8f 100644 --- a/utils.capnp +++ b/utils.capnp @@ -27,3 +27,20 @@ interface L10NString { available @1 () -> ( langs :List(Text) ); # Returns the list of locales this content is available in. } + +struct UUID { + # UUID type used to identify machines. + # Since the exact value has no meaning the encoding rules are not too relevant, but it is + # paramount that you are consistent when encoding and decoding this type. + # + # Consider using this algorithm for assembling the 128-bit integer: + # (assuming ISO9899:2018 shifting & casting rules) + # uint128_t num = (uuid1 << 64) + uuid0; + # And then respectively this code for deconstructing it: + # uint64_t uuid0 = (uint64_t) num; + # uint64_t uuid1 = (uint64_t) (num >> 64); + + lower @0 :UInt64; + upper @1 :UInt64; +} +