diff --git a/claim.capnp b/claim.capnp new file mode 100644 index 0000000..7a2e973 --- /dev/null +++ b/claim.capnp @@ -0,0 +1,22 @@ +@0xf8f8864ba0678056; + +using CSharp = import "programming_language/csharp.capnp"; +$CSharp.namespace("FabAccessAPI.Schema"); + +using import "/capnp/rpc.capnp".SturdyRef; +using import "/capnp/persistent.capnp".Persistent; + +using import "state.capnp".State; + +interface Claimable extends (Persistent) { + restore @0 ( sturdy :SturdyRef ) -> ( claim :Claim ); + # Restore a previously saved SturdyRef pointing to a Claim + + claim @1 () -> ( claim :Claim ); + # returns NULL if the resource is *currently* not claimable. + # drop the returned claim capability to unclaim it. +} + +interface Claim extends (Persistent) { + update @0 ( state :State ) -> (); +} diff --git a/interest.capnp b/interest.capnp new file mode 100644 index 0000000..54ba6ba --- /dev/null +++ b/interest.capnp @@ -0,0 +1,8 @@ +@0xa23cfc5ead0ac055; + +using CSharp = import "programming_language/csharp.capnp"; +$CSharp.namespace("FabAccessAPI.Schema"); + +interface Interestable { + +} diff --git a/main.capnp b/main.capnp index b9db80f..da9852a 100644 --- a/main.capnp +++ b/main.capnp @@ -7,12 +7,25 @@ using Authentication = import "auth.capnp".Authentication; using Resources = import "resources.capnp".Resources; using Users = import "users.capnp".Users; +struct Version +{ + major @0 :Int32 = 0; + minor @1 :Int32 = 4; +} + interface Bootstrap { - mechanisms @0 () -> ( mechs :List(Text) ); + getAPIVersion @0 () -> Version; + + getServerRelease @1 () -> ( name :Text, release :Text ); + # Returns the server implementation name and version/build number + # Designed only for human-facing debugging output so should be informative over machine-readable + # Example: ( name = "bffhd", release = "0.3.1-f397e1e [rustc 1.57.0 (f1edd0429 2021-11-29)]") + + mechanisms @2 () -> ( mechs :List(Text) ); # Get a list of Mechanisms this server allows in this context. - createSession @1 ( mechanism :Text, initialData :Data ) -> ( authentication :Authentication ); + createSession @3 ( mechanism :Text, initialData :Data ) -> ( authentication :Authentication ); # Create a new session with the server that you wish to authenticate using `mechanism`. # If the mechanism is a client-first mechanism you MAY set `initialData` to contain the data you # want to send. If the mechanism is server-first or you do not wish to send initial data, make diff --git a/notify.capnp b/notify.capnp new file mode 100644 index 0000000..81457ea --- /dev/null +++ b/notify.capnp @@ -0,0 +1,16 @@ +@0xc0787ef6e3cb87e1; + +using CSharp = import "programming_language/csharp.capnp"; +$CSharp.namespace("FabAccessAPI.Schema"); + +using State = import "state.capnp".State; + +interface Notifyable { + subscribe @0 ( subscriber :Subscriber ) -> ( subscription :Subscription ); +} + +interface Subscriber { + newState @0 ( state :State ) -> (); +} + +interface Subscription { } diff --git a/resource.capnp b/resource.capnp index 159fc97..30d9f36 100644 --- a/resource.capnp +++ b/resource.capnp @@ -3,261 +3,32 @@ using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); -using Persistent = import "/capnp/persistent.capnp".Persistent; -using Value = import "/capnp/schema.capnp".Value; +using import "/capnp/persistent.capnp".Persistent; -using User = import "user.capnp".User; -using L10NString = import "utils.capnp".L10NString; -using UUID = import "utils.capnp".UUID; -using OID = import "utils.capnp".OID; +using import "notify.capnp".Notifyable; +using import "interest.capnp".Interestable; +using import "claim.capnp".Claimable; -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". +using import "utils.capnp".OID; - 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%. +interface Resource extends (Persistent) { + # 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". + type @0 () -> ( types :List(OID) ); + # The 'type' of Resource. Each OID in the list specifies certain behaviours + # that this Resource follows. - 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. + notify @1 () -> ( notify :Notifyable ); + # NULL if the user does not have permission to read this resource, or if + # this resource is not notifiable - 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") + interest @2 () -> ( interest :Interestable ); + # NULL if this resource is not interestable or the user does not have + # permission to set interests for this resource. - 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. - - grants @4 :ResourceCaps; - - grant :union { - # If the current session has already been given a grant this field will contain a reference - # to it. Since stronger grants extend weaker grants only one of these needs to be set at any - # given point. - # This is mostly useful for session resumption. - - none @5 :Void; - # No previous grant for this resource exists for the current user - - notify @6 :Notify; - interest @7 :Interest; - claim @8 :Claim; - # The user has a respective grant on the resource - } -} - -interface ResourceCaps { - # Capabilities transfered for a resource. Users will have some or all of these set to non-null - # depending on their permission level. - - getState @0 () -> State; - # 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". - - setNotify @1 ( callback :Callback ) -> ( notify :Notify ); - # Notify allows clients to be informed about state changes asyncronously. A client can register - # a callback that is called every time state changes happen to the resource in question. - # Notify callbacks are ephermal. If the connection to the server is lost any callbacks from that - # client for any resource are unregistered. - - claim @2 () -> ClaimResponse; - # Request 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. - - interest @3 () -> ( interest :Interest ); - # Register an "Interest" on this resource. - # 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. - - override @4 () -> ( claim :Claim ); - # Override forces a claim to a resource, even if it is already exhausted. This is - # primarely useful for administrative overrides. -} - -struct ClaimResponse { - enum Error { - # Error describing why a claim failed. - - exhausted @0; - # There are no more free Claim slots - - locked @1; - # The resource was locked - - precondition @2; - # Some precondition was not met - - dependencies @3; - # Resource failed to secure dependencies - } - - union { - failed :group { - error @0 :Error; - reason @1 :L10NString; - } - success @2 :Claim; - } -} - -struct Map(Key, Value) { - # A serialized key-value map represented as a list of (k,v) tuples. - - entries @0 :List(Entry); - - struct Entry { - key @0 :Key; - val @1 :Value; - } -} - -using State = Map(OID, 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 Notify { - # If an user has a notify callback registered it can use this capability to remove it again - - remove @0 (); - # Remove any notify callbacks from this user for this resource. - - install @1 ( callback :Callback ); - # Install a notify callback, replacing any existing one. This method is useful when getting this - # interface implicitly via Interest or Claim. -} - -interface Callback { - # This callback interface needs to be implemented on the 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. -} - -interface Interest extends(Notify) { - # "Interest" right now tells BFFH that the client wants at least one `Claim` to remain. - # However, more generally an Interest allows hooking into state changes and block or modify - # them. - - dropInterest @0 (); - # Remove this interest from a resource. - - lock @1 (); - # Lock a resource, making all future state changes from any user but the current one fail until - # the lock is released. - - unlock @2 (); - # Unlock the resource again, allowing other users to change state again. -} - -interface Claim extends(Interest) { - # TODO: extend Persistance. Claims and Interests need to be able to survive a connection loss, - # which is exactly what `SturdyRef`/Persistance are designed to provide. The Persistance - # interface only provides one method, `save`, returning a `SturdyRef`. A SturdyRef is a generic - # and generally speaking opaque type that can be restored to a live capability using some sort - # of `Restorer` service. - # In this case the `Restorer` service could be `Claimable` / `Interestable` providing a - # `restore( ref: SturdyRef )` method. - - 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 - - dropClaim @2 (); - # Drop this claim - - struct UpdateResult { - enum Error { - # Reason why the update failed - - 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 - - locked @4; - # The state is currently locked and can not be modified by anybody but the user that - # issued the lock. - } - - union { - failed :group { - error @0 :Error; - field @1 :OID; - reason @2 :L10NString; - } - success @3 :Void; - } - } + claim @3 () -> ( claim :Claimable ); + # NULL if the user does not have permission to write to this resource, or if + # this resource is not (ever!) claimable } diff --git a/resources.capnp b/resources.capnp index 67ac819..5e71557 100644 --- a/resources.capnp +++ b/resources.capnp @@ -3,10 +3,19 @@ using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); +using import "/capnp/rpc.capnp".SturdyRef; + using Resource = import "resource.capnp".Resource; -interface Resources -{ - listAll @0 () -> ( list :List(Resource) ); - get @1 ( name :Text ) -> Resource; +interface Resources { + restore @0 ( sturdy :SturdyRef ) -> ( resources :Resource ); + # Restore a previously saved SturdyRef pointing to a Resource + + list @1 () -> ( resources :List(Resource) ); + + getByUrn @2 ( urn :Text ) -> ( resource :Resource ); + # Returns a NULL capability if the resource doesn't exist or an user + # doesn't have disclose permission for that resource. + + getByName @3 ( name :Text ) -> ( resource :Resource ); } diff --git a/role.capnp b/role.capnp index 383a180..19c39c9 100644 --- a/role.capnp +++ b/role.capnp @@ -3,7 +3,6 @@ using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); -struct Role -{ - name @0 :Text; -} \ No newline at end of file +interface Role { + name @0 () -> ( name :Text ); +} diff --git a/state.capnp b/state.capnp new file mode 100644 index 0000000..2820d95 --- /dev/null +++ b/state.capnp @@ -0,0 +1,8 @@ +@0x9d6da2edc6588d6e; + +using CSharp = import "programming_language/csharp.capnp"; +$CSharp.namespace("FabAccessAPI.Schema"); + +interface State { + +} diff --git a/user.capnp b/user.capnp index 2fa4a58..80cce37 100644 --- a/user.capnp +++ b/user.capnp @@ -3,42 +3,35 @@ using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); -using UUID = import "utils.capnp".UUID; -using Role = import "role.capnp".Role; +using import "role.capnp".Role; -struct User { +interface User { # Intergalactic lifeform that wants to use BFFH - 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; + info @0 () -> ( info :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. } - passwd @3 :Passwd; + passwd @1 () -> ( passwd :Passwd ); interface Passwd { changepw @0 ( old :Text, new :Text ); } - manage @4 :Manage; + manage @2 () -> ( manage :Manage ); interface Manage $CSharp.name("ManageInterface") { - addRole @0 Role; - removeRole @1 Role; + addRole @0 ( role :Role ); + removeRole @1 ( role :Role ); } - admin @5 :Admin; + admin @3 () -> ( admin :Admin ); interface Admin $CSharp.name("AdminInterface") { setpw @0 ( new :Text ); } - cardDESFireEV2 @6 :CardDESFireEV2; + cardDESFireEV2 @4 () -> ( carddesfireev2 :CardDESFireEV2 ); interface CardDESFireEV2 $CSharp.name("CardDESFireInterface") { # Card authentication using NXP/MiFare DESFire cards. # These cards have the ability to restrict access for data on the cards using symmetric diff --git a/users.capnp b/users.capnp index 2cfa9a4..e324cfb 100644 --- a/users.capnp +++ b/users.capnp @@ -5,16 +5,15 @@ $CSharp.namespace("FabAccessAPI.Schema"); using User = import "user.capnp".User; -interface Users -{ - whoami @0 () -> User; +interface Users { + whoami @0 () -> ( user :User ); manage @1 () -> ( manage :Manage ); interface Manage $CSharp.name("ManageInterface") { list @0 () -> ( users :List(User) ); - addUser @1 ( username :Text, password :Text ) -> User; + addUser @1 ( username :Text, password :Text ) -> ( user :User ); - removeUser @2 User; + removeUser @2 ( user :User ); } } diff --git a/utils.capnp b/utils.capnp index f906ade..01b40b5 100644 --- a/utils.capnp +++ b/utils.capnp @@ -1,35 +1,41 @@ @0xed0c02f41fea6b5a; interface L10NString { - # Any string type that is intended to be displayed to an user that is more than an identifier to - # be used as-is must be able to be localized into the users preferred language. This includes - # description, help messages, etc. but of course does not extend to usernames. - # TODO: Potentially make generic over the localized content (e.g. dates)? Can be done after the - # fact without braking protocol, so no big issue. + # Any string type that is intended to be displayed to an user that is more + # than an identifier to be used as-is must be able to be localized into the + # users preferred language. This includes description, help messages, etc. + # but of course does not extend to usernames. + # TODO: Potentially make generic over the localized content (e.g. dates)? + # Can be done after the fact without braking protocol, so no big issue. get @0 ( lang :Text ) -> ( lang :Text, content :Text ); - # Retrieve the string in the given locale. The input parameter MUST be a RFC5646-formatted - # locale identifier (e.g: "en-US", "de-DE", "az-Arab-IR"). + # Retrieve the string in the given locale. The input parameter MUST be a + # RFC5646-formatted locale identifier (e.g: "en-US", "de-DE", "az-Arab-IR"). # - # If a server can't find a localized version matching exactly it MUST try to substitute it. - # Substitution MUST always return more specific matches for general queries. - # e.g. if "it" is requested and the server has "it-CH" available it returns this string. + # If a server can't find a localized version matching exactly it MUST try to + # substitute it. Substitution MUST always return more specific matches for + # general queries. e.g. if "it" is requested and the server has "it-CH" + # available it returns this string. # - # Substitution SHOULD NOT cross language barriers, e.g. returning "en-GB" for a string requested - # in "cy-GB". Substitution MUST NOT return a localization in a different language unless server - # has a priori knowledge that the user can read and understand said language. + # Substitution SHOULD NOT cross language barriers, e.g. returning "en-GB" + # for a string requested in "cy-GB". Substitution MUST NOT return a + # localization in a different language unless server has a priori knowledge + # that the user can read and understand said language. # - # Substitution SHOULD prefer unspecified subtags over wrong subtags. If "es-AR" is requested and a - # server has "es", and "es-VE" available, "es" should be selected. + # Substitution SHOULD prefer unspecified subtags over wrong subtags. If + # "es-AR" is requested and a server has "es", and "es-VE" available, "es" + # should be selected. # - # A server MUST set the output `lang` field to the exact tag that the content it sends was written - # in and `content` to the localized string. - # E.g. If a string is requested for "sr" and the server has found a string that was configured as - # "sr-Cyrl-BA" the server sets lang to "sr-Cyrl-BA". + # A server MUST set the output `lang` field to the exact tag that the + # content it sends was written in and `content` to the localized string. + # e.g. If a string is requested for "sr" and the server has found a string + # that was configured as "sr-Cyrl-BA" the server sets lang to "sr-Cyrl-BA". # - # If a server can't find a suitable substitute it MUST set the output `content` to a NULL pointer - # and set the output `lang` to the input `lang` it was passed. - # If a server can't parse a given `lang` tag it MUST set the output `lang` to a NULL pointer. + # If a server can't find a suitable substitute it MUST set the output + # `content` to a NULL pointer and set the output `lang` to the input `lang` + # it was passed. + # If a server can't parse a given `lang` tag it MUST set the output `lang` + # to NULL. available @1 () -> ( langs :List(Text) ); # Returns the list of locales this content is available in. @@ -52,16 +58,14 @@ struct UUID { # upper 8 bytes of the uuid, containing the MSB. } -struct OID { - bytes @0 :Data; - # The OID, encoded as a sequence of varints. In this encoding the lower 7 bits of each octet - # contain data bits while the MSB indicates if the *following* octet is still part of this edge. - # It is the same encoding UTF-8 uses. To decode you simply collect octets until you find an - # octet <128 and then concat the data bits of all the octets you've accumulated, including the - # current one. This gives you the value of one node. Continue until you've exhausted the - # available data. - # This is a rather efficient encoding since almost all edges of the OID tree are smaller than - # 128 and thus encode into one byte. - # X.208 does *not* limit the size of nodes! However, a reasonable size limit is 128 bit per - # node, which is the size of the UUID nodes in the `2.25` subtree. -} +using OID = Data; +# An OID is encoded as a sequence of varints. In this encoding the lower 7 bits +# of each octet contain data bits while the MSB indicates if the *following* +# octet is still part of this edge. It is the same encoding UTF-8 uses. To +# decode you simply collect octets until you find an octet <128 and then concat +# the data bits of all the octets you've accumulated, including the current one. +# This gives you the value of one node. Continue until you've exhausted the +# available data. This is a rather efficient encoding since almost all edges of +# the OID tree are smaller than 128 and thus encode into one byte. +# X.208 does *not* limit the size of nodes! However, a reasonable size limit is +# 128 bit per node, which is the size of the UUID nodes in the `2.25` subtree.