# Visibility modifiers **Author**: @wesb <!-- Status of the document - draft, in-review, etc. - is conveyed via HackMD labels --> [Github issue](https://github.com/TBD54566975/ftl/issues/1164) ## Description Currently, all `verbs` within the cluster are visible without restrictions. We should establish a mechanism where `verbs` and `types` are private by default (only visible within the same module), but can be explicitly tagged with `export` to make them visible to other `modules`. ## Goals - Allow developers to control visibility of `verbs` and `types` ## Non-Goals (optional) - This is not RBAC (that will come later) - Not implementing module `groups` but those might come in later, which would further limit the scope of `internal` to be within a module group - Not implementing module `visibility` given that each verb must declare its `visibility` ## Design Using the term `visibility` over `access` to allow for future RBAC updates when they are defined. ### Annotations #### Private `private` refers to `verbs` and `types` that are only visible with their `module`. ```go //ftl:verb //ftl:data //ftl:enum ``` #### Exported These will be visible to other modules within FTL ```go //ftl:verb export //ftl:data export //ftl:enum export ``` #### Special cases `//ftl:cron` is always private as it provides no benefit being exposed to other modules `//ftl:ingress` is always public as `ingress` provides no benefit to the outside world being private #### Schema ```graphql module one { export data User {} export verb one(one.Request) one.Reponse verb another(one.Request) one.Request } module two { export verb two(req two.Request) two.Response +calls one.one # valid because `one.one` is `exported`. } module three { export verb three(HttpRequest<[three.Request]>) HttpResponse<[three.Response], Empty> +ingress http GET /three +calls two.two # valid because `two.two` is exported. } ``` #### Required changes 1. Add new `Visibility` enum and field to schema 2. Update parser for these new fields. Update all usages of `ftl:export` to `ftl:internal`. We'll need to go through other repos like `ftl-examples` and customer repos as well. Kotlin `@Export` needs to be converted to `@Internal`. 3. Update `ftl.Call`/`controller` logic to enforce `visibility` rules 4. Update external module code generator to not generate `decls` based on `visibility` 5. Console updates to enforce these rules as well ## Questions - [x] What happens with `ingress` and `visibility private`. Does `ingress override`? Do we `error`? - With the new `ftl:internal` style syntax, all verbs will explicity specify `visibility` and `ingress` will go away in favor of `ftl:public http GET /path` style syntax - [x] Should we be explicit about visibility? Meaning, should we always include `visibility` for each verb in the schema even though it's defined at the module level? - Yes, see above - [x] Should we omit non-visible types during code gen - Yes - [x] What are the defaults if not provided (private?) - There are no defaults `visibility` must be declared for all `verbs` ## Rejected Alternatives (optional) #### Groups `groups` act as visibility boundaries within the system, categorizing modules into distinct collections that share specific visibility rules. Each module can belong to multiple groups. #### Visibility levels - `public`: Accessible from anywhere, including ingress or cli - `internal`: Accessible only from other modules and cli. - `private`: Only accessible within the same module. There is no default and every verb must declare its `visibility` or it won't actually be a verb. #### Schema ```graphql module one { internal verb one(one.Request) one.Reponse } module two { internal verb two(req two.Request) two.Response +calls one.one # valid because `one.one` is `internal`. } module three { public verb three(HttpRequest<[three.Request]>) HttpResponse<[three.Response], Empty> +ingress http GET /three +calls two.two # valid because `two.two` is internal. } ``` #### Go ```go package one //ftl:internal func One(ctx context.Context, req OneRequest) (OneResponse, error) {} package two //ftl:internal func Two(ctx context.Context, req TwoRequest) (TwoResponse, error) { resp, err := ftl.Call(ctx, one.One,... } package three //ftl:public http GET /users/{userId}/posts/{postId} func Three(ctx context.Context, req builtin.HttpRequest[ThreeRequest]) (builtin.HttpResponse[ThreeResponse, string], error) { resp, err := ftl.Call(ctx, two.Two,... } ``` ```graphql module one { visibility protected groups a, b verb one(req one.Request) one.Response +visibility protected # This verb is protected, visible only within the groups it belongs to } module two { visibility private groups a, c verb two(req two.Request) two.Response +calls one.one # Calls 'one.one' verb from 'module one', valid because both share group 'a' +visibility protected # Overrides module visibility } module three { visibility public groups c verb three(three.Request) three.Response +ingress http GET /three +calls two.two # Calls 'two.two' which is protected in 'module two', valid because both share group 'c' +visibility public # This verb is publicly visible } ```