From 2d1c82dd94fe4293cfc524457bebeedc11686fc7 Mon Sep 17 00:00:00 2001 From: christian-stauffer Date: Tue, 6 Feb 2024 16:52:55 +0100 Subject: [PATCH 1/3] first running version behind eliona's nginx reverse proxy --- apiservices/api_configuration_service.go | 66 ++++++------------------ app.go | 11 ++-- conf/init.sql | 4 +- eliona/others.go | 44 +++++++++++++++- eliona/single_sign_on.go | 56 +++++++++++++------- openapi.yaml | 10 ++-- 6 files changed, 113 insertions(+), 78 deletions(-) diff --git a/apiservices/api_configuration_service.go b/apiservices/api_configuration_service.go index 0d57876..74d195e 100644 --- a/apiservices/api_configuration_service.go +++ b/apiservices/api_configuration_service.go @@ -17,9 +17,9 @@ package apiservices import ( "context" - "errors" "net/http" "saml-sso/apiserver" + "saml-sso/conf" ) // ConfigurationApiService is a service that implements the logic for the ConfigurationAPIServicer @@ -35,88 +35,56 @@ func NewConfigurationApiService() apiserver.ConfigurationAPIServicer { // GetAdvancedConfiguration - Get Advanced Configuration func (s *ConfigurationApiService) GetAdvancedConfiguration(ctx context.Context) (apiserver.ImplResponse, error) { - // TODO - update GetAdvancedConfiguration with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + advCnf, err := conf.GetAdvancedConfig(ctx) - //TODO: Uncomment the next line to return response Response(200, AdvancedConfiguration{}) or use other options such as http.Ok ... - //return Response(200, AdvancedConfiguration{}), nil - - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("GetAdvancedConfiguration method not implemented") + return apiserver.Response(http.StatusOK, advCnf), err } // GetAttributeMapping - Get Attribute Mapping func (s *ConfigurationApiService) GetAttributeMapping(ctx context.Context) (apiserver.ImplResponse, error) { - // TODO - update GetAttributeMapping with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. - - //TODO: Uncomment the next line to return response Response(200, AttributeMap{}) or use other options such as http.Ok ... - //return Response(200, AttributeMap{}), nil + attrMap, err := conf.GetAttributeMapping(ctx) - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("GetAttributeMapping method not implemented") + return apiserver.Response(http.StatusOK, attrMap), err } // GetBasicConfiguration - Get Basic Configurations func (s *ConfigurationApiService) GetBasicConfiguration(ctx context.Context) (apiserver.ImplResponse, error) { - // TODO - update GetBasicConfiguration with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + basicCnf, err := conf.GetBasicConfig(ctx) - //TODO: Uncomment the next line to return response Response(200, BasicConfiguration{}) or use other options such as http.Ok ... - //return Response(200, BasicConfiguration{}), nil - - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("GetBasicConfiguration method not implemented") + return apiserver.Response(http.StatusOK, basicCnf), err } // GetPermissionMapping - Get Permission Mapping func (s *ConfigurationApiService) GetPermissionMapping(ctx context.Context) (apiserver.ImplResponse, error) { - // TODO - update GetPermissionMapping with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. - - //TODO: Uncomment the next line to return response Response(200, Permissions{}) or use other options such as http.Ok ... - //return Response(200, Permissions{}), nil + permMap, err := conf.GetPermissionSettings(ctx) - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("GetPermissionMapping method not implemented") + return apiserver.Response(http.StatusOK, permMap), err } // PutAdvancedConfiguration - Creates or Update Advanced Configuration func (s *ConfigurationApiService) PutAdvancedConfiguration(ctx context.Context, advancedConfiguration apiserver.AdvancedConfiguration) (apiserver.ImplResponse, error) { - // TODO - update PutAdvancedConfiguration with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + cnfRet, err := conf.SetAdvancedConfig(ctx, &advancedConfiguration) - //TODO: Uncomment the next line to return response Response(200, AdvancedConfiguration{}) or use other options such as http.Ok ... - //return Response(200, AdvancedConfiguration{}), nil - - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("PutAdvancedConfiguration method not implemented") + return apiserver.Response(http.StatusOK, cnfRet), err } // PutAttributeMapping - Creates or Update Attribute Mapping func (s *ConfigurationApiService) PutAttributeMapping(ctx context.Context, attributeMap apiserver.AttributeMap) (apiserver.ImplResponse, error) { - // TODO - update PutAttributeMapping with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. - - //TODO: Uncomment the next line to return response Response(200, AttributeMap{}) or use other options such as http.Ok ... - //return Response(200, AttributeMap{}), nil + mapRet, err := conf.SetAttributeMapping(ctx, &attributeMap) - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("PutAttributeMapping method not implemented") + return apiserver.Response(http.StatusOK, mapRet), err } // PutBasicConfiguration - Creates or Update Basic Configuration func (s *ConfigurationApiService) PutBasicConfiguration(ctx context.Context, basicConfiguration apiserver.BasicConfiguration) (apiserver.ImplResponse, error) { - // TODO - update PutBasicConfiguration with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + cnfRet, err := conf.SetBasicConfig(ctx, &basicConfiguration) - //TODO: Uncomment the next line to return response Response(200, BasicConfiguration{}) or use other options such as http.Ok ... - //return Response(200, BasicConfiguration{}), nil - - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("PutBasicConfiguration method not implemented") + return apiserver.Response(http.StatusOK, cnfRet), err } // PutPermissionMapping - Creates or Update Permission Mapping Configurations func (s *ConfigurationApiService) PutPermissionMapping(ctx context.Context, permissions apiserver.Permissions) (apiserver.ImplResponse, error) { - // TODO - update PutPermissionMapping with the required logic for this service method. - // Add api_configuration_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. - - //TODO: Uncomment the next line to return response Response(200, Permissions{}) or use other options such as http.Ok ... - //return Response(200, Permissions{}), nil + permRet, err := conf.SetPermissionSettings(ctx, &permissions) - return apiserver.Response(http.StatusNotImplemented, nil), errors.New("PutPermissionMapping method not implemented") + return apiserver.Response(http.StatusOK, permRet), err } diff --git a/app.go b/app.go index fc1ed9f..2752db3 100644 --- a/app.go +++ b/app.go @@ -17,7 +17,6 @@ package main import ( "context" - "fmt" "io" "net/http" "saml-sso/apiserver" @@ -34,7 +33,7 @@ import ( const ( LOG_REGIO = "app" API_SERVER_PORT = 3000 - SSO_SERVER_PORT = 8081 // Publicly accessible. See wiki. + SSO_SERVER_PORT = 8081 // Publicly accessible without auth. See wiki. SAML_SPECIFIC_ENDPOINT_PATH = "/saml/" ) @@ -83,7 +82,8 @@ func run() { apiPort := common.Getenv("API_SERVER_PORT", strconv.Itoa(API_SERVER_PORT)) samlSpPort := common.Getenv("SSO_SERVER_PORT", strconv.Itoa(SSO_SERVER_PORT)) - fmt.Println(basicConfig.OwnUrl + ":" + apiPort) + log.Debug(LOG_REGIO, "own url: %v, api port: %v", basicConfig.OwnUrl, apiPort) + sp, err := saml.NewServiceProviderAdvanced( basicConfig.ServiceProviderCertificate, basicConfig.ServiceProviderPrivateKey, @@ -129,6 +129,11 @@ func run() { sp.GetMiddleWare().RequireAccount(authHandleFunc)) http.Handle(SAML_SPECIFIC_ENDPOINT_PATH, sp.GetMiddleWare()) + // for backwards compatibility, can be removed when the frontend is reworked to the new generic /sso/* endpoints + http.Handle("/adfs/active/", activeHandleFunc) + http.Handle("/adfs/auth/", + sp.GetMiddleWare().RequireAccount(authHandleFunc)) + log.Info(LOG_REGIO, "started @ %v", samlSpPort) err = http.ListenAndServe(":"+samlSpPort, nil) if err != nil { diff --git a/conf/init.sql b/conf/init.sql index e763436..27d9832 100644 --- a/conf/init.sql +++ b/conf/init.sql @@ -49,8 +49,8 @@ CREATE TABLE IF NOT EXISTS saml_sp.advanced_config ( CREATE TABLE IF NOT EXISTS saml_sp.permissions ( id INT PRIMARY KEY NOT NULL DEFAULT 1 REFERENCES saml_sp.basic_config(id) ON UPDATE CASCADE, - default_system_role TEXT NOT NULL DEFAULT 'regular' , -- reference to is maybe a bad idea (due to the new ACL) - default_proj_role TEXT NOT NULL DEFAULT 'operator' , + default_system_role TEXT NOT NULL DEFAULT 'System user' , -- reference to is maybe a bad idea (due to the new ACL) + default_proj_role TEXT NOT NULL DEFAULT 'Project user' , system_role_saml_attribute TEXT , system_role_map JSON , proj_role_saml_attribute TEXT , diff --git a/eliona/others.go b/eliona/others.go index 0ea438b..b094755 100644 --- a/eliona/others.go +++ b/eliona/others.go @@ -43,6 +43,14 @@ const ( "JOIN public.eliona_secret " + "USING (schema), public.claim_jwt(role, now() + validity,user_id,null) jwt " + "WHERE lower(u.email) = lower($1) AND NOT u.archived)" + + OTHERS_GET_JWT_QUERY_V12 = "(SELECT public.make_jwt(jwt,secret) " + + "FROM public.eliona_user u " + + "JOIN public.acl_role r ON (u.role_id = r.role_id) " + + "JOIN public.project_user USING (user_id) " + + "JOIN public.eliona_secret USING (schema), " + + "public.claim_jwt(role, now() + validity,user_id,proj_id,r.role_id::text,schema) jwt " + + "WHERE lower(u.email) = lower($1) AND NOT u.archived)" ) func GetElionaJsonWebToken(email string) (*string, error) { @@ -70,9 +78,13 @@ func GetElionaJsonWebToken(email string) (*string, error) { if strings.Contains(version, "v10.") { log.Debug(LOG_REGIO, "eliona v10") jwtQuery = OTHERS_GET_JWT_QUERY_V10 + } else if strings.Contains(version, "v11.") && + !strings.Contains(version, "v11.1.5") { + log.Debug(LOG_REGIO, "eliona v11") + jwtQuery = OTHERS_GET_JWT_QUERY_V11 } else { // assume, that the version is newer (with ACL) - jwtQuery = OTHERS_GET_JWT_QUERY_V11 + jwtQuery = OTHERS_GET_JWT_QUERY_V12 } row = db.QueryRow(jwtQuery, email) @@ -96,6 +108,36 @@ func UpdateElionaUserArchivedPhone(email string, phone *string, archived bool) e return err } +func GetFirstProjectId() (projectId string, err error) { + row := getDb().QueryRow("SELECT proj_id FROM eliona_project ORDER BY proj_id LIMIT 1") + + if err = row.Err(); err != nil { + return + } + + err = row.Scan(&projectId) + return +} + +func GetRoleIdByDisplayName(displayName string) (roleId int, err error) { + row := getDb().QueryRow("SELECT role_id FROM acl_role WHERE displayname = $1", displayName) + + if err = row.Err(); err != nil { + return + } + + err = row.Scan(&roleId) + return +} + +func SetProjectUser(projectId string, userId *string, roleId int) (err error) { + + _, err = getDb().Exec("INSERT INTO project_user (proj_id, user_id, role_id) "+ + "VALUES ($1, $2, $3)", projectId, userId, roleId) + + return +} + func getDb() *sql.DB { return db.Database(app.AppName()) } diff --git a/eliona/single_sign_on.go b/eliona/single_sign_on.go index 32ac2c0..00f076d 100644 --- a/eliona/single_sign_on.go +++ b/eliona/single_sign_on.go @@ -18,7 +18,7 @@ package eliona import ( "context" "encoding/json" - "errors" + "fmt" "net/http" "saml-sso/apiserver" "saml-sso/conf" @@ -79,7 +79,9 @@ func (s *SingleSignOn) ActiveHandle(w http.ResponseWriter, r *http.Request) { } } + w.Header().Add("Content-Type", "application/json") w.WriteHeader(responseCode) + _, err = w.Write(responseMsg) if err != nil { log.Error(LOG_REGIO, "write internal server error: %v", err) @@ -94,8 +96,9 @@ func (s *SingleSignOn) Authentication(w http.ResponseWriter, r *http.Request) { mapping *apiserver.AttributeMap - loginEmail, userIp string - firstname, lastname, phone string + loginEmail, userIp string + firstname, lastname string + phone *string user *api.User jwt *string @@ -133,7 +136,8 @@ func (s *SingleSignOn) Authentication(w http.ResponseWriter, r *http.Request) { lastname = samlsp.AttributeFromContext(r.Context(), *mapping.LastName) } if mapping.Phone != nil && *mapping.Phone != "" { - phone = samlsp.AttributeFromContext(r.Context(), *mapping.Phone) + phoneS := samlsp.AttributeFromContext(r.Context(), *mapping.Phone) + phone = &phoneS } log.Info(LOG_REGIO, "User with firstname: %v, lastname: %v, email/login: "+ @@ -156,18 +160,19 @@ func (s *SingleSignOn) Authentication(w http.ResponseWriter, r *http.Request) { errorMessage = []byte(err.Error()) goto internalServerError } - } - err = UpdateElionaUserArchivedPhone(user.Email, &phone, s.userToArchive) - if err != nil { - log.Error(LOG_REGIO, "cannot set phone and archive flag: %v", err) - errorMessage = []byte(err.Error()) - goto internalServerError - } + err = UpdateElionaUserArchivedPhone(user.Email, phone, s.userToArchive) + if err != nil { + log.Error(LOG_REGIO, "cannot set phone and archive flag: %v", err) + errorMessage = []byte(err.Error()) + goto internalServerError + } - err = s.setUserPermissions(user.Email) - if err != nil { - log.Error(LOG_REGIO, "cannot set user permissions") + err = s.setUserPermissions(user.Id.Get()) + if err != nil { + log.Error(LOG_REGIO, "cannot set user permissions: %v", err) + goto notAuthenticated + } } // obtain a jwt to login via cookies @@ -218,7 +223,7 @@ internalServerError: } } -func (s *SingleSignOn) setUserPermissions(email string) error { +func (s *SingleSignOn) setUserPermissions(userId *string) error { var ( err error @@ -231,9 +236,24 @@ func (s *SingleSignOn) setUserPermissions(email string) error { } log.Info(LOG_REGIO, - "ToDo: add user to a project and set permissions according the configurations. %v", + "ToDo: permission map not finished yet. %v", permissions) - err = errors.New("not implemented") - return err + projectId, err := GetFirstProjectId() + if err != nil { + return fmt.Errorf("cannot look up project id: %v", err) + } + + roleId, err := GetRoleIdByDisplayName(permissions.DefaultProjRole) + if err != nil { + return fmt.Errorf("cannot get role id for role %s: %v", + permissions.DefaultProjRole, err) + } + + if roleId <= 0 { + return fmt.Errorf("cannot get role id for %s", + permissions.DefaultProjRole) + } + + return SetProjectUser(projectId, userId, roleId) } diff --git a/openapi.yaml b/openapi.yaml index b81df85..b06d541 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -474,14 +474,14 @@ components: type: string readOnly: false nullable: false - default: 'regular' - example: 'regular' + default: 'System user' + example: 'System user' default_proj_role: type: string nullable: false readOnly: false - default: 'operator' - example: 'operator' + default: 'Project user' + example: 'Project user' system_role_saml_attribute: type: string nullable: true @@ -512,7 +512,7 @@ components: elionaRole: type: string nullable: false - example: 'admin' + example: 'System user' samlValue: type: string nullable: false From 9e8a444908635dd28d392e28dc4f7245ff89454b Mon Sep 17 00:00:00 2001 From: christian-stauffer Date: Tue, 6 Feb 2024 18:34:46 +0100 Subject: [PATCH 2/3] add, mod tests --- README.md | 2 +- apiservices/api_configuration_service_test.go | 83 +++++++++++++++++++ app.go | 2 +- eliona/others_test.go | 11 +++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 apiservices/api_configuration_service_test.go diff --git a/README.md b/README.md index 92cb1c7..b48eff6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This initialization can be handled by the `reset.sql` script. - `LOG_LEVEL`(optional): defines the minimum level that should be [logged](https://github.com/eliona-smart-building-assistant/go-utils/blob/main/log/README.md). The default level is `info`. -- `SSO_SERVER_PORT` (optional): defines the port for Single Sign On Services, here SAML 2.0. The default value is Port `8080`. +- `SSO_SERVER_PORT` (optional): defines the port for Single Sign On Services, here SAML 2.0. The default value is Port `80`. ### Database tables ### diff --git a/apiservices/api_configuration_service_test.go b/apiservices/api_configuration_service_test.go new file mode 100644 index 0000000..5068eb3 --- /dev/null +++ b/apiservices/api_configuration_service_test.go @@ -0,0 +1,83 @@ +// This file is part of the eliona project. +// Copyright © 2023 Eliona by IoTEC AG. All Rights Reserved. +// ______ _ _ +// | ____| (_) +// | |__ | |_ ___ _ __ __ _ +// | __| | | |/ _ \| '_ \ / _` | +// | |____| | | (_) | | | | (_| | +// |______|_|_|\___/|_| |_|\__,_| +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package apiservices_test + +import ( + "context" + "database/sql" + "fmt" + "saml-sso/apiservices" + "saml-sso/conf" + "testing" + + "github.com/eliona-smart-building-assistant/go-eliona/app" + "github.com/eliona-smart-building-assistant/go-utils/db" +) + +func TestApp_AppApi_Configuration_InitDB(t *testing.T) { + err := conf.UserLeicomInit() + if err != nil { + t.Log("user leicom, ", err) + } + err = conf.DropOwnSchema() + if err != nil { + // no error, if schema not exist + t.Log("drop schema, ", err) + } + + execFunc := app.ExecSqlFile("../conf/init.sql") + err = execFunc(db.NewConnection()) + if err != nil { + t.Error("init.sql failed, ", err) + } +} + +func TestApp_AppApi_Configuration_GetBasicConfig(t *testing.T) { + apiService := apiservices.NewConfigurationApiService() + cnf, err := apiService.GetBasicConfiguration(context.Background()) + if err != sql.ErrNoRows { + t.Error(err) + } + fmt.Println(cnf) +} + +func TestApp_AppApi_Configuration_GetAdvancedConfig(t *testing.T) { + +} + +func TestApp_AppApi_Configuration_GetAttributeMapping(t *testing.T) { + +} + +func TestApp_AppApi_Configuration_GetPermissionMap(t *testing.T) { + +} + +func TestApp_AppApi_Configuration_PutBasicConfig(t *testing.T) { + +} + +func TestApp_AppApi_Configuration_PutAdvancedConfig(t *testing.T) { + +} + +func TestApp_AppApi_Configuration_PutAttributeMapping(t *testing.T) { + +} + +func TestApp_AppApi_Configuration_PutPermissionMap(t *testing.T) { + +} diff --git a/app.go b/app.go index 2752db3..fed6a5b 100644 --- a/app.go +++ b/app.go @@ -33,7 +33,7 @@ import ( const ( LOG_REGIO = "app" API_SERVER_PORT = 3000 - SSO_SERVER_PORT = 8081 // Publicly accessible without auth. See wiki. + SSO_SERVER_PORT = 80 // Publicly accessible without auth. See wiki. SAML_SPECIFIC_ENDPOINT_PATH = "/saml/" ) diff --git a/eliona/others_test.go b/eliona/others_test.go index c1967a0..5441e30 100644 --- a/eliona/others_test.go +++ b/eliona/others_test.go @@ -16,6 +16,7 @@ package eliona_test import ( + "os" "saml-sso/eliona" "testing" @@ -23,6 +24,16 @@ import ( ) func TestApp_Others(t *testing.T) { + + // this test needs a real eliona db to due missing tables + // in the test db + _, realDb := os.LookupEnv("REAL_DB") + if !realDb { + log.Warn("TestApp_Others", "test disabled because missing env var REAL_DB") + t.Log("TestApp_Others: test disabled because missing env var REAL_DB") + return + } + token, err := eliona.GetElionaJsonWebToken("su#@eliona.io") if err != nil { t.Error(err) From 15a077f2a9244cd420605b4768626bcb6eef2677 Mon Sep 17 00:00:00 2001 From: christian-stauffer Date: Wed, 7 Feb 2024 09:50:54 +0100 Subject: [PATCH 3/3] undo changing port 80 to 8081, because it's used for public endpoints in app concept. --- README.md | 2 +- app.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b48eff6..e833c16 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This initialization can be handled by the `reset.sql` script. - `LOG_LEVEL`(optional): defines the minimum level that should be [logged](https://github.com/eliona-smart-building-assistant/go-utils/blob/main/log/README.md). The default level is `info`. -- `SSO_SERVER_PORT` (optional): defines the port for Single Sign On Services, here SAML 2.0. The default value is Port `80`. +- `SSO_SERVER_PORT` (optional): defines the port for Single Sign On Services, here SAML 2.0. The default value is Port `8081`. ### Database tables ### diff --git a/app.go b/app.go index fed6a5b..2752db3 100644 --- a/app.go +++ b/app.go @@ -33,7 +33,7 @@ import ( const ( LOG_REGIO = "app" API_SERVER_PORT = 3000 - SSO_SERVER_PORT = 80 // Publicly accessible without auth. See wiki. + SSO_SERVER_PORT = 8081 // Publicly accessible without auth. See wiki. SAML_SPECIFIC_ENDPOINT_PATH = "/saml/" )