Files
bridge/internal/permissions/client.go

165 lines
4.5 KiB
Go

package permissions
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/labstack/echo/v4"
"go.infratographer.com/x/gidx"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.uber.org/zap"
)
const (
defaultHTTPClientTimeout = 5 * time.Second
defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net"
permsPathAllow = "/api/v1/allow"
permsPathResourceRelationshipsFormat = "/api/v1/resources/%s/relationships"
permsPathResourceRelationshipsFrom = "/api/v1/relationships/from/"
permsPathResourceRelationshipsTo = "/api/v1/relationships/to/"
permsPathResourceRolesFormat = "/api/v1/resources/%s/roles"
permsPathRoleAssignmentsFormat = "/api/v1/roles/%s/assignments"
)
// DefaultHTTPClient is the default HTTP client for the Permissions Client.
var DefaultHTTPClient = &http.Client{
Timeout: defaultHTTPClientTimeout,
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
// Client defines the Permissions API client interface.
type Client interface {
AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error
CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error)
DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error
DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error
FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error)
ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error)
ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error)
ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error)
ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error)
RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error)
UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error
}
// client is the permissions client.
type client struct {
logger *zap.SugaredLogger
httpClient *http.Client
token string
baseURL *url.URL
allowURL *url.URL
}
// Do executes the provided request.
// If the out value is provided, the response will attempt to be json decoded.
func (c *client) Do(req *http.Request, out any) (*http.Response, error) {
if c.token != "" {
req.Header.Set(echo.HeaderAuthorization, "Bearer "+c.token)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
if out != nil {
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return resp, err
}
}
return resp, nil
}
// DoRequest creates a new request from the provided parameters and executes the request.
func (c *client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) {
path = strings.TrimPrefix(path, c.baseURL.Path)
pathURL, err := url.Parse(path)
if err != nil {
return nil, err
}
url := c.baseURL.JoinPath(pathURL.Path)
query := url.Query()
for k, v := range pathURL.Query() {
query[k] = v
}
url.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, method, url.String(), body)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
resp, err := c.Do(req, out)
if err != nil {
return nil, fmt.Errorf("failed to get response: %w", err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return resp, nil
}
func encodeJSON(v any) (*bytes.Buffer, error) {
var buff bytes.Buffer
if err := json.NewEncoder(&buff).Encode(v); err != nil {
return nil, err
}
return &buff, nil
}
// NewClient creats a new permissions client.
func NewClient(token string, options ...Option) (Client, error) {
client := &client{
logger: zap.NewNop().Sugar(),
httpClient: DefaultHTTPClient,
token: token,
}
for _, opt := range options {
if err := opt(client); err != nil {
return nil, err
}
}
if client.baseURL == nil {
uri, err := url.Parse(defaultPermissionsURL)
if err != nil {
return nil, err
}
client.baseURL = uri
}
client.allowURL = client.baseURL.JoinPath("/api/v1/allow")
return client, nil
}