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.uber.org/zap" ) const ( defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net" defaultHTTPClientTimeout = 5 * time.Second ) // DefaultHTTPClient is the default HTTP client for the Permissions Client. var DefaultHTTPClient = &http.Client{ Timeout: defaultHTTPClientTimeout, } // 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 }