Skip to content

Commit

Permalink
rbac: implement rbac client
Browse files Browse the repository at this point in the history
because the insights rbac client use other Filter Data type, we need to reimplement the rbac client for inventory needs.

FIXES: https://issues.redhat.com/browse/THEEDGE-3624
FIXES: https://issues.redhat.com/browse/THEEDGE-3734
  • Loading branch information
ldjebran authored Nov 20, 2023
1 parent 083c0eb commit 775bf58
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 215 deletions.
4 changes: 0 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module github.com/redhatinsights/edge-api

require (
github.com/RedHatInsights/rbac-client-go v1.0.0
github.com/Unleash/unleash-client-go/v3 v3.9.0
github.com/aws/aws-sdk-go v1.47.13
github.com/bxcodec/faker/v3 v3.8.1
Expand Down Expand Up @@ -32,7 +31,6 @@ require (
)

require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand Down Expand Up @@ -80,7 +78,6 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/swag v1.16.2 // indirect
github.com/twmb/murmur3 v1.1.5 // indirect
go.mongodb.org/mongo-driver v1.11.3 // indirect
go.uber.org/atomic v1.9.0 // indirect
Expand All @@ -90,7 +87,6 @@ require (
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
Expand Down
7 changes: 0 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -719,14 +719,10 @@ git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3p
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/RedHatInsights/rbac-client-go v1.0.0 h1:vA6y4V/vj4T00H0+V6LxT8bKnYNjoVYiNpKAKURlUkE=
github.com/RedHatInsights/rbac-client-go v1.0.0/go.mod h1:+7A7JULqhAnpSnWYXM4WsYol3tEoCR8AVeob0Qby3Zc=
github.com/Unleash/unleash-client-go/v3 v3.9.0 h1:Lr3GKDBUmyu3PpECu9aA0+1H0pBeg0THKCykaPBHsbM=
github.com/Unleash/unleash-client-go/v3 v3.9.0/go.mod h1:jAf7F2WWpfJbfn1n8bZ74p7hkAhijrqH4TpWoT7kWLc=
github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA=
Expand Down Expand Up @@ -1236,8 +1232,6 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk=
Expand Down Expand Up @@ -1681,7 +1675,6 @@ golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
56 changes: 56 additions & 0 deletions pkg/clients/rbac/access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package rbac

import "strings"

const permissionDelimiter = ":"

// AccessList is a slice of Accesses and is generally used to represent a principal's
// full set of permissions for an application
type AccessList []Access

// Access represents a permission and an optional resource definition
type Access struct {
ResourceDefinitions []ResourceDefinition `json:"resourceDefinitions,omitempty"`
Permission string `json:"permission"`
}

// ResourceDefinition limits an Access to specific resources
type ResourceDefinition struct {
Filter ResourceDefinitionFilter `json:"attributeFilter"`
}

// ResourceDefinitionFilter represents the key/values used for filtering
type ResourceDefinitionFilter struct {
Key string `json:"key"`
Operation string `json:"operation"`
Value []*string `json:"value"`
}

// Application returns the name of the application in the permission
func (a Access) Application() string {
return permIndex(a.Permission, 0)
}

// Resource returns the name of the resource in the permission
func (a Access) Resource() string {
return permIndex(a.Permission, 1)
}

// AccessType returns the access type in the permission
func (a Access) AccessType() string {
return permIndex(a.Permission, 2)
}

// permIndex return the permission item value at index when splitting by permission delimiter
// the permission looks like "inventory:hosts:read" where:
// inventory is the application name and locate at index 0
// hosts is the resource and located at index 1
// read is the access type and located at index 2
func permIndex(permission string, index int) string {
permissionItems := strings.Split(permission, permissionDelimiter)
// a correct permission must have 3 items application:resource:access-type
if len(permissionItems) == 3 {
return permissionItems[index]
}
return ""
}
129 changes: 86 additions & 43 deletions pkg/clients/rbac/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,32 @@ import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
url2 "net/url"
"time"

"github.com/redhatinsights/edge-api/config"
"github.com/redhatinsights/edge-api/pkg/clients"
"github.com/redhatinsights/edge-api/pkg/routes/common"

rbacClient "github.com/RedHatInsights/rbac-client-go"

"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)

var ErrCreatingRbacURL = errors.New("error occurred when creating rbac url")
var ErrGettingIdentityFromContext = errors.New("error getting x-rh-identity from context")
var ErrInvalidAttributeFilterKey = errors.New("invalid value for attributeFilter.key in RBAC response")
var ErrInvalidAttributeFilterOperation = errors.New("invalid value for attributeFilter.operation in RBAC response")
var ErrInvalidAttributeFilterValueType = errors.New("did not receive a list for attributeFilter.value in RBAC response")
var ErrInvalidAttributeFilterValue = errors.New("received invalid UUIDs for attributeFilter.value in RBAC response")
var ErrFailedToBuildAccessRequest = errors.New("failed to build access request")
var ErrRbacRequestResponse = errors.New("rbac response error")

// IOReadAll The io body reader
var IOReadAll = io.ReadAll

// HTTPGetCommand the http get command
var HTTPGetCommand = http.MethodGet

// APIPath the rbac base path
const APIPath = "/api/rbac/v1"

type ResourceType string
Expand All @@ -44,15 +50,35 @@ const (

const DefaultTimeDuration = 1 * time.Second

// ClientInterface is an Interface to make request to insights rbac
type ClientInterface interface {
GetAccessList(application Application) (rbacClient.AccessList, error)
GetInventoryGroupsAccess(acl rbacClient.AccessList, resource ResourceType, accessType AccessType) (bool, []string, bool, error)
// PaginationLimit to get a maximum of 1000 records
const PaginationLimit = "1000"

// ResponseBody represents the response body format from the RBAC service
type ResponseBody struct {
Meta PaginationMeta `json:"meta"`
Links PaginationLinks `json:"links"`
Data AccessList `json:"data"`
}

// PaginationMeta contains metadata for pagination
type PaginationMeta struct {
Count int `json:"count"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}

// WrappedClientInterface is an interface of the original rbac client
type WrappedClientInterface interface {
GetAccess(ctx context.Context, identity string, username string) (rbacClient.AccessList, error)
// PaginationLinks provides links to additional pages of response data
type PaginationLinks struct {
First string `json:"first"`
Next string `json:"next"`
Previous string `json:"previous"`
Last string `json:"last"`
}

// ClientInterface is an Interface to make request to insights rbac
type ClientInterface interface {
GetAccessList(application Application) (AccessList, error)
GetInventoryGroupsAccess(acl AccessList, resource ResourceType, accessType AccessType) (bool, []string, bool, error)
}

// Client is the implementation of an ClientInterface
Expand All @@ -66,52 +92,73 @@ func InitClient(ctx context.Context, log *log.Entry) ClientInterface {
return &Client{ctx: ctx, log: log.WithField("client-context", "rbac-client")}
}

// NewRbacClient create a new rbac client
func (c *Client) NewRbacClient(application Application) (WrappedClientInterface, error) {
url, err := url2.JoinPath(config.Get().RbacBaseURL, APIPath)
func (c *Client) GetRBacAccessHTTPRequest(ctx context.Context, application Application) (*http.Request, error) {
url, err := url2.JoinPath(config.Get().RbacBaseURL, APIPath, "access/")
if err != nil {
c.log.WithField("error", err.Error()).Error(ErrCreatingRbacURL.Error())
return nil, ErrCreatingRbacURL
}

wrappedClient := rbacClient.NewClient(url, string(application))
wrappedClient.HTTPClient = clients.ConfigureClientWithTLS(wrappedClient.HTTPClient)
return &wrappedClient, nil
req, err := http.NewRequestWithContext(ctx, HTTPGetCommand, url, nil)
if err != nil {
return nil, ErrFailedToBuildAccessRequest
}
q := req.URL.Query()
q.Add("application", string(application))
q.Add("limit", PaginationLimit)
req.URL.RawQuery = q.Encode()
req.Header.Add("Content-Type", "application/json")
headers := clients.GetOutgoingHeaders(c.ctx)
for key, value := range headers {
req.Header.Add(key, value)
}
return req, nil
}

// GetAccessList return the application rbac access list
func (c *Client) GetAccessList(application Application) (rbacClient.AccessList, error) {
conf := config.Get()
rbacTimeout := time.Duration(conf.RbacTimeout) * DefaultTimeDuration

func (c *Client) GetAccessList(application Application) (AccessList, error) {
rbacTimeout := time.Duration(config.Get().RbacTimeout) * DefaultTimeDuration
ctx, cancel := context.WithTimeout(c.ctx, rbacTimeout)
defer cancel()

wrappedClient, err := c.NewRbacClient(application)
req, err := c.GetRBacAccessHTTPRequest(ctx, application)
if err != nil {
c.log.WithField("error", err.Error()).Error("error occurred when creating rbac client")
c.log.WithField("error", err.Error()).Error("error occurred while creating rbac access request")
return nil, err
}

var identity string
if config.Get().Auth {
identity, err = common.GetOriginalIdentity(ctx)
if err != nil {
c.log.WithField("error", err.Error()).Error("error getting identity from context")
return nil, ErrGettingIdentityFromContext
}
client := clients.ConfigureClientWithTLS(&http.Client{})
res, err := client.Do(req)
if err != nil {
c.log.WithField("error", err.Error()).Error("rbac request failed")
return nil, err
}
defer res.Body.Close()

acl, err := wrappedClient.GetAccess(ctx, identity, "")
body, err := IOReadAll(res.Body)
if err != nil {
c.log.WithField("error", err.Error()).Error("error occurred getting rbac AccessList")
c.log.WithFields(log.Fields{"statusCode": res.StatusCode, "error": err.Error()}).Error("rbac read response body error")
return nil, err
}
return acl, nil
if res.StatusCode != http.StatusOK {
c.log.WithFields(
log.Fields{"statusCode": res.StatusCode, "responseBody": string(body)},
).Error("rbac request error response")
return nil, ErrRbacRequestResponse
}

var responseAccess ResponseBody
err = json.Unmarshal(body, &responseAccess)
if err != nil {
c.log.WithFields(log.Fields{"responseBody": string(body), "error": err.Error()}).Error("error occurred when unmarshalling response body to repository")
return nil, err
}

return responseAccess.Data, nil
}

// getAssessGroupsFromResourceDefinition validate and return the access groups
func (c *Client) getAssessGroupsFromResourceDefinition(resourceDefinition rbacClient.ResourceDefinition) ([]*string, error) {
func (c *Client) getAssessGroupsFromResourceDefinition(resourceDefinition ResourceDefinition) ([]*string, error) {
if resourceDefinition.Filter.Key != "group.id" {
c.log.WithField("filter-key", resourceDefinition.Filter.Key).Error("received an unexpected resource filter key value")
return nil, ErrInvalidAttributeFilterKey
Expand All @@ -120,12 +167,8 @@ func (c *Client) getAssessGroupsFromResourceDefinition(resourceDefinition rbacCl
c.log.WithField("filter-operation", resourceDefinition.Filter.Key).Error("received an unexpected resource filter operation value")
return nil, ErrInvalidAttributeFilterOperation
}
var accessGroups []*string
if err := json.Unmarshal([]byte(resourceDefinition.Filter.Value), &accessGroups); err != nil {
c.log.WithField("filter-value", resourceDefinition.Filter.Value).Error("received an unexpected resource filter value type")
return nil, ErrInvalidAttributeFilterValueType
}
return accessGroups, nil

return resourceDefinition.Filter.Value, nil
}

// getGroupsFromAccessGroups validate access groups and return groups and whether to ungrouped hosts should be included
Expand All @@ -147,14 +190,14 @@ func (c *Client) getGroupsFromAccessGroups(accessGroups []*string) ([]string, bo
}

// GetInventoryGroupsAccess return whether access is allowed and the groups configurations
func (c *Client) GetInventoryGroupsAccess(acl rbacClient.AccessList, resource ResourceType, accessType AccessType) (bool, []string, bool, error) {
func (c *Client) GetInventoryGroupsAccess(acl AccessList, resource ResourceType, accessType AccessType) (bool, []string, bool, error) {
var overallGroupIDS []string
var overallGroupIDSMap = make(map[string]bool)
var allowedAccess bool
var globalUnGroupedHosts bool
for _, ac := range acl {
// check if the resource with accessType has access to the current access item
if ac.Application() == string(ApplicationInventory) && ResourceMatch(ResourceType(ac.Resource()), resource) && AccessMatch(AccessType(ac.Verb()), accessType) {
if ac.Application() == string(ApplicationInventory) && ResourceMatch(ResourceType(ac.Resource()), resource) && AccessMatch(AccessType(ac.AccessType()), accessType) {
allowedAccess = true
for _, resourceDef := range ac.ResourceDefinitions {
// validate if the resource definition is correct and get all access groups from the resource definition value
Expand Down
Loading

0 comments on commit 775bf58

Please sign in to comment.