2025-09-01 17:10:13 +05:30
package openfgaauthz
import (
"context"
2025-09-04 14:07:11 +05:30
"sync"
2025-09-01 17:10:13 +05:30
authz "github.com/SigNoz/signoz/pkg/authz"
2025-09-04 14:07:11 +05:30
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
2025-09-02 16:30:47 +05:30
2025-09-01 17:10:13 +05:30
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
2025-09-02 16:30:47 +05:30
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
openfgapkgserver "github.com/openfga/openfga/pkg/server"
"google.golang.org/protobuf/encoding/protojson"
2025-09-01 17:10:13 +05:30
)
2025-09-04 14:07:11 +05:30
var (
openfgaDefaultStore = valuer . NewString ( "signoz" )
)
2025-09-01 17:10:13 +05:30
type provider struct {
2025-09-02 16:30:47 +05:30
config authz . Config
settings factory . ScopedProviderSettings
openfgaSchema [ ] openfgapkgtransformer . ModuleFile
openfgaServer * openfgapkgserver . Server
2025-09-04 14:07:11 +05:30
storeID string
modelID string
mtx sync . RWMutex
2025-09-02 16:30:47 +05:30
stopChan chan struct { }
2025-09-01 17:10:13 +05:30
}
2025-09-04 14:07:11 +05:30
func NewProviderFactory ( sqlstore sqlstore . SQLStore , openfgaSchema [ ] openfgapkgtransformer . ModuleFile ) factory . ProviderFactory [ authz . AuthZ , authz . Config ] {
2025-09-01 17:10:13 +05:30
return factory . NewProviderFactory ( factory . MustNewName ( "openfga" ) , func ( ctx context . Context , ps factory . ProviderSettings , config authz . Config ) ( authz . AuthZ , error ) {
2025-09-04 14:07:11 +05:30
return newOpenfgaProvider ( ctx , ps , config , sqlstore , openfgaSchema )
2025-09-01 17:10:13 +05:30
} )
}
2025-09-04 14:07:11 +05:30
func newOpenfgaProvider ( ctx context . Context , settings factory . ProviderSettings , config authz . Config , sqlstore sqlstore . SQLStore , openfgaSchema [ ] openfgapkgtransformer . ModuleFile ) ( authz . AuthZ , error ) {
2025-09-01 17:10:13 +05:30
scopedProviderSettings := factory . NewScopedProviderSettings ( settings , "github.com/SigNoz/signoz/pkg/authz/openfgaauthz" )
2025-09-04 14:07:11 +05:30
store , err := NewSQLStore ( sqlstore )
2025-09-01 17:10:13 +05:30
if err != nil {
scopedProviderSettings . Logger ( ) . DebugContext ( ctx , "failed to initialize sqlstore for authz" )
return nil , err
}
// setup the openfga server
2025-09-02 16:30:47 +05:30
opts := [ ] openfgapkgserver . OpenFGAServiceV1Option {
2025-09-04 14:07:11 +05:30
openfgapkgserver . WithDatastore ( store ) ,
2025-09-02 16:30:47 +05:30
openfgapkgserver . WithLogger ( NewLogger ( scopedProviderSettings . Logger ( ) ) ) ,
2025-09-01 17:10:13 +05:30
}
2025-09-02 16:30:47 +05:30
openfgaServer , err := openfgapkgserver . NewServerWithOpts ( opts ... )
2025-09-01 17:10:13 +05:30
if err != nil {
scopedProviderSettings . Logger ( ) . DebugContext ( ctx , "failed to create authz server" )
return nil , err
}
return & provider {
2025-09-02 16:30:47 +05:30
config : config ,
settings : scopedProviderSettings ,
openfgaServer : openfgaServer ,
openfgaSchema : openfgaSchema ,
2025-09-04 14:07:11 +05:30
mtx : sync . RWMutex { } ,
2025-09-02 16:30:47 +05:30
stopChan : make ( chan struct { } ) ,
2025-09-01 17:10:13 +05:30
} , nil
}
func ( provider * provider ) Start ( ctx context . Context ) error {
2025-09-04 14:07:11 +05:30
storeId , err := provider . getOrCreateStore ( ctx , openfgaDefaultStore . StringValue ( ) )
2025-09-01 17:10:13 +05:30
if err != nil {
return err
}
2025-09-04 14:07:11 +05:30
modelID , err := provider . getOrCreateModel ( ctx , storeId )
2025-09-01 17:10:13 +05:30
if err != nil {
return err
}
2025-09-04 14:07:11 +05:30
provider . mtx . Lock ( )
provider . modelID = modelID
provider . storeID = storeId
provider . mtx . Unlock ( )
2025-09-01 17:10:13 +05:30
<- provider . stopChan
return nil
}
func ( provider * provider ) Stop ( ctx context . Context ) error {
2025-09-02 16:30:47 +05:30
provider . openfgaServer . Close ( )
2025-09-01 17:10:13 +05:30
close ( provider . stopChan )
return nil
}
2025-09-02 16:30:47 +05:30
func ( provider * provider ) getOrCreateStore ( ctx context . Context , name string ) ( string , error ) {
stores , err := provider . openfgaServer . ListStores ( ctx , & openfgav1 . ListStoresRequest { } )
2025-09-01 17:10:13 +05:30
if err != nil {
return "" , err
}
for _ , store := range stores . GetStores ( ) {
2025-09-02 16:30:47 +05:30
if store . GetName ( ) == name {
2025-09-01 17:10:13 +05:30
return store . Id , nil
}
}
2025-09-02 16:30:47 +05:30
store , err := provider . openfgaServer . CreateStore ( ctx , & openfgav1 . CreateStoreRequest { Name : name } )
2025-09-01 17:10:13 +05:30
if err != nil {
return "" , err
}
return store . Id , nil
}
2025-09-02 16:30:47 +05:30
func ( provider * provider ) getOrCreateModel ( ctx context . Context , storeID string ) ( string , error ) {
schema , err := openfgapkgtransformer . TransformModuleFilesToModel ( provider . openfgaSchema , "1.1" )
2025-09-01 17:10:13 +05:30
if err != nil {
2025-09-02 16:30:47 +05:30
return "" , err
2025-09-01 17:10:13 +05:30
}
2025-09-02 16:30:47 +05:30
authorisationModels , err := provider . openfgaServer . ReadAuthorizationModels ( ctx , & openfgav1 . ReadAuthorizationModelsRequest { StoreId : storeID } )
2025-09-01 17:10:13 +05:30
if err != nil {
2025-09-02 16:30:47 +05:30
return "" , err
2025-09-01 17:10:13 +05:30
}
for _ , authModel := range authorisationModels . GetAuthorizationModels ( ) {
2025-09-02 16:30:47 +05:30
equal , err := provider . isModelEqual ( schema , authModel )
if err != nil {
return "" , err
}
if equal {
return authModel . Id , nil
2025-09-01 17:10:13 +05:30
}
}
2025-09-02 16:30:47 +05:30
authorizationModel , err := provider . openfgaServer . WriteAuthorizationModel ( ctx , & openfgav1 . WriteAuthorizationModelRequest {
StoreId : storeID ,
2025-09-01 17:10:13 +05:30
TypeDefinitions : schema . TypeDefinitions ,
SchemaVersion : schema . SchemaVersion ,
Conditions : schema . Conditions ,
} )
if err != nil {
2025-09-02 16:30:47 +05:30
return "" , err
2025-09-01 17:10:13 +05:30
}
2025-09-02 16:30:47 +05:30
return authorizationModel . AuthorizationModelId , nil
}
// the language model doesn't have any equality check
// https://github.com/openfga/language/blob/main/pkg/go/transformer/module-to-model_test.go#L38
func ( provider * provider ) isModelEqual ( expected * openfgav1 . AuthorizationModel , actual * openfgav1 . AuthorizationModel ) ( bool , error ) {
// we need to initialize a new model since the model extracted from schema doesn't have id
expectedAuthModel := openfgav1 . AuthorizationModel {
SchemaVersion : expected . SchemaVersion ,
TypeDefinitions : expected . TypeDefinitions ,
Conditions : expected . Conditions ,
}
expectedAuthModelBytes , err := protojson . Marshal ( & expectedAuthModel )
if err != nil {
return false , err
}
actualAuthModel := openfgav1 . AuthorizationModel {
SchemaVersion : actual . SchemaVersion ,
TypeDefinitions : actual . TypeDefinitions ,
Conditions : actual . Conditions ,
}
actualAuthModelBytes , err := protojson . Marshal ( & actualAuthModel )
if err != nil {
return false , err
}
return string ( expectedAuthModelBytes ) == string ( actualAuthModelBytes ) , nil
2025-09-04 14:07:11 +05:30
}
2025-09-29 17:45:52 +05:30
func ( provider * provider ) Check ( ctx context . Context , tupleReq * openfgav1 . TupleKey ) error {
2025-09-04 14:07:11 +05:30
checkResponse , err := provider . openfgaServer . Check (
ctx ,
& openfgav1 . CheckRequest {
StoreId : provider . storeID ,
AuthorizationModelId : provider . modelID ,
2025-09-29 17:45:52 +05:30
TupleKey : & openfgav1 . CheckRequestTupleKey {
User : tupleReq . User ,
Relation : tupleReq . Relation ,
Object : tupleReq . Object ,
} ,
2025-09-04 14:07:11 +05:30
} )
if err != nil {
return errors . Newf ( errors . TypeInternal , authtypes . ErrCodeAuthZUnavailable , "authorization server is unavailable" ) . WithAdditional ( err . Error ( ) )
}
if ! checkResponse . Allowed {
return errors . Newf ( errors . TypeForbidden , authtypes . ErrCodeAuthZForbidden , "subject %s cannot %s object %s" , tupleReq . User , tupleReq . Relation , tupleReq . Object )
}
return nil
2025-09-01 17:10:13 +05:30
}
2025-09-17 21:35:11 +05:30
2025-09-29 17:45:52 +05:30
func ( provider * provider ) BatchCheck ( ctx context . Context , tupleReq [ ] * openfgav1 . TupleKey ) error {
batchCheckItems := make ( [ ] * openfgav1 . BatchCheckItem , 0 )
for _ , tuple := range tupleReq {
batchCheckItems = append ( batchCheckItems , & openfgav1 . BatchCheckItem {
TupleKey : & openfgav1 . CheckRequestTupleKey {
User : tuple . User ,
Relation : tuple . Relation ,
Object : tuple . Object ,
} ,
} )
}
checkResponse , err := provider . openfgaServer . BatchCheck (
ctx ,
& openfgav1 . BatchCheckRequest {
StoreId : provider . storeID ,
AuthorizationModelId : provider . modelID ,
Checks : batchCheckItems ,
} )
if err != nil {
return errors . Newf ( errors . TypeInternal , authtypes . ErrCodeAuthZUnavailable , "authorization server is unavailable" ) . WithAdditional ( err . Error ( ) )
}
for _ , checkResponse := range checkResponse . Result {
if checkResponse . GetAllowed ( ) {
return nil
}
}
return errors . New ( errors . TypeForbidden , authtypes . ErrCodeAuthZForbidden , "" )
}
func ( provider * provider ) CheckWithTupleCreation ( ctx context . Context , claims authtypes . Claims , relation authtypes . Relation , typeable authtypes . Typeable , selectors [ ] authtypes . Selector ) error {
2025-09-17 21:35:11 +05:30
subject , err := authtypes . NewSubject ( authtypes . TypeUser , claims . UserID , authtypes . Relation { } )
if err != nil {
return err
}
2025-09-29 17:45:52 +05:30
tuples , err := typeable . Tuples ( subject , relation , selectors )
2025-09-17 21:35:11 +05:30
if err != nil {
return err
}
2025-09-29 17:45:52 +05:30
err = provider . BatchCheck ( ctx , tuples )
2025-09-17 21:35:11 +05:30
if err != nil {
return err
}
return nil
}
2025-09-29 17:45:52 +05:30
func ( provider * provider ) Write ( ctx context . Context , req * openfgav1 . WriteRequest ) error {
_ , err := provider . openfgaServer . Write ( ctx , & openfgav1 . WriteRequest {
StoreId : provider . storeID ,
AuthorizationModelId : provider . modelID ,
Writes : req . Writes ,
} )
return err
}
func ( provider * provider ) ListObjects ( ctx context . Context , subject string , relation authtypes . Relation , typeable authtypes . Typeable ) ( [ ] * authtypes . Object , error ) {
response , err := provider . openfgaServer . ListObjects ( ctx , & openfgav1 . ListObjectsRequest {
StoreId : provider . storeID ,
AuthorizationModelId : provider . modelID ,
User : subject ,
Relation : relation . StringValue ( ) ,
Type : typeable . Type ( ) . StringValue ( ) ,
} )
if err != nil {
return nil , errors . Wrapf ( err , errors . TypeInternal , authtypes . ErrCodeAuthZUnavailable , "cannot list objects for subject %s with relation %s for type %s" , subject , relation . StringValue ( ) , typeable . Type ( ) . StringValue ( ) )
2025-09-17 21:35:11 +05:30
}
2025-09-29 17:45:52 +05:30
return authtypes . MustNewObjectsFromStringSlice ( response . Objects ) , nil
2025-09-17 21:35:11 +05:30
}