/* * Copyright (c) 2020 InterDigital Communications, Inc * * Licensed under the Apache License, Version 2.0 (the \"License\"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an \"AS IS\" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * AdvantEDGE Platform Controller REST API * * This API is the main Platform Controller API for scenario configuration & sandbox management

**Micro-service**
[meep-pfm-ctrl](https://github.com/InterDigitalInc/AdvantEDGE/tree/master/go-apps/meep-platform-ctrl)

**Type & Usage**
Platform main interface used by controller software to configure scenarios and manage sandboxes in the AdvantEDGE platform

**Details**
API details available at _your-AdvantEDGE-ip-address/api_ * * API version: 1.0.0 * Contact: AdvantEDGE@InterDigital.com * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) */ package server import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strconv" "strings" "sync" "time" dataModel "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-model" log "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger" ms "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-metric-store" mq "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-mq" pcc "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-platform-ctrl-client" sm "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-sessions" users "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-users" "github.com/google/go-github/github" "github.com/gorilla/mux" "github.com/lkysow/go-gitlab" "github.com/roymx/viper" "golang.org/x/oauth2" ) const OAUTH_PROVIDER_GITHUB = "github" const OAUTH_PROVIDER_GITLAB = "gitlab" const OAUTH_PROVIDER_LOCAL = "local" const moduleName = "meep-auth-svc" const moduleNamespace = "default" const postgisUser = "postgres" const postgisPwd = "pwd" const permissionsRoot = "services" const pfmCtrlBasepath = "http://meep-platform-ctrl/platform-ctrl/v1" type LoginRequest struct { provider string timer *time.Timer } type AuthSvc struct { sessionMgr *sm.SessionMgr userStore *users.Connector metricStore *ms.MetricStore mqGlobal *mq.MsgQueue pfmCtrlClient *pcc.APIClient maxSessions int uri string oauthConfigs map[string]*oauth2.Config loginRequests map[string]*LoginRequest } var mutex sync.Mutex var gitlabApiUrl = "" // Declare as variables to enable overwrite in test var redisDBAddr = "meep-redis-master:6379" var influxDBAddr string = "http://meep-influxdb.default.svc.cluster.local:8086" // Auth Service var authSvc *AuthSvc func Init() (err error) { // Create new Platform Controller authSvc = new(AuthSvc) // Create message queue authSvc.mqGlobal, err = mq.NewMsgQueue(mq.GetGlobalName(), moduleName, moduleNamespace, redisDBAddr) if err != nil { log.Error("Failed to create Message Queue with error: ", err) return err } log.Info("Message Queue created") // Create Platform Controller REST API client pfmCtrlClientCfg := pcc.NewConfiguration() pfmCtrlClientCfg.BasePath = pfmCtrlBasepath authSvc.pfmCtrlClient = pcc.NewAPIClient(pfmCtrlClientCfg) if authSvc.pfmCtrlClient == nil { err := errors.New("Failed to create Platform Ctrl REST API client") return err } log.Info("Platform Ctrl REST API client created") // Connect to Session Manager authSvc.sessionMgr, err = sm.NewSessionMgr(moduleName, "", redisDBAddr, redisDBAddr) if err != nil { log.Error("Failed connection to Session Manager: ", err.Error()) return err } log.Info("Connected to Session Manager") // Connect to User Store authSvc.userStore, err = users.NewConnector(moduleName, postgisUser, postgisPwd, "", "") if err != nil { log.Error("Failed connection to User Store: ", err.Error()) return err } _ = authSvc.userStore.CreateTables() log.Info("Connected to User Store") // Set endpoint authorization permissions setPermissions() // Connect to Metric Store authSvc.metricStore, err = ms.NewMetricStore("session-metrics", "global", influxDBAddr, ms.MetricsDbDisabled) if err != nil { log.Error("Failed connection to Metric Store: ", err) return err } // Retrieve maximum session count from environment variable if maxSessions, err := strconv.ParseInt(os.Getenv("MEEP_MAX_SESSIONS"), 10, 0); err == nil { authSvc.maxSessions = int(maxSessions) } log.Info("MEEP_MAX_SESSIONS: ", authSvc.maxSessions) // Get default platform URI authSvc.uri = strings.TrimSpace(os.Getenv("MEEP_HOST_URL")) // Initialize OAuth authSvc.oauthConfigs = make(map[string]*oauth2.Config) authSvc.loginRequests = make(map[string]*LoginRequest) // Initialize Github config githubEnabledStr := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITHUB_ENABLED")) githubEnabled, err := strconv.ParseBool(githubEnabledStr) if err == nil && githubEnabled { clientId := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITHUB_CLIENT_ID")) secret := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITHUB_SECRET")) redirectUri := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITHUB_REDIRECT_URI")) authUrl := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITHUB_AUTH_URL")) tokenUrl := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITHUB_TOKEN_URL")) if clientId != "" && secret != "" && redirectUri != "" && authUrl != "" && tokenUrl != "" { oauthConfig := &oauth2.Config{ ClientID: clientId, ClientSecret: secret, RedirectURL: redirectUri, Scopes: []string{}, Endpoint: oauth2.Endpoint{ AuthURL: authUrl, TokenURL: tokenUrl, }, } authSvc.oauthConfigs[OAUTH_PROVIDER_GITHUB] = oauthConfig log.Info("GitHub OAuth provider enabled") } } // Initialize GitLab config gitlabEnabledStr := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITLAB_ENABLED")) gitlabEnabled, err := strconv.ParseBool(gitlabEnabledStr) if err == nil && gitlabEnabled { gitlabApiUrl = strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITLAB_API_URL")) clientId := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITLAB_CLIENT_ID")) secret := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITLAB_SECRET")) redirectUri := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITLAB_REDIRECT_URI")) authUrl := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITLAB_AUTH_URL")) tokenUrl := strings.TrimSpace(os.Getenv("MEEP_OAUTH_GITLAB_TOKEN_URL")) if clientId != "" && secret != "" && redirectUri != "" && authUrl != "" && tokenUrl != "" { oauthConfig := &oauth2.Config{ ClientID: clientId, ClientSecret: secret, RedirectURL: redirectUri, Scopes: []string{"read_user"}, Endpoint: oauth2.Endpoint{ AuthURL: authUrl, TokenURL: tokenUrl, }, } authSvc.oauthConfigs[OAUTH_PROVIDER_GITLAB] = oauthConfig log.Info("GitLab OAuth provider enabled") } } return nil } func Run() (err error) { // Start Session Watchdog err = authSvc.sessionMgr.StartSessionWatchdog(sessionTimeoutCb) if err != nil { log.Error("Failed start Session Watchdog: ", err.Error()) return err } return nil } func setPermissions() { // Flush old permissions ps := authSvc.sessionMgr.GetPermissionStore() ps.Flush() // Read & apply API permissions from file permissionsFile := "/permissions.yaml" permissions := viper.New() permissions.SetConfigFile(permissionsFile) err := permissions.ReadInConfig() if err != nil { log.Warn("Failed to read permissions from file") log.Warn("Granting full API access for all roles by default") _ = ps.SetDefaultPermission(&sm.Permission{Mode: sm.ModeAllow}) return } // Loop through services for service := range permissions.GetStringMap(permissionsRoot) { // Default permissions if service == "default" { permissionsRoute := permissionsRoot + ".default" permission := new(sm.Permission) permission.Mode = permissions.GetString(permissionsRoute + ".mode") permission.RolePermissions = make(map[string]string) for role, access := range permissions.GetStringMapString(permissionsRoute + ".roles") { permission.RolePermissions[role] = access } _ = ps.SetDefaultPermission(permission) } else { // Service route names permissionsService := permissionsRoot + "." + service for name := range permissions.GetStringMap(permissionsService) { permissionsRoute := permissionsService + "." + name permission := new(sm.Permission) permission.Mode = permissions.GetString(permissionsRoute + ".mode") permission.RolePermissions = make(map[string]string) for role, access := range permissions.GetStringMapString(permissionsRoute + ".roles") { permission.RolePermissions[role] = access } _ = ps.Set(service, name, permission) } } } } func sessionTimeoutCb(session *sm.Session) { log.Info("Session timed out. ID[", session.ID, "] Username[", session.Username, "]") var metric ms.SessionMetric metric.Provider = session.Provider metric.User = session.Username metric.Sandbox = session.Sandbox _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeTimeout, metric) // Destroy session sandbox _, _ = authSvc.pfmCtrlClient.SandboxControlApi.DeleteSandbox(context.TODO(), session.Sandbox) } // Generate a random state string func generateState(n int) (string, error) { data := make([]byte, n) if _, err := io.ReadFull(rand.Reader, data); err != nil { return "", err } return base64.StdEncoding.EncodeToString(data), nil } func getUniqueState() (state string, err error) { for i := 0; i < 3; i++ { // Get random state randState, err := generateState(20) if err != nil { log.Error(err.Error()) return "", err } // Make sure state is unique if _, found := authSvc.loginRequests[randState]; !found { return randState, nil } } return "", errors.New("Failed to generate a random state string") } func getLoginRequest(state string) *LoginRequest { mutex.Lock() defer mutex.Unlock() request, found := authSvc.loginRequests[state] if !found { return nil } return request } func setLoginRequest(state string, request *LoginRequest) { mutex.Lock() defer mutex.Unlock() authSvc.loginRequests[state] = request } func delLoginRequest(state string) { mutex.Lock() defer mutex.Unlock() request, found := authSvc.loginRequests[state] if !found { return } if request.timer != nil { request.timer.Stop() } delete(authSvc.loginRequests, state) } func getErrUrl(err string) string { return authSvc.uri + "?err=" + strings.ReplaceAll(err, " ", "+") } func asAuthenticate(w http.ResponseWriter, r *http.Request) { // Get service & sandbox name from request parameters vars := mux.Vars(r) svcName := vars["svc"] sboxName := vars["sbox"] log.Debug("svcName: ", svcName, " sboxName: ", sboxName) // Get target method & URL from forwarded request headers targetMethod := r.Header.Get("X-Original-Method") targetUrl := r.Header.Get("X-Original-URL") log.Debug("targetMethod: ", targetMethod, " targetUrl: ", targetUrl) // Get target permissions // TODO -- parse permissions file on startup to create regexp table // // Get permission store instance // ps := pfmCtrl.sessionMgr.GetPermissionStore() // if targetMethod != "" && targetUrl != "" { // url, err := url.ParseRequestURI(targetUrl) // if err == nil { // } // } permission := new(sm.Permission) // permission.Mode = sm.ModeAllow permission.Mode = sm.ModeVerify permission.RolePermissions = make(map[string]string) permission.RolePermissions[sm.RoleAdmin] = sm.ModeAllow permission.RolePermissions[sm.RoleUser] = sm.ModeBlock // // Use default permission if none found // if permission == nil { // permission, err = ps.GetDefaultPermission() // if err != nil || permission == nil { // http.Error(w, "Unauthorized", http.StatusUnauthorized) // return // } // } // Get session store instance ss := authSvc.sessionMgr.GetSessionStore() // Handle according to permission mode switch permission.Mode { case sm.ModeBlock: http.Error(w, "Unauthorized", http.StatusUnauthorized) return case sm.ModeAllow: case sm.ModeVerify: // Retrieve user session, if any session, err := ss.Get(r) if err != nil || session == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Verify role permissions role := session.Role if role == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } access := permission.RolePermissions[role] if access != sm.AccessGranted { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // For non-admin users, verify session sandbox matches service sandbox, if any if session.Role != sm.RoleAdmin && sboxName != "" && sboxName != session.Sandbox { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } default: http.Error(w, "Unauthorized", http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } func asAuthorize(w http.ResponseWriter, r *http.Request) { var metric ms.SessionMetric // Retrieve query parameters query := r.URL.Query() code := query.Get("code") state := query.Get("state") // Validate request state request := getLoginRequest(state) if request == nil { err := errors.New("Invalid OAuth state") log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } // Get provider-specific OAuth config provider := request.provider config, found := authSvc.oauthConfigs[provider] if !found { err := errors.New("Provider config not found for: " + provider) log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } metric.Provider = provider // Delete login request & timer delLoginRequest(state) // Retrieve access token token, err := config.Exchange(context.Background(), code) if err != nil { log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } oauthClient := config.Client(context.Background(), token) if oauthClient == nil { err = errors.New("Failed to create new oauth client") log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } // Retrieve User ID var userId string switch provider { case OAUTH_PROVIDER_GITHUB: client := github.NewClient(oauthClient) if client == nil { err = errors.New("Failed to create new GitHub client") log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } user, _, err := client.Users.Get(context.Background(), "") if err != nil { log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl("Failed to retrieve GitHub user ID"), http.StatusFound) return } userId = *user.Login case OAUTH_PROVIDER_GITLAB: client := gitlab.NewOAuthClient(oauthClient, token.AccessToken) if client == nil { err = errors.New("Failed to create new GitLab client") log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } // Override default gitlab base URL if gitlabApiUrl != "" { err = client.SetBaseURL(gitlabApiUrl) if err != nil { log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl("Failed to set GitLab API base url"), http.StatusFound) return } } user, _, err := client.Users.CurrentUser() if err != nil { log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl("Failed to retrieve GitLab user ID"), http.StatusFound) return } userId = user.Username default: } metric.User = userId // Start user session sandboxName, err, errCode := startSession(provider, userId, w, r) if err != nil { log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), errCode) return } metric.Sandbox = sandboxName _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeLogin, metric) // Redirect user to sandbox http.Redirect(w, r, authSvc.uri+"?sbox="+sandboxName+"&user="+userId, http.StatusFound) } func asLogin(w http.ResponseWriter, r *http.Request) { log.Info("----- OAUTH LOGIN -----") var metric ms.SessionMetric // Retrieve query parameters query := r.URL.Query() provider := query.Get("provider") metric.Provider = provider // Get provider-specific OAuth config config, found := authSvc.oauthConfigs[provider] if !found { err := errors.New("Provider config not found for: " + provider) log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } // Generate unique random state string state, err := getUniqueState() if err != nil { log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Redirect(w, r, getErrUrl(err.Error()), http.StatusFound) return } // Track oauth request & handle request := &LoginRequest{ provider: provider, timer: time.NewTimer(10 * time.Minute), } setLoginRequest(state, request) // Start timer to remove request from map go func() { <-request.timer.C delLoginRequest(state) }() // Generate provider-specific oauth redirect uri := config.AuthCodeURL(state, oauth2.AccessTypeOnline) http.Redirect(w, r, uri, http.StatusFound) } func asLoginUser(w http.ResponseWriter, r *http.Request) { log.Info("----- LOGIN -----") var metric ms.SessionMetric // Get form data username := r.FormValue("username") password := r.FormValue("password") metric.Provider = OAUTH_PROVIDER_LOCAL metric.User = username // Validate user credentials authenticated, err := authSvc.userStore.AuthenticateUser(OAUTH_PROVIDER_LOCAL, username, password) if err != nil || !authenticated { if err != nil { metric.Description = err.Error() } else { metric.Description = "Unauthorized" } _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Start user session sandboxName, err, errCode := startSession(OAUTH_PROVIDER_LOCAL, username, w, r) if err != nil { log.Error(err.Error()) metric.Description = err.Error() _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeError, metric) http.Error(w, err.Error(), errCode) return } metric.Sandbox = sandboxName _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeLogin, metric) // Prepare response var sandbox dataModel.Sandbox sandbox.Name = sandboxName // Format response jsonResponse, err := json.Marshal(sandbox) if err != nil { log.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } // Send response w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, string(jsonResponse)) } // Retrieve existing user session or create a new one func startSession(provider string, username string, w http.ResponseWriter, r *http.Request) (sandboxName string, err error, code int) { // Get existing session by user name, if any sessionStore := authSvc.sessionMgr.GetSessionStore() session, err := sessionStore.GetByName(provider, username) if err != nil { // Check if max session count is reached before creating a new one count := sessionStore.GetCount() if count >= authSvc.maxSessions { err = errors.New("Maximum session count exceeded") return "", err, http.StatusServiceUnavailable } // Get requested sandbox name & role from user profile, if any role := users.RoleUser user, err := authSvc.userStore.GetUser(provider, username) if err == nil { sandboxName = user.Sboxname role = user.Role } // Create sandbox var sandboxConfig pcc.SandboxConfig if sandboxName == "" { sandbox, _, err := authSvc.pfmCtrlClient.SandboxControlApi.CreateSandbox(context.TODO(), sandboxConfig) if err != nil { return "", err, http.StatusInternalServerError } sandboxName = sandbox.Name } else { _, err := authSvc.pfmCtrlClient.SandboxControlApi.CreateSandboxWithName(context.TODO(), sandboxName, sandboxConfig) if err != nil { return "", err, http.StatusInternalServerError } } // Create new session session = new(sm.Session) session.ID = "" session.Username = username session.Provider = provider session.Sandbox = sandboxName session.Role = role } else { sandboxName = session.Sandbox } // Set session err, code = sessionStore.Set(session, w, r) if err != nil { log.Error("Failed to set session with err: ", err.Error()) // Remove newly created sandbox on failure if session.ID == "" { _, _ = authSvc.pfmCtrlClient.SandboxControlApi.DeleteSandbox(context.TODO(), sandboxName) } return "", err, code } return sandboxName, nil, http.StatusOK } func asLogout(w http.ResponseWriter, r *http.Request) { log.Info("----- LOGOUT -----") var metric ms.SessionMetric // Get existing session sessionStore := authSvc.sessionMgr.GetSessionStore() session, err := sessionStore.Get(r) if err == nil { metric.Provider = session.Provider metric.User = session.Username metric.Sandbox = session.Sandbox // Delete sandbox _, _ = authSvc.pfmCtrlClient.SandboxControlApi.DeleteSandbox(context.TODO(), session.Sandbox) } // Delete session err, code := sessionStore.Del(w, r) if err != nil { log.Error("Failed to delete session with err: ", err.Error()) http.Error(w, err.Error(), code) return } _ = authSvc.metricStore.SetSessionMetric(ms.SesMetTypeLogout, metric) w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } func asTriggerWatchdog(w http.ResponseWriter, r *http.Request) { // Refresh session sessionStore := authSvc.sessionMgr.GetSessionStore() err, code := sessionStore.Refresh(w, r) if err != nil { log.Error("Failed to refresh session with err: ", err.Error()) http.Error(w, err.Error(), code) return } w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) }