## Backend Refactoring The backend refactor has four immediate goals: - Remove repeated logic from handlers, to make handlers easier to develop and test - Create unit tests for handlers - Consolidate error handling and logging - Type the API request/response/endpoints and export the types as a package These tasks will eventually enable: - Application monitoring and error reporting, using the conslidated error handlers and logging - Better support with well-defined and predictable logging output - Proper CI when developing, which can run unit tests against the backend - Auto-generation of the API spec, using the strongly types API requests/responses ## Explanation of Scopes This refactor abstracts authentication and authorization logic much better than the previous version of the API. The refactor is centered around the concept of _scopes_: each endpoint has a well-defined set of scopes that define which objects that the endpoint can modify or return, thereby enforcing _Role-Based Access Control_. The basic set of scopes operate in the following heirarchy: ``` User | Project / \ Cluster Settings / Namespace | Release ``` That is, all project routes are user-scoped (require authenticated user), all cluster scopes require a project, etc. How do we enforce these scopes? This is the purpose of the `api/server/authz` package, where we make use of Golang's [context](https://go.dev/blog/context) to create middleware that adds resources to the context. For example, the `ProjectScopedMiddleware` object returns a handler that essentially does the following (removed fluff from code): ```go // get the referenced project from the database project, _ := p.config.Repo.Project().ReadProject(projID) // add the project to the request context ctx := NewProjectContext(r.Context(), project) r = r.Clone(ctx) // call the next handler p.next.ServeHTTP(w, r) ``` Thus, subsequent endpoint can simply call the following: ```go proj, _ := r.Context().Value(types.ProjectScope).(*models.Project) ``` Note that **we assume the resource is populated in subsequent handlers** -- we do not check for this condition, since this is the function of the middleware. ## Migrating Existing Handlers The steps for migrating an existing handler are as follows: 1. Create a new file in a sub-folder of `/api/server/handlers`. For example, if creating a cluster-scoped routes, add a new route to `/api/server/handlers/cluster`. 2. Add some boilerplate to this file as follows: ```go type [Resource][Verb]Handler struct { // should extend one of: // handlers.PorterHandler // handlers.PorterHandlerReader // handlers.PorterHandlerWriter // handlers.PorterHandlerReaderWriter // TODO: add any additional fields, or embed existing structs/interfaces } func New[Resource][Verb]Handler( config *config.Config, // TODO: additional arguments ) *[Resource][Verb]Handler { return &[Resource][Verb]Handler{ // TODO: initialize } } func (c *[Resource][Verb]Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // TODO: add handler logic here } ``` 3. Implement the handler logic, you can mostly copy the handler from `/server/api`. Perform the following: - Replace all unnecessary lookups by finding the object in context (see above for explanation) - Replace all error-handling with a method from `apierrors` (look at existing handlers to understand) - Use helper methods attached to the `handlers.PorterHandler` interface, which implements the following: ```go Config() *config.Config Repo() repository.Repository HandleAPIError(w http.ResponseWriter, err apierrors.RequestError) ``` 4. Add the route to the corresponding file in `/api/server/router`. Each route has a corresponding endpoint and handler. For example, if creating a new cluster-scoped route, like `GET /api/projects/{project_id}/clusters/{cluster_id}`, you would add the following to `getClusterRoutes`: ```go // GET /api/projects/{project_id}/clusters/{cluster_id} -> project.NewClusterGetHandler getEndpoint := factory.NewAPIEndpoint( &types.APIRequestMetadata{ Verb: types.APIVerbGet, Method: types.HTTPVerbGet, Path: &types.Path{ Parent: basePath, RelativePath: relPath, }, Scopes: []types.PermissionScope{ // NOTE: important. Make sure all relevant scopes are defined: this definition will // automatically add the relevant middleware. types.UserScope, types.ProjectScope, types.ClusterScope, }, }, ) getHandler := cluster.NewClusterGetHandler( config, factory.GetResultWriter(), ) routes = append(routes, &Route{ Endpoint: getEndpoint, Handler: getHandler, Router: r, }) ```