Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 20 additions & 62 deletions e2core/auth/access.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
package auth

import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"

"github.com/labstack/echo/v4"
"github.com/pkg/errors"

"github.com/suborbital/e2core/e2core/options"
"github.com/suborbital/e2core/foundation/common"
"github.com/suborbital/systemspec/system"
)

const (
DefaultCacheTTL = 10 * time.Minute
DefaultCacheTTClean = 2 * time.Minute
)

type TenantInfo struct {
AuthorizedParty string `json:"authorized_party"`
Environment string `json:"environment"`
ID string `json:"id"`
Name string `json:"name"`
}

func AuthorizationMiddleware(opts *options.Options) echo.MiddlewareFunc {
authorizer := NewApiAuthClient(opts)
type Authorizer interface {
Authorize(token system.Credential, identifier, namespace, name string) (TenantInfo, error)
}

func AuthorizationMiddleware(authorizer Authorizer) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
identifier := c.Param("ident")
Expand All @@ -43,63 +48,6 @@ func AuthorizationMiddleware(opts *options.Options) echo.MiddlewareFunc {
}
}

func NewApiAuthClient(opts *options.Options) *AuthzClient {
return &AuthzClient{
httpClient: &http.Client{
Timeout: 20 * time.Second,
Transport: http.DefaultTransport,
},
location: opts.ControlPlane + "/environment/v1/tenant/",
cache: NewAuthorizationCache(opts.AuthCacheTTL),
}
}

type AuthzClient struct {
location string
httpClient *http.Client
cache *AuthorizationCache
}

func (client *AuthzClient) Authorize(token system.Credential, identifier, namespace, name string) (*TenantInfo, error) {
if token == nil {
return nil, common.Error(common.ErrAccess, "no credentials provided")
}

key := filepath.Join(identifier, namespace, name, token.Value())

return client.cache.Get(key, client.loadAuth(token, identifier))
}

func (client *AuthzClient) loadAuth(token system.Credential, identifier string) func() (*TenantInfo, error) {
return func() (*TenantInfo, error) {
authzReq, err := http.NewRequest(http.MethodGet, client.location+identifier, nil)
if err != nil {
return nil, common.Error(err, "post authorization request")
}

// pass token along
headerVal := fmt.Sprintf("%s %s", token.Scheme(), token.Value())
authzReq.Header.Set(http.CanonicalHeaderKey("Authorization"), headerVal)

resp, err := client.httpClient.Do(authzReq)
if err != nil {
return nil, common.Error(err, "dispatch remote authz request")
}

if resp.StatusCode != http.StatusOK {
return nil, common.Error(common.ErrAccess, "non-200 response %d for authorization service", resp.StatusCode)
}
defer resp.Body.Close()

var claims *TenantInfo
if err = json.NewDecoder(resp.Body).Decode(&claims); err != nil {
return nil, common.Error(err, "deserialized authorization response")
}

return claims, nil
}
}

func NewAccessToken(token string) system.Credential {
if token != "" {
return &AccessToken{
Expand Down Expand Up @@ -146,3 +94,13 @@ func ExtractAccessToken(header http.Header) system.Credential {
value: authInfo[splitAt+1:],
}
}

// deriveKey is a utility function that takes a system.Credential token, identifier, namespace, and plugin name as args
// and returns one long string we can use as cache keys.
func deriveKey(token system.Credential, identifier, namespace, name string) (string, error) {
if token == nil {
return "", errors.New("token provided was nil")
}

return filepath.Join(identifier, namespace, name, token.Value()), nil
}
75 changes: 75 additions & 0 deletions e2core/auth/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package auth

import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/pkg/errors"

"github.com/suborbital/e2core/e2core/options"
"github.com/suborbital/systemspec/system"
)

var ErrUnauthorized = errors.New("received 401 Unauthorized from API")

// NewApiAuthClient returns a configured SE2 Auth client that will ask the tenant endpoint for info about a tenant by
// its ID.
func NewApiAuthClient(opts *options.Options) *APIAuthorizer {
return &APIAuthorizer{
httpClient: &http.Client{
Timeout: 20 * time.Second,
},
location: opts.ControlPlane + "/environment/v1/tenant/%s",
}
}

// APIAuthorizer is a bridge between e2core and a REST API. The only thing this one does is asks SE2 information about a
// tenant identified by the identifier over plain old REST API.
type APIAuthorizer struct {
location string
httpClient *http.Client
}

// Authorize implements the Authorizer interface. It doesn't use the namespace and the function name arguments as its
// only job is to use the token it received and the identifier and send a message to SE2 asking info about the tenant.
//
// SE2, in turn, is the system that actually does the check for the token.
func (client *APIAuthorizer) Authorize(token system.Credential, identifier, _, _ string) (TenantInfo, error) {
if token == nil {
return TenantInfo{}, errors.New("no credentials provided")
}

authzReq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(client.location, identifier), nil)
if err != nil {
return TenantInfo{}, errors.Wrap(err, "http.NewRequest GET control plane environment tenant")
}

// pass token along
authzReq.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Scheme(), token.Value()))

resp, err := client.httpClient.Do(authzReq)
if err != nil {
return TenantInfo{}, errors.Wrap(err, "client.httpClient.Do")
}

if resp.StatusCode == http.StatusUnauthorized {
return TenantInfo{}, ErrUnauthorized
}

if resp.StatusCode != http.StatusOK {
return TenantInfo{}, fmt.Errorf("received non-200 and non-401 status code %d from API", resp.StatusCode)
}

defer func() {
_ = resp.Body.Close()
}()

var claims TenantInfo
if err = json.NewDecoder(resp.Body).Decode(&claims); err != nil {
return TenantInfo{}, errors.Wrap(err, "json.Decode into TenantInfo")
}

return claims, nil
}
Loading