diff --git a/Dockerfile b/Dockerfile index 1f874e9e..62ad9070 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN web_apps_src/update-admin.sh RUN web_apps_src/update-web.sh -FROM golang:1.19.2-alpine3.16 as builder +FROM golang:1.22-alpine3.19 as builder # Copy the code from the host and compile it WORKDIR $GOPATH/src/github.com/madappgang/identifo @@ -16,7 +16,7 @@ RUN go mod download RUN go build -o plugins/bin/ github.com/madappgang/identifo/v2/plugins/... RUN go build -o /identifo . -FROM alpine:3.16 +FROM alpine:3.19 RUN apk --no-cache add ca-certificates WORKDIR / diff --git a/Makefile b/Makefile index 867537a4..a40c2add 100644 --- a/Makefile +++ b/Makefile @@ -75,3 +75,8 @@ gen_user_storage_plugin: protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ storage/grpc/proto/user.proto + +gen_impersonation_plugin: + protoc --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=. --go-grpc_opt=paths=source_relative \ + impersonation/grpc/impersonation_provider/impersonation.proto diff --git a/config/configurator.go b/config/configurator.go index 3552922b..4b7c389f 100644 --- a/config/configurator.go +++ b/config/configurator.go @@ -6,6 +6,8 @@ import ( "log" "time" + imp "github.com/madappgang/identifo/v2/impersonation/local" + impPlugin "github.com/madappgang/identifo/v2/impersonation/plugin" "github.com/madappgang/identifo/v2/model" "github.com/madappgang/identifo/v2/server" "github.com/madappgang/identifo/v2/services/mail" @@ -181,11 +183,18 @@ func NewServer(config model.ConfigurationStorage, restartChan chan<- bool) (mode sessionS := model.NewSessionManager(settings.SessionStorage.SessionDuration, session) + impS, err := NewImpersonationProvider(settings.Impersonation) + if err != nil { + log.Printf("Error creating impersonation provider %v", err) + errs = append(errs, fmt.Errorf("error creating impersonation provider: %v", err)) + } + srvs := model.ServerServices{ - SMS: sms, - Email: email, - Token: tokenS, - Session: sessionS, + SMS: sms, + Email: email, + Token: tokenS, + Session: sessionS, + Impersonation: impS, } server, err := server.NewServer(sc, srvs, errs, restartChan) @@ -211,3 +220,18 @@ func NewTokenService(settings model.GeneralServerSettings, storages model.Server ) return tokenService, err } + +func NewImpersonationProvider(settings model.ImpersonationSettings) (model.ImpersonationProvider, error) { + switch settings.Type { + case model.ImpersonationServiceTypeNone, "": + return nil, nil + case model.ImpersonationServiceTypeRole: + return imp.NewAccessRoleImpersonator(settings.Role.AllowedRoles), nil + case model.ImpersonationServiceTypeScope: + return imp.NewScopeImpersonator(settings.Scope.AllowedScopes), nil + case model.ImpersonationServiceTypePlugin: + return impPlugin.NewImpersonationProvider(settings.Plugin, time.Second) + } + + return nil, nil +} diff --git a/go.mod b/go.mod index 5459e386..5190d760 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/madappgang/identifo/v2 -go 1.19 +go 1.22 require ( github.com/MadAppGang/httplog v1.3.0 @@ -35,12 +35,12 @@ require ( github.com/urfave/negroni v1.0.0 github.com/xlzd/gotp v0.0.0-20220915034741-1546cf172da8 go.etcd.io/bbolt v1.3.6 - go.mongodb.org/mongo-driver v1.13.0 - golang.org/x/crypto v0.11.0 + go.mongodb.org/mongo-driver v1.15.0 + golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20221019170559-20944726eadf golang.org/x/net v0.12.0 golang.org/x/oauth2 v0.10.0 - golang.org/x/text v0.11.0 + golang.org/x/text v0.14.0 google.golang.org/grpc v1.58.3 google.golang.org/protobuf v1.31.0 gopkg.in/go-playground/validator.v9 v9.31.0 @@ -98,7 +98,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/sys v0.15.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect diff --git a/go.sum b/go.sum index 26ba977c..3bb3bb7d 100644 --- a/go.sum +++ b/go.sum @@ -207,11 +207,13 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -286,6 +288,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.3.0 h1:kUMoxMoQG3ogk/QWyKh3zibV7BKZ+xBpWil1cTylVqc= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.22.1 h1:pY8O4lBfsHKZHM/6nrxkhVPUznOlIu3quZcKP/M20KI= @@ -353,8 +356,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWUfY= -go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -371,9 +374,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -532,8 +534,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= @@ -547,9 +549,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/impersonation/grpc/impersonation_provider/impersonation.pb.go b/impersonation/grpc/impersonation_provider/impersonation.pb.go new file mode 100644 index 00000000..32284888 --- /dev/null +++ b/impersonation/grpc/impersonation_provider/impersonation.pb.go @@ -0,0 +1,472 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v4.22.4 +// source: impersonation/grpc/impersonation_provider/impersonation.proto + +package impersonation_provider + +import ( + 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 User struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + Active bool `protobuf:"varint,3,opt,name=active,proto3" json:"active,omitempty"` + AccessRole string `protobuf:"bytes,4,opt,name=access_role,json=accessRole,proto3" json:"access_role,omitempty"` + Anonymous bool `protobuf:"varint,5,opt,name=anonymous,proto3" json:"anonymous,omitempty"` + Scopes []string `protobuf:"bytes,6,rep,name=scopes,proto3" json:"scopes,omitempty"` +} + +func (x *User) Reset() { + *x = User{} + if protoimpl.UnsafeEnabled { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_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 User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +func (x *User) GetAccessRole() string { + if x != nil { + return x.AccessRole + } + return "" +} + +func (x *User) GetAnonymous() bool { + if x != nil { + return x.Anonymous + } + return false +} + +func (x *User) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +type CanImpersonateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AppId string `protobuf:"bytes,1,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` + AdminUser *User `protobuf:"bytes,2,opt,name=admin_user,json=adminUser,proto3" json:"admin_user,omitempty"` + ImpersonatedUser *User `protobuf:"bytes,3,opt,name=impersonated_user,json=impersonatedUser,proto3" json:"impersonated_user,omitempty"` +} + +func (x *CanImpersonateRequest) Reset() { + *x = CanImpersonateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CanImpersonateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CanImpersonateRequest) ProtoMessage() {} + +func (x *CanImpersonateRequest) ProtoReflect() protoreflect.Message { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_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 CanImpersonateRequest.ProtoReflect.Descriptor instead. +func (*CanImpersonateRequest) Descriptor() ([]byte, []int) { + return file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescGZIP(), []int{1} +} + +func (x *CanImpersonateRequest) GetAppId() string { + if x != nil { + return x.AppId + } + return "" +} + +func (x *CanImpersonateRequest) GetAdminUser() *User { + if x != nil { + return x.AdminUser + } + return nil +} + +func (x *CanImpersonateRequest) GetImpersonatedUser() *User { + if x != nil { + return x.ImpersonatedUser + } + return nil +} + +type CanImpersonateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` +} + +func (x *CanImpersonateResponse) Reset() { + *x = CanImpersonateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CanImpersonateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CanImpersonateResponse) ProtoMessage() {} + +func (x *CanImpersonateResponse) ProtoReflect() protoreflect.Message { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[2] + 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 CanImpersonateResponse.ProtoReflect.Descriptor instead. +func (*CanImpersonateResponse) Descriptor() ([]byte, []int) { + return file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescGZIP(), []int{2} +} + +func (x *CanImpersonateResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +type CloseRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CloseRequest) Reset() { + *x = CloseRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CloseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseRequest) ProtoMessage() {} + +func (x *CloseRequest) ProtoReflect() protoreflect.Message { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[3] + 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 CloseRequest.ProtoReflect.Descriptor instead. +func (*CloseRequest) Descriptor() ([]byte, []int) { + return file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescGZIP(), []int{3} +} + +type CloseResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CloseResponse) Reset() { + *x = CloseResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CloseResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseResponse) ProtoMessage() {} + +func (x *CloseResponse) ProtoReflect() protoreflect.Message { + mi := &file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[4] + 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 CloseResponse.ProtoReflect.Descriptor instead. +func (*CloseResponse) Descriptor() ([]byte, []int) { + return file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescGZIP(), []int{4} +} + +var File_impersonation_grpc_impersonation_provider_impersonation_proto protoreflect.FileDescriptor + +var file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDesc = []byte{ + 0x0a, 0x3d, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2f, 0x69, 0x6d, 0x70, 0x65, + 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x16, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x9b, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x6f, 0x6c, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x73, 0x22, 0xb6, 0x01, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x49, 0x6d, 0x70, + 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x61, 0x70, 0x70, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x0a, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, + 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6d, 0x70, + 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x49, 0x0a, 0x11, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x10, 0x69, 0x6d, + 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x22, 0x28, + 0x0a, 0x16, 0x43, 0x61, 0x6e, 0x49, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x22, 0x0e, 0x0a, 0x0c, 0x43, 0x6c, 0x6f, 0x73, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x6c, 0x6f, 0x73, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xde, 0x01, 0x0a, 0x15, 0x49, 0x6d, + 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x6f, 0x0a, 0x0e, 0x43, 0x61, 0x6e, 0x49, 0x6d, 0x70, 0x65, 0x72, 0x73, + 0x6f, 0x6e, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x2e, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x43, + 0x61, 0x6e, 0x49, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x43, 0x61, + 0x6e, 0x49, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x05, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x24, 0x2e, + 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x43, 0x6c, 0x6f, + 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x61, 0x64, 0x61, 0x70, 0x70, 0x67, + 0x61, 0x6e, 0x67, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x6f, 0x2f, 0x76, 0x32, 0x2f, + 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x67, 0x72, + 0x70, 0x63, 0x2f, 0x69, 0x6d, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescOnce sync.Once + file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescData = file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDesc +) + +func file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescGZIP() []byte { + file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescOnce.Do(func() { + file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescData = protoimpl.X.CompressGZIP(file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescData) + }) + return file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDescData +} + +var file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_impersonation_grpc_impersonation_provider_impersonation_proto_goTypes = []interface{}{ + (*User)(nil), // 0: impersonation_provider.User + (*CanImpersonateRequest)(nil), // 1: impersonation_provider.CanImpersonateRequest + (*CanImpersonateResponse)(nil), // 2: impersonation_provider.CanImpersonateResponse + (*CloseRequest)(nil), // 3: impersonation_provider.CloseRequest + (*CloseResponse)(nil), // 4: impersonation_provider.CloseResponse +} +var file_impersonation_grpc_impersonation_provider_impersonation_proto_depIdxs = []int32{ + 0, // 0: impersonation_provider.CanImpersonateRequest.admin_user:type_name -> impersonation_provider.User + 0, // 1: impersonation_provider.CanImpersonateRequest.impersonated_user:type_name -> impersonation_provider.User + 1, // 2: impersonation_provider.ImpersonationProvider.CanImpersonate:input_type -> impersonation_provider.CanImpersonateRequest + 3, // 3: impersonation_provider.ImpersonationProvider.Close:input_type -> impersonation_provider.CloseRequest + 2, // 4: impersonation_provider.ImpersonationProvider.CanImpersonate:output_type -> impersonation_provider.CanImpersonateResponse + 4, // 5: impersonation_provider.ImpersonationProvider.Close:output_type -> impersonation_provider.CloseResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_impersonation_grpc_impersonation_provider_impersonation_proto_init() } +func file_impersonation_grpc_impersonation_provider_impersonation_proto_init() { + if File_impersonation_grpc_impersonation_provider_impersonation_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*User); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CanImpersonateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CanImpersonateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CloseRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CloseResponse); 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_impersonation_grpc_impersonation_provider_impersonation_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_impersonation_grpc_impersonation_provider_impersonation_proto_goTypes, + DependencyIndexes: file_impersonation_grpc_impersonation_provider_impersonation_proto_depIdxs, + MessageInfos: file_impersonation_grpc_impersonation_provider_impersonation_proto_msgTypes, + }.Build() + File_impersonation_grpc_impersonation_provider_impersonation_proto = out.File + file_impersonation_grpc_impersonation_provider_impersonation_proto_rawDesc = nil + file_impersonation_grpc_impersonation_provider_impersonation_proto_goTypes = nil + file_impersonation_grpc_impersonation_provider_impersonation_proto_depIdxs = nil +} diff --git a/impersonation/grpc/impersonation_provider/impersonation.proto b/impersonation/grpc/impersonation_provider/impersonation.proto new file mode 100644 index 00000000..54b9b170 --- /dev/null +++ b/impersonation/grpc/impersonation_provider/impersonation.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; +package impersonation_provider; +option go_package = "github.com/madappgang/identifo/v2/impersonation/grpc/impersonation_provider"; + + +message User { + string id = 1; + string email = 2; + bool active = 3; + string access_role = 4; + bool anonymous = 5; + repeated string scopes = 6; +} + +message CanImpersonateRequest { + string app_id = 1; + User admin_user = 2; + User impersonated_user = 3; +} + +message CanImpersonateResponse { + bool ok = 1; +} + +message CloseRequest {} + +message CloseResponse {} + + +service ImpersonationProvider { + rpc CanImpersonate(CanImpersonateRequest) returns (CanImpersonateResponse); + + rpc Close(CloseRequest) returns (CloseResponse); +} diff --git a/impersonation/grpc/impersonation_provider/impersonation_grpc.pb.go b/impersonation/grpc/impersonation_provider/impersonation_grpc.pb.go new file mode 100644 index 00000000..8a728a4e --- /dev/null +++ b/impersonation/grpc/impersonation_provider/impersonation_grpc.pb.go @@ -0,0 +1,146 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.22.4 +// source: impersonation/grpc/impersonation_provider/impersonation.proto + +package impersonation_provider + +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 ( + ImpersonationProvider_CanImpersonate_FullMethodName = "/impersonation_provider.ImpersonationProvider/CanImpersonate" + ImpersonationProvider_Close_FullMethodName = "/impersonation_provider.ImpersonationProvider/Close" +) + +// ImpersonationProviderClient is the client API for ImpersonationProvider 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 ImpersonationProviderClient interface { + CanImpersonate(ctx context.Context, in *CanImpersonateRequest, opts ...grpc.CallOption) (*CanImpersonateResponse, error) + Close(ctx context.Context, in *CloseRequest, opts ...grpc.CallOption) (*CloseResponse, error) +} + +type impersonationProviderClient struct { + cc grpc.ClientConnInterface +} + +func NewImpersonationProviderClient(cc grpc.ClientConnInterface) ImpersonationProviderClient { + return &impersonationProviderClient{cc} +} + +func (c *impersonationProviderClient) CanImpersonate(ctx context.Context, in *CanImpersonateRequest, opts ...grpc.CallOption) (*CanImpersonateResponse, error) { + out := new(CanImpersonateResponse) + err := c.cc.Invoke(ctx, ImpersonationProvider_CanImpersonate_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *impersonationProviderClient) Close(ctx context.Context, in *CloseRequest, opts ...grpc.CallOption) (*CloseResponse, error) { + out := new(CloseResponse) + err := c.cc.Invoke(ctx, ImpersonationProvider_Close_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ImpersonationProviderServer is the server API for ImpersonationProvider service. +// All implementations must embed UnimplementedImpersonationProviderServer +// for forward compatibility +type ImpersonationProviderServer interface { + CanImpersonate(context.Context, *CanImpersonateRequest) (*CanImpersonateResponse, error) + Close(context.Context, *CloseRequest) (*CloseResponse, error) + mustEmbedUnimplementedImpersonationProviderServer() +} + +// UnimplementedImpersonationProviderServer must be embedded to have forward compatible implementations. +type UnimplementedImpersonationProviderServer struct { +} + +func (UnimplementedImpersonationProviderServer) CanImpersonate(context.Context, *CanImpersonateRequest) (*CanImpersonateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CanImpersonate not implemented") +} +func (UnimplementedImpersonationProviderServer) Close(context.Context, *CloseRequest) (*CloseResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Close not implemented") +} +func (UnimplementedImpersonationProviderServer) mustEmbedUnimplementedImpersonationProviderServer() {} + +// UnsafeImpersonationProviderServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ImpersonationProviderServer will +// result in compilation errors. +type UnsafeImpersonationProviderServer interface { + mustEmbedUnimplementedImpersonationProviderServer() +} + +func RegisterImpersonationProviderServer(s grpc.ServiceRegistrar, srv ImpersonationProviderServer) { + s.RegisterService(&ImpersonationProvider_ServiceDesc, srv) +} + +func _ImpersonationProvider_CanImpersonate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CanImpersonateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ImpersonationProviderServer).CanImpersonate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ImpersonationProvider_CanImpersonate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ImpersonationProviderServer).CanImpersonate(ctx, req.(*CanImpersonateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ImpersonationProvider_Close_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ImpersonationProviderServer).Close(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ImpersonationProvider_Close_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ImpersonationProviderServer).Close(ctx, req.(*CloseRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ImpersonationProvider_ServiceDesc is the grpc.ServiceDesc for ImpersonationProvider service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ImpersonationProvider_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "impersonation_provider.ImpersonationProvider", + HandlerType: (*ImpersonationProviderServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CanImpersonate", + Handler: _ImpersonationProvider_CanImpersonate_Handler, + }, + { + MethodName: "Close", + Handler: _ImpersonationProvider_Close_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "impersonation/grpc/impersonation_provider/impersonation.proto", +} diff --git a/impersonation/grpc/shared/client.go b/impersonation/grpc/shared/client.go new file mode 100644 index 00000000..337a8305 --- /dev/null +++ b/impersonation/grpc/shared/client.go @@ -0,0 +1,79 @@ +package grpc + +import ( + "context" + "io" + "time" + + pp "github.com/madappgang/identifo/v2/impersonation/grpc/impersonation_provider" + "github.com/madappgang/identifo/v2/model" + ggrpc "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func NewImpersonationService(address string, timeout time.Duration) (model.ImpersonationProvider, error) { + conn, err := ggrpc.Dial( + address, + ggrpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + + if timeout == 0 { + timeout = time.Second + } + + client := pp.NewImpersonationProviderClient(conn) + + return &GRPCClient{ + Client: client, + Timeout: timeout, + Closable: conn, + }, nil +} + +type GRPCClient struct { + Client pp.ImpersonationProviderClient + Timeout time.Duration + + Closable io.Closer +} + +func (p *GRPCClient) CanImpersonate(ctx context.Context, appID string, adminUser, impUser model.User) (bool, error) { + ctx, cancel := context.WithTimeout(ctx, p.Timeout) + defer cancel() + + resp, err := p.Client.CanImpersonate(ctx, &pp.CanImpersonateRequest{ + AppId: appID, + AdminUser: grpcUser(adminUser), + ImpersonatedUser: grpcUser(impUser), + }) + if err != nil { + return false, err + } + + return resp.Ok, nil +} + +func grpcUser(u model.User) *pp.User { + return &pp.User{ + Id: u.ID, + Email: u.Email, + Active: u.Active, + AccessRole: u.AccessRole, + Anonymous: u.Anonymous, + Scopes: u.Scopes, + } +} + +func (p *GRPCClient) Close() error { + ctx, cancel := context.WithTimeout(context.Background(), p.Timeout) + defer cancel() + + p.Client.Close(ctx, &pp.CloseRequest{}) + if p.Closable != nil { + return p.Closable.Close() + } + + return nil +} diff --git a/impersonation/grpc/shared/server.go b/impersonation/grpc/shared/server.go new file mode 100644 index 00000000..f0e616b7 --- /dev/null +++ b/impersonation/grpc/shared/server.go @@ -0,0 +1,51 @@ +package grpc + +import ( + "context" + "io" + + pp "github.com/madappgang/identifo/v2/impersonation/grpc/impersonation_provider" + "github.com/madappgang/identifo/v2/model" +) + +// Here is the gRPC server that GRPCClient talks to. +type GRPCServer struct { + // This is the real implementation + Impl model.ImpersonationProvider + pp.UnimplementedImpersonationProviderServer +} + +func (s *GRPCServer) CanImpersonate(ctx context.Context, req *pp.CanImpersonateRequest) (*pp.CanImpersonateResponse, error) { + ok, err := s.Impl.CanImpersonate( + ctx, + req.AppId, + userFromGRPC(req.AdminUser), + userFromGRPC(req.ImpersonatedUser), + ) + if err != nil { + return nil, err + } + + return &pp.CanImpersonateResponse{ + Ok: ok, + }, nil +} + +func userFromGRPC(u *pp.User) model.User { + return model.User{ + ID: u.Id, + Email: u.Email, + Active: u.Active, + AccessRole: u.AccessRole, + Anonymous: u.Anonymous, + Scopes: u.Scopes, + } +} + +func (s *GRPCServer) Close(ctx context.Context, _ *pp.CloseRequest) (*pp.CloseResponse, error) { + c, ok := s.Impl.(io.Closer) + if ok { + c.Close() + } + return &pp.CloseResponse{}, nil +} diff --git a/impersonation/local/access.go b/impersonation/local/access.go new file mode 100644 index 00000000..781f2578 --- /dev/null +++ b/impersonation/local/access.go @@ -0,0 +1,28 @@ +package local + +import ( + "context" + "slices" + + "github.com/madappgang/identifo/v2/model" +) + +type AccessRoleImpersonator struct { + allowedAccessRoles []string +} + +func NewAccessRoleImpersonator(allowedAccessRoles []string) *AccessRoleImpersonator { + return &AccessRoleImpersonator{allowedAccessRoles: allowedAccessRoles} +} + +func (si *AccessRoleImpersonator) CanImpersonate(ctx context.Context, appID string, adminUser model.User, user model.User) (bool, error) { + if !adminUser.Active || adminUser.Anonymous { + return false, nil + } + + if slices.Contains(si.allowedAccessRoles, adminUser.AccessRole) { + return true, nil + } + + return false, nil +} diff --git a/impersonation/local/scope.go b/impersonation/local/scope.go new file mode 100644 index 00000000..d5842284 --- /dev/null +++ b/impersonation/local/scope.go @@ -0,0 +1,30 @@ +package local + +import ( + "context" + "slices" + + "github.com/madappgang/identifo/v2/model" +) + +type ScopeImpersonator struct { + allowedScopes []string +} + +func NewScopeImpersonator(allowedScopes []string) *ScopeImpersonator { + return &ScopeImpersonator{allowedScopes: allowedScopes} +} + +func (si *ScopeImpersonator) CanImpersonate(ctx context.Context, appID string, adminUser model.User, user model.User) (bool, error) { + if !adminUser.Active || adminUser.Anonymous { + return false, nil + } + + for _, scope := range si.allowedScopes { + if slices.Contains(adminUser.Scopes, scope) { + return true, nil + } + } + + return false, nil +} diff --git a/impersonation/plugin/provider.go b/impersonation/plugin/provider.go new file mode 100644 index 00000000..6419db09 --- /dev/null +++ b/impersonation/plugin/provider.go @@ -0,0 +1,59 @@ +package plugin + +import ( + "os/exec" + "time" + + "github.com/hashicorp/go-plugin" + grpcShared "github.com/madappgang/identifo/v2/impersonation/grpc/shared" + "github.com/madappgang/identifo/v2/impersonation/plugin/shared" + "github.com/madappgang/identifo/v2/model" +) + +func NewImpersonationProvider(settings model.PluginSettings, timeout time.Duration) (model.ImpersonationProvider, error) { + var err error + params := []string{} + for k, v := range settings.Params { + params = append(params, "-"+k) + params = append(params, v) + } + + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: shared.Handshake, + Plugins: shared.PluginMap, + Cmd: exec.Command(settings.Cmd, params...), + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + }) + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + // Request the plugin + raw, err := rpcClient.Dispense(shared.PluginName) + if err != nil { + return nil, err + } + + tpp := raw.(*grpcShared.GRPCClient) + + if timeout == 0 { + timeout = time.Second + } + + tpp.Timeout = timeout + tpp.Closable = pluginClosableClient{client: client} + + return tpp, err +} + +type pluginClosableClient struct { + client *plugin.Client +} + +func (g pluginClosableClient) Close() error { + g.client.Kill() + return nil +} diff --git a/impersonation/plugin/shared/interface.go b/impersonation/plugin/shared/interface.go new file mode 100644 index 00000000..de213b85 --- /dev/null +++ b/impersonation/plugin/shared/interface.go @@ -0,0 +1,41 @@ +package shared + +import ( + "context" + + "github.com/hashicorp/go-plugin" + pp "github.com/madappgang/identifo/v2/impersonation/grpc/impersonation_provider" + grpcShared "github.com/madappgang/identifo/v2/impersonation/grpc/shared" + "github.com/madappgang/identifo/v2/model" + "google.golang.org/grpc" +) + +const PluginName = "impersonation-provider" + +// Handshake is a common handshake that is shared by plugin and host. +var Handshake = plugin.HandshakeConfig{ + // This isn't required when using VersionedPlugins + ProtocolVersion: 1, + MagicCookieKey: "BASIC_PLUGIN", + MagicCookieValue: "hello", +} + +// PluginMap is the map of plugins we can dispense. +var PluginMap = map[string]plugin.Plugin{ + PluginName: &ImpersonationProviderPlugin{}, +} + +// This is the implementation of plugin.GRPCPlugin so we can serve/consume this. +type ImpersonationProviderPlugin struct { + plugin.Plugin + Impl model.ImpersonationProvider +} + +func (p *ImpersonationProviderPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + pp.RegisterImpersonationProviderServer(s, &grpcShared.GRPCServer{Impl: p.Impl}) + return nil +} + +func (p *ImpersonationProviderPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &grpcShared.GRPCClient{Client: pp.NewImpersonationProviderClient(c)}, nil +} diff --git a/localization/messages_const.go b/localization/messages_const.go index 7eef13cd..145e225d 100644 --- a/localization/messages_const.go +++ b/localization/messages_const.go @@ -145,6 +145,8 @@ const ( ErrorTokenInvalidError LocalizedString = "error.token.invalid.error" // ErrorTokenBlocked -> The token is blocked and not valid any more. ErrorTokenBlocked LocalizedString = "error.token.blocked" + // ErrorAPIImpersonationForbidden -> Impersonation is forbidden. + ErrorAPIImpersonationForbidden LocalizedString = "error.api.impersonation.forbidden" //=========================================================================== // App errors diff --git a/localization/translations/en.yaml b/localization/translations/en.yaml index 65ca6954..ecc3386e 100644 --- a/localization/translations/en.yaml +++ b/localization/translations/en.yaml @@ -69,6 +69,7 @@ error.token.refresh.empty: Error getting old refresh token from context to repla error.otp.expired: OTP token expired, please get the new one and try again. error.token.invalid.error: "Invalid token. Validation error: %v." error.token.blocked: The token is blocked and not valid any more. +error.api.impersonation.forbidden: Impersonation is forbidden. # App errors error.api.app.inactive: The app is inactive. diff --git a/model/impersonation.go b/model/impersonation.go new file mode 100644 index 00000000..729aa6d6 --- /dev/null +++ b/model/impersonation.go @@ -0,0 +1,7 @@ +package model + +import "context" + +type ImpersonationProvider interface { + CanImpersonate(ctx context.Context, appID string, adminUser, impersonatedUser User) (bool, error) +} diff --git a/model/server.go b/model/server.go index 0be50f39..0ea9b804 100644 --- a/model/server.go +++ b/model/server.go @@ -38,8 +38,9 @@ type ServerStorageCollection struct { } type ServerServices struct { - SMS SMSService - Email EmailService - Token TokenService - Session SessionService + SMS SMSService + Email EmailService + Token TokenService + Session SessionService + Impersonation ImpersonationProvider } diff --git a/model/server_settings.go b/model/server_settings.go index 95d39195..544c3d43 100644 --- a/model/server_settings.go +++ b/model/server_settings.go @@ -30,6 +30,32 @@ type ServerSettings struct { AdminPanel AdminPanelSettings `yaml:"adminPanel" json:"admin_panel"` LoginWebApp FileStorageSettings `yaml:"loginWebApp" json:"login_web_app"` EmailTemplates FileStorageSettings `yaml:"emailTemplates" json:"email_templates"` + Impersonation ImpersonationSettings `yaml:"impersonation" json:"impersonation"` +} + +type ImpersonationServiceType string + +const ( + ImpersonationServiceTypeNone ImpersonationServiceType = "none" + ImpersonationServiceTypeScope ImpersonationServiceType = "scope" + ImpersonationServiceTypeRole ImpersonationServiceType = "role" + ImpersonationServiceTypePlugin ImpersonationServiceType = "plugin" +) + +// ImpersonationSettings are settings for impersonation. +type ImpersonationSettings struct { + Type ImpersonationServiceType `yaml:"type" json:"type"` + Plugin PluginSettings `yaml:"plugin" json:"plugin"` + Scope ImpersonationScopeSettings `yaml:"scope" json:"scope"` + Role ImpersonationRoleSettings `yaml:"role" json:"role"` +} + +type ImpersonationScopeSettings struct { + AllowedScopes []string `yaml:"allowed_scopes" json:"allowed_scopes"` +} + +type ImpersonationRoleSettings struct { + AllowedRoles []string `yaml:"allowed_roles" json:"allowed_roles"` } // GeneralServerSettings are general server settings. diff --git a/plugins/mongo-user-storage/main.go b/plugins/mongo-user-storage/main.go index b0baab53..d0aa0c7a 100644 --- a/plugins/mongo-user-storage/main.go +++ b/plugins/mongo-user-storage/main.go @@ -41,10 +41,7 @@ func main() { osch := make(chan os.Signal, 1) signal.Notify(osch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - for { - <-osch - s.Close() - log.Println("Mongo user storage is terminated.") - return - } + <-osch + s.Close() + log.Println("Mongo user storage is terminated.") } diff --git a/server-config.yaml b/server-config.yaml index f19dad8c..cebd2b04 100644 --- a/server-config.yaml +++ b/server-config.yaml @@ -26,6 +26,20 @@ storage: verificationCodeStorage: *storage_settings inviteStorage: *storage_settings + + +impersonation: + type: scope + scope: + allowed_scopes: ["admin"] + role: + allowed_roles: ["admin"] + plugin: + cmd: /path/to/plugin + params: { "a": "b" } + + + # Storage for admin sessions. sessionStorage: type: memory # Supported values are "memory", "redis", and "dynamodb". diff --git a/web/api/2fa.go b/web/api/2fa.go index 50db8279..d55a2a42 100644 --- a/web/api/2fa.go +++ b/web/api/2fa.go @@ -180,10 +180,6 @@ func (ar *Router) ResendTFA() http.HandlerFunc { } userID := token.Subject() - if err != nil { - ar.Error(w, locale, http.StatusInternalServerError, l.ErrorAPIRequestTokenSubError, err) - return - } user, err := ar.server.Storages().User.UserByID(userID) if err != nil { @@ -197,7 +193,9 @@ func (ar *Router) ResendTFA() http.HandlerFunc { return } - authResult, err := ar.loginFlow(app, user, strings.Split(token.Scopes(), " ")) + scopes := strings.Split(token.Scopes(), " ") + + authResult, err := ar.loginFlow(app, user, scopes, nil) if err != nil { ar.Error(w, locale, http.StatusInternalServerError, l.APIInternalServerErrorWithError, err) return diff --git a/web/api/federated_login.go b/web/api/federated_login.go index 69a91034..3da12c54 100644 --- a/web/api/federated_login.go +++ b/web/api/federated_login.go @@ -203,7 +203,7 @@ func (ar *Router) FederatedLoginComplete() http.HandlerFunc { return } - authResult, err := ar.loginFlow(app, user, fsess.Scopes) + authResult, err := ar.loginFlow(app, user, fsess.Scopes, nil) if err != nil { ar.Error(w, locale, http.StatusInternalServerError, l.ErrorFederatedLoginError, err) return diff --git a/web/api/federated_oidc_login.go b/web/api/federated_oidc_login.go index ea193929..55755bd7 100644 --- a/web/api/federated_oidc_login.go +++ b/web/api/federated_oidc_login.go @@ -237,7 +237,7 @@ func (ar *Router) OIDCLoginComplete(useSession bool) http.HandlerFunc { // map OIDC scopes to Identifo scopes requestedScopes = mapScopes(app.OIDCSettings.ScopeMapping, requestedScopes) - authResult, err := ar.loginFlow(app, user, requestedScopes) + authResult, err := ar.loginFlow(app, user, requestedScopes, nil) if err != nil { ar.Error(w, locale, http.StatusInternalServerError, l.ErrorFederatedLoginError, err) return diff --git a/web/api/impersonate_as.go b/web/api/impersonate_as.go new file mode 100644 index 00000000..c7316e74 --- /dev/null +++ b/web/api/impersonate_as.go @@ -0,0 +1,99 @@ +package api + +import ( + "context" + "errors" + "log" + "net/http" + + l "github.com/madappgang/identifo/v2/localization" + "github.com/madappgang/identifo/v2/model" + "github.com/madappgang/identifo/v2/web/middleware" +) + +func (ar *Router) ImpersonateAs() http.HandlerFunc { + type impersonateData struct { + UserID string `json:"user_id"` + } + + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + locale := r.Header.Get("Accept-Language") + + app := middleware.AppFromContext(r.Context()) + if len(app.ID) == 0 { + ar.Error(w, locale, http.StatusBadRequest, l.ErrorAPIAPPNoAPPInContext) + return + } + + userID := tokenFromContext(r.Context()).UserID() + adminUser, err := ar.server.Storages().User.UserByID(userID) + if err != nil { + ar.Error(w, locale, http.StatusUnauthorized, l.ErrorStorageFindUserIDError, userID, err) + return + } + + log.Println("admin for impersonation", adminUser.ID, adminUser.Scopes) + + d := impersonateData{} + if ar.MustParseJSON(w, r, &d) != nil { + return + } + + user, err := ar.server.Storages().User.UserByID(d.UserID) + if err != nil { + ar.Error(w, locale, http.StatusUnauthorized, l.ErrorStorageFindUserIDError, d.UserID, err) + return + } + + ok, err := ar.checkImpersonationPermissions(ctx, app, adminUser, user) + if err != nil { + log.Printf("can not check impersonation: %v\n", err) + ar.Error(w, locale, http.StatusForbidden, l.ErrorAPIImpersonationForbidden) + return + } + + if !ok { + ar.Error(w, locale, http.StatusForbidden, l.ErrorAPIImpersonationForbidden) + return + } + + ap := map[string]any{ + "impersonated_by": adminUser.ID, + } + + authResult, err := ar.loginFlow(app, user, nil, ap) + if err != nil { + ar.Error(w, locale, http.StatusInternalServerError, l.ErrorAPILoginError, err) + return + } + + // do not allow refresh for impersonated user + authResult.RefreshToken = "" + + ar.ServeJSON(w, locale, http.StatusOK, authResult) + } +} + +func (ar *Router) checkImpersonationPermissions( + ctx context.Context, + app model.AppData, + adminUser, impUser model.User, +) (bool, error) { + imps := ar.server.Services().Impersonation + if imps == nil { + return false, errors.New("impersonation service is not set") + } + + ok, err := imps.CanImpersonate(ctx, app.ID, adminUser, impUser) + if err != nil { + return false, err + } + + if !ok { + return false, nil + } + + return true, nil +} diff --git a/web/api/login.go b/web/api/login.go index b60632a1..760f909d 100644 --- a/web/api/login.go +++ b/web/api/login.go @@ -182,7 +182,7 @@ func (ar *Router) LoginWithPassword() http.HandlerFunc { return } - authResult, err := ar.loginFlow(app, user, ld.Scopes) + authResult, err := ar.loginFlow(app, user, ld.Scopes, nil) if err != nil { ar.Error(w, locale, http.StatusInternalServerError, l.ErrorAPILoginError, err) return @@ -330,7 +330,12 @@ func (ar *Router) loginUser(user model.User, scopes []string, app model.AppData, return accessTokenString, refreshTokenString, nil } -func (ar *Router) loginFlow(app model.AppData, user model.User, requestedScopes []string) (AuthResponse, error) { +func (ar *Router) loginFlow( + app model.AppData, + user model.User, + requestedScopes []string, + additionalPayload map[string]any, +) (AuthResponse, error) { // check if the user has the scope, that allows to login to the app // user has to have at least one scope app expecting if len(app.Scopes) > 0 && len(model.SliceIntersect(app.Scopes, user.Scopes)) == 0 { @@ -359,6 +364,14 @@ func (ar *Router) loginFlow(app model.AppData, user model.User, requestedScopes return AuthResponse{}, err } + if tokenPayload == nil { + tokenPayload = additionalPayload + } else { + for k, v := range additionalPayload { + tokenPayload[k] = v + } + } + accessToken, refreshToken, err := ar.loginUser(user, scopes, app, offline, require2FA, tokenPayload) if err != nil { return AuthResponse{}, err diff --git a/web/api/registration.go b/web/api/registration.go index 245f0fa5..eb9962af 100644 --- a/web/api/registration.go +++ b/web/api/registration.go @@ -179,7 +179,7 @@ func (ar *Router) RegisterWithPassword() http.HandlerFunc { // return // Do login flow. - authResult, err := ar.loginFlow(app, user, rd.Scopes) + authResult, err := ar.loginFlow(app, user, rd.Scopes, nil) if err != nil { ar.Error(w, locale, http.StatusInternalServerError, l.ErrorAPILoginError, err) return diff --git a/web/api/routes.go b/web/api/routes.go index 24e539dc..4749f187 100644 --- a/web/api/routes.go +++ b/web/api/routes.go @@ -140,6 +140,7 @@ func (ar *Router) buildMeRoutes(middleware *negroni.Negroni) http.Handler { me.Path("").HandlerFunc(ar.GetUser()).Methods(http.MethodGet) me.Path("").HandlerFunc(ar.UpdateUser()).Methods(http.MethodPut) me.Path("/logout").HandlerFunc(ar.Logout()).Methods(http.MethodPost) + me.Path("/impersonate_as").HandlerFunc(ar.ImpersonateAs()).Methods(http.MethodPost) return with(middleware, ar.SignatureHandler(),