From 23570135351957876eae44bce3b5b1eb215ec52f Mon Sep 17 00:00:00 2001 From: cowsed Date: Tue, 1 Sep 2026 13:36:06 -0400 Subject: [PATCH] Add support for getting auth plugin providers via graphql and made the /login endpoint support the different providers --- cmd/make-backend/main.go | 38 +-- internal/auth/sessions.go | 46 ++++ internal/gql/generated.go | 246 +++++++++++++++++- internal/gql/model/models_gen.go | 6 + .../gql/resolvers/auth_provider.resolvers.go | 24 ++ internal/gql/resolvers/resolver.go | 6 +- internal/gql/schema/auth_provider.graphqls | 9 + internal/plugins/auth/auth_plugin.go | 1 + internal/plugins/auth/grpc.go | 11 + internal/plugins/auth/interface.pb.go | 118 +++++++-- internal/plugins/auth/interface.proto | 7 + internal/plugins/auth/interface_grpc.pb.go | 38 +++ internal/plugins/loader.go | 20 +- plugins/auth/saml/main.go | 7 + 14 files changed, 514 insertions(+), 63 deletions(-) create mode 100644 internal/gql/resolvers/auth_provider.resolvers.go create mode 100644 internal/gql/schema/auth_provider.graphqls diff --git a/cmd/make-backend/main.go b/cmd/make-backend/main.go index eae31875..23e927f6 100644 --- a/cmd/make-backend/main.go +++ b/cmd/make-backend/main.go @@ -10,7 +10,6 @@ import ( "make-backend/internal/gql" "make-backend/internal/logging" "make-backend/internal/plugins" - auth_plugin "make-backend/internal/plugins/auth" "time" acsmqtt "make-backend/internal/api/acs/acs-mqtt" @@ -51,13 +50,6 @@ const mqttPort = 23002 var glblPlugins = plugins.PluginStore{} -func GetAuthPlugin() auth_plugin.AuthProvider { - for _, p := range glblPlugins.Auth { - return p - } - return nil -} - func main() { sigs := make(chan os.Signal, 1) done := make(chan bool, 1) @@ -169,7 +161,11 @@ func StartReverseProxy(port string, httpPort, mqttPort int, pluginForwards []plu func startHttp(db *sql.DB, store *database.Store, logger *logging.Logger, port int, sessionManager *scs.SessionManager) *http.Server { // GraphQL - graphqlConfig := gql.Config{Resolvers: &resolvers.Resolver{Store: store}} + graphqlConfig := gql.Config{Resolvers: &resolvers.Resolver{ + Store: store, + Logger: logger, + Plugins: &glblPlugins, + }} directives.SetupDirectives(&graphqlConfig, store) srv := handler.New(gql.NewExecutableSchema(graphqlConfig)) @@ -191,31 +187,9 @@ func startHttp(db *sql.DB, store *database.Store, logger *logging.Logger, port i mux.Handle("/playground", playground.Handler("GraphQL playground", "/query")) mux.Handle("/query", protectedQueryHandler) - loginHandler := func(w http.ResponseWriter, r *http.Request) { - a := GetAuthPlugin() - - req, err := a.GenerateLoginRequest(&auth_plugin.UserLoginStartRequest{ - OriginalURL: "http://localhost:8080", - }) - if err != nil { - slog.Error("failed to get login url", "err", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - h := w.Header() - for _, header := range req.SetHeaders { - h.Add(header.Key, header.Value) - } - w.WriteHeader(int(req.Code)) - _, err = w.Write(req.Body) - if err != nil { - slog.Error("failed to write login redirect", "err", err) - } - } - fileHandler := http.StripPrefix("/app/", http.FileServer(http.Dir("./client"))) mux.Handle("/app/", fileHandler) - mux.HandleFunc("/login", loginHandler) + mux.HandleFunc("/login", auth.LoginHandler(&glblPlugins)) mux.Handle("/", http.RedirectHandler("/app/", http.StatusFound)) diff --git a/internal/auth/sessions.go b/internal/auth/sessions.go index aaa6c533..9dc0f33a 100644 --- a/internal/auth/sessions.go +++ b/internal/auth/sessions.go @@ -2,6 +2,11 @@ package auth import ( "database/sql" + "log/slog" + "make-backend/internal/plugins" + auth_plugin "make-backend/internal/plugins/auth" + + "net/http" "github.com/alexedwards/scs/postgresstore" "github.com/alexedwards/scs/v2" @@ -13,3 +18,44 @@ func SetupSessions(db *sql.DB) *scs.SessionManager { sessionManager.Store = postgresstore.New(db) return sessionManager } + +func LoginHandler(plugins *plugins.PluginStore) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + vs := r.URL.Query() + pluginId := vs.Get("plugin") + if pluginId == "" { + // no plugin specified. should redirect to login chooser page + w.WriteHeader(http.StatusNotFound) + return + } + fromUrl := vs.Get("from") + if fromUrl == "" { + fromUrl = "http://localhost:8080" + } + + a, ok := plugins.Auth[pluginId] + if !ok { + // no plugin found. should redirect to login chooser page + w.WriteHeader(http.StatusNotFound) + return + } + + req, err := a.Provider.GenerateLoginRequest(&auth_plugin.UserLoginStartRequest{ + OriginalURL: fromUrl, + }) + if err != nil { + slog.Error("failed to get login url", "err", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + h := w.Header() + for _, header := range req.SetHeaders { + h.Add(header.Key, header.Value) + } + w.WriteHeader(int(req.Code)) + _, err = w.Write(req.Body) + if err != nil { + slog.Error("failed to write login redirect", "err", err) + } + } +} diff --git a/internal/gql/generated.go b/internal/gql/generated.go index eba96ac9..bca22e1b 100644 --- a/internal/gql/generated.go +++ b/internal/gql/generated.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "make-backend/internal/database/models" + "make-backend/internal/gql/model" "make-backend/internal/gql/scalars" "math" "strconv" @@ -104,6 +105,12 @@ type ComplexityRoot struct { Subgroups func(childComplexity int) int } + AuthProvider struct { + ImageURL func(childComplexity int) int + Name func(childComplexity int) int + PluginID func(childComplexity int) int + } + CustomLink struct { LongUrl func(childComplexity int) int ShortUrl func(childComplexity int) int @@ -246,6 +253,7 @@ type ComplexityRoot struct { Query struct { AccessDevice func(childComplexity int, id int) int + AuthProviders func(childComplexity int) int CanGroupManageGroup func(childComplexity int, managerID int, groupID int) int CanUserManageGroup func(childComplexity int, managerID int, groupID int) int CurrentUser func(childComplexity int) int @@ -378,6 +386,7 @@ type OptionBlockOptionResolver interface { } type QueryResolver interface { Makerspace(ctx context.Context, id int) (*models.Makerspace, error) + AuthProviders(ctx context.Context) ([]*model.AuthProvider, error) Device(ctx context.Context, id int) (*models.Device, error) AccessDevice(ctx context.Context, id int) (*models.AccessDevice, error) Group(ctx context.Context, id int) (*models.Group, error) @@ -596,6 +605,25 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.AnonymousGroup.Subgroups(childComplexity), true + case "AuthProvider.ImageURL": + if e.ComplexityRoot.AuthProvider.ImageURL == nil { + break + } + + return e.ComplexityRoot.AuthProvider.ImageURL(childComplexity), true + case "AuthProvider.Name": + if e.ComplexityRoot.AuthProvider.Name == nil { + break + } + + return e.ComplexityRoot.AuthProvider.Name(childComplexity), true + case "AuthProvider.PluginId": + if e.ComplexityRoot.AuthProvider.PluginID == nil { + break + } + + return e.ComplexityRoot.AuthProvider.PluginID(childComplexity), true + case "CustomLink.long_url": if e.ComplexityRoot.CustomLink.LongUrl == nil { break @@ -1200,6 +1228,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.Query.AccessDevice(childComplexity, args["id"].(int)), true + case "Query.authProviders": + if e.ComplexityRoot.Query.AuthProviders == nil { + break + } + + return e.ComplexityRoot.Query.AuthProviders(childComplexity), true case "Query.canGroupManageGroup": if e.ComplexityRoot.Query.CanGroupManageGroup == nil { break @@ -1714,7 +1748,7 @@ func newExecutionContext( } } -//go:embed "schema/access_channel.graphqls" "schema/announcement.graphqls" "schema/card.graphqls" "schema/device.graphqls" "schema/equipment.graphqls" "schema/group.graphqls" "schema/hold.graphqls" "schema/hours.graphqls" "schema/image.graphqls" "schema/link.graphqls" "schema/makerspace.graphqls" "schema/organization.graphqls" "schema/reservation.graphqls" "schema/restriction.graphqls" "schema/training.graphqls" "schema/user.graphqls" "schema/zones.graphqls" +//go:embed "schema/access_channel.graphqls" "schema/announcement.graphqls" "schema/auth_provider.graphqls" "schema/card.graphqls" "schema/device.graphqls" "schema/equipment.graphqls" "schema/group.graphqls" "schema/hold.graphqls" "schema/hours.graphqls" "schema/image.graphqls" "schema/link.graphqls" "schema/makerspace.graphqls" "schema/organization.graphqls" "schema/reservation.graphqls" "schema/restriction.graphqls" "schema/training.graphqls" "schema/user.graphqls" "schema/zones.graphqls" var sourcesFS embed.FS func sourceData(filename string) string { @@ -1728,6 +1762,7 @@ func sourceData(filename string) string { var sources = []*ast.Source{ {Name: "schema/access_channel.graphqls", Input: sourceData("schema/access_channel.graphqls"), BuiltIn: false}, {Name: "schema/announcement.graphqls", Input: sourceData("schema/announcement.graphqls"), BuiltIn: false}, + {Name: "schema/auth_provider.graphqls", Input: sourceData("schema/auth_provider.graphqls"), BuiltIn: false}, {Name: "schema/card.graphqls", Input: sourceData("schema/card.graphqls"), BuiltIn: false}, {Name: "schema/device.graphqls", Input: sourceData("schema/device.graphqls"), BuiltIn: false}, {Name: "schema/equipment.graphqls", Input: sourceData("schema/equipment.graphqls"), BuiltIn: false}, @@ -1820,6 +1855,18 @@ func (ec *executionContext) childFields_AnonymousGroup(ctx context.Context, fiel return nil, fmt.Errorf("no field named %q was found under type AnonymousGroup", field.Name) } +func (ec *executionContext) childFields_AuthProvider(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Name": + return ec.fieldContext_AuthProvider_Name(ctx, field) + case "ImageURL": + return ec.fieldContext_AuthProvider_ImageURL(ctx, field) + case "PluginId": + return ec.fieldContext_AuthProvider_PluginId(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AuthProvider", field.Name) +} + func (ec *executionContext) childFields_Device(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": @@ -3321,6 +3368,75 @@ func (ec *executionContext) fieldContext_AnonymousGroup_subgroups(_ context.Cont return fc, nil } +func (ec *executionContext) _AuthProvider_Name(ctx context.Context, field graphql.CollectedField, obj *model.AuthProvider) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AuthProvider_Name(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Name, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AuthProvider_Name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AuthProvider", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _AuthProvider_ImageURL(ctx context.Context, field graphql.CollectedField, obj *model.AuthProvider) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AuthProvider_ImageURL(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ImageURL, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AuthProvider_ImageURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AuthProvider", field, false, false, errors.New("field of type String does not have child fields")) +} + +func (ec *executionContext) _AuthProvider_PluginId(ctx context.Context, field graphql.CollectedField, obj *model.AuthProvider) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AuthProvider_PluginId(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PluginID, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { + return ec.marshalNString2string(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AuthProvider_PluginId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AuthProvider", field, false, false, errors.New("field of type String does not have child fields")) +} + func (ec *executionContext) _CustomLink_short_url(ctx context.Context, field graphql.CollectedField, obj *models.CustomLink) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -5850,6 +5966,38 @@ func (ec *executionContext) fieldContext_Query_makerspace(ctx context.Context, f return fc, nil } +func (ec *executionContext) _Query_authProviders(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Query_authProviders(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().AuthProviders(ctx) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []*model.AuthProvider) graphql.Marshaler { + return ec.marshalNAuthProvider2ᚕᚖmakeᚑbackendᚋinternalᚋgqlᚋmodelᚐAuthProviderᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Query_authProviders(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_AuthProvider(ctx, field) + }, + } + return fc, nil +} + func (ec *executionContext) _Query_device(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -9308,6 +9456,54 @@ func (ec *executionContext) _AnonymousGroup(ctx context.Context, sel ast.Selecti return out } +var authProviderImplementors = []string{"AuthProvider"} + +func (ec *executionContext) _AuthProvider(ctx context.Context, sel ast.SelectionSet, obj *model.AuthProvider) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, authProviderImplementors) + + out := graphql.NewFieldSet(fields) + deferredFieldSet := graphql.NewFieldSet(nil) + deferLabelToView := make(map[string]*graphql.FieldSetView) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AuthProvider") + case "Name": + out.Values[i] = ec._AuthProvider_Name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "ImageURL": + out.Values[i] = ec._AuthProvider_ImageURL(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "PluginId": + out.Values[i] = ec._AuthProvider_PluginId(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) + + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Defers: deferLabelToView, + Path: graphql.GetPath(ctx), + FieldSet: deferredFieldSet, + Context: ctx, + }) + + return out +} + var customLinkImplementors = []string{"CustomLink"} func (ec *executionContext) _CustomLink(ctx context.Context, sel ast.SelectionSet, obj *models.CustomLink) graphql.Marshaler { @@ -10751,6 +10947,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "authProviders": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_authProviders(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "device": field := field @@ -12063,6 +12281,32 @@ func (ec *executionContext) marshalNAccessDeviceFlags2makeᚑbackendᚋinternal return ec._AccessDeviceFlags(ctx, sel, &v) } +func (ec *executionContext) marshalNAuthProvider2ᚕᚖmakeᚑbackendᚋinternalᚋgqlᚋmodelᚐAuthProviderᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AuthProvider) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNAuthProvider2ᚖmakeᚑbackendᚋinternalᚋgqlᚋmodelᚐAuthProvider(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNAuthProvider2ᚖmakeᚑbackendᚋinternalᚋgqlᚋmodelᚐAuthProvider(ctx context.Context, sel ast.SelectionSet, v *model.AuthProvider) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._AuthProvider(ctx, sel, v) +} + func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/internal/gql/model/models_gen.go b/internal/gql/model/models_gen.go index 475457f6..63a04e04 100644 --- a/internal/gql/model/models_gen.go +++ b/internal/gql/model/models_gen.go @@ -2,6 +2,12 @@ package model +type AuthProvider struct { + Name string `json:"Name"` + ImageURL string `json:"ImageURL"` + PluginID string `json:"PluginId"` +} + type Mutation struct { } diff --git a/internal/gql/resolvers/auth_provider.resolvers.go b/internal/gql/resolvers/auth_provider.resolvers.go new file mode 100644 index 00000000..8d62b5c9 --- /dev/null +++ b/internal/gql/resolvers/auth_provider.resolvers.go @@ -0,0 +1,24 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.93 + +import ( + "context" + "make-backend/internal/gql/model" +) + +// AuthProviders is the resolver for the authProviders field. +func (r *queryResolver) AuthProviders(ctx context.Context) ([]*model.AuthProvider, error) { + providers := []*model.AuthProvider{} + for id, p := range r.Plugins.Auth { + providers = append(providers, &model.AuthProvider{ + Name: p.Name, + ImageURL: p.ImageUrl, + PluginID: id, + }) + } + return providers, nil +} diff --git a/internal/gql/resolvers/resolver.go b/internal/gql/resolvers/resolver.go index d3ff5be6..3d696839 100644 --- a/internal/gql/resolvers/resolver.go +++ b/internal/gql/resolvers/resolver.go @@ -3,6 +3,7 @@ package resolvers import ( "make-backend/internal/database" "make-backend/internal/logging" + "make-backend/internal/plugins" ) // This file will not be regenerated automatically. @@ -11,6 +12,7 @@ import ( // here. type Resolver struct { - Store *database.Store - Logger *logging.Logger + Store *database.Store + Logger *logging.Logger + Plugins *plugins.PluginStore } diff --git a/internal/gql/schema/auth_provider.graphqls b/internal/gql/schema/auth_provider.graphqls new file mode 100644 index 00000000..2176fd53 --- /dev/null +++ b/internal/gql/schema/auth_provider.graphqls @@ -0,0 +1,9 @@ +type AuthProvider { + Name: String! + ImageURL: String! + PluginId: String! +} + +extend type Query { + authProviders: [AuthProvider!]! +} \ No newline at end of file diff --git a/internal/plugins/auth/auth_plugin.go b/internal/plugins/auth/auth_plugin.go index 56a24eaa..edb79520 100644 --- a/internal/plugins/auth/auth_plugin.go +++ b/internal/plugins/auth/auth_plugin.go @@ -20,6 +20,7 @@ type AuthCallbackProvider interface { // What auth plugins expose/what the core can ask type AuthProvider interface { common.BasePlugin + GetAuthDescription() (*AuthDescription, error) // General status information about the plugin and if its still working Heartbeat() (*common.HeartbeatInfo, error) // Generate a URL/Body/headers for diff --git a/internal/plugins/auth/grpc.go b/internal/plugins/auth/grpc.go index 47a07df5..f2e76830 100644 --- a/internal/plugins/auth/grpc.go +++ b/internal/plugins/auth/grpc.go @@ -100,6 +100,13 @@ func (g *GRPCClient) GenerateLoginRequest(req *UserLoginStartRequest) (*LoginReq return res, nil } +// GetAuthDescription implements [AuthProvider]. +func (g *GRPCClient) GetAuthDescription() (*AuthDescription, error) { + e := common.Empty{} + return g.client.GetAuthDescription(context.Background(), &e) + +} + func (g *GRPCClient) Heartbeat() (*common.HeartbeatInfo, error) { e := common.Empty{} return g.client.Heartbeat(context.Background(), &e) @@ -126,6 +133,10 @@ func (g *GRPCServer) GetLoginRequest(ctx context.Context, req *UserLoginStartReq } +func (g *GRPCServer) GetAuthDescription(context.Context, *common.Empty) (*AuthDescription, error) { + return g.Impl.GetAuthDescription() +} + // Heartbeat implements [AuthPluginServer]. func (g *GRPCServer) Heartbeat(context.Context, *common.Empty) (*common.HeartbeatInfo, error) { hb, err := g.Impl.Heartbeat() diff --git a/internal/plugins/auth/interface.pb.go b/internal/plugins/auth/interface.pb.go index 8079bb18..25d61e5c 100644 --- a/internal/plugins/auth/interface.pb.go +++ b/internal/plugins/auth/interface.pb.go @@ -607,6 +607,66 @@ func (x *PluginInitRequest) GetCallbackBrokerId() uint64 { return 0 } +type AuthDescription struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + ImageUrl string `protobuf:"bytes,3,opt,name=imageUrl,proto3" json:"imageUrl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthDescription) Reset() { + *x = AuthDescription{} + mi := &file_auth_interface_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthDescription) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthDescription) ProtoMessage() {} + +func (x *AuthDescription) ProtoReflect() protoreflect.Message { + mi := &file_auth_interface_proto_msgTypes[9] + 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 AuthDescription.ProtoReflect.Descriptor instead. +func (*AuthDescription) Descriptor() ([]byte, []int) { + return file_auth_interface_proto_rawDescGZIP(), []int{9} +} + +func (x *AuthDescription) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AuthDescription) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *AuthDescription) GetImageUrl() string { + if x != nil { + return x.ImageUrl + } + return "" +} + var File_auth_interface_proto protoreflect.FileDescriptor const file_auth_interface_proto_rawDesc = "" + @@ -647,17 +707,22 @@ const file_auth_interface_proto_rawDesc = "" + "\tSubserver\x12\x12\n" + "\x04port\x18\x01 \x01(\x05R\x04port\"A\n" + "\x11PluginInitRequest\x12,\n" + - "\x12callback_broker_id\x18\x01 \x01(\x04R\x10callbackBrokerId*\x96\x01\n" + + "\x12callback_broker_id\x18\x01 \x01(\x04R\x10callbackBrokerId\"S\n" + + "\x0fAuthDescription\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + + "\x03url\x18\x02 \x01(\tR\x03url\x12\x1a\n" + + "\bimageUrl\x18\x03 \x01(\tR\bimageUrl*\x96\x01\n" + "\x15UserLoginResponseType\x12(\n" + "$USER_LOGIN_RESPONSE_TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + "\fUSER_CREATED\x10\x01\x12\x0e\n" + "\n" + "USER_FOUND\x10\x02\x12&\n" + "\"USER_ALREADY_EXISTS_OTHER_PROVIDER\x10\x03\x12\t\n" + - "\x05ERROR\x10\x042\xb6\x02\n" + + "\x05ERROR\x10\x042\xf2\x02\n" + "\n" + "AuthPlugin\x128\n" + - "\x04Info\x12\x1c.common.PluginInitialMessage\x1a\x12.common.PluginInfo\x121\n" + + "\x04Info\x12\x1c.common.PluginInitialMessage\x1a\x12.common.PluginInfo\x12:\n" + + "\x12GetAuthDescription\x12\r.common.Empty\x1a\x15.auth.AuthDescription\x121\n" + "\tHeartbeat\x12\r.common.Empty\x1a\x15.common.HeartbeatInfo\x12B\n" + "\x0fGetLoginRequest\x12\x1b.auth.UserLoginStartRequest\x1a\x12.auth.LoginRequest\x120\n" + "\x06Logout\x12\x17.auth.UserLogOffRequest\x1a\r.common.Empty\x12E\n" + @@ -679,7 +744,7 @@ func file_auth_interface_proto_rawDescGZIP() []byte { } var file_auth_interface_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_auth_interface_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_auth_interface_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_auth_interface_proto_goTypes = []any{ (UserLoginResponseType)(0), // 0: auth.UserLoginResponseType (*UserLoginCallback)(nil), // 1: auth.UserLoginCallback @@ -691,31 +756,34 @@ var file_auth_interface_proto_goTypes = []any{ (*RedirectURL)(nil), // 7: auth.RedirectURL (*Subserver)(nil), // 8: auth.Subserver (*PluginInitRequest)(nil), // 9: auth.PluginInitRequest - (*common.PluginInitialMessage)(nil), // 10: common.PluginInitialMessage - (*common.Empty)(nil), // 11: common.Empty - (*common.PluginInfo)(nil), // 12: common.PluginInfo - (*common.HeartbeatInfo)(nil), // 13: common.HeartbeatInfo + (*AuthDescription)(nil), // 10: auth.AuthDescription + (*common.PluginInitialMessage)(nil), // 11: common.PluginInitialMessage + (*common.Empty)(nil), // 12: common.Empty + (*common.PluginInfo)(nil), // 13: common.PluginInfo + (*common.HeartbeatInfo)(nil), // 14: common.HeartbeatInfo } var file_auth_interface_proto_depIdxs = []int32{ 0, // 0: auth.UserLoginResponse.response_type:type_name -> auth.UserLoginResponseType 5, // 1: auth.LoginRequest.setHeaders:type_name -> auth.SetKV 5, // 2: auth.RedirectURL.setHeaders:type_name -> auth.SetKV - 10, // 3: auth.AuthPlugin.Info:input_type -> common.PluginInitialMessage - 11, // 4: auth.AuthPlugin.Heartbeat:input_type -> common.Empty - 2, // 5: auth.AuthPlugin.GetLoginRequest:input_type -> auth.UserLoginStartRequest - 4, // 6: auth.AuthPlugin.Logout:input_type -> auth.UserLogOffRequest - 9, // 7: auth.AuthPlugin.internalInitializeCallbacks:input_type -> auth.PluginInitRequest - 1, // 8: auth.AuthCallbackService.UserLoggedIn:input_type -> auth.UserLoginCallback - 4, // 9: auth.AuthCallbackService.UserLoggedOut:input_type -> auth.UserLogOffRequest - 12, // 10: auth.AuthPlugin.Info:output_type -> common.PluginInfo - 13, // 11: auth.AuthPlugin.Heartbeat:output_type -> common.HeartbeatInfo - 6, // 12: auth.AuthPlugin.GetLoginRequest:output_type -> auth.LoginRequest - 11, // 13: auth.AuthPlugin.Logout:output_type -> common.Empty - 11, // 14: auth.AuthPlugin.internalInitializeCallbacks:output_type -> common.Empty - 7, // 15: auth.AuthCallbackService.UserLoggedIn:output_type -> auth.RedirectURL - 11, // 16: auth.AuthCallbackService.UserLoggedOut:output_type -> common.Empty - 10, // [10:17] is the sub-list for method output_type - 3, // [3:10] is the sub-list for method input_type + 11, // 3: auth.AuthPlugin.Info:input_type -> common.PluginInitialMessage + 12, // 4: auth.AuthPlugin.GetAuthDescription:input_type -> common.Empty + 12, // 5: auth.AuthPlugin.Heartbeat:input_type -> common.Empty + 2, // 6: auth.AuthPlugin.GetLoginRequest:input_type -> auth.UserLoginStartRequest + 4, // 7: auth.AuthPlugin.Logout:input_type -> auth.UserLogOffRequest + 9, // 8: auth.AuthPlugin.internalInitializeCallbacks:input_type -> auth.PluginInitRequest + 1, // 9: auth.AuthCallbackService.UserLoggedIn:input_type -> auth.UserLoginCallback + 4, // 10: auth.AuthCallbackService.UserLoggedOut:input_type -> auth.UserLogOffRequest + 13, // 11: auth.AuthPlugin.Info:output_type -> common.PluginInfo + 10, // 12: auth.AuthPlugin.GetAuthDescription:output_type -> auth.AuthDescription + 14, // 13: auth.AuthPlugin.Heartbeat:output_type -> common.HeartbeatInfo + 6, // 14: auth.AuthPlugin.GetLoginRequest:output_type -> auth.LoginRequest + 12, // 15: auth.AuthPlugin.Logout:output_type -> common.Empty + 12, // 16: auth.AuthPlugin.internalInitializeCallbacks:output_type -> common.Empty + 7, // 17: auth.AuthCallbackService.UserLoggedIn:output_type -> auth.RedirectURL + 12, // 18: auth.AuthCallbackService.UserLoggedOut:output_type -> common.Empty + 11, // [11:19] is the sub-list for method output_type + 3, // [3:11] is the sub-list for method input_type 3, // [3:3] is the sub-list for extension type_name 3, // [3:3] is the sub-list for extension extendee 0, // [0:3] is the sub-list for field type_name @@ -736,7 +804,7 @@ func file_auth_interface_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_interface_proto_rawDesc), len(file_auth_interface_proto_rawDesc)), NumEnums: 1, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/plugins/auth/interface.proto b/internal/plugins/auth/interface.proto index ae9a124b..cdc6290b 100644 --- a/internal/plugins/auth/interface.proto +++ b/internal/plugins/auth/interface.proto @@ -68,9 +68,16 @@ message PluginInitRequest { uint64 callback_broker_id = 1; } +message AuthDescription { + string name = 1; + string url = 2; + string imageUrl = 3; +} + service AuthPlugin { rpc Info(common.PluginInitialMessage) returns (common.PluginInfo); + rpc GetAuthDescription(common.Empty) returns (AuthDescription); rpc Heartbeat(common.Empty) returns (common.HeartbeatInfo); rpc GetLoginRequest(UserLoginStartRequest) returns (LoginRequest); rpc Logout(UserLogOffRequest) returns (common.Empty); diff --git a/internal/plugins/auth/interface_grpc.pb.go b/internal/plugins/auth/interface_grpc.pb.go index 511bf4a4..0844c751 100644 --- a/internal/plugins/auth/interface_grpc.pb.go +++ b/internal/plugins/auth/interface_grpc.pb.go @@ -21,6 +21,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( AuthPlugin_Info_FullMethodName = "/auth.AuthPlugin/Info" + AuthPlugin_GetAuthDescription_FullMethodName = "/auth.AuthPlugin/GetAuthDescription" AuthPlugin_Heartbeat_FullMethodName = "/auth.AuthPlugin/Heartbeat" AuthPlugin_GetLoginRequest_FullMethodName = "/auth.AuthPlugin/GetLoginRequest" AuthPlugin_Logout_FullMethodName = "/auth.AuthPlugin/Logout" @@ -32,6 +33,7 @@ const ( // 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 AuthPluginClient interface { Info(ctx context.Context, in *common.PluginInitialMessage, opts ...grpc.CallOption) (*common.PluginInfo, error) + GetAuthDescription(ctx context.Context, in *common.Empty, opts ...grpc.CallOption) (*AuthDescription, error) Heartbeat(ctx context.Context, in *common.Empty, opts ...grpc.CallOption) (*common.HeartbeatInfo, error) GetLoginRequest(ctx context.Context, in *UserLoginStartRequest, opts ...grpc.CallOption) (*LoginRequest, error) Logout(ctx context.Context, in *UserLogOffRequest, opts ...grpc.CallOption) (*common.Empty, error) @@ -57,6 +59,16 @@ func (c *authPluginClient) Info(ctx context.Context, in *common.PluginInitialMes return out, nil } +func (c *authPluginClient) GetAuthDescription(ctx context.Context, in *common.Empty, opts ...grpc.CallOption) (*AuthDescription, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthDescription) + err := c.cc.Invoke(ctx, AuthPlugin_GetAuthDescription_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *authPluginClient) Heartbeat(ctx context.Context, in *common.Empty, opts ...grpc.CallOption) (*common.HeartbeatInfo, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(common.HeartbeatInfo) @@ -102,6 +114,7 @@ func (c *authPluginClient) InternalInitializeCallbacks(ctx context.Context, in * // for forward compatibility. type AuthPluginServer interface { Info(context.Context, *common.PluginInitialMessage) (*common.PluginInfo, error) + GetAuthDescription(context.Context, *common.Empty) (*AuthDescription, error) Heartbeat(context.Context, *common.Empty) (*common.HeartbeatInfo, error) GetLoginRequest(context.Context, *UserLoginStartRequest) (*LoginRequest, error) Logout(context.Context, *UserLogOffRequest) (*common.Empty, error) @@ -120,6 +133,9 @@ type UnimplementedAuthPluginServer struct{} func (UnimplementedAuthPluginServer) Info(context.Context, *common.PluginInitialMessage) (*common.PluginInfo, error) { return nil, status.Error(codes.Unimplemented, "method Info not implemented") } +func (UnimplementedAuthPluginServer) GetAuthDescription(context.Context, *common.Empty) (*AuthDescription, error) { + return nil, status.Error(codes.Unimplemented, "method GetAuthDescription not implemented") +} func (UnimplementedAuthPluginServer) Heartbeat(context.Context, *common.Empty) (*common.HeartbeatInfo, error) { return nil, status.Error(codes.Unimplemented, "method Heartbeat not implemented") } @@ -171,6 +187,24 @@ func _AuthPlugin_Info_Handler(srv interface{}, ctx context.Context, dec func(int return interceptor(ctx, in, info, handler) } +func _AuthPlugin_GetAuthDescription_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(common.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthPluginServer).GetAuthDescription(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthPlugin_GetAuthDescription_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthPluginServer).GetAuthDescription(ctx, req.(*common.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _AuthPlugin_Heartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(common.Empty) if err := dec(in); err != nil { @@ -254,6 +288,10 @@ var AuthPlugin_ServiceDesc = grpc.ServiceDesc{ MethodName: "Info", Handler: _AuthPlugin_Info_Handler, }, + { + MethodName: "GetAuthDescription", + Handler: _AuthPlugin_GetAuthDescription_Handler, + }, { MethodName: "Heartbeat", Handler: _AuthPlugin_Heartbeat_Handler, diff --git a/internal/plugins/loader.go b/internal/plugins/loader.go index 2657194d..65c7f91c 100644 --- a/internal/plugins/loader.go +++ b/internal/plugins/loader.go @@ -116,9 +116,14 @@ func initMessageForPlugin(host string, desc common.PluginDescription) *common.Pl return &msg } +type AuthProviderWithInfo struct { + Provider auth.AuthProvider + Name string + ImageUrl string +} type PluginStore struct { Notification map[string]notify.NotificationProvider - Auth map[string]auth.AuthProvider + Auth map[string]AuthProviderWithInfo HttpForwards []PluginHTTPForwarding } @@ -133,7 +138,7 @@ func StartPlugins(host string, store *database.Store, sessionManager *scs.Sessio forwards := []PluginHTTPForwarding{} var plugins = PluginStore{ Notification: map[string]notify.NotificationProvider{}, - Auth: map[string]auth.AuthProvider{}, + Auth: map[string]AuthProviderWithInfo{}, } var pluginMap = generatePluginMap(wanted_plugins) @@ -193,7 +198,16 @@ func StartPlugins(host string, store *database.Store, sessionManager *scs.Sessio slog.Warn("plugin lied about type", "wanted", plugin_desc.PluginType, "plugin", plugin_desc.Name) } authPlugin.RegisterCallbackProvider(&TestAuthCBProvider{sessionManager, store}) - plugins.Auth[plugin_desc.Name] = authPlugin + desc, err := authPlugin.GetAuthDescription() + if err != nil { + slog.Warn("cant use auth plugin bc we could not get auth description", "plugin_id", plugin_desc.Name, "err", err) + continue + } + plugins.Auth[plugin_desc.Name] = AuthProviderWithInfo{ + Provider: authPlugin, + Name: desc.Name, + ImageUrl: desc.ImageUrl, + } case common.PluginType_Notification: notifyPlugin, ok := raw.(notify.NotificationProvider) diff --git a/plugins/auth/saml/main.go b/plugins/auth/saml/main.go index 9c8e460a..dbb674a6 100644 --- a/plugins/auth/saml/main.go +++ b/plugins/auth/saml/main.go @@ -43,6 +43,13 @@ func (s *SAMLAuth) RegisterCallbackProvider(cb auth.AuthCallbackProvider) { var _ http.ResponseWriter = &common.WriteAdapter{} +func (s *SAMLAuth) GetAuthDescription() (*auth.AuthDescription, error) { + return &auth.AuthDescription{ + Name: pluginName, + Url: s.config.BaseURL + "/login", + ImageUrl: "https://www.shibboleth.net/wp-content/uploads/2020/10/shibboleth-icon-white-233x300.png", + }, nil +} func (s *SAMLAuth) GenerateLoginRequest(start *auth.UserLoginStartRequest) (*auth.LoginRequest, error) { if s.saml == nil { return nil, errors.New("SAML provider degraded")