Description
There is ongoing work to define what we call a service and how these services are structured as packaged and built into binaries. I want to propose a further standard for how we define what is and what isn't authorized from a service perspective.
Today, authorization is often implemented as a middleware (decorator) with the identical signature to the type it decorates. This has the perceived convenience that types don't need to care whether they need authorization to call the service or not. Along with the ability to swap out non-authorized or authorized versions in an ad hock fashion.
I want to contest the notion that this is advantageous and that it leads to more authorization related errors that it solves.
Due to this lax contract design, we put a lot of responsibility on the careful composition of services and their middleware layers (often in a main.go). If you fail to pass the correctly decorated service to another service or transport layer, then to leave it wide open for anyone who is purely authenticated, but not necessarily authorized.
My proposal is this:
Where we consume services we explicitly define whether we want an authorized or non-authorized version via having an interface for both. For example:
type BucketService interface {
FindBucket(context.Context, influxdb.ID) (*influxdb.Bucket, error)
// ...
}
type AuthorizedBucketService interface {
FindBucket(context.Context, influxdb.Authorizer, influxdb.ID) (*influxdb.Bucket, error)
// ...
}
At the transport layer, we would mostly consume the Authorized* interface. Wherever we deem it safe to use the non-authorized, we would user the former interface.
The existing types currently defined in the authorizer
package would implement the latter (e.g. AuthorizedBucketService
) consume the former interface (e.g. BucketService
) and delegate to it. Same goes for all the new locations.
Any services which consume other services would also be expected to consume services with or without explicit auth.
This makes authorization a consumer contract and not a configuration one. Forcing us to consider wherever we delegate to another service whether we have authority and to handle the error case.