Folders: let unified storage handle authorization (#110825)

This commit is contained in:
Ryan McKinley
2025-09-24 16:26:47 +03:00
committed by GitHub
parent 305dab3a54
commit a4dabc31ed
21 changed files with 73 additions and 423 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // indirect
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
+2 -2
View File
@@ -95,8 +95,8 @@ github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25d
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
+1 -1
View File
@@ -21,7 +21,7 @@ replace github.com/grafana/grafana/pkg/aggregator => ../../pkg/aggregator
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
require (
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c
github.com/grafana/grafana v6.1.6+incompatible
github.com/grafana/grafana-app-sdk v0.45.0
github.com/grafana/grafana-app-sdk/logging v0.45.0
+2 -2
View File
@@ -723,8 +723,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20250915130141-a8ee25091876 h1:BzoGpzARwRCNOHcqQdYPAFp2LS1pqnkLWhIuDdq1zho=
github.com/grafana/alerting v0.0.0-20250915130141-a8ee25091876/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
+1 -1
View File
@@ -35,7 +35,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // indirect
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/grafana/grafana-app-sdk/logging v0.45.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
+2 -2
View File
@@ -50,8 +50,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.24.6
require (
github.com/google/go-github/v70 v70.0.0
github.com/google/uuid v1.6.0
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c
github.com/grafana/grafana-app-sdk/logging v0.45.0
github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2
+2 -2
View File
@@ -52,8 +52,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
+1 -1
View File
@@ -33,7 +33,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // indirect
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c // indirect
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
+2 -2
View File
@@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
+1 -1
View File
@@ -87,7 +87,7 @@ require (
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20250915130141-a8ee25091876 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // @grafana/identity-access-team
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 // @grafana/identity-access-team
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics
+2 -2
View File
@@ -1587,8 +1587,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20250915130141-a8ee25091876 h1:BzoGpzARwRCNOHcqQdYPAFp2LS1pqnkLWhIuDdq1zho=
github.com/grafana/alerting v0.0.0-20250915130141-a8ee25091876/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery
go 1.24.6
require (
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // @grafana/identity-access-team
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 // @grafana/identity-access-team
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
+2 -2
View File
@@ -30,8 +30,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
+1 -1
View File
@@ -44,7 +44,7 @@ require (
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 // indirect
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
+2 -2
View File
@@ -63,8 +63,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781 h1:sdI2P18j78expDtLB6YbVI5dV+HguV719iiQNqx3wqw=
github.com/grafana/authlib v0.0.0-20250917093142-83a502239781/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c h1:8GIMe1KclDdfogaeRsiU69Ev2zTF9kmjqjQqqZMzerc=
github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 h1:jymmOFIWnW26DeUjFgYEoltI170KeT5r1rI8a/dUf0E=
github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
-106
View File
@@ -1,106 +0,0 @@
package folders
import (
"context"
"errors"
"slices"
"k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
)
// newLegacyAuthorizer creates an authorizer using legacy access control, this is only usable for single tenant api.
func newLegacyAuthorizer(ac accesscontrol.AccessControl) authorizer.Authorizer {
return authorizer.AuthorizerFunc(func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
in, err := authorizerFunc(ctx, attr)
if err != nil {
if errors.Is(err, errNoUser) {
return authorizer.DecisionDeny, "", nil
}
return authorizer.DecisionNoOpinion, "", nil
}
ok, err := ac.Evaluate(ctx, in.user, in.evaluator)
if ok {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "folder", err
})
}
func authorizerFunc(ctx context.Context, attr authorizer.Attributes) (*authorizerParams, error) {
allowedVerbs := []string{utils.VerbCreate, utils.VerbDelete, utils.VerbList}
verb := attr.GetVerb()
name := attr.GetName()
if (!attr.IsResourceRequest()) || (name == "" && verb != utils.VerbCreate && slices.Contains(allowedVerbs, verb)) {
return nil, errNoResource
}
// require a user
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, errNoUser
}
scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(name)
var eval accesscontrol.Evaluator
// "get" is used for sub-resources with GET http (parents, access, count)
switch verb {
case utils.VerbCreate:
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)
case utils.VerbPatch:
fallthrough
case utils.VerbUpdate:
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, scope)
case utils.VerbDeleteCollection:
fallthrough
case utils.VerbDelete:
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, scope)
case utils.VerbList:
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersRead)
default:
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersRead, scope)
}
return &authorizerParams{evaluator: eval, user: user}, nil
}
// newMultiTenantAuthorizer creates an authorizer suitable to multi-tenant setup.
// For now it only allow authorization of access tokens.
func newMultiTenantAuthorizer(ac types.AccessClient) authorizer.Authorizer {
return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
info, ok := types.AuthInfoFrom(ctx)
if !ok {
return authorizer.DecisionDeny, "missing auth info", nil
}
// For now we only allow access policy to authorize with multi-tenant setup
if !types.IsIdentityType(info.GetIdentityType(), types.TypeAccessPolicy) {
return authorizer.DecisionDeny, "permission denied", nil
}
res, err := ac.Check(ctx, info, types.CheckRequest{
Verb: a.GetVerb(),
Group: a.GetAPIGroup(),
Resource: a.GetResource(),
Name: a.GetName(),
Namespace: a.GetNamespace(),
Subresource: a.GetSubresource(),
})
if err != nil {
return authorizer.DecisionDeny, "faild to perform authorization", err
}
if !res.Allowed {
return authorizer.DecisionDeny, "permission denied", nil
}
return authorizer.DecisionAllow, "", nil
})
}
@@ -1,278 +0,0 @@
package folders
import (
"context"
"testing"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/grafana/authlib/authn"
"github.com/grafana/authlib/authz"
"github.com/grafana/authlib/types"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
)
func TestLegacyAuthorizer(t *testing.T) {
type input struct {
user identity.Requester
verb string
}
type expect struct {
authorized authorizer.Decision
err error
}
var orgID int64 = 1
tests := []struct {
name string
input input
expect expect
}{
{
name: "user with create permissions should be able to create a folder",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
},
},
verb: string(utils.VerbCreate),
},
expect: expect{
authorized: authorizer.DecisionAllow,
},
},
{
name: "not possible to create a folder without a user",
input: input{
user: nil,
verb: string(utils.VerbCreate),
},
expect: expect{authorized: authorizer.DecisionDeny},
},
{
name: "user without permissions should not be able to create a folder",
input: input{
user: &user.SignedInUser{},
verb: string(utils.VerbCreate),
},
expect: expect{authorized: authorizer.DecisionDeny},
},
{
name: "user in another orgId should not be able to create a folder ",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: 2,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
},
},
verb: string(utils.VerbCreate),
},
expect: expect{authorized: authorizer.DecisionDeny},
},
{
name: "user with read permissions should be able to list folders",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {},
},
},
verb: string(utils.VerbList),
},
expect: expect{authorized: authorizer.DecisionDeny},
},
{
name: "user with delete permissions should be able to delete a folder",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {dashboards.ActionFoldersDelete: {dashboards.ScopeFoldersAll}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
},
},
verb: string(utils.VerbDelete),
},
expect: expect{authorized: authorizer.DecisionAllow},
},
{
name: "user without delete permissions should NOT be able to delete a folder",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {},
},
},
verb: string(utils.VerbDelete),
},
expect: expect{authorized: authorizer.DecisionDeny},
},
{
name: "user with write permissions should be able to update a folder",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
},
},
verb: string(utils.VerbUpdate),
},
expect: expect{authorized: authorizer.DecisionAllow},
},
{
name: "user without write permissions should NOT be able to update a folder",
input: input{
user: &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Name: "123",
Permissions: map[int64]map[string][]string{
orgID: {},
},
},
verb: string(utils.VerbUpdate),
},
expect: expect{authorized: authorizer.DecisionDeny},
},
}
authz := newLegacyAuthorizer(acimpl.ProvideAccessControl(featuremgmt.WithFeatures()))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authorized, _, err := authz.Authorize(
identity.WithRequester(context.Background(), tt.input.user),
authorizer.AttributesRecord{User: tt.input.user, Verb: tt.input.verb, Resource: "folders", ResourceRequest: true, Name: "123"},
)
if tt.expect.err != nil {
require.Error(t, err)
require.Equal(t, authorizer.DecisionDeny, authorized)
return
}
require.NoError(t, err)
require.Equal(t, tt.expect.authorized, authorized)
})
}
}
func TestMultiTenantAuthorizer(t *testing.T) {
type input struct {
verb string
info types.AuthInfo
client types.AccessClient
}
type expected struct {
authorized authorizer.Decision
err bool
}
tests := []struct {
name string
input input
expeted expected
}{
{
name: "non access policy idenity should not be able to authorize",
input: input{
verb: utils.VerbGet,
info: &identity.StaticRequester{
Type: types.TypeUser,
UserID: 1,
UserUID: "1",
},
},
expeted: expected{
authorized: authorizer.DecisionDeny,
},
},
{
name: "access policy identity with correct permissions should be able to authorize",
input: input{
verb: utils.VerbGet,
info: authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
Claims: jwt.Claims{
Subject: "access-policy:123",
},
Rest: authn.AccessTokenClaims{
Namespace: "stacks-1",
Permissions: []string{
"folder.grafana.app/folders:get",
},
},
}),
client: authz.NewClient(nil),
},
expeted: expected{
authorized: authorizer.DecisionAllow,
},
},
{
name: "access policy identity without correct permissions should not be able to authorize",
input: input{
verb: utils.VerbGet,
info: authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
Claims: jwt.Claims{
Subject: "access-policy:123",
},
Rest: authn.AccessTokenClaims{
Namespace: "stacks-1",
Permissions: []string{
"folder.grafana.app/folders:create",
},
},
}),
client: authz.NewClient(nil),
},
expeted: expected{
authorized: authorizer.DecisionDeny,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authz := newMultiTenantAuthorizer(tt.input.client)
authorized, _, err := authz.Authorize(
types.WithAuthInfo(context.Background(), tt.input.info),
authorizer.AttributesRecord{User: tt.input.info, Verb: tt.input.verb, APIGroup: folders.GROUP, Resource: "folders", ResourceRequest: true, Name: "123", Namespace: "stacks-1"},
)
if tt.expeted.err {
require.Error(t, err)
require.Equal(t, authorizer.DecisionDeny, authorized)
return
}
require.NoError(t, err)
require.Equal(t, tt.expeted.authorized, authorized)
})
}
}
+4 -14
View File
@@ -2,7 +2,6 @@ package folders
import (
"context"
"errors"
"fmt"
"strings"
@@ -20,12 +19,13 @@ import (
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/apps/iam/pkg/reconcilers"
"github.com/grafana/grafana/pkg/apimachinery/identity"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
@@ -42,16 +42,12 @@ var _ builder.APIGroupValidation = (*FolderAPIBuilder)(nil)
var resourceInfo = folders.FolderResourceInfo
var errNoUser = errors.New("valid user is required")
var errNoResource = errors.New("resource name is required")
// This is used just so wire has something unique to return
type FolderAPIBuilder struct {
features featuremgmt.FeatureToggles
namespacer request.NamespaceMapper
storage grafanarest.Storage
permissionStore reconcilers.PermissionStore
authorizer authorizer.Authorizer
accessClient authlib.AccessClient
parents parentsGetter
searcher resourcepb.ResourceIndexClient
@@ -85,7 +81,6 @@ func RegisterAPIService(cfg *setting.Cfg,
ac: accessControl,
accessClient: accessClient,
permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"),
authorizer: newLegacyAuthorizer(accessControl),
searcher: unified,
permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient),
}
@@ -96,7 +91,6 @@ func RegisterAPIService(cfg *setting.Cfg,
func NewAPIService(ac authlib.AccessClient, searcher resource.ResourceClient, features featuremgmt.FeatureToggles, zanzanaClient zanzana.Client) *FolderAPIBuilder {
return &FolderAPIBuilder{
features: features,
authorizer: newMultiTenantAuthorizer(ac),
searcher: searcher,
permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient),
}
@@ -219,13 +213,9 @@ func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAP
return oas, nil
}
type authorizerParams struct {
user identity.Requester
evaluator accesscontrol.Evaluator
}
// The default authorizer is fine because authorization happens in storage where we know the parent folder
func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return b.authorizer
return grafanaauthorizer.NewServiceAuthorizer()
}
func (b *FolderAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
@@ -0,0 +1,44 @@
package authorizer
import (
"context"
"errors"
authzlib "github.com/grafana/authlib/authz"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// Use this only when user authorization is not needed as it's performed downstream (e.g. in the UniStore).
// This authorizer makes a final decision based on the service permissions only, it does not consider the user permissions.
func NewServiceAuthorizer() authorizer.Authorizer {
return authorizer.AuthorizerFunc(
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if !attr.IsResourceRequest() {
return authorizer.DecisionNoOpinion, "", nil
}
ident, ok := types.AuthInfoFrom(ctx)
if !ok {
return authorizer.DecisionDeny, "", errors.New("no identity found for request")
}
res := authzlib.CheckServicePermissions(ident, attr.GetAPIGroup(), attr.GetResource(), attr.GetVerb())
if !res.Allowed {
log := logging.FromContext(ctx)
log.Info("calling service lacks required permissions",
"isServiceCall", res.ServiceCall,
"apiGroup", attr.GetAPIGroup(),
"resource", attr.GetResource(),
"verb", attr.GetVerb(),
"permissions", len(res.Permissions),
)
return authorizer.DecisionDeny, "calling service lacks required permissions", nil
}
return authorizer.DecisionAllow, "", nil
},
)
}
+1 -1
View File
@@ -40,7 +40,7 @@ func TestIntegrationFolderTree(t *testing.T) {
}
modes := []grafanarest.DualWriterMode{
// grafanarest.Mode1, (nothing new tested in mode 0 or 1)
grafanarest.Mode0, // legacy only
grafanarest.Mode2, // write both, read legacy
grafanarest.Mode3, // write both, read unified
grafanarest.Mode4,