diff --git a/central/auth/m2m/claims.go b/central/auth/m2m/claims.go index 93fd3c8ccad26..899dbeb1b9c72 100644 --- a/central/auth/m2m/claims.go +++ b/central/auth/m2m/claims.go @@ -3,7 +3,6 @@ package m2m import ( "fmt" - "github.com/coreos/go-oidc/v3/oidc" "github.com/pkg/errors" "github.com/stackrox/rox/generated/storage" "github.com/stackrox/rox/pkg/auth/tokens" @@ -17,11 +16,14 @@ var ( ) type claimExtractor interface { - ExtractRoxClaims(idToken *oidc.IDToken) (tokens.RoxClaims, error) + ExtractRoxClaims(idToken *IDToken) (tokens.RoxClaims, error) } func newClaimExtractorFromConfig(config *storage.AuthMachineToMachineConfig) claimExtractor { - if config.GetType() == storage.AuthMachineToMachineConfig_GENERIC { + switch config.GetType() { + case storage.AuthMachineToMachineConfig_KUBE_TOKEN_REVIEW: + fallthrough + case storage.AuthMachineToMachineConfig_GENERIC: return &genericClaimExtractor{configID: config.GetId()} } @@ -32,7 +34,7 @@ type genericClaimExtractor struct { configID string } -func (g *genericClaimExtractor) ExtractRoxClaims(idToken *oidc.IDToken) (tokens.RoxClaims, error) { +func (g *genericClaimExtractor) ExtractRoxClaims(idToken *IDToken) (tokens.RoxClaims, error) { var unstructured map[string]interface{} if err := idToken.Claims(&unstructured); err != nil { return tokens.RoxClaims{}, errors.Wrap(err, "extracting claims") @@ -109,7 +111,7 @@ type githubActionClaims struct { WorkflowSHA string `json:"workflow_sha"` } -func (g *githubClaimExtractor) ExtractRoxClaims(idToken *oidc.IDToken) (tokens.RoxClaims, error) { +func (g *githubClaimExtractor) ExtractRoxClaims(idToken *IDToken) (tokens.RoxClaims, error) { // OIDC tokens issued for GitHub Actions have special claims, we'll reuse them. var claims githubActionClaims if err := idToken.Claims(&claims); err != nil { diff --git a/central/auth/m2m/exchanger.go b/central/auth/m2m/exchanger.go index a8e79b1e60460..422a1b3d89626 100644 --- a/central/auth/m2m/exchanger.go +++ b/central/auth/m2m/exchanger.go @@ -133,6 +133,12 @@ func mapToStringClaims(claims map[string]interface{}) map[string][]string { stringClaims[key] = []string{value} case []string: stringClaims[key] = value + case []any: + for _, v := range value { + if s, ok := v.(string); ok { + stringClaims[key] = append(stringClaims[key], s) + } + } default: log.Debugf("Dropping value %v for claim %s since its a nested claim or a non-string type %T", value, key, value) } diff --git a/central/auth/m2m/exchanger_test.go b/central/auth/m2m/exchanger_test.go index 618be8cf9feb0..7633f891f36b0 100644 --- a/central/auth/m2m/exchanger_test.go +++ b/central/auth/m2m/exchanger_test.go @@ -22,10 +22,12 @@ func TestMapStringToClaims(t *testing.T) { []string{"four", "five", "six"}, }, }, + "groups": []any{"group1", "group2"}, } expectedResult := map[string][]string{ - "sub": {"my-subject"}, - "aud": {"audience-1", "audience-2", "audience-3"}, + "sub": {"my-subject"}, + "aud": {"audience-1", "audience-2", "audience-3"}, + "groups": {"group1", "group2"}, } result := mapToStringClaims(claims) diff --git a/central/auth/m2m/id_token.go b/central/auth/m2m/id_token.go index b5851c50b8935..ecf420ec35f1c 100644 --- a/central/auth/m2m/id_token.go +++ b/central/auth/m2m/id_token.go @@ -1,16 +1,26 @@ package m2m import ( + "strings" + "github.com/golang-jwt/jwt/v4" "github.com/pkg/errors" "github.com/stackrox/rox/pkg/errox" ) -// IssuerFromRawIDToken retrieves the issuer from a raw ID token. +const KubernetesTokenIssuer = "https://kubernetes" + +// IssuerFromRawIDToken retrieves the issuer from a raw ID token. If the token +// is not a JWT, assume Kubernetes opaque token. // In case the token is malformed (i.e. jwt.ErrTokenMalformed is met), it will return an error. // Other errors such as an expired token will be ignored. // Note: This does **not** verify the token's signature or any other claim value. func IssuerFromRawIDToken(rawIDToken string) (string, error) { + if strings.HasPrefix(rawIDToken, "sha256~") { + // Not a JWT. Assume Kubernetes opaque token. + return KubernetesTokenIssuer, nil + } + standardClaims := &jwt.RegisteredClaims{} // Explicitly ignore the signature of the ID token for now. // This will be handled in a latter part, when the metadata from the provider will be used to verify the signature. diff --git a/central/auth/m2m/kube_token_review.go b/central/auth/m2m/kube_token_review.go new file mode 100644 index 0000000000000..88b58511914d9 --- /dev/null +++ b/central/auth/m2m/kube_token_review.go @@ -0,0 +1,48 @@ +package m2m + +import ( + "context" + "encoding/json" + + "github.com/pkg/errors" + v1 "k8s.io/api/authentication/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// kubeTokenReviewVerifier verifies tokens using the Kubernetes TokenReview API. +type kubeTokenReviewVerifier struct { + clientset kubernetes.Interface +} + +func (k *kubeTokenReviewVerifier) VerifyIDToken(ctx context.Context, rawIDToken string) (*IDToken, error) { + tr := &v1.TokenReview{ + Spec: v1.TokenReviewSpec{ + Token: rawIDToken, + }, + } + trResp, err := k.clientset.AuthenticationV1().TokenReviews(). + Create(ctx, tr, metaV1.CreateOptions{}) + if err != nil { + return nil, errors.Wrap(err, "performing TokenReview request") + } + if !trResp.Status.Authenticated { + return nil, errors.Errorf("token not authenticated: %s", trResp.Status.Error) + } + + claims := map[string]any{ + "sub": trResp.Status.User.UID, + "name": trResp.Status.User.Username, + "groups": trResp.Status.User.Groups, + } + rawClaims, _ := json.Marshal(claims) + + token := &IDToken{ + Subject: trResp.Status.User.UID, + Claims: func(v any) error { + return json.Unmarshal(rawClaims, v) + }, + Audience: trResp.Status.Audiences, + } + return token, nil +} diff --git a/central/auth/m2m/role_mapper_test.go b/central/auth/m2m/role_mapper_test.go index eb6a8e5ede8b1..eece55903c4b3 100644 --- a/central/auth/m2m/role_mapper_test.go +++ b/central/auth/m2m/role_mapper_test.go @@ -27,6 +27,7 @@ func TestResolveRolesForClaims(t *testing.T) { "aud": {"something", "somewhere"}, "repository": {"github.com/sample-org/sample-repo:main:062348SHA"}, "iss": {"https://stackrox.io"}, + "groups": {"system:admins"}, } config := &storage.AuthMachineToMachineConfig{ Mappings: []*storage.AuthMachineToMachineConfig_Mapping{ @@ -59,6 +60,10 @@ func TestResolveRolesForClaims(t *testing.T) { Key: "iss", ValueExpression: ".*", Role: authn.NoneRole, + }, { + Key: "groups", + ValueExpression: "system:admins", + Role: "Analyst", }, }, } @@ -79,3 +84,31 @@ func TestResolveRolesForClaims(t *testing.T) { assert.NoError(t, err) assert.ElementsMatch(t, resolvedRoles, []permissions.ResolvedRole{roles["Admin"], roles["Analyst"], roles["roxctl"]}) } + +func TestResolveRolesForClaimsList(t *testing.T) { + claims := map[string][]string{ + "groups": {"system:admins"}, + } + config := &storage.AuthMachineToMachineConfig{ + Mappings: []*storage.AuthMachineToMachineConfig_Mapping{ + { + Key: "groups", + ValueExpression: "system:admins", + Role: "Analyst", + }, + }, + } + roles := map[string]permissions.ResolvedRole{ + "Analyst": &testResolvedRole{name: "Analyst"}, + } + + roleDSMock := mocks.NewMockDataStore(gomock.NewController(t)) + + for roleName, resolvedRole := range roles { + roleDSMock.EXPECT().GetAndResolveRole(gomock.Any(), roleName).Return(resolvedRole, nil) + } + + resolvedRoles, err := resolveRolesForClaims(context.Background(), claims, roleDSMock, config.GetMappings(), createRegexp(config)) + assert.NoError(t, err) + assert.ElementsMatch(t, resolvedRoles, []permissions.ResolvedRole{roles["Analyst"]}) +} diff --git a/central/auth/m2m/verifier.go b/central/auth/m2m/verifier.go index fb89f3b873c71..10a0acaf69cdb 100644 --- a/central/auth/m2m/verifier.go +++ b/central/auth/m2m/verifier.go @@ -13,7 +13,9 @@ import ( "github.com/pkg/errors" "github.com/stackrox/rox/generated/storage" "github.com/stackrox/rox/pkg/httputil/proxy" + "github.com/stackrox/rox/pkg/k8sutil" "github.com/stackrox/rox/pkg/logging" + "k8s.io/client-go/kubernetes" ) const ( @@ -30,8 +32,14 @@ var ( _ tokenVerifier = (*genericTokenVerifier)(nil) ) +type IDToken struct { + Claims func(any) error + Subject string + Audience []string +} + type tokenVerifier interface { - VerifyIDToken(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) + VerifyIDToken(ctx context.Context, rawIDToken string) (*IDToken, error) } type authenticatedRoundTripper struct { @@ -68,7 +76,8 @@ func tokenVerifierFromConfig(ctx context.Context, config *storage.AuthMachineToM } roundTripper := proxy.RoundTripper(proxy.WithTLSConfig(tlsConfig)) - if config.Type == storage.AuthMachineToMachineConfig_KUBE_SERVICE_ACCOUNT { + switch config.Type { + case storage.AuthMachineToMachineConfig_KUBE_SERVICE_ACCOUNT: token, err := kubeServiceAccountTokenReader{}.readToken() if err != nil { return nil, errors.Wrap(err, "Failed to read kube service account token") @@ -77,6 +86,17 @@ func tokenVerifierFromConfig(ctx context.Context, config *storage.AuthMachineToM // By default k8s requires authentication to fetch the OIDC resources for service account tokens // https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-issuer-discovery roundTripper = authenticatedRoundTripper{roundTripper: roundTripper, token: token} + + case storage.AuthMachineToMachineConfig_KUBE_TOKEN_REVIEW: + cfg, err := k8sutil.GetK8sInClusterConfig() + if err != nil { + return nil, err + } + c, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + return &kubeTokenReviewVerifier{c}, nil } provider, err := oidc.NewProvider( @@ -94,7 +114,7 @@ type genericTokenVerifier struct { provider *oidc.Provider } -func (g *genericTokenVerifier) VerifyIDToken(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) { +func (g *genericTokenVerifier) VerifyIDToken(ctx context.Context, rawIDToken string) (*IDToken, error) { verifier := g.provider.Verifier(&oidc.Config{ // We currently provide no config to expose the client ID that's associated with the ID token. // The reason for this is the following: @@ -106,7 +126,15 @@ func (g *genericTokenVerifier) VerifyIDToken(ctx context.Context, rawIDToken str SkipClientIDCheck: true, }) - return verifier.Verify(ctx, rawIDToken) + token, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, err + } + return &IDToken{ + Subject: token.Subject, + Audience: token.Audience, + Claims: token.Claims, + }, err } func tlsConfigWithCustomCertPool() (*tls.Config, error) { diff --git a/generated/api/v1/auth_service.pb.go b/generated/api/v1/auth_service.pb.go index 2a2d7e3ed081a..ddd122a01e0c9 100644 --- a/generated/api/v1/auth_service.pb.go +++ b/generated/api/v1/auth_service.pb.go @@ -33,6 +33,7 @@ const ( AuthMachineToMachineConfig_GENERIC AuthMachineToMachineConfig_Type = 0 AuthMachineToMachineConfig_GITHUB_ACTIONS AuthMachineToMachineConfig_Type = 1 AuthMachineToMachineConfig_KUBE_SERVICE_ACCOUNT AuthMachineToMachineConfig_Type = 2 + AuthMachineToMachineConfig_KUBE_TOKEN_REVIEW AuthMachineToMachineConfig_Type = 3 ) // Enum value maps for AuthMachineToMachineConfig_Type. @@ -41,11 +42,13 @@ var ( 0: "GENERIC", 1: "GITHUB_ACTIONS", 2: "KUBE_SERVICE_ACCOUNT", + 3: "KUBE_TOKEN_REVIEW", } AuthMachineToMachineConfig_Type_value = map[string]int32{ "GENERIC": 0, "GITHUB_ACTIONS": 1, "KUBE_SERVICE_ACCOUNT": 2, + "KUBE_TOKEN_REVIEW": 3, } ) @@ -752,7 +755,7 @@ const file_api_v1_auth_service_proto_rawDesc = "" + "\tuser_info\x18\x06 \x01(\v2\x11.storage.UserInfoR\buserInfo\x12:\n" + "\x0fuser_attributes\x18\a \x03(\v2\x11.v1.UserAttributeR\x0euserAttributes\x12\x1b\n" + "\tidp_token\x18\b \x01(\tR\bidpTokenB\x04\n" + - "\x02id\"\x9c\x03\n" + + "\x02id\"\xb3\x03\n" + "\x1aAuthMachineToMachineConfig\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x127\n" + "\x04type\x18\x02 \x01(\x0e2#.v1.AuthMachineToMachineConfig.TypeR\x04type\x12:\n" + @@ -762,11 +765,12 @@ const file_api_v1_auth_service_proto_rawDesc = "" + "\aMapping\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12)\n" + "\x10value_expression\x18\x02 \x01(\tR\x0fvalueExpression\x12\x12\n" + - "\x04role\x18\x03 \x01(\tR\x04role\"A\n" + + "\x04role\x18\x03 \x01(\tR\x04role\"X\n" + "\x04Type\x12\v\n" + "\aGENERIC\x10\x00\x12\x12\n" + "\x0eGITHUB_ACTIONS\x10\x01\x12\x18\n" + - "\x14KUBE_SERVICE_ACCOUNT\x10\x02\"b\n" + + "\x14KUBE_SERVICE_ACCOUNT\x10\x02\x12\x15\n" + + "\x11KUBE_TOKEN_REVIEW\x10\x03\"b\n" + "&ListAuthMachineToMachineConfigResponse\x128\n" + "\aconfigs\x18\x01 \x03(\v2\x1e.v1.AuthMachineToMachineConfigR\aconfigs\"_\n" + "%GetAuthMachineToMachineConfigResponse\x126\n" + diff --git a/generated/api/v1/auth_service.swagger.json b/generated/api/v1/auth_service.swagger.json index 005774e34fa54..f555c5fe2fa43 100644 --- a/generated/api/v1/auth_service.swagger.json +++ b/generated/api/v1/auth_service.swagger.json @@ -583,7 +583,8 @@ "enum": [ "GENERIC", "GITHUB_ACTIONS", - "KUBE_SERVICE_ACCOUNT" + "KUBE_SERVICE_ACCOUNT", + "KUBE_TOKEN_REVIEW" ], "default": "GENERIC", "description": "The type of the auth machine to machine config.\nCurrently supports GitHub actions or any other generic OIDC provider to use for verifying and\nexchanging the token." diff --git a/generated/storage/auth_machine_to_machine.pb.go b/generated/storage/auth_machine_to_machine.pb.go index bcaf0227b6edb..cd6f5ae7a44d1 100644 --- a/generated/storage/auth_machine_to_machine.pb.go +++ b/generated/storage/auth_machine_to_machine.pb.go @@ -27,6 +27,7 @@ const ( AuthMachineToMachineConfig_GENERIC AuthMachineToMachineConfig_Type = 0 AuthMachineToMachineConfig_GITHUB_ACTIONS AuthMachineToMachineConfig_Type = 1 AuthMachineToMachineConfig_KUBE_SERVICE_ACCOUNT AuthMachineToMachineConfig_Type = 2 + AuthMachineToMachineConfig_KUBE_TOKEN_REVIEW AuthMachineToMachineConfig_Type = 3 ) // Enum value maps for AuthMachineToMachineConfig_Type. @@ -35,11 +36,13 @@ var ( 0: "GENERIC", 1: "GITHUB_ACTIONS", 2: "KUBE_SERVICE_ACCOUNT", + 3: "KUBE_TOKEN_REVIEW", } AuthMachineToMachineConfig_Type_value = map[string]int32{ "GENERIC": 0, "GITHUB_ACTIONS": 1, "KUBE_SERVICE_ACCOUNT": 2, + "KUBE_TOKEN_REVIEW": 3, } ) @@ -216,7 +219,7 @@ var File_storage_auth_machine_to_machine_proto protoreflect.FileDescriptor const file_storage_auth_machine_to_machine_proto_rawDesc = "" + "\n" + - "%storage/auth_machine_to_machine.proto\x12\astorage\"\xa6\x03\n" + + "%storage/auth_machine_to_machine.proto\x12\astorage\"\xbd\x03\n" + "\x1aAuthMachineToMachineConfig\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12<\n" + "\x04type\x18\x02 \x01(\x0e2(.storage.AuthMachineToMachineConfig.TypeR\x04type\x12:\n" + @@ -226,11 +229,12 @@ const file_storage_auth_machine_to_machine_proto_rawDesc = "" + "\aMapping\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12)\n" + "\x10value_expression\x18\x02 \x01(\tR\x0fvalueExpression\x12\x12\n" + - "\x04role\x18\x03 \x01(\tR\x04role\"A\n" + + "\x04role\x18\x03 \x01(\tR\x04role\"X\n" + "\x04Type\x12\v\n" + "\aGENERIC\x10\x00\x12\x12\n" + "\x0eGITHUB_ACTIONS\x10\x01\x12\x18\n" + - "\x14KUBE_SERVICE_ACCOUNT\x10\x02B.\n" + + "\x14KUBE_SERVICE_ACCOUNT\x10\x02\x12\x15\n" + + "\x11KUBE_TOKEN_REVIEW\x10\x03B.\n" + "\x19io.stackrox.proto.storageZ\x11./storage;storageb\x06proto3" var ( diff --git a/proto/api/v1/auth_service.proto b/proto/api/v1/auth_service.proto index 7faf9ba6b8eea..1655682ddc305 100644 --- a/proto/api/v1/auth_service.proto +++ b/proto/api/v1/auth_service.proto @@ -48,6 +48,7 @@ message AuthMachineToMachineConfig { GENERIC = 0; GITHUB_ACTIONS = 1; KUBE_SERVICE_ACCOUNT = 2; + KUBE_TOKEN_REVIEW = 3; } Type type = 2; diff --git a/proto/storage/auth_machine_to_machine.proto b/proto/storage/auth_machine_to_machine.proto index d4074efc06810..35106a9cf6209 100644 --- a/proto/storage/auth_machine_to_machine.proto +++ b/proto/storage/auth_machine_to_machine.proto @@ -16,6 +16,7 @@ message AuthMachineToMachineConfig { GENERIC = 0; GITHUB_ACTIONS = 1; KUBE_SERVICE_ACCOUNT = 2; + KUBE_TOKEN_REVIEW = 3; } Type type = 2; diff --git a/ui/apps/platform/plugin-cr.yaml b/ui/apps/platform/plugin-cr.yaml new file mode 100644 index 0000000000000..afdd1b79a0b8c --- /dev/null +++ b/ui/apps/platform/plugin-cr.yaml @@ -0,0 +1,21 @@ +apiVersion: console.openshift.io/v1 +kind: ConsolePlugin +metadata: + name: 'advanced-cluster-security-plugin' +spec: + displayName: 'Advanced Cluster Security plugin' + backend: + service: + name: central + namespace: stackrox + port: 443 + basePath: '/' + proxy: + - type: Service + alias: acs-api-service + endpoint: + type: Service + service: + name: central + namespace: stackrox + port: 443 diff --git a/ui/apps/platform/src/ConsolePlugin/PluginContent.tsx b/ui/apps/platform/src/ConsolePlugin/PluginContent.tsx new file mode 100644 index 0000000000000..2b7a5a3f4f344 --- /dev/null +++ b/ui/apps/platform/src/ConsolePlugin/PluginContent.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import usePermissions from 'hooks/usePermissions'; +import LoadingSection from 'Components/PatternFly/LoadingSection'; + +function PluginContent({ children }: { children: React.ReactNode }) { + const { isLoadingPermissions } = usePermissions(); + + if (isLoadingPermissions) { + return ; + } + + return <>{children}; +} + +export default PluginContent; diff --git a/ui/apps/platform/src/ConsolePlugin/PluginProvider.tsx b/ui/apps/platform/src/ConsolePlugin/PluginProvider.tsx index 14a1cc381a2d2..ad559aa69ef1d 100644 --- a/ui/apps/platform/src/ConsolePlugin/PluginProvider.tsx +++ b/ui/apps/platform/src/ConsolePlugin/PluginProvider.tsx @@ -1,9 +1,11 @@ -import React from 'react'; +import React, { useMemo, type ReactNode } from 'react'; import { ApolloProvider } from '@apollo/client'; import axios from 'services/instance'; import configureApolloClient from '../init/configureApolloClient'; import consoleFetchAxiosAdapter from './consoleFetchAxiosAdapter'; +import { UserPermissionProvider } from './UserPermissionProvider'; +import PluginContent from './PluginContent'; // The console requires a custom fetch implementation via `consoleFetch` to correctly pass headers such // as X-CSRFToken to API requests. All of our current code uses `axios` to make API requests, so we need @@ -13,6 +15,18 @@ axios.defaults.adapter = (config) => consoleFetchAxiosAdapter(proxyBaseURL, conf const apolloClient = configureApolloClient(); -export default function PluginProvider({ children }) { - return {children}; +export function PluginProvider({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} + +// If there is any data that needs to be shared across plugin entry points that isn't covered by +// a general purpose hook, we can add it here. +export function usePluginContext() { + return useMemo(() => ({}), []); } diff --git a/ui/apps/platform/src/ConsolePlugin/SecurityVulnerabilitiesPage/Index.tsx b/ui/apps/platform/src/ConsolePlugin/SecurityVulnerabilitiesPage/Index.tsx index 799b0c3e9e544..c24aa44427c24 100644 --- a/ui/apps/platform/src/ConsolePlugin/SecurityVulnerabilitiesPage/Index.tsx +++ b/ui/apps/platform/src/ConsolePlugin/SecurityVulnerabilitiesPage/Index.tsx @@ -1,24 +1,32 @@ import React from 'react'; import { PageSection, Title } from '@patternfly/react-core'; import { CheckCircleIcon } from '@patternfly/react-icons'; +import usePermissions from 'hooks/usePermissions'; import SummaryCounts from 'Containers/Dashboard/SummaryCounts'; import ViolationsByPolicyCategory from 'Containers/Dashboard/Widgets/ViolationsByPolicyCategory'; -import PluginProvider from '../PluginProvider'; export function Index() { + const { hasReadAccess } = usePermissions(); + const hasReadAccessForAlert = hasReadAccess('Alert'); + const hasReadAccessForCluster = hasReadAccess('Cluster'); + const hasReadAccessForDeployment = hasReadAccess('Deployment'); + const hasReadAccessForImage = hasReadAccess('Image'); + const hasReadAccessForNode = hasReadAccess('Node'); + const hasReadAccessForSecret = hasReadAccess('Secret'); + return ( - + <> {'Hello, Plugin!'} @@ -31,6 +39,6 @@ export function Index() { {'Your plugin is working.'}

-
+ ); } diff --git a/ui/apps/platform/src/ConsolePlugin/UserPermissionProvider.tsx b/ui/apps/platform/src/ConsolePlugin/UserPermissionProvider.tsx new file mode 100644 index 0000000000000..f4a711c144069 --- /dev/null +++ b/ui/apps/platform/src/ConsolePlugin/UserPermissionProvider.tsx @@ -0,0 +1,17 @@ +import React, { type ReactNode } from 'react'; +import { UserPermissionContext } from 'hooks/usePermissions'; + +import { fetchUserRolePermissions } from 'services/RolesService'; +import useRestQuery from 'hooks/useRestQuery'; + +export function UserPermissionProvider({ children }: { children: ReactNode }) { + const { data, isLoading } = useRestQuery(fetchUserRolePermissions); + const userRolePermissions = data?.response || { resourceToAccess: {} }; + const isLoadingPermissions = isLoading; + + return ( + + {children} + + ); +} diff --git a/ui/apps/platform/webpack.ocp-plugin.config.js b/ui/apps/platform/webpack.ocp-plugin.config.js index 34d1b9e5f90f3..3b184eb64ec4a 100644 --- a/ui/apps/platform/webpack.ocp-plugin.config.js +++ b/ui/apps/platform/webpack.ocp-plugin.config.js @@ -98,6 +98,7 @@ const config = { displayName: 'Red Hat Advanced Cluster Security for OpenShift', description: 'OCP Console Plugin for Advanced Cluster Security', exposedModules: { + context: './ConsolePlugin/PluginProvider', SecurityVulnerabilitiesPage: './ConsolePlugin/SecurityVulnerabilitiesPage/Index', WorkloadSecurityTab: './ConsolePlugin/WorkloadSecurityTab/Index', @@ -110,6 +111,14 @@ const config = { }, }, extensions: [ + // General Context Provider to be shared across all extensions + { + type: 'console.context-provider', + properties: { + provider: { $codeRef: 'context.PluginProvider' }, + useValueHook: { $codeRef: 'context.usePluginContext' }, + }, + }, // Security Vulnerabilities Page { type: 'console.page/route',