initial commit
This commit is contained in:
84
internal/permissions/assignments.go
Normal file
84
internal/permissions/assignments.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.infratographer.com/x/gidx"
|
||||
)
|
||||
|
||||
type RoleAssign struct {
|
||||
SubjectID string `json:"subject_id"`
|
||||
}
|
||||
|
||||
type RoleAssignResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type roleAssignmentData struct {
|
||||
Data []struct {
|
||||
SubjectID string `json:"subject_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func (c *Client) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error {
|
||||
path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String())
|
||||
|
||||
body, err := encodeJSON(RoleAssign{
|
||||
SubjectID: memberID.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var response RoleAssignResponse
|
||||
|
||||
if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return ErrAssignmentFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) {
|
||||
path := fmt.Sprintf("/api/v1/roles/%s/assignments", roleID.String())
|
||||
|
||||
var response roleAssignmentData
|
||||
|
||||
if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assignments := make([]gidx.PrefixedID, len(response.Data))
|
||||
|
||||
for i, assignment := range response.Data {
|
||||
id, err := gidx.Parse(assignment.SubjectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: failed parsing id %s", err, assignment.SubjectID)
|
||||
}
|
||||
|
||||
assignments[i] = id
|
||||
}
|
||||
|
||||
return assignments, nil
|
||||
}
|
||||
|
||||
func (c *Client) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) {
|
||||
assignments, err := c.ListRoleAssignments(ctx, roleID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, assignment := range assignments {
|
||||
if assignment == memberID {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
128
internal/permissions/client.go
Normal file
128
internal/permissions/client.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const defaultPermissionsURL = "https://permissions-api.hollow-a.sv15.metalkube.net"
|
||||
|
||||
var defaultHTTPClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
httpClient *http.Client
|
||||
token string
|
||||
|
||||
baseURL *url.URL
|
||||
|
||||
allowURL *url.URL
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
73
internal/permissions/config.go
Normal file
73
internal/permissions/config.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"go.infratographer.com/x/viperx"
|
||||
)
|
||||
|
||||
// Config provides configuration for connecting to the Equinix Metal API provider.
|
||||
type Config struct {
|
||||
// BaseURL is the baseurl to use when connecting to the Equinix Metal API Provider.
|
||||
BaseURL string
|
||||
|
||||
// BearerToken is the token to interact with the Equinix Metal API
|
||||
BearerToken string
|
||||
}
|
||||
|
||||
func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
||||
flags.String("permissions-baseurl", "", "permissions base url")
|
||||
viperx.MustBindFlag(v, "permissions.baseurl", flags.Lookup("permissions-baseurl"))
|
||||
|
||||
flags.String("permissions-token", "", "permissions bearer url")
|
||||
viperx.MustBindFlag(v, "permissions.bearertoken", flags.Lookup("permissions-token"))
|
||||
}
|
||||
|
||||
// WithConfig applies all configurations defined in the config.
|
||||
func WithConfig(config Config) Option {
|
||||
return func(c *Client) error {
|
||||
var options []Option
|
||||
|
||||
if config.BaseURL != "" {
|
||||
options = append(options, WithBaseURL(config.BaseURL))
|
||||
}
|
||||
|
||||
if config.BearerToken != "" {
|
||||
options = append(options, WithBearerToken(config.BearerToken))
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
if err := opt(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBaseURL updates the baseurl used by the client.
|
||||
func WithBaseURL(baseURL string) Option {
|
||||
return func(c *Client) error {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse emapi base url %s: %w", baseURL, err)
|
||||
}
|
||||
|
||||
c.baseURL = u
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBearerToken sets the bearer token to authenticate the request with.
|
||||
func WithBearerToken(token string) Option {
|
||||
return func(c *Client) error {
|
||||
c.token = token
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
8
internal/permissions/errors.go
Normal file
8
internal/permissions/errors.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package permissions
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrRoleNotFound = errors.New("role not found")
|
||||
ErrAssignmentFailed = errors.New("assignment failed")
|
||||
)
|
||||
15
internal/permissions/options.go
Normal file
15
internal/permissions/options.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Option func(*Client) error
|
||||
|
||||
func WithLogger(logger *zap.SugaredLogger) Option {
|
||||
return func(c *Client) error {
|
||||
c.logger = logger
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
82
internal/permissions/roles.go
Normal file
82
internal/permissions/roles.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.infratographer.com/x/gidx"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type ResourceRoleCreate struct {
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
||||
type ResourceRoleCreateResponse struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type ResourceRoles []ResourceRole
|
||||
|
||||
type ResourceRole struct {
|
||||
ID gidx.PrefixedID `json:"id"`
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
||||
func (c *Client) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) {
|
||||
path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String())
|
||||
|
||||
body, err := encodeJSON(ResourceRoleCreate{
|
||||
Actions: actions,
|
||||
})
|
||||
if err != nil {
|
||||
return gidx.NullPrefixedID, err
|
||||
}
|
||||
|
||||
var response ResourceRoleCreateResponse
|
||||
|
||||
if _, err = c.DoRequest(ctx, http.MethodPost, path, body, &response); err != nil {
|
||||
return gidx.NullPrefixedID, err
|
||||
}
|
||||
|
||||
roleID, err := gidx.Parse(response.ID)
|
||||
if err != nil {
|
||||
return gidx.NullPrefixedID, err
|
||||
}
|
||||
|
||||
return roleID, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) {
|
||||
path := fmt.Sprintf("/api/v1/resources/%s/roles", resourceID.String())
|
||||
|
||||
var response ResourceRoles
|
||||
|
||||
if _, err := c.DoRequest(ctx, http.MethodGet, path, nil, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) {
|
||||
roles, err := c.ListResourceRoles(ctx, resourceID)
|
||||
if err != nil {
|
||||
return ResourceRole{}, err
|
||||
}
|
||||
|
||||
slices.Sort(actions)
|
||||
|
||||
for _, role := range roles {
|
||||
roleActions := role.Actions
|
||||
|
||||
slices.Sort(roleActions)
|
||||
|
||||
if slices.Equal(actions, roleActions) {
|
||||
return role, nil
|
||||
}
|
||||
}
|
||||
|
||||
return ResourceRole{}, ErrRoleNotFound
|
||||
}
|
||||
Reference in New Issue
Block a user