From 4efc1668d5393a3c4a4e43ff96053df18e1e1352 Mon Sep 17 00:00:00 2001 From: Abhishek koserwal Date: Tue, 16 Apr 2024 17:06:54 +0530 Subject: [PATCH] Adding check permission endpoint --- api/rebac/v1/check.pb.go | 282 ++++++++++++++++++++++++++++++++++ api/rebac/v1/check.proto | 31 ++++ api/rebac/v1/check_grpc.pb.go | 109 +++++++++++++ api/rebac/v1/check_http.pb.go | 78 ++++++++++ cmd/ciam-rebac/wire_gen.go | 6 +- go.mod | 2 +- go.sum | 1 + internal/biz/biz.go | 2 +- internal/biz/relationships.go | 15 ++ internal/data/spicedb.go | 26 ++++ internal/data/spicedb_test.go | 60 ++++++++ internal/server/grpc.go | 3 +- internal/server/http.go | 3 +- internal/service/check.go | 31 ++++ internal/service/service.go | 2 +- openapi.yaml | 41 +++++ 16 files changed, 685 insertions(+), 7 deletions(-) create mode 100644 api/rebac/v1/check.pb.go create mode 100644 api/rebac/v1/check.proto create mode 100644 api/rebac/v1/check_grpc.pb.go create mode 100644 api/rebac/v1/check_http.pb.go create mode 100644 internal/service/check.go diff --git a/api/rebac/v1/check.pb.go b/api/rebac/v1/check.pb.go new file mode 100644 index 0000000..ecdb9b9 --- /dev/null +++ b/api/rebac/v1/check.pb.go @@ -0,0 +1,282 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v3.19.4 +// source: rebac/v1/check.proto + +package v1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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) +) + +type CheckPermissionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` + Subjectid string `protobuf:"bytes,2,opt,name=subjectid,proto3" json:"subjectid,omitempty"` + Operation string `protobuf:"bytes,3,opt,name=operation,proto3" json:"operation,omitempty"` + Resourcetype string `protobuf:"bytes,4,opt,name=resourcetype,proto3" json:"resourcetype,omitempty"` + Resourceid string `protobuf:"bytes,5,opt,name=resourceid,proto3" json:"resourceid,omitempty"` +} + +func (x *CheckPermissionRequest) Reset() { + *x = CheckPermissionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rebac_v1_check_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckPermissionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckPermissionRequest) ProtoMessage() {} + +func (x *CheckPermissionRequest) ProtoReflect() protoreflect.Message { + mi := &file_rebac_v1_check_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckPermissionRequest.ProtoReflect.Descriptor instead. +func (*CheckPermissionRequest) Descriptor() ([]byte, []int) { + return file_rebac_v1_check_proto_rawDescGZIP(), []int{0} +} + +func (x *CheckPermissionRequest) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *CheckPermissionRequest) GetSubjectid() string { + if x != nil { + return x.Subjectid + } + return "" +} + +func (x *CheckPermissionRequest) GetOperation() string { + if x != nil { + return x.Operation + } + return "" +} + +func (x *CheckPermissionRequest) GetResourcetype() string { + if x != nil { + return x.Resourcetype + } + return "" +} + +func (x *CheckPermissionRequest) GetResourceid() string { + if x != nil { + return x.Resourceid + } + return "" +} + +type CheckPermissionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Result bool `protobuf:"varint,1,opt,name=result,proto3" json:"result,omitempty"` + Auditlog bool `protobuf:"varint,2,opt,name=auditlog,proto3" json:"auditlog,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` +} + +func (x *CheckPermissionResponse) Reset() { + *x = CheckPermissionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rebac_v1_check_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckPermissionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckPermissionResponse) ProtoMessage() {} + +func (x *CheckPermissionResponse) ProtoReflect() protoreflect.Message { + mi := &file_rebac_v1_check_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckPermissionResponse.ProtoReflect.Descriptor instead. +func (*CheckPermissionResponse) Descriptor() ([]byte, []int) { + return file_rebac_v1_check_proto_rawDescGZIP(), []int{1} +} + +func (x *CheckPermissionResponse) GetResult() bool { + if x != nil { + return x.Result + } + return false +} + +func (x *CheckPermissionResponse) GetAuditlog() bool { + if x != nil { + return x.Auditlog + } + return false +} + +func (x *CheckPermissionResponse) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +var File_rebac_v1_check_proto protoreflect.FileDescriptor + +var file_rebac_v1_check_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x72, 0x65, 0x62, 0x61, 0x63, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x62, 0x61, + 0x63, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xb2, 0x01, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x69, 0x64, 0x22, 0x6f, 0x0a, 0x17, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, + 0x64, 0x69, 0x74, 0x6c, 0x6f, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x75, + 0x64, 0x69, 0x74, 0x6c, 0x6f, 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x87, 0x01, 0x0a, 0x05, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x12, 0x7e, 0x0a, 0x0f, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x62, 0x61, + 0x63, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x72, 0x65, 0x62, 0x61, 0x63, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x68, 0x65, + 0x63, 0x6b, 0x42, 0x2c, 0x0a, 0x0c, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x62, 0x61, 0x63, 0x2e, + 0x76, 0x31, 0x50, 0x01, 0x5a, 0x1a, 0x63, 0x69, 0x61, 0x6d, 0x2d, 0x72, 0x65, 0x62, 0x61, 0x63, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x62, 0x61, 0x63, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_rebac_v1_check_proto_rawDescOnce sync.Once + file_rebac_v1_check_proto_rawDescData = file_rebac_v1_check_proto_rawDesc +) + +func file_rebac_v1_check_proto_rawDescGZIP() []byte { + file_rebac_v1_check_proto_rawDescOnce.Do(func() { + file_rebac_v1_check_proto_rawDescData = protoimpl.X.CompressGZIP(file_rebac_v1_check_proto_rawDescData) + }) + return file_rebac_v1_check_proto_rawDescData +} + +var file_rebac_v1_check_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_rebac_v1_check_proto_goTypes = []interface{}{ + (*CheckPermissionRequest)(nil), // 0: api.rebac.v1.CheckPermissionRequest + (*CheckPermissionResponse)(nil), // 1: api.rebac.v1.CheckPermissionResponse +} +var file_rebac_v1_check_proto_depIdxs = []int32{ + 0, // 0: api.rebac.v1.Check.CheckPermission:input_type -> api.rebac.v1.CheckPermissionRequest + 1, // 1: api.rebac.v1.Check.CheckPermission:output_type -> api.rebac.v1.CheckPermissionResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_rebac_v1_check_proto_init() } +func file_rebac_v1_check_proto_init() { + if File_rebac_v1_check_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_rebac_v1_check_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckPermissionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rebac_v1_check_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckPermissionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_rebac_v1_check_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_rebac_v1_check_proto_goTypes, + DependencyIndexes: file_rebac_v1_check_proto_depIdxs, + MessageInfos: file_rebac_v1_check_proto_msgTypes, + }.Build() + File_rebac_v1_check_proto = out.File + file_rebac_v1_check_proto_rawDesc = nil + file_rebac_v1_check_proto_goTypes = nil + file_rebac_v1_check_proto_depIdxs = nil +} diff --git a/api/rebac/v1/check.proto b/api/rebac/v1/check.proto new file mode 100644 index 0000000..d029917 --- /dev/null +++ b/api/rebac/v1/check.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package api.rebac.v1; +import "google/api/annotations.proto"; +option go_package = "ciam-rebac/api/rebac/v1;v1"; +option java_multiple_files = true; +option java_package = "api.rebac.v1"; + +service Check { + rpc CheckPermission (CheckPermissionRequest) returns (CheckPermissionResponse) { + option (google.api.http) = { + post: "/api/authz/v1/check" + body: "*" + }; + }; +} + +message CheckPermissionRequest { + string subject = 1; + string subjectid = 2; + string operation = 3; + string resourcetype = 4; + string resourceid = 5; +} + +message CheckPermissionResponse { + bool result = 1; + bool auditlog = 2; + string description = 3; +} + diff --git a/api/rebac/v1/check_grpc.pb.go b/api/rebac/v1/check_grpc.pb.go new file mode 100644 index 0000000..b54b21e --- /dev/null +++ b/api/rebac/v1/check_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.19.4 +// source: rebac/v1/check.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Check_CheckPermission_FullMethodName = "/api.rebac.v1.Check/CheckPermission" +) + +// CheckClient is the client API for Check service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CheckClient interface { + CheckPermission(ctx context.Context, in *CheckPermissionRequest, opts ...grpc.CallOption) (*CheckPermissionResponse, error) +} + +type checkClient struct { + cc grpc.ClientConnInterface +} + +func NewCheckClient(cc grpc.ClientConnInterface) CheckClient { + return &checkClient{cc} +} + +func (c *checkClient) CheckPermission(ctx context.Context, in *CheckPermissionRequest, opts ...grpc.CallOption) (*CheckPermissionResponse, error) { + out := new(CheckPermissionResponse) + err := c.cc.Invoke(ctx, Check_CheckPermission_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CheckServer is the server API for Check service. +// All implementations must embed UnimplementedCheckServer +// for forward compatibility +type CheckServer interface { + CheckPermission(context.Context, *CheckPermissionRequest) (*CheckPermissionResponse, error) + mustEmbedUnimplementedCheckServer() +} + +// UnimplementedCheckServer must be embedded to have forward compatible implementations. +type UnimplementedCheckServer struct { +} + +func (UnimplementedCheckServer) CheckPermission(context.Context, *CheckPermissionRequest) (*CheckPermissionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckPermission not implemented") +} +func (UnimplementedCheckServer) mustEmbedUnimplementedCheckServer() {} + +// UnsafeCheckServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CheckServer will +// result in compilation errors. +type UnsafeCheckServer interface { + mustEmbedUnimplementedCheckServer() +} + +func RegisterCheckServer(s grpc.ServiceRegistrar, srv CheckServer) { + s.RegisterService(&Check_ServiceDesc, srv) +} + +func _Check_CheckPermission_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckPermissionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CheckServer).CheckPermission(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Check_CheckPermission_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CheckServer).CheckPermission(ctx, req.(*CheckPermissionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Check_ServiceDesc is the grpc.ServiceDesc for Check service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Check_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.rebac.v1.Check", + HandlerType: (*CheckServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CheckPermission", + Handler: _Check_CheckPermission_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "rebac/v1/check.proto", +} diff --git a/api/rebac/v1/check_http.pb.go b/api/rebac/v1/check_http.pb.go new file mode 100644 index 0000000..c1b3fae --- /dev/null +++ b/api/rebac/v1/check_http.pb.go @@ -0,0 +1,78 @@ +// Code generated by protoc-gen-go-http. DO NOT EDIT. +// versions: +// - protoc-gen-go-http v2.7.2 +// - protoc v3.19.4 +// source: rebac/v1/check.proto + +package v1 + +import ( + context "context" + http "github.com/go-kratos/kratos/v2/transport/http" + binding "github.com/go-kratos/kratos/v2/transport/http/binding" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the kratos package it is being compiled against. +var _ = new(context.Context) +var _ = binding.EncodeURL + +const _ = http.SupportPackageIsVersion1 + +const OperationCheckCheckPermission = "/api.rebac.v1.Check/CheckPermission" + +type CheckHTTPServer interface { + CheckPermission(context.Context, *CheckPermissionRequest) (*CheckPermissionResponse, error) +} + +func RegisterCheckHTTPServer(s *http.Server, srv CheckHTTPServer) { + r := s.Route("/") + r.POST("/api/authz/v1/check", _Check_CheckPermission0_HTTP_Handler(srv)) +} + +func _Check_CheckPermission0_HTTP_Handler(srv CheckHTTPServer) func(ctx http.Context) error { + return func(ctx http.Context) error { + var in CheckPermissionRequest + if err := ctx.Bind(&in); err != nil { + return err + } + if err := ctx.BindQuery(&in); err != nil { + return err + } + http.SetOperation(ctx, OperationCheckCheckPermission) + h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.CheckPermission(ctx, req.(*CheckPermissionRequest)) + }) + out, err := h(ctx, &in) + if err != nil { + return err + } + reply := out.(*CheckPermissionResponse) + return ctx.Result(200, reply) + } +} + +type CheckHTTPClient interface { + CheckPermission(ctx context.Context, req *CheckPermissionRequest, opts ...http.CallOption) (rsp *CheckPermissionResponse, err error) +} + +type CheckHTTPClientImpl struct { + cc *http.Client +} + +func NewCheckHTTPClient(client *http.Client) CheckHTTPClient { + return &CheckHTTPClientImpl{client} +} + +func (c *CheckHTTPClientImpl) CheckPermission(ctx context.Context, in *CheckPermissionRequest, opts ...http.CallOption) (*CheckPermissionResponse, error) { + var out CheckPermissionResponse + pattern := "/api/authz/v1/check" + path := binding.EncodeURL(pattern, in, false) + opts = append(opts, http.Operation(OperationCheckCheckPermission)) + opts = append(opts, http.PathTemplate(pattern)) + err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...) + if err != nil { + return nil, err + } + return &out, err +} diff --git a/cmd/ciam-rebac/wire_gen.go b/cmd/ciam-rebac/wire_gen.go index d538c54..532ebda 100644 --- a/cmd/ciam-rebac/wire_gen.go +++ b/cmd/ciam-rebac/wire_gen.go @@ -33,8 +33,10 @@ func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (* deleteRelationshipsUsecase := biz.NewDeleteRelationshipsUsecase(spiceDbRepository, logger) relationshipsService := service.NewRelationshipsService(logger, createRelationshipsUsecase, readRelationshipsUsecase, deleteRelationshipsUsecase) healthService := service.NewHealthService() - grpcServer := server.NewGRPCServer(confServer, relationshipsService, healthService, logger) - httpServer := server.NewHTTPServer(confServer, relationshipsService, healthService, logger) + checkPermissionUsecase := biz.NewCheckPermissionUsecase(spiceDbRepository, logger) + checkService := service.NewCheckService(logger, checkPermissionUsecase) + grpcServer := server.NewGRPCServer(confServer, relationshipsService, healthService, checkService, logger) + httpServer := server.NewHTTPServer(confServer, relationshipsService, healthService, checkService, logger) app := newApp(logger, grpcServer, httpServer) return app, func() { cleanup() diff --git a/go.mod b/go.mod index 982cdd5..8900e94 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module ciam-rebac go 1.21 - require ( github.com/authzed/authzed-go v0.11.1 github.com/authzed/grpcutil v0.0.0-20230908193239-4286bb1d6403 @@ -34,6 +33,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/form/v4 v4.2.0 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/subcommands v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect diff --git a/go.sum b/go.sum index 047b063..d8fe262 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,7 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 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= diff --git a/internal/biz/biz.go b/internal/biz/biz.go index a042807..7614ffe 100644 --- a/internal/biz/biz.go +++ b/internal/biz/biz.go @@ -5,4 +5,4 @@ import ( ) // ProviderSet is biz providers. -var ProviderSet = wire.NewSet(NewCreateRelationshipsUsecase, NewReadRelationshipsUsecase, NewDeleteRelationshipsUsecase) +var ProviderSet = wire.NewSet(NewCreateRelationshipsUsecase, NewReadRelationshipsUsecase, NewDeleteRelationshipsUsecase, NewCheckPermissionUsecase) diff --git a/internal/biz/relationships.go b/internal/biz/relationships.go index 9ff495a..c11634c 100644 --- a/internal/biz/relationships.go +++ b/internal/biz/relationships.go @@ -11,11 +11,26 @@ import ( type TouchSemantics bool type ZanzibarRepository interface { + CheckPermission(ctx context.Context, request *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) CreateRelationships(context.Context, []*v1.Relationship, TouchSemantics) error ReadRelationships(context.Context, *v1.RelationshipFilter) ([]*v1.Relationship, error) DeleteRelationships(context.Context, *v1.RelationshipFilter) error } +type CheckPermissionUsecase struct { + repo ZanzibarRepository + log *log.Helper +} + +func NewCheckPermissionUsecase(repo ZanzibarRepository, logger log.Logger) *CheckPermissionUsecase { + return &CheckPermissionUsecase{repo: repo, log: log.NewHelper(logger)} +} + +func (rc *CheckPermissionUsecase) CheckPermission(ctx context.Context, check *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) { + rc.log.WithContext(ctx).Infof("CheckPermission: %v", check) + return rc.repo.CheckPermission(ctx, check) +} + type CreateRelationshipsUsecase struct { repo ZanzibarRepository log *log.Helper diff --git a/internal/data/spicedb.go b/internal/data/spicedb.go index 3c0f38e..9424f77 100644 --- a/internal/data/spicedb.go +++ b/internal/data/spicedb.go @@ -149,6 +149,32 @@ func (s *SpiceDbRepository) DeleteRelationships(ctx context.Context, filter *api return nil } +func (s *SpiceDbRepository) CheckPermission(ctx context.Context, check *apiV1.CheckPermissionRequest) (*apiV1.CheckPermissionResponse, error) { + subject := &v1.SubjectReference{Object: &v1.ObjectReference{ + ObjectType: check.Subject, + ObjectId: check.Subjectid, + }} + + object := &v1.ObjectReference{ + ObjectType: check.Resourcetype, + ObjectId: check.Resourceid, + } + checkResponse, err := s.client.CheckPermission(ctx, &v1.CheckPermissionRequest{ + Resource: object, + Permission: check.Operation, + Subject: subject, + }) + if err != nil { + return &apiV1.CheckPermissionResponse{Result: false}, err + } + + if checkResponse.Permissionship == v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION { + return &apiV1.CheckPermissionResponse{Result: true}, nil + } + + return &apiV1.CheckPermissionResponse{Result: false}, nil +} + func createSpiceDbRelationshipFilter(filter *apiV1.RelationshipFilter) *v1.RelationshipFilter { spiceDbRelationshipFilter := &v1.RelationshipFilter{ ResourceType: filter.GetObjectType(), diff --git a/internal/data/spicedb_test.go b/internal/data/spicedb_test.go index 865c33a..3a0f088 100644 --- a/internal/data/spicedb_test.go +++ b/internal/data/spicedb_test.go @@ -262,6 +262,66 @@ func TestWriteReadBackDeleteAndReadBackRelationships(t *testing.T) { } +func TestSpiceDbRepository_CheckPermission(t *testing.T) { + t.Parallel() + + ctx := context.Background() + spiceDbRepo, err := container.CreateSpiceDbRepository() + if !assert.NoError(t, err) { + return + } + + //group:bob_club#member@user:bob + //workspace:test#user_grant@role_binding:rb_test + //role_binding:rb_test#granted@role:rl1 + //role_binding:rb_test#subject@user:bob + //role:rl1#view_the_thing@user:* + rels := []*apiV1.Relationship{ + createRelationship("bob", "user", "", "member", "group", "bob_club"), + createRelationship("rb_test", "role_binding", "", "user_grant", "workspace", "test"), + createRelationship("rl1", "role", "", "granted", "role_binding", "rb_test"), + createRelationship("bob", "user", "", "subject", "role_binding", "rb_test"), + createRelationship("*", "user", "", "view_the_thing", "role", "rl1"), + } + + err = spiceDbRepo.CreateRelationships(ctx, rels, biz.TouchSemantics(true)) + if !assert.NoError(t, err) { + return + } + + // zed permission check workspace:test view_the_thing user:bob --explain + check := apiV1.CheckPermissionRequest{ + Subject: "user", Subjectid: "bob", Operation: "view_the_thing", Resourcetype: "workspace", Resourceid: "test", + } + resp, err := spiceDbRepo.CheckPermission(ctx, &check) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, true, resp.Result) + + //Remove // role_binding:rb_test#subject@user:bob + err = spiceDbRepo.DeleteRelationships(ctx, &apiV1.RelationshipFilter{ + ObjectId: "rb_test", + ObjectType: "role_binding", + Relation: "subject", + SubjectFilter: &apiV1.SubjectFilter{ + SubjectId: "bob", + SubjectType: "user", + }, + }) + if !assert.NoError(t, err) { + return + } + + checkv2 := apiV1.CheckPermissionRequest{ + Subject: "user", Subjectid: "bob", Operation: "view_the_thing", Resourcetype: "workspace", Resourceid: "test", + } + resp2, err := spiceDbRepo.CheckPermission(ctx, &checkv2) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, false, resp2.Result) +} func createRelationship(subjectId string, subjectType string, subjectRelationship string, relationship string, objectType string, objectId string) *apiV1.Relationship { subject := &apiV1.SubjectReference{ Object: &apiV1.ObjectReference{ diff --git a/internal/server/grpc.go b/internal/server/grpc.go index 855edee..be5ede8 100644 --- a/internal/server/grpc.go +++ b/internal/server/grpc.go @@ -12,7 +12,7 @@ import ( ) // NewGRPCServer new a gRPC server. -func NewGRPCServer(c *conf.Server, relations *service.RelationshipsService, health *service.HealthService, logger log.Logger) *grpc.Server { +func NewGRPCServer(c *conf.Server, relations *service.RelationshipsService, health *service.HealthService, check *service.CheckService, logger log.Logger) *grpc.Server { var opts = []grpc.ServerOption{ grpc.Middleware( recovery.Recovery(), @@ -29,6 +29,7 @@ func NewGRPCServer(c *conf.Server, relations *service.RelationshipsService, heal } srv := grpc.NewServer(opts...) v1.RegisterRelationshipsServer(srv, relations) + v1.RegisterCheckServer(srv, check) h.RegisterHealthServer(srv, health) return srv } diff --git a/internal/server/http.go b/internal/server/http.go index 1e6428b..9e681d6 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -12,7 +12,7 @@ import ( ) // NewHTTPServer new an HTTP server. -func NewHTTPServer(c *conf.Server, relationships *service.RelationshipsService, health *service.HealthService, logger log.Logger) *http.Server { +func NewHTTPServer(c *conf.Server, relationships *service.RelationshipsService, health *service.HealthService, check *service.CheckService, logger log.Logger) *http.Server { var opts = []http.ServerOption{ http.Middleware( recovery.Recovery(), @@ -30,6 +30,7 @@ func NewHTTPServer(c *conf.Server, relationships *service.RelationshipsService, srv := http.NewServer(opts...) v1.RegisterRelationshipsHTTPServer(srv, relationships) + v1.RegisterCheckHTTPServer(srv, check) h.RegisterHealthHTTPServer(srv, health) return srv } diff --git a/internal/service/check.go b/internal/service/check.go new file mode 100644 index 0000000..d6b1891 --- /dev/null +++ b/internal/service/check.go @@ -0,0 +1,31 @@ +package service + +import ( + "ciam-rebac/internal/biz" + "context" + "github.com/go-kratos/kratos/v2/log" + + pb "ciam-rebac/api/rebac/v1" +) + +type CheckService struct { + pb.UnimplementedCheckServer + check *biz.CheckPermissionUsecase + log *log.Helper +} + +func NewCheckService(logger log.Logger, checkUseCase *biz.CheckPermissionUsecase) *CheckService { + return &CheckService{ + check: checkUseCase, + log: log.NewHelper(logger), + } +} + +func (s *CheckService) CheckPermission(ctx context.Context, req *pb.CheckPermissionRequest) (*pb.CheckPermissionResponse, error) { + s.log.Infof("Check permission: %v", req) + resp, err := s.check.CheckPermission(ctx, req) + if err != nil { + return nil, err + } + return &pb.CheckPermissionResponse{Result: resp.Result, Auditlog: false}, nil +} diff --git a/internal/service/service.go b/internal/service/service.go index 5f7cfe2..cac484b 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -3,4 +3,4 @@ package service import "github.com/google/wire" // ProviderSet is service providers. -var ProviderSet = wire.NewSet(NewRelationshipsService, NewHealthService) +var ProviderSet = wire.NewSet(NewRelationshipsService, NewHealthService, NewCheckService) diff --git a/openapi.yaml b/openapi.yaml index 900a2ea..c353aee 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -6,6 +6,24 @@ info: title: "" version: 0.0.1 paths: + /api/authz/v1/check: + post: + tags: + - Check + operationId: Check_CheckPermission + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/api.rebac.v1.CheckPermissionRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/api.rebac.v1.CheckPermissionResponse' /api/authz/v1/relationships: get: tags: @@ -128,6 +146,28 @@ components: api.health.v1.GetReadyzReply: type: object properties: {} + api.rebac.v1.CheckPermissionRequest: + type: object + properties: + subject: + type: string + subjectid: + type: string + operation: + type: string + resourcetype: + type: string + resourceid: + type: string + api.rebac.v1.CheckPermissionResponse: + type: object + properties: + result: + type: boolean + auditlog: + type: boolean + description: + type: string api.rebac.v1.CreateRelationshipsRequest: type: object properties: @@ -174,5 +214,6 @@ components: object: $ref: '#/components/schemas/api.rebac.v1.ObjectReference' tags: + - name: Check - name: Health - name: Relationships