diff --git a/galaxy_ng/tests/integration/aap/__init__.py b/galaxy_ng/tests/integration/aap/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/galaxy_ng/tests/integration/aap/test_aap_rbac.py b/galaxy_ng/tests/integration/aap/test_aap_rbac.py new file mode 100644 index 0000000000..9491dd7fd0 --- /dev/null +++ b/galaxy_ng/tests/integration/aap/test_aap_rbac.py @@ -0,0 +1,111 @@ +import json +import os + +import pytest + +# from galaxykit.client import GalaxyClient +from galaxykit.client import BasicAuthClient +# from galaxykit.collections import upload_test_collection +# from galaxykit.utils import wait_for_task +# from galaxy_ng.tests.integration.utils import set_certification + + +pytestmark = pytest.mark.qa # noqa: F821 + + +@pytest.mark.deployment_standalone +@pytest.mark.skipif( + not os.environ.get('JWT_PROXY'), + reason="relies on jwt proxy" +) +def test_aap_service_index_and_claims_processing( + settings, + ansible_config, + galaxy_client, + random_username +): + + gc = galaxy_client("admin", ignore_cache=True) + ga = BasicAuthClient(gc.galaxy_root, 'admin', 'admin') + + org_name = random_username.replace('user_', 'org_') + team_name = random_username.replace('user_', 'team_') + + # make the org in the gateway + org_data = ga.post( + '/api/gateway/v1/organizations/', + body=json.dumps({'name': org_name}) + ) + + # make the team in the gateway + team_data = ga.post( + '/api/gateway/v1/teams/', + body=json.dumps({'name': team_name, 'organization': org_data['id']}) + ) + + # make the user in the gateway + user_data = ga.post( + '/api/gateway/v1/users/', + body=json.dumps({'username': random_username, 'password': 'redhat1234'}) + ) + + # get all of gateway's roledefs ... + gateway_roledefs = ga.get('/api/gateway/v1/role_definitions/') + gateway_roledefs = dict((x['name'], x) for x in gateway_roledefs['results']) + + # get all of galaxy's roledefs ... + galaxy_roledefs = ga.get('/api/galaxy/_ui/v2/role_definitions/') + galaxy_roledefs = dict((x['name'], x) for x in galaxy_roledefs['results']) + + # make the user a team member in the gateway ... + ga.post( + '/api/gateway/v1/role_user_assignments/', + body=json.dumps({ + 'user': user_data['id'], + 'role_definition': gateway_roledefs['Team Member']['id'], + 'object_id': team_data['id'], + }) + ) + + # access galaxy as the user to process claims ... + uc = BasicAuthClient(gc.galaxy_root, random_username, 'redhat1234') + new_data = uc.get(f'/api/galaxy/_ui/v2/users/?username={random_username}') + assert new_data['count'] == 1 + new_user = new_data['results'][0] + assert new_user['username'] == random_username + + # the inheritied orgs should not show memberships ... + assert not new_user['organizations'] + + # the team should be shown ... + new_teams = [x['name'] for x in new_user['teams']] + assert new_teams == [team_name] + + # delete the user in the gateway ... + uid = user_data['id'] + rr = ga.delete(f'/api/gateway/v1/users/{uid}/', parse_json=False) + + # make sure the user is gone from galaxy ... + rr = ga.get(f'/api/galaxy/_ui/v2/users/?username={random_username}') + assert rr['count'] == 0 + + # delete the team in the gateway + tid = team_data['id'] + rr = ga.delete(f'/api/gateway/v1/teams/{tid}/', parse_json=False) + + # make sure the team is gone from galaxy ... + rr = ga.get(f'/api/galaxy/_ui/v2/teams/?name={team_name}') + assert rr['count'] == 0 + + # FIXME: cascade delete on the group from the team isn't working + # group_name = org_name + '::' + team_name + # rr = ga.get(f'/api/galaxy/_ui/v2/groups/?name={group_name}') + # assert rr['count'] == 0 + + # delete the org in the gateway + oid = org_data['id'] + rr = ga.delete(f'/api/gateway/v1/organizations/{oid}/', parse_json=False) + + # make sure the org is gone from galaxy ... + rr = ga.get(f'/api/galaxy/_ui/v2/organizations/?name={org_name}') + assert rr['count'] == 0 diff --git a/profiles/dab_jwt/proxy/.air.toml b/profiles/dab_jwt/proxy/.air.toml index e22fc5b027..ad8d69f09a 100644 --- a/profiles/dab_jwt/proxy/.air.toml +++ b/profiles/dab_jwt/proxy/.air.toml @@ -1,6 +1,6 @@ [build] bin = "proxy" -cmd = "go build -o proxy proxy.go" +cmd = "go build -o proxy *.go" full_bin = "./proxy" exclude_regex = ["*.swp"] diff --git a/profiles/dab_jwt/proxy/proxy.go b/profiles/dab_jwt/proxy/proxy.go index bf4f750441..5f66cc1f73 100644 --- a/profiles/dab_jwt/proxy/proxy.go +++ b/profiles/dab_jwt/proxy/proxy.go @@ -24,194 +24,25 @@ package main import ( - "bytes" "crypto/rand" "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/pem" "fmt" - "io" - "io/ioutil" "log" "net/http" "net/http/httputil" "net/url" - "os" - "sort" - "strconv" - "strings" "sync" - "time" - "crypto/hmac" - "crypto/sha256" - "encoding/json" - "errors" - - "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" ) -/************************************************************ - TYPES -************************************************************/ - -// UserSession represents the user session information -type UserSession struct { - Username string - CSRFToken string - SessionID string -} - -// User represents a user's information -type User struct { - Id int - Username string - Password string - FirstName string - LastName string - IsSuperuser bool - Email string - Organizations []string - Teams []string - IsSystemAuditor bool - Sub string -} - -/* -pulp-1 | {'aud': 'ansible-services', -pulp-1 | 'exp': 1718658788, -pulp-1 | 'global_roles': [], -pulp-1 | 'iss': 'ansible-issuer', -pulp-1 | 'object_roles': {'Team Member': {'content_type': 'team', 'objects': [0]}}, -pulp-1 | 'objects': {'organization': [{'ansible_id': 'bc243368-a9d4-4f8f-9ffe-5d2d921fcee5', -pulp-1 | 'name': 'Default'}], -pulp-1 | 'team': [{'ansible_id': '34a58292-1e0f-49f0-9383-fb7e63d771d9', -pulp-1 | 'name': 'ateam', -pulp-1 | 'org': 0}]}, -pulp-1 | 'sub': '4f6499bf-3ad2-45ff-8411-0188c4f817c1', -pulp-1 | 'user_data': {'email': 'sa@localhost.com', -pulp-1 | 'first_name': 'sa', -pulp-1 | 'is_superuser': True, -pulp-1 | 'last_name': 'asdfasdf', -pulp-1 | 'username': 'superauditor'}, -pulp-1 | 'version': '1'} -*/ - -// JWT claims -type UserClaims struct { - Version int `json:"version"` - Iss string `json:"iss"` - Aud string `json:"aud"` - Expires int64 `json:"exp"` - GlobalRoles []string `json:"global_roles"` - UserData UserData `json:"user_data"` - Sub string `json:"sub"` - ObjectRoles map[string]interface{} `json:"object_roles"` - Objects map[string]interface{} `json:"objects"` -} - -// Implement the jwt.Claims interface -func (c UserClaims) Valid() error { - if time.Unix(c.Expires, 0).Before(time.Now()) { - return fmt.Errorf("token is expired") - } - return nil -} - -type UserData struct { - Username string `json:"username"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - IsSuperuser bool `json:"is_superuser"` - Email string `json:"email"` -} - -type Organization struct { - Id int `json:"id"` - AnsibleId string `json:"ansible_id"` - Name string `json:"name"` - CodeName string `json:"code_name"` -} - -type Team struct { - Id int `json:"id"` - AnsibleId string `json:"ansible_id"` - Name string `json:"name"` - Org string `json:"org"` -} - -type TeamObject struct { - AnsibleId string `json:"ansible_id"` - Name string `json:"name"` - Org int `json:"org"` -} - -type ObjectRole struct { - ContentType string `json:"content_type"` - Objects []int `json:"objects"` -} - -// LoginRequest represents the login request payload -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// LoginResponse represents the login response payload -type LoginResponse struct { - CSRFToken string `json:"csrfToken"` -} - -type OrgResponse struct { - ID int `json:"id"` - Name string `json:"name"` - SummaryFields struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - } `json:"summary_fields"` -} - -type TeamResponse struct { - ID int `json:"id"` - Name string `json:"name"` - Organization int `json:"organization"` - SummaryFields struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - } `json:"summary_fields"` -} - -type UserResponse struct { - ID int `json:"id"` - Username string `json:"username"` - SummaryFields struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - } `json:"summary_fields"` -} - -type OrgRequest struct { - Name string `json:"name"` -} - -type TeamRequest struct { - Name string `json:"name"` - Organization int `json:"organization"` -} - -type AssociationRequest struct { - Instances []int `json:"instances"` -} - /************************************************************ GLOBALS & SETTINGS ************************************************************/ +var ANSIBLE_BASE_SHARED_SECRET = "redhat1234" +var SERVICE_ID = uuid.New().String() + // Global table to store CSRF tokens, session IDs, and usernames var tokenTable = struct { sync.RWMutex @@ -233,1217 +64,45 @@ var ( rsaPublicKey *rsa.PublicKey ) -var ANSIBLE_BASE_SHARED_SECRET = "redhat1234" - -/* -var orgmap = map[string]int{ - "default": 0, - "org1": 1, - "org2": 2, -} -*/ - -var prepopulatedOrgs = map[string]Organization{ - "default": { - Id: 1, - AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee0", - Name: "Default", - CodeName: "default", - }, - "org1": { - Id: 2, - AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee1", - Name: "Organization 1", - CodeName: "org1", - }, - "org2": { - Id: 3, - AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee2", - Name: "Organization 2", - CodeName: "org2", - }, - "pe": { - Id: 4, - AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee3", - Name: "system:partner-engineers", - CodeName: "pe", - }, -} - -var prepopulatedTeams = map[string]Team{ - "ateam": { - Id: 1, - AnsibleId: "34a58292-1e0f-49f0-9383-fb7e63d771aa", - Name: "ateam", - Org: "org1", - }, - "bteam": { - Id: 2, - AnsibleId: "34a58292-1e0f-49f0-9383-fb7e63d771ab", - Name: "bteam", - Org: "default", - }, - "peteam": { - Id: 3, - AnsibleId: "34a58292-1e0f-49f0-9383-fb7e63d771ac", - Name: "peteam", - Org: "pe", - }, -} - -// Define users -var prepopulatedUsers = map[string]User{ - "admin": { - Id: 1, - Username: "admin", - Password: "admin", - FirstName: "ad", - LastName: "min", - IsSuperuser: true, - Email: "admin@example.com", - Organizations: []string{"default"}, - Teams: []string{}, - IsSystemAuditor: true, - Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce99", - }, - "notifications_admin": { - Id: 2, - Username: "notifications_admin", - Password: "redhat", - FirstName: "notifications", - LastName: "admin", - IsSuperuser: true, - Email: "notifications_admin@example.com", - Organizations: []string{"default"}, - Teams: []string{}, - IsSystemAuditor: true, - Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce98", - }, - "ee_admin": { - Id: 3, - Username: "ee_admin", - Password: "redhat", - FirstName: "ee", - LastName: "admin", - IsSuperuser: true, - Email: "ee_admin@example.com", - Organizations: []string{"default"}, - Teams: []string{}, - IsSystemAuditor: true, - Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce97", - }, - "jdoe": { - Id: 4, - Username: "jdoe", - Password: "redhat", - FirstName: "John", - LastName: "Doe", - //IsSuperuser: false, - IsSuperuser: true, - Email: "john.doe@example.com", - Organizations: []string{ - "default", - "org1", - "org2", - "pe", - }, - Teams: []string{"peteam"}, - IsSystemAuditor: false, - Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce96", - }, - "iqe_normal_user": { - Id: 5, - Username: "iqe_normal_user", - Password: "redhat", - FirstName: "iqe", - LastName: "normal_user", - IsSuperuser: false, - Email: "iqe_normal_user@example.com", - Organizations: []string{ - "default", - "org1", - "org2", - }, - //Teams: []string{"ateam", "bteam"}, - //Teams: []string{"bteam"}, - Teams: []string{}, - IsSystemAuditor: false, - Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce95", - }, -} - var ( - users = map[string]User{} + users = map[int]User{} usersMutex = &sync.Mutex{} - idCounter = 6 ) var ( - teams = map[string]Team{} - teamsMutex = &sync.Mutex{} - teamIdCounter = 4 + teams = map[int]Team{} + teamsMutex = &sync.Mutex{} ) var ( - orgs = map[string]Organization{} - orgsMutex = &sync.Mutex{} - orgIdCounter = 4 + orgs = map[int]Organization{} + orgsMutex = &sync.Mutex{} ) -/************************************************************ - FUNCTIONS -************************************************************/ - -func containsString(list []string, str string) bool { - for _, v := range list { - if v == str { - return true - } - } - return false -} - -// PrintHeaders prints all headers from the request -func PrintHeaders(r *http.Request) { - for name, values := range r.Header { - for _, value := range values { - log.Printf("\trequest header \t%s: %s\n", name, value) - } - } -} - -func PrintResponseHeaders(resp *http.Response) { - if resp == nil { - fmt.Println("Response is nil") - return - } - - fmt.Println("Response Headers:") - for key, values := range resp.Header { - for _, value := range values { - //fmt.Printf("%s: %s\n", key, value) - log.Printf("\tresponse header \t%s: %s\n", key, value) - } - } -} - -// PrintFormValues prints all form values from the request -func PrintFormValues(r *http.Request) { - for key, values := range r.MultipartForm.Value { - for _, value := range values { - log.Printf("\tform %s: %s\n", key, value) - } - } -} - -func getEnv(key string, fallback string) string { - if key, ok := os.LookupEnv(key); ok { - return key - } - return fallback -} - -// GetCookieValue retrieves the value of a specific cookie by name -func GetCookieValue(r *http.Request, name string) (string, error) { - cookie, err := r.Cookie(name) - if err != nil { - return "", err - } - return cookie.Value, nil -} - -// GetCookieValue retrieves the value of a specific cookie by name -// -// Set-Cookie: csrftoken=TXb2gLP6dGd8pksgZ88ICXhbW664wCbQ; expires=Thu, 29 May 2025 ... -func ExtractCSRFCookie(resp *http.Response) (string, error) { - if resp == nil { - return "", fmt.Errorf("response is nil") - } - - // Retrieve the Set-Cookie headers - cookies := resp.Cookies() - - // Loop through cookies to find the CSRF token - for _, cookie := range cookies { - if cookie.Name == "csrftoken" { - return cookie.Value, nil - } - } - - return "", fmt.Errorf("CSRF token not found") -} - -func pathHasPrefix(path string, prefixes []string) bool { - for _, prefix := range prefixes { - if strings.HasPrefix(path, prefix) { - return true - } - } - return false -} - -// GenerateCSRFToken generates a new CSRF token -func GenerateCSRFToken() string { - return uuid.New().String() -} - -// GenerateSessionID generates a new session ID -func GenerateSessionID() string { - return uuid.New().String() -} - -// sessionIDToUsername checks the tokenTable for the sessionID and returns the username or nil -func sessionIDToUsername(sessionID *string) *string { - if sessionID == nil || *sessionID == "" { - return nil - } - - tokenTable.RLock() - defer tokenTable.RUnlock() - - userSession, exists := tokenTable.data[*sessionID] - if !exists { - return nil - } - - return &userSession.Username -} - -func generateHmacSha256SharedSecret(nonce *string) (string, error) { - - //const ANSIBLE_BASE_SHARED_SECRET = "redhat1234" - var SharedSecretNotFound = errors.New("the setting ANSIBLE_BASE_SHARED_SECRET was not set, some functionality may be disabled") - - if ANSIBLE_BASE_SHARED_SECRET == "" { - log.Println("The setting ANSIBLE_BASE_SHARED_SECRET was not set, some functionality may be disabled.") - return "", SharedSecretNotFound - } - - if nonce == nil { - currentNonce := fmt.Sprintf("%d", time.Now().Unix()) - nonce = ¤tNonce - } - - message := map[string]string{ - "nonce": *nonce, - "shared_secret": ANSIBLE_BASE_SHARED_SECRET, - } - - messageBytes, err := json.Marshal(message) - if err != nil { - return "", err - } - - mac := hmac.New(sha256.New, []byte(ANSIBLE_BASE_SHARED_SECRET)) - mac.Write(messageBytes) - signature := fmt.Sprintf("%x", mac.Sum(nil)) - - secret := fmt.Sprintf("%s:%s", *nonce, signature) - return secret, nil -} - -// generateJWT generates a JWT for the user -func generateJWT(argUser User) (string, error) { - - orgsMutex.Lock() - defer orgsMutex.Unlock() - - teamsMutex.Lock() - defer teamsMutex.Unlock() - - usersMutex.Lock() - defer usersMutex.Unlock() - - /* - oData1, _ := json.MarshalIndent(orgs, "", " ") - oString1 := string(oData1) - fmt.Println(oString1) - */ - - user := users[argUser.Username] - - // make a list of org structs for this user ... - userOrgs := []Organization{} - userTeams := []TeamObject{} - - // what orgs is this user a direct member of? ... - localOrgMap := map[string]int{} - counter := -1 - for _, orgName := range user.Organizations { - counter += 1 - localOrgMap[orgName] = counter - fmt.Println("# ADD ORG", orgName, "TO USERORGS ..") - - thisOrg, ok := orgs[orgName] - if !ok { - fmt.Println(orgName, "was not in the orgs map!!!") - panic("FUDGE!!!") - } - - userOrgs = append(userOrgs, thisOrg) - - /* - oData, _ := json.MarshalIndent(thisOrg, "", " ") - oString := string(oData) - fmt.Println(oString) - */ - - } - - // what orgs is this user an indirect member of? ... - for _, teamCodeName := range user.Teams { - team := teams[teamCodeName] - orgName := team.Org - fmt.Println("# TEAM:", team, "ORG:", orgName) - - /* - jData, _ := json.MarshalIndent(localOrgMap, "", " ") - jString := string(jData) - fmt.Println(jString) - */ - - // check if related org is in the list+map ... - found := false - highestIndex := -1 - for orgName2, orgIndex2 := range localOrgMap { - fmt.Println("\t#ORG2", fmt.Sprintf("ix: %d", orgIndex2), "name:", orgName2) - if orgName2 == orgName { - found = true - break - } - if highestIndex < orgIndex2 { - highestIndex = orgIndex2 - } - } - - // add it to the list+map if not already there ... - if found == false { - newIndex := highestIndex + 1 - localOrgMap[orgName] = newIndex - userOrgs = append(userOrgs, orgs[orgName]) - } - } - - for _, team := range user.Teams { - orgName := teams[team].Org - orgIndex := localOrgMap[orgName] - userTeams = append(userTeams, TeamObject{ - AnsibleId: teams[team].AnsibleId, - Name: team, - Org: orgIndex, - }) - } - - /* - oData1, _ := json.MarshalIndent(orgs, "", " ") - oString1 := string(oData1) - fmt.Println(oString1) - */ - - oData, _ := json.MarshalIndent(userOrgs, "", " ") - oString := string(oData) - fmt.Println(oString) - - fmt.Println("# userorgs", userOrgs) - fmt.Println("# userteams", userTeams) - - objects := map[string]interface{}{ - "organization": userOrgs, - "team": userTeams, - } - objectRoles := map[string]interface{}{} - if len(userTeams) > 0 { - objectRoles["Team Member"] = ObjectRole{ - ContentType: "team", - Objects: []int{0}, - } - } - - // make the expiration time - numericDate := jwt.NewNumericDate(time.Now().Add(time.Hour)) - unixTime := numericDate.Unix() - - claims := UserClaims{ - Version: 1, - Iss: "ansible-issuer", - Aud: "ansible-services", - Expires: unixTime, - GlobalRoles: []string{}, - UserData: UserData{ - Username: user.Username, - FirstName: user.FirstName, - LastName: user.LastName, - IsSuperuser: user.IsSuperuser, - Email: user.Email, - }, - Sub: user.Sub, - ObjectRoles: objectRoles, - Objects: objects, - } - - jsonData, _ := json.MarshalIndent(claims, "", " ") - jsonString := string(jsonData) - - log.Printf("-------------------------------------\n") - log.Printf("Created JWT for %s ...\n", user.Username) - log.Println(jsonString) - log.Printf("-------------------------------------\n") - - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - return token.SignedString(rsaPrivateKey) -} - -// Function to check if a CSRF token is known -func isCSRFTokenKnown(token string) bool { - tokenTable.RLock() - defer tokenTable.RUnlock() - - for _, session := range tokenTable.data { - if session.CSRFToken == token { - return true - } - } - return false -} +var ( + roleDefinitions = map[int]RoleDefinition{} + roleDefinitionsMutex = &sync.Mutex{} +) -// check if we've ever seen this token come back in the response headers -// from the downstream service -func isDownstreamCSRFToken(token string) bool { - csrfTokenStore.RLock() - defer csrfTokenStore.RUnlock() - for _, t := range csrfTokenStore.tokens { - if t == token { - return true - } - } - return false -} +var ( + roleUserAssignments = map[int]RoleUserAssignment{} + roleUserAssignmentsMutex = &sync.Mutex{} +) -func printKnownCSRFTokens() { - tokenTable.RLock() - defer tokenTable.RUnlock() +var ( + roleTeamAssignments = map[int]RoleTeamAssignment{} + roleTeamAssignmentsMutex = &sync.Mutex{} +) - for _, session := range tokenTable.data { - log.Printf("\t\tcsrf:%s sid:%s uid:%s\n", session.CSRFToken, session.SessionID, session.Username) - } -} +var ( + deletedEntities = map[DeletedEntityKey]bool{} + deletedEntitiesMutex = &sync.Mutex{} +) /************************************************************ - HANDLERS + PREPOPULATED DATA LOADER ************************************************************/ -// BasicAuth middleware -func BasicAuth(next http.Handler) http.Handler { - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - log.Printf("Request: %s %s", r.Method, r.URL.String()) - PrintHeaders(r) - - // don't muck the auth header for these paths - prefixes := []string{"/v2", "/token"} - - // the path CAN determine if auth should be mucked - path := r.URL.Path - - // extract the authorization header - auth := r.Header.Get("Authorization") - log.Printf("\tAuthorization: %s", auth) - - // normalize the header for comparison - lowerAuth := strings.ToLower(auth) - - // is there a csrftoken and is it valid? - csrftoken, err := GetCookieValue(r, "csrftoken") - log.Printf("CHECKING CSRFTOKEN %s", csrftoken) - if err == nil && !isCSRFTokenKnown(csrftoken) { - - // allow if this was a token from the downstream ... - if isDownstreamCSRFToken(csrftoken) { - log.Printf("Found known downstream csrftoken in request headers: %s\n", csrftoken) - next.ServeHTTP(w, r) - return - } - - log.Printf("Unauthorized Invalid csrftoken\n") - printKnownCSRFTokens() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(403) - responseBody := fmt.Sprintf(`{"error": "invalid csrftoken"}`) - w.Write([]byte(responseBody)) - return - } - - // is there a sessionid ...? - gatewaySessionID, _ := GetCookieValue(r, "gateway_sessionid") - gatewaySessionIDPtr := &gatewaySessionID - sessionUsernamePtr := sessionIDToUsername(gatewaySessionIDPtr) - - // Check if the pointer is nil and convert it to a string - var sessionUsername string - if sessionUsernamePtr != nil { - sessionUsername = *sessionUsernamePtr - } else { - sessionUsername = "" - } - - if (strings.HasPrefix(lowerAuth, "basic") || sessionUsername != "") && !pathHasPrefix(path, prefixes) { - - if sessionUsername != "" { - var user User - user, _ = users[sessionUsername] - log.Printf("*****************************************") - log.Printf("username:%s user:%s\n", sessionUsername, user) - log.Printf("*****************************************") - - // Generate the JWT token - token, err := generateJWT(user) - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - // Set the X-DAB-JW-TOKEN header - r.Header.Set("X-DAB-JW-TOKEN", token) - - } else { - - const basicPrefix = "Basic " - if !strings.HasPrefix(auth, basicPrefix) { - log.Printf("Unauthorized2\n") - http.Error(w, "Unauthorized2", http.StatusUnauthorized) - return - } - - decoded, err := base64.StdEncoding.DecodeString(auth[len(basicPrefix):]) - if err != nil { - log.Printf("Unauthorized3\n") - http.Error(w, "Unauthorized3", http.StatusUnauthorized) - return - } - - credentials := strings.SplitN(string(decoded), ":", 2) - fmt.Printf("credentials %s\n", credentials) - if len(credentials) != 2 { - log.Printf("Unauthorized4\n") - http.Error(w, "Unauthorized4", http.StatusUnauthorized) - return - } - - user, exists := users[credentials[0]] - log.Printf("extracted user:%s from creds[0]:%s creds:%s\n", user, credentials[0], credentials) - if !exists || user.Password != credentials[1] { - log.Printf("Unauthorized5\n") - http.Error(w, "Unauthorized5", http.StatusUnauthorized) - return - } - - // Generate the JWT token - token, err := generateJWT(user) - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - // Set the X-DAB-JW-TOKEN header - r.Header.Set("X-DAB-JW-TOKEN", token) - } - - // Remove the Authorization header - r.Header.Del("Authorization") - } - - next.ServeHTTP(w, r) - }) -} - -// jwtKeyHandler handles requests to /api/gateway/v1/jwt_key/ -func jwtKeyHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("Request: %s %s", r.Method, r.URL.String()) - - pubKeyBytes, err := x509.MarshalPKIXPublicKey(rsaPublicKey) - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - pubKeyPem := pem.EncodeToMemory(&pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubKeyBytes, - }) - - w.Header().Set("Content-Type", "application/x-pem-file") - w.Write(pubKeyPem) -} - -// LoginHandler handles the login requests -func LoginHandler(w http.ResponseWriter, r *http.Request) { - - log.Printf("Request: %s %s", r.Method, r.URL.String()) - PrintHeaders(r) - - switch r.Method { - case http.MethodGet: - // Generate a CSRF token for the GET request - csrfToken := GenerateCSRFToken() - - // Set the CSRF token as a cookie - http.SetCookie(w, &http.Cookie{ - Name: "csrfToken", - Value: csrfToken, - Expires: time.Now().Add(24 * time.Hour), - }) - - // Manually format the response to match the regex pattern - responseBody := fmt.Sprintf(`{"csrfToken": "%s"}`, csrfToken) - - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(responseBody)) - - case http.MethodPost: - - // Parse the multipart form - err := r.ParseMultipartForm(10 << 20) // 10 MB max memory - if err != nil { - http.Error(w, "Failed to parse multipart form", http.StatusBadRequest) - return - } - - PrintFormValues(r) - - // Extract form values - username := r.FormValue("username") - //password := r.FormValue("password") - //csrfTokenForm := r.FormValue("csrfToken") - - // Retrieve the CSRF token from the request header - csrfTokenHeader := r.Header.Get("X-CSRFtoken") - - // Retrieve the CSRF token from the cookies - cookie, err := r.Cookie("csrfToken") - if err != nil { - http.Error(w, "CSRF token cookie not found", http.StatusForbidden) - return - } - - if csrfTokenHeader == "" { - http.Error(w, "CSRF token header not found", http.StatusForbidden) - return - } - - if cookie.Value != csrfTokenHeader { - http.Error(w, "CSRF token in cookie does not match header", http.StatusForbidden) - return - } - - // Here you would normally validate the username and password. - // For this example, we assume the login is always successful. - - // Set the CSRF token as a cookie - csrfToken := GenerateCSRFToken() - http.SetCookie(w, &http.Cookie{ - Name: "csrftoken", - Value: csrfToken, - Expires: time.Now().Add(24 * time.Hour), - }) - - // Set the sessionid token as a cookie - gatewaySessionID := GenerateSessionID() - http.SetCookie(w, &http.Cookie{ - Name: "gateway_sessionid", - Value: gatewaySessionID, - Expires: time.Now().Add(24 * time.Hour), - }) - - // add this session to the table - tokenTable.Lock() - tokenTable.data[gatewaySessionID] = UserSession{ - Username: username, - CSRFToken: csrfToken, - SessionID: gatewaySessionID, - } - tokenTable.Unlock() - - // Respond with a success message (you can customize this as needed) - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"message": "Login successful"}`)) - - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -// LoginHandler handles the login requests -func LogoutHandler(w http.ResponseWriter, r *http.Request) { -} - -func OrganizationHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - getOrgs(w, r) - case http.MethodPost: - addOrg(w, r) - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func TeamHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - getTeams(w, r) - case http.MethodPost: - addTeam(w, r) - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func AssociateTeamUsersHandler(w http.ResponseWriter, r *http.Request) { - - teamsMutex.Lock() - defer teamsMutex.Unlock() - - usersMutex.Lock() - defer usersMutex.Unlock() - - if r.Method != http.MethodPost { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - // Extract the team_id from the URL path - pathParts := strings.Split(r.URL.Path, "/") - if len(pathParts) < 9 || pathParts[7] != "associate" { - http.Error(w, "Invalid URL path", http.StatusBadRequest) - return - } - teamIDStr := pathParts[5] - teamID, _ := strconv.Atoi(teamIDStr) - fmt.Println("teamid", teamID) - - // find the team codename ... - var teamName = "" - for _, team := range teams { - fmt.Println(team) - if team.Id == teamID { - fmt.Println(team) - teamName = team.Name - break - } - } - fmt.Println("teamname", teamName) - - // fmt.Println("body", r.Body) - body, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - fmt.Println("Request Body:", string(body)) - r.Body = io.NopCloser(bytes.NewBuffer(body)) - - // AssociationRequest - var newAssociationRequest AssociationRequest - if err := json.NewDecoder(r.Body).Decode(&newAssociationRequest); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - fmt.Println(newAssociationRequest) - - for _, uid := range newAssociationRequest.Instances { - fmt.Println("uid", uid) - // find this user .. - for _, user := range users { - if user.Id == uid { - fmt.Println(user) - fmt.Println(user.Teams) - if containsString(user.Teams, teamName) == false { - fmt.Println("add", user.Username, "to", teamName) - user.Teams = append(user.Teams, teamName) - users[user.Username] = user - } else { - fmt.Println("do not need to add", teamName, "to", user) - } - } else { - fmt.Println(user.Id, "!=", uid) - } - } - } - -} - -func UserHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - getUsers(w, r) - case http.MethodPost: - addUser(w, r) - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func getOrgs(w http.ResponseWriter, r *http.Request) { - orgsMutex.Lock() - defer orgsMutex.Unlock() - - var orgList []Organization - for _, org := range orgs { - orgList = append(orgList, org) - } - sort.Slice(orgList, func(i, j int) bool { - return orgList[i].Id < orgList[j].Id - }) - - var responseOrgs []OrgResponse - - for _, orgdata := range orgList { - responseOrg := OrgResponse{ - ID: orgdata.Id, - Name: orgdata.Name, - SummaryFields: struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - }{Resource: struct { - AnsibleID string `json:"ansible_id"` - }{AnsibleID: orgdata.AnsibleId}}, - } - responseOrgs = append(responseOrgs, responseOrg) - } - - response := map[string][]OrgResponse{ - "results": responseOrgs, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -func getTeams(w http.ResponseWriter, r *http.Request) { - teamsMutex.Lock() - defer teamsMutex.Unlock() - - orgsMutex.Lock() - defer orgsMutex.Unlock() - - var teamList []Team - for _, team := range teams { - teamList = append(teamList, team) - } - sort.Slice(teamList, func(i, j int) bool { - return teamList[i].Id < teamList[j].Id - }) - - var responseTeams []TeamResponse - - for _, teamdata := range teamList { - - var orgId = 0 - for _, org := range orgs { - if org.CodeName == teamdata.Org { - orgId = org.Id - break - } - } - - responseTeam := TeamResponse{ - ID: teamdata.Id, - Name: teamdata.Name, - Organization: orgId, - SummaryFields: struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - }{Resource: struct { - AnsibleID string `json:"ansible_id"` - }{AnsibleID: teamdata.AnsibleId}}, - } - responseTeams = append(responseTeams, responseTeam) - } - - response := map[string][]TeamResponse{ - "results": responseTeams, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -func getUsers(w http.ResponseWriter, r *http.Request) { - usersMutex.Lock() - defer usersMutex.Unlock() - - var userList []User - for _, user := range users { - userList = append(userList, user) - } - sort.Slice(userList, func(i, j int) bool { - return userList[i].Id < userList[j].Id - }) - - var responseUsers []UserResponse - - for _, userdata := range userList { - responseUser := UserResponse{ - ID: userdata.Id, - Username: userdata.Username, - SummaryFields: struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - }{Resource: struct { - AnsibleID string `json:"ansible_id"` - }{AnsibleID: userdata.Sub}}, - } - responseUsers = append(responseUsers, responseUser) - } - - response := map[string][]UserResponse{ - "results": responseUsers, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -func addOrg(w http.ResponseWriter, r *http.Request) { - // fmt.Println("body", r.Body) - body, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - fmt.Println("Request Body:", string(body)) - r.Body = io.NopCloser(bytes.NewBuffer(body)) - - var newOrgRequest OrgRequest - if err := json.NewDecoder(r.Body).Decode(&newOrgRequest); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if newOrgRequest.Name == "" { - http.Error(w, "Org name can not be blank", http.StatusConflict) - return - } - - orgsMutex.Lock() - defer orgsMutex.Unlock() - - highestId := 0 - for _, org := range orgs { - if org.Name == newOrgRequest.Name || org.CodeName == newOrgRequest.Name { - http.Error(w, "org name is already taken", http.StatusBadRequest) - return - } - if org.Id > highestId { - highestId = org.Id - } - } - - var newOrg Organization - - newOrg.CodeName = newOrgRequest.Name - newOrg.Name = newOrgRequest.Name - newOrg.Id = highestId + 1 - newAnsibleID := uuid.NewString() - newOrg.AnsibleId = newAnsibleID - orgs[newOrg.CodeName] = newOrg - - fmt.Println(newOrg) - - responseOrg := OrgResponse{ - ID: newOrg.Id, - Name: newOrg.Name, - SummaryFields: struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - }{Resource: struct { - AnsibleID string `json:"ansible_id"` - }{AnsibleID: newOrg.AnsibleId}}, - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(responseOrg) -} - -func addTeam(w http.ResponseWriter, r *http.Request) { - - // fmt.Println("body", r.Body) - body, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - fmt.Println("Request Body:", string(body)) - r.Body = io.NopCloser(bytes.NewBuffer(body)) - - var newTeamRequest TeamRequest - if err := json.NewDecoder(r.Body).Decode(&newTeamRequest); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if newTeamRequest.Name == "" { - http.Error(w, "Team name can not be blank", http.StatusConflict) - return - } - - orgsMutex.Lock() - defer orgsMutex.Unlock() - - teamsMutex.Lock() - defer teamsMutex.Unlock() - - if _, exists := teams[newTeamRequest.Name]; exists { - http.Error(w, "Team already exists", http.StatusConflict) - return - } - - orgName := "" - for _, org := range orgs { - if org.Id == newTeamRequest.Organization { - orgName = org.CodeName - break - } - } - if orgName == "" { - http.Error(w, "Org not found", http.StatusConflict) - return - } - - highestId := 0 - for _, team := range teams { - if team.Id > highestId { - highestId = team.Id - } - } - - var newTeam Team - - newTeam.Name = newTeamRequest.Name - newTeam.Id = highestId + 1 - newAnsibleID := uuid.NewString() - newTeam.AnsibleId = newAnsibleID - newTeam.Org = orgName - teams[newTeam.Name] = newTeam - - responseTeam := TeamResponse{ - ID: newTeam.Id, - Name: newTeam.Name, - Organization: newTeamRequest.Organization, - SummaryFields: struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - }{Resource: struct { - AnsibleID string `json:"ansible_id"` - }{AnsibleID: newAnsibleID}}, - } - - idCounter++ - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(responseTeam) -} - -func addUser(w http.ResponseWriter, r *http.Request) { - /* - # PAYLOAD - {"username": "foo", "password": "redhat1234"} - - # RESPONSE - { - "id":96, - "url":"/api/gateway/v1/users/96/", - "related":{ - "personal_tokens":"/api/gateway/v1/users/96/personal_tokens/", - "authorized_tokens":"/api/gateway/v1/users/96/authorized_tokens/", - "tokens":"/api/gateway/v1/users/96/tokens/", - "activity_stream":"/api/gateway/v1/activitystream/?content_type=1&object_id=96", - "created_by":"/api/gateway/v1/users/6/", - "modified_by":"/api/gateway/v1/users/6/", - "authenticators":"/api/gateway/v1/users/96/authenticators/" - }, - "summary_fields":{ - "modified_by":{"id":6,"username":"dev","first_name":"","last_name":""}, - "created_by":{"id":6,"username":"dev","first_name":"","last_name":""}, - "resource":{"ansible_id":"5fc36ea7-5c54-4f12-b47c-5213d648b2c0","resource_type":"shared.user"} - }, - "created":"2024-07-31T02:20:31.734404Z", - "created_by":6, - "modified":"2024-07-31T02:20:31.734386Z", - "modified_by":6, - "username":"foo", - "email":"", - "first_name":"", - "last_name":"", - "last_login":null, - "password":"$encrypted$", - "is_superuser":false, - "is_platform_auditor":false, - "managed":false, - "last_login_results":{}, - "authenticators":[], - "authenticator_uid":"" - } - */ - - var newUser User - if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if newUser.Username == "" { - http.Error(w, "username can not be blank.", http.StatusBadRequest) - return - } - - usersMutex.Lock() - defer usersMutex.Unlock() - - if _, exists := users[newUser.Username]; exists { - http.Error(w, "User already exists", http.StatusConflict) - return - } - - highestId := 0 - for _, user := range users { - if user.Id > highestId { - highestId = user.Id - } - } - - newUser.Id = highestId + 1 - newAnsibleID := uuid.NewString() - newUser.Sub = newAnsibleID - users[newUser.Username] = newUser - - responseUser := UserResponse{ - ID: idCounter, - Username: newUser.Username, - SummaryFields: struct { - Resource struct { - AnsibleID string `json:"ansible_id"` - } `json:"resource"` - }{Resource: struct { - AnsibleID string `json:"ansible_id"` - }{AnsibleID: newAnsibleID}}, - } - - idCounter++ - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(responseUser) -} - func init() { // Generate RSA keys log.Printf("# Making RSA keys\n") @@ -1459,7 +118,7 @@ func init() { orgsMutex.Lock() defer orgsMutex.Unlock() for _, org := range prepopulatedOrgs { - orgs[org.CodeName] = org + orgs[org.Id] = org } // build teams ... @@ -1467,7 +126,7 @@ func init() { teamsMutex.Lock() defer teamsMutex.Unlock() for _, team := range prepopulatedTeams { - teams[team.Name] = team + teams[team.Id] = team } // build users ... @@ -1475,8 +134,18 @@ func init() { usersMutex.Lock() defer usersMutex.Unlock() for _, user := range prepopulatedUsers { - users[user.Username] = user + users[user.Id] = user } + + // build roledefs .. + log.Printf("# Making roledefs\n") + roleDefinitionsMutex.Lock() + defer roleDefinitionsMutex.Unlock() + //roleDefinitions = append(roleDefinitions, prepopulatedRoleDefinitions...) + for _, roledef := range prepopulatedRoleDefinitions { + roleDefinitions[roledef.Id] = roledef + } + } func main() { @@ -1538,20 +207,25 @@ func main() { // serve /api/gateway/v1/jwt_key/ from this service so the client can // get the decryption keys for the jwts http.HandleFunc("/api/gateway/v1/jwt_key/", jwtKeyHandler) + http.HandleFunc("/api/gateway/v1/my_jwt/", MyJWTHandler) // allow direct logins http.HandleFunc("/api/gateway/v1/login/", LoginHandler) http.HandleFunc("/api/gateway/v1/logout/", LogoutHandler) http.HandleFunc("/api/gateway/v1/users/", UserHandler) - // http.HandleFunc("/api/gateway/v1/teams/", TeamHandler) - http.HandleFunc("/api/gateway/v1/teams/", func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/gateway/v1/teams/") && strings.Contains(r.URL.Path, "/users/associate/") { - AssociateTeamUsersHandler(w, r) - } else { - TeamHandler(w, r) - } - }) + /* + http.HandleFunc("/api/gateway/v1/teams/", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/gateway/v1/teams/") && strings.Contains(r.URL.Path, "/users/associate/") { + AssociateTeamUsersHandler(w, r) + } else { + TeamHandler(w, r) + } + }) + */ + http.HandleFunc("/api/gateway/v1/teams/", TeamHandler) http.HandleFunc("/api/gateway/v1/organizations/", OrganizationHandler) + http.HandleFunc("/api/gateway/v1/role_definitions/", RoleDefinitionsHandler) + http.HandleFunc("/api/gateway/v1/role_user_assignments/", RoleUserAssignmentsHandler) // send everything else downstream http.Handle("/", BasicAuth(proxy)) diff --git a/profiles/dab_jwt/proxy/proxy_default_data.go b/profiles/dab_jwt/proxy/proxy_default_data.go new file mode 100644 index 0000000000..6fdc1fa24a --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_default_data.go @@ -0,0 +1,187 @@ +package main + +var prepopulatedOrgs = map[string]Organization{ + "default": { + Id: 1, + AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee0", + Name: "Default", + CodeName: "default", + }, + "org1": { + Id: 2, + AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee1", + Name: "Organization 1", + CodeName: "org1", + }, + "org2": { + Id: 3, + AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee2", + Name: "Organization 2", + CodeName: "org2", + }, + "pe": { + Id: 4, + AnsibleId: "bc243368-a9d4-4f8f-9ffe-5d2d921fcee3", + Name: "system:partner-engineers", + CodeName: "pe", + }, +} + +var prepopulatedTeams = map[string]Team{ + "ateam": { + Id: 1, + AnsibleId: "34a58292-1e0f-49f0-9383-fb7e63d771aa", + Name: "ateam", + Org: 2, + }, + "bteam": { + Id: 2, + AnsibleId: "34a58292-1e0f-49f0-9383-fb7e63d771ab", + Name: "bteam", + Org: 1, + }, + "peteam": { + Id: 3, + AnsibleId: "34a58292-1e0f-49f0-9383-fb7e63d771ac", + Name: "peteam", + Org: 4, + }, +} + +// Define users +var prepopulatedUsers = map[string]User{ + "admin": { + Id: 1, + Username: "admin", + Password: "admin", + FirstName: "ad", + LastName: "min", + IsSuperuser: true, + Email: "admin@example.com", + Organizations: []string{"default"}, + Teams: []string{}, + IsSystemAuditor: true, + Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce99", + }, + "notifications_admin": { + Id: 2, + Username: "notifications_admin", + Password: "redhat", + FirstName: "notifications", + LastName: "admin", + IsSuperuser: true, + Email: "notifications_admin@example.com", + Organizations: []string{"default"}, + Teams: []string{}, + IsSystemAuditor: true, + Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce98", + }, + "ee_admin": { + Id: 3, + Username: "ee_admin", + Password: "redhat", + FirstName: "ee", + LastName: "admin", + IsSuperuser: true, + Email: "ee_admin@example.com", + Organizations: []string{"default"}, + Teams: []string{}, + IsSystemAuditor: true, + Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce97", + }, + "jdoe": { + Id: 4, + Username: "jdoe", + Password: "redhat", + FirstName: "John", + LastName: "Doe", + //IsSuperuser: false, + IsSuperuser: true, + Email: "john.doe@example.com", + Organizations: []string{ + "default", + "org1", + "org2", + "pe", + }, + Teams: []string{"peteam"}, + IsSystemAuditor: false, + Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce96", + }, + "iqe_normal_user": { + Id: 5, + Username: "iqe_normal_user", + Password: "redhat", + FirstName: "iqe", + LastName: "normal_user", + IsSuperuser: false, + Email: "iqe_normal_user@example.com", + Organizations: []string{ + "default", + "org1", + "org2", + }, + //Teams: []string{"ateam", "bteam"}, + //Teams: []string{"bteam"}, + Teams: []string{}, + IsSystemAuditor: false, + Sub: "bc243368-a9d4-4f8f-9ffe-5d2d921fce95", + }, +} + +var prepopulatedRoleDefinitions = []RoleDefinition{ + { + Id: 1, + Name: "Platform Auditor", + Managed: true, + Permissions: []string{ + "shared.view_organization", + "shared.view_team", + }, + }, + { + Id: 2, + Name: "Team Member", + Managed: false, + Permissions: []string{ + "shared.member_team", + "shared.view_team", + }, + }, + { + Id: 3, + Name: "Team Admin", + Managed: false, + Permissions: []string{ + "shared.change_team", + "shared.delete_team", + "shared.member_team", + "shared.view_team", + }, + }, + { + Id: 4, + Name: "Organization Admin", + Managed: true, + Permissions: []string{ + "shared.change_organization", + "shared.delete_organization", + "shared.member_organization", + "shared.view_organization", + "shared.add_team", + "shared.change_team", + "shared.delete_team", + "shared.member_team", + "shared.view_team", + }, + }, + { + Id: 5, + Name: "Organization Member", + Managed: true, + Permissions: []string{ + "shared.member_organization", + "shared.view_organization", + }, + }, +} diff --git a/profiles/dab_jwt/proxy/proxy_functions.go b/profiles/dab_jwt/proxy/proxy_functions.go new file mode 100644 index 0000000000..756a040218 --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_functions.go @@ -0,0 +1,749 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "sort" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" +) + +/* + func containsString(list []string, str string) bool { + for _, v := range list { + if v == str { + return true + } + } + return false + } +*/ +func uniqueSortedStrings(input []string) []string { + // First, sort the slice + sort.Strings(input) + + // Then, create a new slice to hold unique values + unique := []string{} + + // Loop through the sorted slice and add only unique values to the new slice + for i, str := range input { + if i == 0 || str != input[i-1] { + unique = append(unique, str) + } + } + + return unique +} + +func uniqueInts(nums []int) []int { + if len(nums) == 0 { + return nums + } + + // Create a map to track unique integers + uniqueMap := make(map[int]bool) + for _, num := range nums { + uniqueMap[num] = true + } + + // Convert the map keys back to a slice + uniqueList := make([]int, 0, len(uniqueMap)) + for num := range uniqueMap { + uniqueList = append(uniqueList, num) + } + + // Sort the slice + sort.Ints(uniqueList) + + return uniqueList +} + +func MaxOrDefault(nums []int) int { + if len(nums) == 0 { + return 1 + } + + maxVal := nums[0] // Assume the first element is the max initially + for _, num := range nums { + if num > maxVal { + maxVal = num + } + } + return maxVal +} + +// PrintHeaders prints all headers from the request +func PrintHeaders(r *http.Request) { + for name, values := range r.Header { + for _, value := range values { + log.Printf("\trequest header \t%s: %s\n", name, value) + } + } +} + +func PrintResponseHeaders(resp *http.Response) { + if resp == nil { + fmt.Println("Response is nil") + return + } + + fmt.Println("Response Headers:") + for key, values := range resp.Header { + for _, value := range values { + //fmt.Printf("%s: %s\n", key, value) + log.Printf("\tresponse header \t%s: %s\n", key, value) + } + } +} + +// PrintFormValues prints all form values from the request +func PrintFormValues(r *http.Request) { + for key, values := range r.MultipartForm.Value { + for _, value := range values { + log.Printf("\tform %s: %s\n", key, value) + } + } +} + +func getEnv(key string, fallback string) string { + if key, ok := os.LookupEnv(key); ok { + return key + } + return fallback +} + +// GetCookieValue retrieves the value of a specific cookie by name +func GetCookieValue(r *http.Request, name string) (string, error) { + cookie, err := r.Cookie(name) + if err != nil { + return "", err + } + return cookie.Value, nil +} + +// GetCookieValue retrieves the value of a specific cookie by name +// +// Set-Cookie: csrftoken=TXb2gLP6dGd8pksgZ88ICXhbW664wCbQ; expires=Thu, 29 May 2025 ... +func ExtractCSRFCookie(resp *http.Response) (string, error) { + if resp == nil { + return "", fmt.Errorf("response is nil") + } + + // Retrieve the Set-Cookie headers + cookies := resp.Cookies() + + // Loop through cookies to find the CSRF token + for _, cookie := range cookies { + if cookie.Name == "csrftoken" { + return cookie.Value, nil + } + } + + return "", fmt.Errorf("CSRF token not found") +} + +func pathHasPrefix(path string, prefixes []string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(path, prefix) { + return true + } + } + return false +} + +// GenerateCSRFToken generates a new CSRF token +func GenerateCSRFToken() string { + return uuid.New().String() +} + +// GenerateSessionID generates a new session ID +func GenerateSessionID() string { + return uuid.New().String() +} + +// sessionIDToUsername checks the tokenTable for the sessionID and returns the username or nil +func sessionIDToUsername(sessionID *string) *string { + if sessionID == nil || *sessionID == "" { + return nil + } + + tokenTable.RLock() + defer tokenTable.RUnlock() + + userSession, exists := tokenTable.data[*sessionID] + if !exists { + return nil + } + + return &userSession.Username +} + +func generateHmacSha256SharedSecret(nonce *string) (string, error) { + + //const ANSIBLE_BASE_SHARED_SECRET = "redhat1234" + var SharedSecretNotFound = errors.New("the setting ANSIBLE_BASE_SHARED_SECRET was not set, some functionality may be disabled") + + if ANSIBLE_BASE_SHARED_SECRET == "" { + log.Println("The setting ANSIBLE_BASE_SHARED_SECRET was not set, some functionality may be disabled.") + return "", SharedSecretNotFound + } + + if nonce == nil { + currentNonce := fmt.Sprintf("%d", time.Now().Unix()) + nonce = ¤tNonce + } + + message := map[string]string{ + "nonce": *nonce, + "shared_secret": ANSIBLE_BASE_SHARED_SECRET, + } + + messageBytes, err := json.Marshal(message) + if err != nil { + return "", err + } + + mac := hmac.New(sha256.New, []byte(ANSIBLE_BASE_SHARED_SECRET)) + mac.Write(messageBytes) + signature := fmt.Sprintf("%x", mac.Sum(nil)) + + secret := fmt.Sprintf("%s:%s", *nonce, signature) + return secret, nil +} + +func GetTeamByName(teamname string) Team { + for _, team := range teams { + if team.Name == teamname { + return team + } + } + return Team{} +} + +func GetUserByUserName(username string) User { + for _, user := range users { + if user.Username == username { + return user + } + } + return User{} +} + +// generateJWT generates a JWT for the user +func GenerateUserClaims(user User) (UserClaims, error) { + + log.Printf("generateclaims %s\n", user.Username) + + log.Printf("generateclaims %s\n", user.Username) + //log.Printf("teams %s\n", teams) + + orgNameMap := make(map[string]Organization) + for _, org := range orgs { + orgNameMap[org.Name] = org + } + //log.Printf("orgmap %s\n", orgNameMap) + + // orgs that a team is part of but the user is not a member of + nonMemberOrgs := []string{} + + uniqueOrgMap := make(map[int]bool) + memberOrgs := []Organization{} + adminOrgs := []Organization{} + uniqueTeamMap := make(map[int]bool) + memberTeams := []Team{} + adminTeams := []Team{} + globalRoles := []string{} + + // iterate through role_user_assignments ... + for _, assignment := range roleUserAssignments { + + log.Printf("processing assignment %d\n", assignment.Id) + + if assignment.User != user.Id { + log.Printf("\t ass-userid:%d != user-id:%d", assignment.User, user.Id) + continue + } + roleId := assignment.RoleDefinition + if roleDefinitions[roleId].Name == "Platform Auditor" { + log.Printf("\tfound user assignment for platform auditor") + globalRoles = append(globalRoles, "Platform Auditor") + continue + } + + if roleDefinitions[roleId].Name == "Team Member" || roleDefinitions[roleId].Name != "Team Admin" { + + thisteam := teams[assignment.ObjectId] + log.Printf("\tFOUND TEAM %s\n", thisteam.Name) + if !uniqueTeamMap[thisteam.Id] { + uniqueTeamMap[thisteam.Id] = true + } + + if roleDefinitions[roleId].Name == "Team Member" { + memberTeams = append(memberTeams, thisteam) + } + + if roleDefinitions[roleId].Name == "Team Admin" { + adminTeams = append(adminTeams, thisteam) + } + + thisorg := orgs[thisteam.Org] + log.Printf("\tFOUND ORG %s\n", thisorg.Name) + if !uniqueOrgMap[thisorg.Id] { + uniqueOrgMap[thisorg.Id] = true + memberOrgs = append(memberOrgs, thisorg) + } + + } + + if roleDefinitions[roleId].Name == "Organization Member" || roleDefinitions[roleId].Name != "Organization Admin" { + thisorg := orgs[assignment.ObjectId] + log.Printf("\tFOUND ORG %s\n", thisorg.Name) + if !uniqueOrgMap[thisorg.Id] { + uniqueOrgMap[thisorg.Id] = true + //memberOrgs = append(memberOrgs, thisorg) + } + + if roleDefinitions[roleId].Name == "Organization Member" { + memberOrgs = append(memberOrgs, thisorg) + } + + if roleDefinitions[roleId].Name == "Organization Admin" { + adminOrgs = append(adminOrgs, thisorg) + } + + } + + } + + for _, assignment := range roleTeamAssignments { + + // is this for a team the user is a member of? + thisteam := teams[assignment.Team] + if !uniqueTeamMap[thisteam.Id] { + continue + } + + roleId := assignment.RoleDefinition + if roleDefinitions[roleId].Name == "Platform Auditor" { + log.Printf("\tfound user assignment for platform auditor") + globalRoles = append(globalRoles, "Platform Auditor") + continue + } + } + + // make a list of org structs for this user ... + claimOrgObjectsIndex := map[string]int{} + claimOrgObjects := []OrganizationObject{} + claimTeamObjectsIndex := map[string]int{} + claimTeamObjects := []TeamObject{} + + // iterate admin orgs + for _, org := range adminOrgs { + orgObj := OrganizationObject{ + AnsibleId: org.AnsibleId, + Name: org.Name, + } + exists := false + for _, iorg := range claimOrgObjects { + if iorg.Name == org.Name { + exists = true + break + } + } + if !exists { + claimOrgObjects = append(claimOrgObjects, orgObj) + claimOrgObjectsIndex[org.Name] = len(claimOrgObjects) - 1 + } + } + + // iterate member orgs + for _, org := range memberOrgs { + orgObj := OrganizationObject{ + AnsibleId: org.AnsibleId, + Name: org.Name, + } + exists := false + for _, iorg := range claimOrgObjects { + if iorg.Name == org.Name { + exists = true + break + } + } + if !exists { + claimOrgObjects = append(claimOrgObjects, orgObj) + claimOrgObjectsIndex[org.Name] = len(claimOrgObjects) - 1 + } + } + + // iterate admin teams + for _, team := range adminTeams { + orgName := orgs[team.Org].Name + orgIx := claimOrgObjectsIndex[orgName] + teamObj := TeamObject{ + AnsibleId: team.AnsibleId, + Name: team.Name, + Org: orgIx, + } + nonMemberOrgs = append(nonMemberOrgs, orgName) + exists := false + for _, iteam := range claimTeamObjects { + if iteam.Name == team.Name { + exists = true + break + } + } + if !exists { + claimTeamObjects = append(claimTeamObjects, teamObj) + claimTeamObjectsIndex[team.Name] = len(claimTeamObjects) - 1 + } + } + // iterate memberteams + for _, team := range memberTeams { + orgName := orgs[team.Org].Name + orgIx := claimOrgObjectsIndex[orgName] + teamObj := TeamObject{ + AnsibleId: team.AnsibleId, + Name: team.Name, + Org: orgIx, + } + nonMemberOrgs = append(nonMemberOrgs, orgName) + exists := false + for _, iteam := range claimTeamObjects { + if iteam.Name == team.Name { + exists = true + break + } + } + if !exists { + claimTeamObjects = append(claimTeamObjects, teamObj) + claimTeamObjectsIndex[team.Name] = len(claimTeamObjects) - 1 + } + } + + objects := map[string]interface{}{ + "organization": claimOrgObjects, + "team": claimTeamObjects, + } + objectRoles := map[string]interface{}{} + + if len(claimOrgObjects) > 0 { + if len(adminOrgs) > 0 { + adminorgids := []int{} + for _, org := range adminOrgs { + ix := claimTeamObjectsIndex[org.Name] + adminorgids = append(adminorgids, ix) + } + objectRoles["Organization Admin"] = ObjectRole{ + ContentType: "organization", + Objects: adminorgids, + } + } + + if len(memberOrgs) > 0 { + memberorgids := []int{} + for _, org := range memberOrgs { + + // was this inherited from a team? + inherited := false + for _, orgname := range nonMemberOrgs { + if orgname == org.Name { + inherited = true + break + } + } + if inherited { + continue + } + + ix := claimTeamObjectsIndex[org.Name] + memberorgids = append(memberorgids, ix) + } + if len(memberorgids) > 0 { + objectRoles["Organization Member"] = ObjectRole{ + ContentType: "organization", + Objects: memberorgids, + } + } + } + } + + if len(claimTeamObjects) > 0 { + if len(adminTeams) > 0 { + adminteamids := []int{} + for _, team := range adminTeams { + ix := claimTeamObjectsIndex[team.Name] + adminteamids = append(adminteamids, ix) + } + objectRoles["Team Admin"] = ObjectRole{ + ContentType: "team", + Objects: adminteamids, + } + } + + if len(memberTeams) > 0 { + memberteamids := []int{} + for _, team := range memberTeams { + ix := claimTeamObjectsIndex[team.Name] + memberteamids = append(memberteamids, ix) + } + objectRoles["Team Member"] = ObjectRole{ + ContentType: "team", + Objects: memberteamids, + } + } + } + + // finalize the global roles + finalGlobalRoles := uniqueSortedStrings(globalRoles) + + // make the expiration time + numericDate := jwt.NewNumericDate(time.Now().Add(time.Hour)) + unixTime := numericDate.Unix() + + claims := UserClaims{ + Version: 1, + Iss: "ansible-issuer", + Aud: "ansible-services", + Expires: unixTime, + GlobalRoles: finalGlobalRoles, + UserData: UserData{ + Username: user.Username, + FirstName: user.FirstName, + LastName: user.LastName, + IsSuperuser: user.IsSuperuser, + Email: user.Email, + }, + Sub: user.Sub, + ObjectRoles: objectRoles, + Objects: objects, + } + + return claims, nil +} + +// generateJWT generates a JWT for the user +func generateJWT(argUser User) (string, error) { + + claims, _ := GenerateUserClaims(argUser) + + jsonData, _ := json.MarshalIndent(claims, "", " ") + jsonString := string(jsonData) + + log.Printf("-------------------------------------\n") + log.Printf("Created JWT for %s ...\n", argUser.Username) + log.Println(jsonString) + log.Printf("-------------------------------------\n") + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + return token.SignedString(rsaPrivateKey) +} + +// Function to check if a CSRF token is known +func isCSRFTokenKnown(token string) bool { + tokenTable.RLock() + defer tokenTable.RUnlock() + + for _, session := range tokenTable.data { + if session.CSRFToken == token { + return true + } + } + return false +} + +// check if we've ever seen this token come back in the response headers +// from the downstream service +func isDownstreamCSRFToken(token string) bool { + csrfTokenStore.RLock() + defer csrfTokenStore.RUnlock() + for _, t := range csrfTokenStore.tokens { + if t == token { + return true + } + } + return false +} + +func printKnownCSRFTokens() { + tokenTable.RLock() + defer tokenTable.RUnlock() + + for _, session := range tokenTable.data { + log.Printf("\t\tcsrf:%s sid:%s uid:%s\n", session.CSRFToken, session.SessionID, session.Username) + } +} + +func GetRequestUser(r *http.Request) (User, error) { + // Get the Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + return User{}, fmt.Errorf("authorization header missing") + } + + // The token normally comes in the format "Basic " + if !strings.HasPrefix(authHeader, "Basic ") { + return User{}, fmt.Errorf("invalid authorization method") + } + + // Decode the base64 encoded credentials + encodedCredentials := strings.TrimPrefix(authHeader, "Basic ") + decodedCredentials, err := base64.StdEncoding.DecodeString(encodedCredentials) + if err != nil { + return User{}, fmt.Errorf("invalid base64 encoded credentials") + } + + // Split the decoded string into username and password + credentials := strings.SplitN(string(decodedCredentials), ":", 2) + if len(credentials) != 2 { + return User{}, fmt.Errorf("invalid credentials format") + } + + username := credentials[0] + user := GetUserByUserName(username) + return user, nil +} + +func GetOrganizationByName(orgname string) Organization { + for _, org := range orgs { + if org.Name == orgname || org.CodeName == orgname { + return org + } + } + return Organization{} +} + +func GetOrganizationById(orgid int) Organization { + for _, org := range orgs { + if org.Id == orgid { + return org + } + } + return Organization{} +} + +func GetLastNumericPathElement(path string) int { + // Split the path into components + parts := strings.Split(strings.Trim(path, "/"), "/") + + // Get the last part of the path + if len(parts) == 0 { + return -1 + } + lastPart := parts[len(parts)-1] + + // Check if the last part is numeric + if num, err := strconv.Atoi(lastPart); err == nil { + return num + } + + // If not numeric, return nil + return -1 +} + +func DeleteOrganization(org Organization) { + DeleteUserAssignmentByObjectIdAndRoleNameSubstring(org.Id, "org") + // delete all teams under this org + for _, team := range teams { + if team.Org == org.Id { + DeleteTeam(team) + } + } + // put it in the deleted entities map + entity := DeletedEntityKey{ + ID: org.Id, + ContentType: "org", + } + deletedEntities[entity] = true + delete(orgs, org.Id) +} + +func DeleteTeam(team Team) { + DeleteUserAssignmentByObjectIdAndRoleNameSubstring(team.Id, "team") + entity := DeletedEntityKey{ + ID: team.Id, + ContentType: "team", + } + deletedEntities[entity] = true + delete(teams, team.Id) +} + +func DeleteUser(user User) { + DeleteUserAssignmentsByUserid(user.Id) + entity := DeletedEntityKey{ + ID: user.Id, + ContentType: "user", + } + deletedEntities[entity] = true + delete(users, user.Id) +} + +func GetRoleDefinitionById(id int) RoleDefinition { + for _, roledef := range roleDefinitions { + if roledef.Id == id { + return roledef + } + } + return RoleDefinition{} +} + +func DeleteUserAssignmentsByUserid(userid int) { + idsToDelete := []int{} + + for _, assignment := range roleUserAssignments { + if assignment.User != userid { + continue + } + idsToDelete = append(idsToDelete, assignment.Id) + } + + uniqueIds := uniqueInts(idsToDelete) + + for _, idToDelete := range uniqueIds { + log.Printf("FIXME delete %d", idToDelete) + entity := DeletedEntityKey{ + ID: idToDelete, + ContentType: "role_user_assignment", + } + deletedEntities[entity] = true + delete(roleUserAssignments, idToDelete) + } +} + +func DeleteUserAssignmentByObjectIdAndRoleNameSubstring(object_id int, substring string) { + + idsToDelete := []int{} + + for _, assignment := range roleUserAssignments { + if assignment.ObjectId != object_id { + continue + } + roledef := GetRoleDefinitionById(assignment.RoleDefinition) + if !strings.Contains(strings.ToLower(roledef.Name), strings.ToLower(substring)) { + continue + } + idsToDelete = append(idsToDelete, assignment.Id) + } + + uniqueIds := uniqueInts(idsToDelete) + + for _, idToDelete := range uniqueIds { + log.Printf("FIXME delete %d", idToDelete) + entity := DeletedEntityKey{ + ID: idToDelete, + ContentType: "role_user_assignment", + } + deletedEntities[entity] = true + delete(roleUserAssignments, idToDelete) + } +} diff --git a/profiles/dab_jwt/proxy/proxy_handler_auth.go b/profiles/dab_jwt/proxy/proxy_handler_auth.go new file mode 100644 index 0000000000..73df75eaec --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_handler_auth.go @@ -0,0 +1,258 @@ +package main + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "log" + "net/http" + "strings" + "time" +) + +// BasicAuth middleware +func BasicAuth(next http.Handler) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + log.Printf("Request: %s %s", r.Method, r.URL.String()) + PrintHeaders(r) + + // don't muck the auth header for these paths + prefixes := []string{"/v2", "/token"} + + // the path CAN determine if auth should be mucked + path := r.URL.Path + + // extract the authorization header + auth := r.Header.Get("Authorization") + log.Printf("\tAuthorization: %s", auth) + + // normalize the header for comparison + lowerAuth := strings.ToLower(auth) + + // is there a csrftoken and is it valid? + csrftoken, err := GetCookieValue(r, "csrftoken") + log.Printf("CHECKING CSRFTOKEN %s", csrftoken) + if err == nil && !isCSRFTokenKnown(csrftoken) { + + // allow if this was a token from the downstream ... + if isDownstreamCSRFToken(csrftoken) { + log.Printf("Found known downstream csrftoken in request headers: %s\n", csrftoken) + next.ServeHTTP(w, r) + return + } + + log.Printf("Unauthorized Invalid csrftoken\n") + printKnownCSRFTokens() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + responseBody := `{"error": "invalid csrftoken"}` + w.Write([]byte(responseBody)) + return + } + + // is there a sessionid ...? + gatewaySessionID, _ := GetCookieValue(r, "gateway_sessionid") + gatewaySessionIDPtr := &gatewaySessionID + sessionUsernamePtr := sessionIDToUsername(gatewaySessionIDPtr) + + // Check if the pointer is nil and convert it to a string + var sessionUsername string + if sessionUsernamePtr != nil { + sessionUsername = *sessionUsernamePtr + } else { + sessionUsername = "" + } + + if (strings.HasPrefix(lowerAuth, "basic") || sessionUsername != "") && !pathHasPrefix(path, prefixes) { + + if sessionUsername != "" { + //var user User + // user, _ = users[sessionUsername] + user := GetUserByUserName(sessionUsername) + log.Printf("*****************************************") + log.Printf("username:%s user:%d\n", sessionUsername, user.Id) + log.Printf("*****************************************") + + // Generate the JWT token + token, err := generateJWT(user) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Set the X-DAB-JW-TOKEN header + r.Header.Set("X-DAB-JW-TOKEN", token) + + } else { + + const basicPrefix = "Basic " + if !strings.HasPrefix(auth, basicPrefix) { + log.Printf("Unauthorized2\n") + http.Error(w, "Unauthorized2", http.StatusUnauthorized) + return + } + + decoded, err := base64.StdEncoding.DecodeString(auth[len(basicPrefix):]) + if err != nil { + log.Printf("Unauthorized3\n") + http.Error(w, "Unauthorized3", http.StatusUnauthorized) + return + } + + credentials := strings.SplitN(string(decoded), ":", 2) + fmt.Printf("credentials %s\n", credentials) + if len(credentials) != 2 { + log.Printf("Unauthorized4\n") + http.Error(w, "Unauthorized4", http.StatusUnauthorized) + return + } + + //user, exists := users[credentials[0]] + user := GetUserByUserName(credentials[0]) + //log.Printf("extracted user:%s from creds[0]:%s creds:%s\n", user, credentials[0], credentials) + if user.Password != credentials[1] { + log.Printf("Unauthorized5\n") + http.Error(w, "Unauthorized5", http.StatusUnauthorized) + return + } + + // Generate the JWT token + token, err := generateJWT(user) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Set the X-DAB-JW-TOKEN header + r.Header.Set("X-DAB-JW-TOKEN", token) + } + + // Remove the Authorization header + r.Header.Del("Authorization") + } + + next.ServeHTTP(w, r) + }) +} + +// jwtKeyHandler handles requests to /api/gateway/v1/jwt_key/ +func jwtKeyHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("Request: %s %s", r.Method, r.URL.String()) + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(rsaPublicKey) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + pubKeyPem := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + }) + + w.Header().Set("Content-Type", "application/x-pem-file") + w.Write(pubKeyPem) +} + +// LoginHandler handles the login requests +func LoginHandler(w http.ResponseWriter, r *http.Request) { + + log.Printf("Request: %s %s", r.Method, r.URL.String()) + PrintHeaders(r) + + switch r.Method { + case http.MethodGet: + // Generate a CSRF token for the GET request + csrfToken := GenerateCSRFToken() + + // Set the CSRF token as a cookie + http.SetCookie(w, &http.Cookie{ + Name: "csrfToken", + Value: csrfToken, + Expires: time.Now().Add(24 * time.Hour), + }) + + // Manually format the response to match the regex pattern + responseBody := fmt.Sprintf(`{"csrfToken": "%s"}`, csrfToken) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(responseBody)) + + case http.MethodPost: + + // Parse the multipart form + err := r.ParseMultipartForm(10 << 20) // 10 MB max memory + if err != nil { + http.Error(w, "Failed to parse multipart form", http.StatusBadRequest) + return + } + + PrintFormValues(r) + + // Extract form values + username := r.FormValue("username") + //password := r.FormValue("password") + //csrfTokenForm := r.FormValue("csrfToken") + + // Retrieve the CSRF token from the request header + csrfTokenHeader := r.Header.Get("X-CSRFtoken") + + // Retrieve the CSRF token from the cookies + cookie, err := r.Cookie("csrfToken") + if err != nil { + http.Error(w, "CSRF token cookie not found", http.StatusForbidden) + return + } + + if csrfTokenHeader == "" { + http.Error(w, "CSRF token header not found", http.StatusForbidden) + return + } + + if cookie.Value != csrfTokenHeader { + http.Error(w, "CSRF token in cookie does not match header", http.StatusForbidden) + return + } + + // Here you would normally validate the username and password. + // For this example, we assume the login is always successful. + + // Set the CSRF token as a cookie + csrfToken := GenerateCSRFToken() + http.SetCookie(w, &http.Cookie{ + Name: "csrftoken", + Value: csrfToken, + Expires: time.Now().Add(24 * time.Hour), + }) + + // Set the sessionid token as a cookie + gatewaySessionID := GenerateSessionID() + http.SetCookie(w, &http.Cookie{ + Name: "gateway_sessionid", + Value: gatewaySessionID, + Expires: time.Now().Add(24 * time.Hour), + }) + + // add this session to the table + tokenTable.Lock() + tokenTable.data[gatewaySessionID] = UserSession{ + Username: username, + CSRFToken: csrfToken, + SessionID: gatewaySessionID, + } + tokenTable.Unlock() + + // Respond with a success message (you can customize this as needed) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"message": "Login successful"}`)) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// LoginHandler handles the login requests +func LogoutHandler(w http.ResponseWriter, r *http.Request) { +} diff --git a/profiles/dab_jwt/proxy/proxy_handler_jwt.go b/profiles/dab_jwt/proxy/proxy_handler_jwt.go new file mode 100644 index 0000000000..83d04b0862 --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_handler_jwt.go @@ -0,0 +1,56 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "log" + "net/http" + "strings" +) + +func MyJWTHandler(w http.ResponseWriter, r *http.Request) { + + //usersMutex.Lock() + //defer usersMutex.Unlock() + + // Get the Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header missing", http.StatusUnauthorized) + return + } + + // The token normally comes in the format "Basic " + if !strings.HasPrefix(authHeader, "Basic ") { + http.Error(w, "Invalid authorization method", http.StatusUnauthorized) + return + } + + // Decode the base64 encoded credentials + encodedCredentials := strings.TrimPrefix(authHeader, "Basic ") + decodedCredentials, err := base64.StdEncoding.DecodeString(encodedCredentials) + if err != nil { + http.Error(w, "Invalid base64 encoded credentials", http.StatusUnauthorized) + return + } + + // Split the decoded string into username and password + credentials := strings.SplitN(string(decodedCredentials), ":", 2) + if len(credentials) != 2 { + http.Error(w, "Invalid credentials format", http.StatusUnauthorized) + return + } + + username := credentials[0] + log.Printf("USERNAME:%s\n", username) + //user := users[username] + user := GetUserByUserName(username) + log.Printf("MAKE CLAIMS") + claims, _ := GenerateUserClaims(user) + //log.Printf("CLAIMS:%s\n", claims) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(claims) + log.Printf("DONE") + +} diff --git a/profiles/dab_jwt/proxy/proxy_handler_orgs.go b/profiles/dab_jwt/proxy/proxy_handler_orgs.go new file mode 100644 index 0000000000..499c04a779 --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_handler_orgs.go @@ -0,0 +1,182 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "sort" + + "github.com/google/uuid" +) + +func OrganizationHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + getOrgs(w) + case http.MethodPost: + addOrg(w, r) + case http.MethodDelete: + deleteOrg(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func getOrgs(w http.ResponseWriter) { + orgsMutex.Lock() + defer orgsMutex.Unlock() + + var orgList []Organization + for _, org := range orgs { + orgList = append(orgList, org) + } + sort.Slice(orgList, func(i, j int) bool { + return orgList[i].Id < orgList[j].Id + }) + + var responseOrgs []OrgResponse + + for _, orgdata := range orgList { + responseOrg := OrgResponse{ + ID: orgdata.Id, + Name: orgdata.Name, + SummaryFields: struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + }{Resource: struct { + AnsibleID string `json:"ansible_id"` + }{AnsibleID: orgdata.AnsibleId}}, + } + responseOrgs = append(responseOrgs, responseOrg) + } + + response := map[string][]OrgResponse{ + "results": responseOrgs, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func addOrg(w http.ResponseWriter, r *http.Request) { + // fmt.Println("body", r.Body) + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } + fmt.Println("Request Body:", string(body)) + r.Body = io.NopCloser(bytes.NewBuffer(body)) + + var newOrgRequest OrgRequest + if err := json.NewDecoder(r.Body).Decode(&newOrgRequest); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if newOrgRequest.Name == "" { + http.Error(w, "Org name can not be blank", http.StatusConflict) + return + } + + orgsMutex.Lock() + defer orgsMutex.Unlock() + + for _, org := range orgs { + if org.Name == newOrgRequest.Name || org.CodeName == newOrgRequest.Name { + http.Error(w, "org name is already taken", http.StatusBadRequest) + return + } + } + + keys := []int{} + for key := range orgs { + keys = append(keys, key) + } + for key := range deletedEntities { + if key.ContentType != "org" { + continue + } + keys = append(keys, key.ID) + } + highestId := MaxOrDefault(keys) + newId := highestId + 1 + + var newOrg Organization + + newOrg.CodeName = newOrgRequest.Name + newOrg.Name = newOrgRequest.Name + newOrg.Id = newId + newAnsibleID := uuid.NewString() + newOrg.AnsibleId = newAnsibleID + orgs[newOrg.Id] = newOrg + + fmt.Println(newOrg) + + responseOrg := OrgResponse{ + ID: newOrg.Id, + Name: newOrg.Name, + SummaryFields: struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + }{Resource: struct { + AnsibleID string `json:"ansible_id"` + }{AnsibleID: newOrg.AnsibleId}}, + } + + // create the org in the downstream service index ... + client := NewServiceIndexClient() + payload := ServiceIndexPayload{ + AnsibleId: newOrg.AnsibleId, + ServiceId: SERVICE_ID, + ResourceType: "shared.organization", + ResourceData: ServiceIndexResourceData{ + Name: newOrg.Name, + Description: "", + }, + } + user, _ := GetRequestUser(r) + client.PostData(user, payload) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(responseOrg) +} + +func deleteOrg(w http.ResponseWriter, r *http.Request) { + + orgsMutex.Lock() + defer orgsMutex.Unlock() + + teamsMutex.Lock() + defer teamsMutex.Unlock() + + roleUserAssignmentsMutex.Lock() + defer roleUserAssignmentsMutex.Unlock() + + deletedEntitiesMutex.Lock() + defer deletedEntitiesMutex.Unlock() + + orgId := GetLastNumericPathElement(r.URL.Path) + org := orgs[orgId] + + // delete org last + ansibleId := org.AnsibleId + DeleteOrganization(org) + + // Perform any additional cleanup or API calls + user, _ := GetRequestUser(r) + client := NewServiceIndexClient() + if err := client.Delete(user, ansibleId); err != nil { + log.Printf("Failed to notify Service Index Client: %v", err) + } + + // Respond with success + w.WriteHeader(http.StatusNoContent) +} diff --git a/profiles/dab_jwt/proxy/proxy_handler_roles.go b/profiles/dab_jwt/proxy/proxy_handler_roles.go new file mode 100644 index 0000000000..ee4a80117a --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_handler_roles.go @@ -0,0 +1,201 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func RoleDefinitionsHandler(w http.ResponseWriter, r *http.Request) { + roleDefinitionsMutex.Lock() + defer roleDefinitionsMutex.Unlock() + + results := []RoleDefinition{} + for _, roledef := range roleDefinitions { + results = append(results, roledef) + } + response := map[string][]RoleDefinition{ + "results": results, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func RoleUserAssignmentsHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + getRoleUserAssignments(w) + case http.MethodPost: + addRoleUserAssignments(w, r) + case http.MethodDelete: + deleteRoleUserAssignment(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func getRoleUserAssignments(w http.ResponseWriter) { + roleUserAssignmentsMutex.Lock() + defer roleUserAssignmentsMutex.Unlock() + + results := []RoleUserAssignment{} + for _, assignment := range roleUserAssignments { + results = append(results, assignment) + } + response := map[string][]RoleUserAssignment{ + "results": results, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func addRoleUserAssignments(w http.ResponseWriter, r *http.Request) { + roleUserAssignmentsMutex.Lock() + defer roleUserAssignmentsMutex.Unlock() + + var newAssignment RoleUserAssignmentRequest + if err := json.NewDecoder(r.Body).Decode(&newAssignment); err != nil { + log.Printf("%\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + keys := []int{} + for key := range roleUserAssignments { + keys = append(keys, key) + } + for key := range deletedEntities { + if key.ContentType != "role_user_assignment" { + continue + } + keys = append(keys, key.ID) + } + highestId := MaxOrDefault(keys) + newId := highestId + 1 + + newUserAssignment := RoleUserAssignment{ + Id: newId, + RoleDefinition: newAssignment.RoleDefinition, + User: newAssignment.User, + ObjectId: newAssignment.ObjectId, + } + roleUserAssignments[newId] = newUserAssignment + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(newUserAssignment) +} + +func deleteRoleUserAssignment(w http.ResponseWriter, r *http.Request) { + roleUserAssignmentsMutex.Lock() + defer roleUserAssignmentsMutex.Unlock() + deletedEntitiesMutex.Lock() + defer deletedEntitiesMutex.Unlock() + + // get the id from the path + assignmentId := GetLastNumericPathElement(r.URL.Path) + + // store the deleted entity + entity := DeletedEntityKey{ + ID: assignmentId, + ContentType: "role_user_assignment", + } + deletedEntities[entity] = true + + // actually delete it now + delete(roleUserAssignments, assignmentId) + + // Respond with success + w.WriteHeader(http.StatusNoContent) +} + +// TEAMS ... + +func RoleTeamAssignmentsHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + getRoleTeamAssignments(w) + case http.MethodPost: + addRoleTeamAssignments(w, r) + case http.MethodDelete: + deleteRoleUserAssignment(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func getRoleTeamAssignments(w http.ResponseWriter) { + roleTeamAssignmentsMutex.Lock() + defer roleTeamAssignmentsMutex.Unlock() + + results := []RoleTeamAssignment{} + for _, assignment := range roleTeamAssignments { + results = append(results, assignment) + } + response := map[string][]RoleTeamAssignment{ + "results": results, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func addRoleTeamAssignments(w http.ResponseWriter, r *http.Request) { + roleTeamAssignmentsMutex.Lock() + defer roleTeamAssignmentsMutex.Unlock() + + var newAssignment RoleTeamAssignmentRequest + if err := json.NewDecoder(r.Body).Decode(&newAssignment); err != nil { + log.Printf("%\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + keys := []int{} + for key := range roleTeamAssignments { + keys = append(keys, key) + } + for key := range deletedEntities { + if key.ContentType != "role_team_assignment" { + continue + } + keys = append(keys, key.ID) + } + highestId := MaxOrDefault(keys) + newId := highestId + 1 + + newTeamAssignment := RoleTeamAssignment{ + Id: newId, + RoleDefinition: newAssignment.RoleDefinition, + Team: newAssignment.Team, + ObjectId: newAssignment.ObjectId, + } + roleTeamAssignments[newId] = newTeamAssignment + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(newTeamAssignment) +} + +func deleteRoleTeamAssignment(w http.ResponseWriter, r *http.Request) { + roleTeamAssignmentsMutex.Lock() + defer roleTeamAssignmentsMutex.Unlock() + deletedEntitiesMutex.Lock() + defer deletedEntitiesMutex.Unlock() + + // get the id from the path + assignmentId := GetLastNumericPathElement(r.URL.Path) + + // store the deleted entity + entity := DeletedEntityKey{ + ID: assignmentId, + ContentType: "role_team_assignment", + } + deletedEntities[entity] = true + + // actually delete it now + delete(roleTeamAssignments, assignmentId) + + // Respond with success + w.WriteHeader(http.StatusNoContent) +} diff --git a/profiles/dab_jwt/proxy/proxy_handler_teams.go b/profiles/dab_jwt/proxy/proxy_handler_teams.go new file mode 100644 index 0000000000..988ebe217c --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_handler_teams.go @@ -0,0 +1,201 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "sort" + + "github.com/google/uuid" +) + +func TeamHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + getTeams(w) + case http.MethodPost: + addTeam(w, r) + case http.MethodDelete: + deleteTeam(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func getTeams(w http.ResponseWriter) { + teamsMutex.Lock() + defer teamsMutex.Unlock() + + orgsMutex.Lock() + defer orgsMutex.Unlock() + + var teamList []Team + for _, team := range teams { + teamList = append(teamList, team) + } + sort.Slice(teamList, func(i, j int) bool { + return teamList[i].Id < teamList[j].Id + }) + + var responseTeams []TeamResponse + + for _, teamdata := range teamList { + + var orgId = teamdata.Org + /* + for _, org := range orgs { + if org.CodeName == teamdata.Org { + orgId = org.Id + break + } + } + */ + + responseTeam := TeamResponse{ + ID: teamdata.Id, + Name: teamdata.Name, + Organization: orgId, + SummaryFields: struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + }{Resource: struct { + AnsibleID string `json:"ansible_id"` + }{AnsibleID: teamdata.AnsibleId}}, + } + responseTeams = append(responseTeams, responseTeam) + } + + response := map[string][]TeamResponse{ + "results": responseTeams, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func addTeam(w http.ResponseWriter, r *http.Request) { + + // fmt.Println("body", r.Body) + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } + fmt.Println("Request Body:", string(body)) + r.Body = io.NopCloser(bytes.NewBuffer(body)) + + var newTeamRequest TeamRequest + if err := json.NewDecoder(r.Body).Decode(&newTeamRequest); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if newTeamRequest.Name == "" { + http.Error(w, "Team name can not be blank", http.StatusConflict) + return + } + + orgsMutex.Lock() + defer orgsMutex.Unlock() + + teamsMutex.Lock() + defer teamsMutex.Unlock() + + checkTeam := GetTeamByName(newTeamRequest.Name) + if checkTeam.Name == newTeamRequest.Name { + http.Error(w, "Team already exists", http.StatusConflict) + return + } + + keys := []int{} + for key := range teams { + keys = append(keys, key) + } + for key := range deletedEntities { + if key.ContentType != "team" { + continue + } + keys = append(keys, key.ID) + } + highestId := MaxOrDefault(keys) + newId := highestId + 1 + + var newTeam Team + + newTeam.Name = newTeamRequest.Name + newTeam.Id = newId + newAnsibleID := uuid.NewString() + newTeam.AnsibleId = newAnsibleID + newTeam.Org = newTeamRequest.Organization + teams[newTeam.Id] = newTeam + + responseTeam := TeamResponse{ + ID: newTeam.Id, + Name: newTeam.Name, + Organization: newTeamRequest.Organization, + SummaryFields: struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + }{Resource: struct { + AnsibleID string `json:"ansible_id"` + }{AnsibleID: newAnsibleID}}, + } + + // create the team in the downstream service index ... + org := orgs[newTeamRequest.Organization] + client := NewServiceIndexClient() + payload := ServiceIndexPayload{ + AnsibleId: newTeam.AnsibleId, + ServiceId: SERVICE_ID, + ResourceType: "shared.team", + ResourceData: ServiceIndexResourceData{ + Name: newTeam.Name, + Organization: &org.AnsibleId, + Description: "", + }, + } + user, _ := GetRequestUser(r) + client.PostData(user, payload) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(responseTeam) +} + +func deleteTeam(w http.ResponseWriter, r *http.Request) { + + orgsMutex.Lock() + defer orgsMutex.Unlock() + + teamsMutex.Lock() + defer teamsMutex.Unlock() + + roleUserAssignmentsMutex.Lock() + defer roleUserAssignmentsMutex.Unlock() + + deletedEntitiesMutex.Lock() + defer deletedEntitiesMutex.Unlock() + + teamId := GetLastNumericPathElement(r.URL.Path) + team := teams[teamId] + + // delete org last + ansibleId := team.AnsibleId + DeleteTeam(team) + + // Perform any additional cleanup or API calls + user, _ := GetRequestUser(r) + client := NewServiceIndexClient() + if err := client.Delete(user, ansibleId); err != nil { + log.Printf("Failed to notify Service Index Client: %v", err) + } + + // Respond with success + w.WriteHeader(http.StatusNoContent) +} diff --git a/profiles/dab_jwt/proxy/proxy_handler_users.go b/profiles/dab_jwt/proxy/proxy_handler_users.go new file mode 100644 index 0000000000..01bbac5589 --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_handler_users.go @@ -0,0 +1,211 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "sort" + + "github.com/google/uuid" +) + +func UserHandler(w http.ResponseWriter, r *http.Request) { + //usersMutex.Lock() + //defer usersMutex.Unlock() + + switch r.Method { + case http.MethodGet: + getUsers(w) + case http.MethodPost: + addUser(w, r) + case http.MethodDelete: + deleteUser(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func getUsers(w http.ResponseWriter) { + //usersMutex.Lock() + //defer usersMutex.Unlock() + + var userList []User + for _, user := range users { + userList = append(userList, user) + } + sort.Slice(userList, func(i, j int) bool { + return userList[i].Id < userList[j].Id + }) + + var responseUsers []UserResponse + + for _, userdata := range userList { + responseUser := UserResponse{ + ID: userdata.Id, + Username: userdata.Username, + SummaryFields: struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + }{Resource: struct { + AnsibleID string `json:"ansible_id"` + }{AnsibleID: userdata.Sub}}, + } + responseUsers = append(responseUsers, responseUser) + } + + response := map[string][]UserResponse{ + "results": responseUsers, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func addUser(w http.ResponseWriter, r *http.Request) { + /* + # PAYLOAD + {"username": "foo", "password": "redhat1234"} + + # RESPONSE + { + "id":96, + "url":"/api/gateway/v1/users/96/", + "related":{ + "personal_tokens":"/api/gateway/v1/users/96/personal_tokens/", + "authorized_tokens":"/api/gateway/v1/users/96/authorized_tokens/", + "tokens":"/api/gateway/v1/users/96/tokens/", + "activity_stream":"/api/gateway/v1/activitystream/?content_type=1&object_id=96", + "created_by":"/api/gateway/v1/users/6/", + "modified_by":"/api/gateway/v1/users/6/", + "authenticators":"/api/gateway/v1/users/96/authenticators/" + }, + "summary_fields":{ + "modified_by":{"id":6,"username":"dev","first_name":"","last_name":""}, + "created_by":{"id":6,"username":"dev","first_name":"","last_name":""}, + "resource":{"ansible_id":"5fc36ea7-5c54-4f12-b47c-5213d648b2c0","resource_type":"shared.user"} + }, + "created":"2024-07-31T02:20:31.734404Z", + "created_by":6, + "modified":"2024-07-31T02:20:31.734386Z", + "modified_by":6, + "username":"foo", + "email":"", + "first_name":"", + "last_name":"", + "last_login":null, + "password":"$encrypted$", + "is_superuser":false, + "is_platform_auditor":false, + "managed":false, + "last_login_results":{}, + "authenticators":[], + "authenticator_uid":"" + } + */ + + usersMutex.Lock() + defer usersMutex.Unlock() + + var newUser User + if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if newUser.Username == "" { + http.Error(w, "username can not be blank.", http.StatusBadRequest) + return + } + + //usersMutex.Lock() + //defer usersMutex.Unlock() + + checkUser := GetUserByUserName(newUser.Username) + if checkUser.Username == newUser.Username { + http.Error(w, "User already exists", http.StatusConflict) + return + } + + keys := []int{} + for key := range users { + keys = append(keys, key) + } + for key := range deletedEntities { + if key.ContentType != "user" { + continue + } + keys = append(keys, key.ID) + } + highestId := MaxOrDefault(keys) + newId := highestId + 1 + + newUser.Id = newId + newAnsibleID := uuid.NewString() + newUser.Sub = newAnsibleID + users[newUser.Id] = newUser + + responseUser := UserResponse{ + ID: newUser.Id, + Username: newUser.Username, + SummaryFields: struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + }{Resource: struct { + AnsibleID string `json:"ansible_id"` + }{AnsibleID: newAnsibleID}}, + } + + // create the team in the downstream service index ... + client := NewServiceIndexClient() + payload := ServiceIndexPayload{ + AnsibleId: newUser.Sub, + ServiceId: SERVICE_ID, + ResourceType: "shared.user", + ResourceData: ServiceIndexResourceData{ + UserName: newUser.Username, + Email: newUser.Email, + FirstName: newUser.FirstName, + LastName: newUser.LastName, + SuperUser: newUser.IsSuperuser, + }, + } + user, _ := GetRequestUser(r) + client.PostData(user, payload) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(responseUser) +} + +func deleteUser(w http.ResponseWriter, r *http.Request) { + + orgsMutex.Lock() + defer orgsMutex.Unlock() + + teamsMutex.Lock() + defer teamsMutex.Unlock() + + roleUserAssignmentsMutex.Lock() + defer roleUserAssignmentsMutex.Unlock() + + deletedEntitiesMutex.Lock() + defer deletedEntitiesMutex.Unlock() + + userId := GetLastNumericPathElement(r.URL.Path) + user := users[userId] + + ansibleId := user.Sub + DeleteUser(user) + + // Perform any additional cleanup or API calls + ruser, _ := GetRequestUser(r) + client := NewServiceIndexClient() + if err := client.Delete(ruser, ansibleId); err != nil { + log.Printf("Failed to notify Service Index Client: %v", err) + } + + // Respond with success + w.WriteHeader(http.StatusNoContent) +} diff --git a/profiles/dab_jwt/proxy/proxy_service_index_client.go b/profiles/dab_jwt/proxy/proxy_service_index_client.go new file mode 100644 index 0000000000..e36dcdf47a --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_service_index_client.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" +) + +type ServiceIndexPayload struct { + AnsibleId string `json:"ansible_id"` + ServiceId string `json:"service_id"` + ResourceType string `json:"resource_type"` + ResourceData ServiceIndexResourceData `json:"resource_data"` +} + +// ResourceData represents the data structure for resource_data in the payload +type ServiceIndexResourceData struct { + Name string `json:"name,omitempty"` + UserName string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + SuperUser bool `json:"is_superuser,omitempty"` + Description string `json:"description,omitempty"` + Organization *string `json:"organization,omitempty"` +} + +// Payload represents an arbitrary map of data +type Payload map[string]interface{} + +// ServiceIndexClient is the client that will handle the HTTP requests +type ServiceIndexClient struct { + BaseURL string + Client *http.Client +} + +// NewServiceIndexClient initializes and returns a ServiceIndexClient +func NewServiceIndexClient() *ServiceIndexClient { + return &ServiceIndexClient{ + Client: &http.Client{}, + } +} + +// PostData sends a POST request to the remote endpoint with User and Payload +func (c *ServiceIndexClient) PostData(user User, payload ServiceIndexPayload) error { + + log.Printf("starting indexclient POST ...") + + target := getEnv("UPSTREAM_URL", "http://localhost:5001") + endpoint := target + getEnv("UPSTREAM_API_PREFIX", "/api/galaxy") + "/service-index/resources/" + + log.Printf("using endpoint %s", endpoint) + + // Generate the JWT token + log.Printf("generating token for request user ...") + token, err := generateJWT(user) + if err != nil { + //http.Error(w, "Internal Server Error", http.StatusInternalServerError) + log.Printf("token generation failed %s", err) + return err + } + log.Printf("generated token %s", token) + + // Marshal the data into JSON + jsonData, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal data: %v", err) + } + + // Create a new POST request + log.Printf("POST %s to %s", jsonData, endpoint) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) + } + + // Set headers + req.Header.Set("X-DAB-JW-TOKEN", token) + req.Header.Set("Content-Type", "application/json") + + // Perform the request + resp, err := c.Client.Do(req) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("received non-200 response: %d", resp.StatusCode) + } + + // Optionally, read and process response body here + return nil +} + +// PostData sends a POST request to the remote endpoint with User and Payload +func (c *ServiceIndexClient) Delete(user User, resourceid string) error { + + log.Printf("starting indexclient DELETE ...") + + target := getEnv("UPSTREAM_URL", "http://localhost:5001") + endpoint := target + getEnv("UPSTREAM_API_PREFIX", "/api/galaxy") + "/service-index/resources/" + resourceid + "/" + + log.Printf("using endpoint %s", endpoint) + + // Generate the JWT token + log.Printf("generating token for request user ...") + token, err := generateJWT(user) + if err != nil { + //http.Error(w, "Internal Server Error", http.StatusInternalServerError) + log.Printf("token generation failed %s", err) + return err + } + log.Printf("generated token %s", token) + + // Create a new POST request + log.Printf("DELETE %s", endpoint) + req, err := http.NewRequest("DELETE", endpoint, nil) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) + } + + // Set headers + req.Header.Set("X-DAB-JW-TOKEN", token) + req.Header.Set("Content-Type", "application/json") + + // Perform the request + resp, err := c.Client.Do(req) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + defer resp.Body.Close() + + // Check for expected status codes + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("received unexpected response status: %d", resp.StatusCode) + } + + log.Printf("DELETE request successful with status code %d", resp.StatusCode) + + // Optionally, read and process response body here + return nil +} diff --git a/profiles/dab_jwt/proxy/proxy_types.go b/profiles/dab_jwt/proxy/proxy_types.go new file mode 100644 index 0000000000..81307cb322 --- /dev/null +++ b/profiles/dab_jwt/proxy/proxy_types.go @@ -0,0 +1,209 @@ +package main + +import ( + "fmt" + "time" +) + +// UserSession represents the user session information +type UserSession struct { + Username string + CSRFToken string + SessionID string +} + +// User represents a user's information +type User struct { + Id int + Username string + Password string + FirstName string + LastName string + IsSuperuser bool + Email string + Organizations []string + Teams []string + IsSystemAuditor bool + Sub string +} + +/* +pulp-1 | {'aud': 'ansible-services', +pulp-1 | 'exp': 1718658788, +pulp-1 | 'global_roles': [], +pulp-1 | 'iss': 'ansible-issuer', +pulp-1 | 'object_roles': {'Team Member': {'content_type': 'team', 'objects': [0]}}, +pulp-1 | 'objects': {'organization': [{'ansible_id': 'bc243368-a9d4-4f8f-9ffe-5d2d921fcee5', +pulp-1 | 'name': 'Default'}], +pulp-1 | 'team': [{'ansible_id': '34a58292-1e0f-49f0-9383-fb7e63d771d9', +pulp-1 | 'name': 'ateam', +pulp-1 | 'org': 0}]}, +pulp-1 | 'sub': '4f6499bf-3ad2-45ff-8411-0188c4f817c1', +pulp-1 | 'user_data': {'email': 'sa@localhost.com', +pulp-1 | 'first_name': 'sa', +pulp-1 | 'is_superuser': True, +pulp-1 | 'last_name': 'asdfasdf', +pulp-1 | 'username': 'superauditor'}, +pulp-1 | 'version': '1'} +*/ + +// JWT claims +type UserClaims struct { + Version int `json:"version"` + Iss string `json:"iss"` + Aud string `json:"aud"` + Expires int64 `json:"exp"` + GlobalRoles []string `json:"global_roles"` + UserData UserData `json:"user_data"` + Sub string `json:"sub"` + ObjectRoles map[string]interface{} `json:"object_roles"` + Objects map[string]interface{} `json:"objects"` +} + +// Implement the jwt.Claims interface +func (c UserClaims) Valid() error { + if time.Unix(c.Expires, 0).Before(time.Now()) { + return fmt.Errorf("token is expired") + } + return nil +} + +type UserData struct { + Username string `json:"username"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + IsSuperuser bool `json:"is_superuser"` + Email string `json:"email"` +} + +type Organization struct { + Id int `json:"id"` + AnsibleId string `json:"ansible_id"` + Name string `json:"name"` + CodeName string `json:"code_name"` +} + +// for the jwt +type OrganizationObject struct { + AnsibleId string `json:"ansible_id"` + Name string `json:"name"` +} + +type Team struct { + Id int `json:"id"` + AnsibleId string `json:"ansible_id"` + Name string `json:"name"` + Org int `json:"org"` +} + +// for the jwt +type TeamObject struct { + AnsibleId string `json:"ansible_id"` + Name string `json:"name"` + Org int `json:"org"` +} + +type ObjectRole struct { + ContentType string `json:"content_type"` + Objects []int `json:"objects"` +} + +// LoginRequest represents the login request payload +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// LoginResponse represents the login response payload +type LoginResponse struct { + CSRFToken string `json:"csrfToken"` +} + +type OrgResponse struct { + ID int `json:"id"` + Name string `json:"name"` + SummaryFields struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + } `json:"summary_fields"` +} + +type TeamResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Organization int `json:"organization"` + SummaryFields struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + } `json:"summary_fields"` +} + +type UserResponse struct { + ID int `json:"id"` + Username string `json:"username"` + SummaryFields struct { + Resource struct { + AnsibleID string `json:"ansible_id"` + } `json:"resource"` + } `json:"summary_fields"` +} + +type OrgRequest struct { + Name string `json:"name"` +} + +type TeamRequest struct { + Name string `json:"name"` + Organization int `json:"organization"` +} + +type AssociationRequest struct { + Instances []int `json:"instances"` +} + +type RoleDefinition struct { + Id int `json:"id"` + Name string `json:"name"` + Managed bool `json:"managed"` + Permissions []string `json:"permissions"` +} + +// payload for adding roledefs to users +type RoleUserAssignmentRequest struct { + User int `json:"user"` + RoleDefinition int `json:"role_definition"` + ObjectId int `json:"object_id"` +} + +// list view +type RoleUserAssignment struct { + Id int `json:"id"` + ContentType string `json:"content_type"` + RoleDefinition int `json:"role_definition"` + User int `json:"user"` + ObjectId int `json:"object_id"` +} + +// list view +type RoleTeamAssignment struct { + Id int `json:"id"` + ContentType string `json:"content_type"` + RoleDefinition int `json:"role_definition"` + Team int `json:"team"` + ObjectId int `json:"object_id"` +} + +// payload for adding roledefs to users +type RoleTeamAssignmentRequest struct { + Team int `json:"team"` + RoleDefinition int `json:"role_definition"` + ObjectId int `json:"object_id"` +} + +// Object Deletion +type DeletedEntityKey struct { + ID int + ContentType string +}