Skip to content

Commit

Permalink
rbac: implement inventory rbac access permissions.
Browse files Browse the repository at this point in the history
- Fixed deviceview api add inventory group fields
- Added Configuration for rbac base url and timeout
- Added feauture flag edgeParity.inventory-rba.
- Added github.com/RedHatInsights/rbac-client-go dependency.
- Created rbac client and unit-tests.
- Updated deviceview GET/POST to handle rbac access checking and covered by unittests.

FIXES: https://issues.redhat.com/browse/THEEDGE-3624
  • Loading branch information
ldjebran authored Nov 15, 2023
1 parent d9aad12 commit 88ab52f
Show file tree
Hide file tree
Showing 15 changed files with 1,214 additions and 4 deletions.
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type EdgeConfig struct {
RepoFileUploadDelay uint `json:"repo_file_upload_delay"`
DeleteFilesAttempts uint `json:"delete_files_attempts"`
DeleteFilesRetryDelay uint `json:"delete_files_retry_delay"`
RbacBaseURL string `json:"rbac_base_url"`
RbacTimeout uint `mapstructure:"rbac_timeout,omitempty"`
}

type dbConfig struct {
Expand Down Expand Up @@ -164,6 +166,8 @@ func CreateEdgeAPIConfig() (*EdgeConfig, error) {
options.SetDefault("RepoFileUploadDelay", 1)
options.SetDefault("DeleteFilesAttempts", 10)
options.SetDefault("DeleteFilesRetryDelay", 5)
options.SetDefault("RBAC_BASE_URL", "http://rbac-service:8080")
options.SetDefault("RbacTimeout", 30)
options.AutomaticEnv()

if options.GetBool("Debug") {
Expand Down Expand Up @@ -268,6 +272,8 @@ func CreateEdgeAPIConfig() (*EdgeConfig, error) {
RepoFileUploadAttempts: options.GetUint("RepoFileUploadAttempts"),
DeleteFilesAttempts: options.GetUint("DeleteFilesAttempts"),
DeleteFilesRetryDelay: options.GetUint("DeleteFilesRetryDelay"),
RbacBaseURL: options.GetString("RBAC_BASE_URL"),
RbacTimeout: options.GetUint("RbacTimeout"),
}
if edgeConfig.TenantTranslatorHost != "" && edgeConfig.TenantTranslatorPort != "" {
edgeConfig.TenantTranslatorURL = fmt.Sprintf("http://%s:%s", edgeConfig.TenantTranslatorHost, edgeConfig.TenantTranslatorPort)
Expand Down
14 changes: 14 additions & 0 deletions deploy/clowdapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ objects:
value: ${TENANT_TRANSLATOR_HOST}
- name: TENANT_TRANSLATOR_PORT
value: ${TENANT_TRANSLATOR_PORT}
- name : RBAC_BASE_URL
value: ${RBAC_BASE_URL}
resources:
limits:
cpu: ${{CPU_LIMIT}}
Expand Down Expand Up @@ -198,6 +200,8 @@ objects:
value: ${TENANT_TRANSLATOR_HOST}
- name: TENANT_TRANSLATOR_PORT
value: ${TENANT_TRANSLATOR_PORT}
- name : RBAC_BASE_URL
value: ${RBAC_BASE_URL}
resources:
limits:
cpu: 250m
Expand Down Expand Up @@ -263,6 +267,8 @@ objects:
value: ${TENANT_TRANSLATOR_HOST}
- name: TENANT_TRANSLATOR_PORT
value: ${TENANT_TRANSLATOR_PORT}
- name : RBAC_BASE_URL
value: ${RBAC_BASE_URL}
resources:
limits:
cpu: 250m
Expand Down Expand Up @@ -328,6 +334,8 @@ objects:
value: ${TENANT_TRANSLATOR_HOST}
- name: TENANT_TRANSLATOR_PORT
value: ${TENANT_TRANSLATOR_PORT}
- name : RBAC_BASE_URL
value: ${RBAC_BASE_URL}
resources:
limits:
cpu: 250m
Expand Down Expand Up @@ -397,6 +405,8 @@ objects:
value: ${TENANT_TRANSLATOR_HOST}
- name: TENANT_TRANSLATOR_PORT
value: ${TENANT_TRANSLATOR_PORT}
- name : RBAC_BASE_URL
value: ${RBAC_BASE_URL}
resources:
limits:
cpu: 900m
Expand Down Expand Up @@ -542,3 +552,7 @@ parameters:
name: CLEANUP_SUSPEND
required: false
value: "false"
- description: RBAC service base URL
name: RBAC_BASE_URL
required: false
value: "http://rbac-service:8080"
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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.11
github.com/bxcodec/faker/v3 v3.8.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
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
197 changes: 197 additions & 0 deletions pkg/clients/rbac/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package rbac

import (
"context"
"encoding/json"
"errors"
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")

const APIPath = "/api/rbac/v1"

type ResourceType string
type AccessType string
type Application string

const (
AccessTypeAny AccessType = "*"
AccessTypeRead AccessType = "read"
)

const ApplicationInventory Application = "inventory"

const (
ResourceTypeAny ResourceType = "*"
ResourceTypeHOSTS ResourceType = "hosts"
)

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)
}

// WrappedClientInterface is an interface of the original rbac client
type WrappedClientInterface interface {
GetAccess(ctx context.Context, identity string, username string) (rbacClient.AccessList, error)
}

// Client is the implementation of an ClientInterface
type Client struct {
ctx context.Context
log *log.Entry
}

// InitClient initializes the client for Rbac service
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)
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
}

// 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

ctx, cancel := context.WithTimeout(c.ctx, rbacTimeout)
defer cancel()

wrappedClient, err := c.NewRbacClient(application)
if err != nil {
c.log.WithField("error", err.Error()).Error("error occurred when creating rbac client")
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
}
}

acl, err := wrappedClient.GetAccess(ctx, identity, "")
if err != nil {
c.log.WithField("error", err.Error()).Error("error occurred getting rbac AccessList")
return nil, err
}
return acl, nil
}

// getAssessGroupsFromResourceDefinition validate and return the access groups
func (c *Client) getAssessGroupsFromResourceDefinition(resourceDefinition rbacClient.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
}
if resourceDefinition.Filter.Operation != "in" {
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
}

// getGroupsFromAccessGroups validate access groups and return groups and whether to ungrouped hosts should be included
func (c *Client) getGroupsFromAccessGroups(accessGroups []*string) ([]string, bool, error) {
var unGroupedHosts bool
var groups []string
for _, groupUUID := range accessGroups {
if groupUUID == nil {
unGroupedHosts = true
} else {
if _, err := uuid.Parse(*groupUUID); err != nil {
c.log.WithField("filter-uuid", *groupUUID).Error("error occurred while parsing uuid value")
return nil, false, ErrInvalidAttributeFilterValue
}
groups = append(groups, *groupUUID)
}
}
return groups, unGroupedHosts, nil
}

// 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) {
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) {
allowedAccess = true
for _, resourceDef := range ac.ResourceDefinitions {
// validate if the resource definition is correct and get all access groups from the resource definition value
accessGroups, err := c.getAssessGroupsFromResourceDefinition(resourceDef)
if err != nil {
return false, nil, false, err
}
// validate if all access groups are valid, as access groups is a list of groups uuids with pointers to string []*string
// this function call will return static groups list []string and if any null value is in the list, this means that ungrouped Hosts
// are needed and unGroupedHosts will be set to true
groups, unGroupedHosts, err := c.getGroupsFromAccessGroups(accessGroups)
if err != nil {
return false, nil, false, err
}
if unGroupedHosts {
globalUnGroupedHosts = true
}
for _, groupUUID := range groups {
// add the group to global groups list when it's not in the global map, to make sure there is no duplicates
if _, ok := overallGroupIDSMap[groupUUID]; !ok {
// put it in the map for later duplicate check
overallGroupIDSMap[groupUUID] = true
overallGroupIDS = append(overallGroupIDS, groupUUID)
}
}
}
}
}
return allowedAccess, overallGroupIDS, globalUnGroupedHosts, nil
}

// AccessMatch return whether the access type matches the required resource type
func AccessMatch(access1, access2 AccessType) bool {
return access1 == access2 || access1 == AccessTypeAny
}

// ResourceMatch return whether the resource type matches the required resource type
func ResourceMatch(resource1, resource2 ResourceType) bool {
return resource1 == resource2 || resource1 == ResourceTypeAny
}
13 changes: 13 additions & 0 deletions pkg/clients/rbac/client_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package rbac_test

import (
"testing"

. "github.com/onsi/ginkgo" // nolint: revive
. "github.com/onsi/gomega" // nolint: revive
)

func TestRbacClient(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Rbac Client Suite")
}
Loading

0 comments on commit 88ab52f

Please sign in to comment.