From 717b70cc69c5c8933689e3335c6bb68fcbb41662 Mon Sep 17 00:00:00 2001 From: Alexey Fayzullin Date: Mon, 19 Oct 2026 08:45:47 +0300 Subject: [PATCH 1/3] NOTASK: Add the exframe.auth.v1 method authorization option --- .github/workflows/ci.yml | 35 ++++++++ README.md | 58 ++++++++++++- buf.gen.yaml | 5 ++ buf.yaml | 10 +++ exframe/auth/v1/auth.pb.go | 160 +++++++++++++++++++++++++++++++++++ exframe/auth/v1/auth.proto | 20 +++++ exframe/auth/v1/auth_test.go | 57 +++++++++++++ go.mod | 7 ++ go.sum | 4 + 9 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 exframe/auth/v1/auth.pb.go create mode 100644 exframe/auth/v1/auth.proto create mode 100644 exframe/auth/v1/auth_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..29281cf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + buf: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bufbuild/buf-action@v1 + with: + token: ${{ secrets.BUF_TOKEN }} + push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: bufbuild/buf-action@v1 + with: + setup_only: true + - name: Check generated code + run: | + buf generate + git diff --exit-code + - run: go test ./... diff --git a/README.md b/README.md index 87fd6f0..bd50faf 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ -# exframe-proto \ No newline at end of file +# exframe-proto + +Protobuf extensions used by the exFrame Go framework. + +## exframe.auth.v1 + +`exframe/auth/v1/auth.proto` declares the method option `(exframe.auth.v1.method)`, the access policy of an RPC method: + +- `scopes`: access is granted when the caller has any of the scopes; +- `public`: requests that fail authorization are let through and counted. + +A method without the option is closed for every caller. + +```proto +import "exframe/auth/v1/auth.proto"; + +service ContactService { + rpc GetUserPhoneV1(GetUserPhoneV1Request) returns (GetUserPhoneV1Response) { + option (exframe.auth.v1.method) = { + scopes: ["contact-service.phone.read", "contact-service.phone.read-write"] + }; + } +} +``` + +| `public` | `scopes` | Caller without a required scope | Caller with a required scope | +|---|---|---|---| +| false | set | denied | allowed | +| false | empty | denied | denied | +| true | set | allowed and counted | allowed | +| true | empty | allowed | allowed | + +## Usage + +buf: add the module to `deps` in `buf.yaml` and run `buf dep update`. + +```yaml +deps: + - buf.build/exness/exframe-proto +``` + +Go: the generated code imports `github.com/exness/exframe-proto/exframe/auth/v1`, no `go_package` override is needed. + +```sh +go get github.com/exness/exframe-proto +``` + +## Development + +```sh +buf lint +buf format -d --exit-code +buf generate +go test ./... +``` + +The extension number `52000` and the field numbers of `MethodAuth` never change. diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..de37493 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,5 @@ +version: v2 +plugins: + - local: ["go", "tool", "protoc-gen-go"] + out: . + opt: paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..c28d4e2 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,10 @@ +version: v2 +modules: + - path: . + name: buf.build/exness/exframe-proto +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/exframe/auth/v1/auth.pb.go b/exframe/auth/v1/auth.pb.go new file mode 100644 index 0000000..9aaf83b --- /dev/null +++ b/exframe/auth/v1/auth.pb.go @@ -0,0 +1,160 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: exframe/auth/v1/auth.proto + +package authv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + descriptorpb "google.golang.org/protobuf/types/descriptorpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// MethodAuth is the access policy of an RPC method. +type MethodAuth struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Access is granted when the caller has any of the scopes. + Scopes []string `protobuf:"bytes,1,rep,name=scopes,proto3" json:"scopes,omitempty"` + // Requests that fail authorization are let through and counted. + Public bool `protobuf:"varint,2,opt,name=public,proto3" json:"public,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MethodAuth) Reset() { + *x = MethodAuth{} + mi := &file_exframe_auth_v1_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MethodAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MethodAuth) ProtoMessage() {} + +func (x *MethodAuth) ProtoReflect() protoreflect.Message { + mi := &file_exframe_auth_v1_auth_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MethodAuth.ProtoReflect.Descriptor instead. +func (*MethodAuth) Descriptor() ([]byte, []int) { + return file_exframe_auth_v1_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *MethodAuth) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *MethodAuth) GetPublic() bool { + if x != nil { + return x.Public + } + return false +} + +var file_exframe_auth_v1_auth_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*descriptorpb.MethodOptions)(nil), + ExtensionType: (*MethodAuth)(nil), + Field: 52000, + Name: "exframe.auth.v1.method", + Tag: "bytes,52000,opt,name=method", + Filename: "exframe/auth/v1/auth.proto", + }, +} + +// Extension fields to descriptorpb.MethodOptions. +var ( + // A method without the option is closed for every caller. + // + // optional exframe.auth.v1.MethodAuth method = 52000; + E_Method = &file_exframe_auth_v1_auth_proto_extTypes[0] +) + +var File_exframe_auth_v1_auth_proto protoreflect.FileDescriptor + +const file_exframe_auth_v1_auth_proto_rawDesc = "" + + "\n" + + "\x1aexframe/auth/v1/auth.proto\x12\x0fexframe.auth.v1\x1a google/protobuf/descriptor.proto\"<\n" + + "\n" + + "MethodAuth\x12\x16\n" + + "\x06scopes\x18\x01 \x03(\tR\x06scopes\x12\x16\n" + + "\x06public\x18\x02 \x01(\bR\x06public:U\n" + + "\x06method\x12\x1e.google.protobuf.MethodOptions\x18\xa0\x96\x03 \x01(\v2\x1b.exframe.auth.v1.MethodAuthR\x06methodB8Z6github.com/exness/exframe-proto/exframe/auth/v1;authv1b\x06proto3" + +var ( + file_exframe_auth_v1_auth_proto_rawDescOnce sync.Once + file_exframe_auth_v1_auth_proto_rawDescData []byte +) + +func file_exframe_auth_v1_auth_proto_rawDescGZIP() []byte { + file_exframe_auth_v1_auth_proto_rawDescOnce.Do(func() { + file_exframe_auth_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_exframe_auth_v1_auth_proto_rawDesc), len(file_exframe_auth_v1_auth_proto_rawDesc))) + }) + return file_exframe_auth_v1_auth_proto_rawDescData +} + +var file_exframe_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_exframe_auth_v1_auth_proto_goTypes = []any{ + (*MethodAuth)(nil), // 0: exframe.auth.v1.MethodAuth + (*descriptorpb.MethodOptions)(nil), // 1: google.protobuf.MethodOptions +} +var file_exframe_auth_v1_auth_proto_depIdxs = []int32{ + 1, // 0: exframe.auth.v1.method:extendee -> google.protobuf.MethodOptions + 0, // 1: exframe.auth.v1.method:type_name -> exframe.auth.v1.MethodAuth + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 1, // [1:2] is the sub-list for extension type_name + 0, // [0:1] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_exframe_auth_v1_auth_proto_init() } +func file_exframe_auth_v1_auth_proto_init() { + if File_exframe_auth_v1_auth_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_exframe_auth_v1_auth_proto_rawDesc), len(file_exframe_auth_v1_auth_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 1, + NumServices: 0, + }, + GoTypes: file_exframe_auth_v1_auth_proto_goTypes, + DependencyIndexes: file_exframe_auth_v1_auth_proto_depIdxs, + MessageInfos: file_exframe_auth_v1_auth_proto_msgTypes, + ExtensionInfos: file_exframe_auth_v1_auth_proto_extTypes, + }.Build() + File_exframe_auth_v1_auth_proto = out.File + file_exframe_auth_v1_auth_proto_goTypes = nil + file_exframe_auth_v1_auth_proto_depIdxs = nil +} diff --git a/exframe/auth/v1/auth.proto b/exframe/auth/v1/auth.proto new file mode 100644 index 0000000..470e259 --- /dev/null +++ b/exframe/auth/v1/auth.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package exframe.auth.v1; + +import "google/protobuf/descriptor.proto"; + +option go_package = "github.com/exness/exframe-proto/exframe/auth/v1;authv1"; + +// MethodAuth is the access policy of an RPC method. +message MethodAuth { + // Access is granted when the caller has any of the scopes. + repeated string scopes = 1; + // Requests that fail authorization are let through and counted. + bool public = 2; +} + +extend google.protobuf.MethodOptions { + // A method without the option is closed for every caller. + MethodAuth method = 52000; +} diff --git a/exframe/auth/v1/auth_test.go b/exframe/auth/v1/auth_test.go new file mode 100644 index 0000000..eb59a5d --- /dev/null +++ b/exframe/auth/v1/auth_test.go @@ -0,0 +1,57 @@ +package authv1 + +import ( + "testing" + + "google.golang.org/protobuf/reflect/protoreflect" +) + +func TestExtension(t *testing.T) { + t.Parallel() + + ext := E_Method.TypeDescriptor() + if got := ext.Number(); got != 52000 { + t.Fatalf("extension number = %d, want 52000", got) + } + if got := ext.FullName(); got != "exframe.auth.v1.method" { + t.Fatalf("extension name = %q, want exframe.auth.v1.method", got) + } + if got := ext.ContainingMessage().FullName(); got != "google.protobuf.MethodOptions" { + t.Fatalf("extended message = %q, want google.protobuf.MethodOptions", got) + } + if got := ext.Message().FullName(); got != "exframe.auth.v1.MethodAuth" { + t.Fatalf("extension type = %q, want exframe.auth.v1.MethodAuth", got) + } +} + +func TestMethodAuthFields(t *testing.T) { + t.Parallel() + + fields := (&MethodAuth{}).ProtoReflect().Descriptor().Fields() + tests := []struct { + name string + number protoreflect.FieldNumber + kind protoreflect.Kind + list bool + }{ + {name: "scopes", number: 1, kind: protoreflect.StringKind, list: true}, + {name: "public", number: 2, kind: protoreflect.BoolKind}, + } + if fields.Len() != len(tests) { + t.Fatalf("fields = %d, want %d", fields.Len(), len(tests)) + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + f := fields.ByName(protoreflect.Name(tt.name)) + if f == nil { + t.Fatalf("field %s not found", tt.name) + } + if f.Number() != tt.number || f.Kind() != tt.kind || f.IsList() != tt.list { + t.Fatalf("field %s = (%d, %s, list %t), want (%d, %s, list %t)", + tt.name, f.Number(), f.Kind(), f.IsList(), tt.number, tt.kind, tt.list) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8e86e13 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/exness/exframe-proto + +go 1.24 + +require google.golang.org/protobuf v1.36.12 + +tool google.golang.org/protobuf/cmd/protoc-gen-go diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a0c9758 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= From bfae27672b970d4d6cc949102bef36b4114aac76 Mon Sep 17 00:00:00 2001 From: Alexey Fayzullin Date: Mon, 19 Oct 2026 08:59:04 +0300 Subject: [PATCH 2/3] NOTASK: Allow the buf job to comment on pull requests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29281cf..71a3482 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,9 @@ permissions: jobs: buf: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@v4 - uses: bufbuild/buf-action@v1 From df2efb897b1dd4368a8252129cbb4f69c5fee99e Mon Sep 17 00:00:00 2001 From: Alexey Fayzullin Date: Mon, 19 Oct 2026 09:00:58 +0300 Subject: [PATCH 3/3] NOTASK: Use 52008 as the method option number --- README.md | 2 +- exframe/auth/v1/auth.pb.go | 8 ++++---- exframe/auth/v1/auth.proto | 2 +- exframe/auth/v1/auth_test.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bd50faf..881ec62 100644 --- a/README.md +++ b/README.md @@ -54,4 +54,4 @@ buf generate go test ./... ``` -The extension number `52000` and the field numbers of `MethodAuth` never change. +The extension number `52008` and the field numbers of `MethodAuth` never change. diff --git a/exframe/auth/v1/auth.pb.go b/exframe/auth/v1/auth.pb.go index 9aaf83b..0803bfb 100644 --- a/exframe/auth/v1/auth.pb.go +++ b/exframe/auth/v1/auth.pb.go @@ -81,9 +81,9 @@ var file_exframe_auth_v1_auth_proto_extTypes = []protoimpl.ExtensionInfo{ { ExtendedType: (*descriptorpb.MethodOptions)(nil), ExtensionType: (*MethodAuth)(nil), - Field: 52000, + Field: 52008, Name: "exframe.auth.v1.method", - Tag: "bytes,52000,opt,name=method", + Tag: "bytes,52008,opt,name=method", Filename: "exframe/auth/v1/auth.proto", }, } @@ -92,7 +92,7 @@ var file_exframe_auth_v1_auth_proto_extTypes = []protoimpl.ExtensionInfo{ var ( // A method without the option is closed for every caller. // - // optional exframe.auth.v1.MethodAuth method = 52000; + // optional exframe.auth.v1.MethodAuth method = 52008; E_Method = &file_exframe_auth_v1_auth_proto_extTypes[0] ) @@ -105,7 +105,7 @@ const file_exframe_auth_v1_auth_proto_rawDesc = "" + "MethodAuth\x12\x16\n" + "\x06scopes\x18\x01 \x03(\tR\x06scopes\x12\x16\n" + "\x06public\x18\x02 \x01(\bR\x06public:U\n" + - "\x06method\x12\x1e.google.protobuf.MethodOptions\x18\xa0\x96\x03 \x01(\v2\x1b.exframe.auth.v1.MethodAuthR\x06methodB8Z6github.com/exness/exframe-proto/exframe/auth/v1;authv1b\x06proto3" + "\x06method\x12\x1e.google.protobuf.MethodOptions\x18\xa8\x96\x03 \x01(\v2\x1b.exframe.auth.v1.MethodAuthR\x06methodB8Z6github.com/exness/exframe-proto/exframe/auth/v1;authv1b\x06proto3" var ( file_exframe_auth_v1_auth_proto_rawDescOnce sync.Once diff --git a/exframe/auth/v1/auth.proto b/exframe/auth/v1/auth.proto index 470e259..059f021 100644 --- a/exframe/auth/v1/auth.proto +++ b/exframe/auth/v1/auth.proto @@ -16,5 +16,5 @@ message MethodAuth { extend google.protobuf.MethodOptions { // A method without the option is closed for every caller. - MethodAuth method = 52000; + MethodAuth method = 52008; } diff --git a/exframe/auth/v1/auth_test.go b/exframe/auth/v1/auth_test.go index eb59a5d..9c19799 100644 --- a/exframe/auth/v1/auth_test.go +++ b/exframe/auth/v1/auth_test.go @@ -10,8 +10,8 @@ func TestExtension(t *testing.T) { t.Parallel() ext := E_Method.TypeDescriptor() - if got := ext.Number(); got != 52000 { - t.Fatalf("extension number = %d, want 52000", got) + if got := ext.Number(); got != 52008 { + t.Fatalf("extension number = %d, want 52008", got) } if got := ext.FullName(); got != "exframe.auth.v1.method" { t.Fatalf("extension name = %q, want exframe.auth.v1.method", got)