From 73c32c9e58c89bf834b9cac1d93ace0d265527bf Mon Sep 17 00:00:00 2001 From: Bobby Iliev Date: Mon, 8 Jul 2024 20:08:59 +0300 Subject: [PATCH] Update to 0.8.3 (#114) --- CHANGELOG.md | 5 +- examples/mocks/frontegg/go.mod | 5 +- examples/mocks/frontegg/go.sum | 2 + examples/mocks/frontegg/mock_server.go | 1623 ++++++++--------- .../pulumi-resource-materialize/schema.json | 41 +- provider/go.mod | 2 +- provider/go.sum | 4 +- provider/resources.go | 1 + 8 files changed, 808 insertions(+), 875 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 210cf38..e572e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ CHANGELOG ========= -## 0.3.2 - 2024-07-01 +## 0.3.3 - 2024-07-08 +Update to Terraform Provider [0.8.3](https://github.com/MaterializeInc/terraform-provider-materialize/releases/tag/v0.8.3). + +## 0.3.2 - 2024-07-01 Update to Terraform Provider [0.8.2](https://github.com/MaterializeInc/terraform-provider-materialize/releases/tag/v0.8.2). ## 0.3.1 - 2024-06-20 diff --git a/examples/mocks/frontegg/go.mod b/examples/mocks/frontegg/go.mod index 67becda..c2a8973 100644 --- a/examples/mocks/frontegg/go.mod +++ b/examples/mocks/frontegg/go.mod @@ -2,4 +2,7 @@ module frontegg-mockserver go 1.20 -require github.com/google/uuid v1.5.0 +require ( + github.com/google/uuid v1.5.0 + github.com/gorilla/mux v1.8.1 +) diff --git a/examples/mocks/frontegg/go.sum b/examples/mocks/frontegg/go.sum index 040e221..92cd7e9 100644 --- a/examples/mocks/frontegg/go.sum +++ b/examples/mocks/frontegg/go.sum @@ -1,2 +1,4 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/examples/mocks/frontegg/mock_server.go b/examples/mocks/frontegg/mock_server.go index 9613278..bd7e4c9 100644 --- a/examples/mocks/frontegg/mock_server.go +++ b/examples/mocks/frontegg/mock_server.go @@ -1,20 +1,23 @@ package main import ( - "bytes" "encoding/base64" "encoding/json" "fmt" - "io" "log" "net/http" + "os" + "sort" + "strconv" "strings" "sync" "time" "github.com/google/uuid" + "github.com/gorilla/mux" ) +// Struct definitions type AppPassword struct { ClientID string `json:"clientId"` Secret string `json:"secret"` @@ -88,7 +91,6 @@ type SSOConfig struct { RoleIds []string `json:"roleIds"` } -// SCIM 2.0 Configurations API response type SCIM2Configuration struct { ID string `json:"id"` Source string `json:"source"` @@ -99,9 +101,6 @@ type SCIM2Configuration struct { Token string `json:"token"` } -type SCIM2ConfigurationsResponse []SCIM2Configuration - -// GroupCreateParams represents the parameters for creating a new group. type GroupCreateParams struct { Name string `json:"name"` Description string `json:"description,omitempty"` @@ -109,7 +108,6 @@ type GroupCreateParams struct { Metadata string `json:"metadata,omitempty"` } -// GroupUpdateParams represents the parameters for updating an existing group. type GroupUpdateParams struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` @@ -117,7 +115,6 @@ type GroupUpdateParams struct { Metadata string `json:"metadata,omitempty"` } -// ScimGroup represents the structure of a group in the response. type ScimGroup struct { ID string `json:"id"` Name string `json:"name"` @@ -129,7 +126,6 @@ type ScimGroup struct { Color string `json:"color"` } -// ScimRole represents the structure of a role within a group. type ScimRole struct { ID string `json:"id"` Key string `json:"key"` @@ -138,31 +134,26 @@ type ScimRole struct { IsDefault bool `json:"is_default"` } -// ScimUser represents the structure of a user within a group. type ScimUser struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } -// SCIMGroupsResponse represents the overall structure of the response from the SCIM groups API. type SCIMGroupsResponse struct { Groups []ScimGroup `json:"groups"` } -// AddRolesToGroupParams represents the parameters for adding roles to a group. type AddRolesToGroupParams struct { RoleIds []string `json:"roleIds"` } -// TenantApiTokenRequest represents the structure of a request to create a tenant API token. type TenantApiTokenRequest struct { Description string `json:"description"` Metadata map[string]string `json:"metadata"` RoleIDs []string `json:"roleIds"` } -// TenantApiTokenResponse represents the structure of a response from creating a tenant API token. type TenantApiTokenResponse struct { ClientID string `json:"clientId"` Description string `json:"description"` @@ -173,137 +164,78 @@ type TenantApiTokenResponse struct { RoleIDs []string `json:"roleIds"` } -var ( - appPasswords = make(map[string]AppPassword) - tenantAppPasswords = make(map[string]TenantApiTokenResponse) - users = make(map[string]User) - ssoConfigs = make(map[string]SSOConfig) - scimConfigurations = make(map[string]SCIM2Configuration) - groups = make(map[string]ScimGroup) - mutex = &sync.Mutex{} -) - -func main() { - http.HandleFunc("/identity/resources/auth/v1/api-token", handleTokenRequest) - http.HandleFunc("/identity/resources/users/api-tokens/v1", handleAppPasswords) - http.HandleFunc("/identity/resources/users/api-tokens/v1/", handleAppPasswordsDelete) - http.HandleFunc("/identity/resources/tenants/api-tokens/v1", handleTenantAppPasswords) - http.HandleFunc("/identity/resources/tenants/api-tokens/v1/", handleTenantAppPasswordsDelete) - http.HandleFunc("/identity/resources/users/v1/", handleUserRequest) - http.HandleFunc("/identity/resources/users/v2", handleUserRequest) - http.HandleFunc("/identity/resources/roles/v2", handleRolesRequest) - http.HandleFunc("/frontegg/team/resources/sso/v1/configurations", handleSSOConfigRequest) - http.HandleFunc("/frontegg/team/resources/sso/v1/configurations/", handleSSOConfigAndDomainRequest) - http.HandleFunc("/frontegg/identity/resources/groups/v1", handleSCIMGroupsRequest) - http.HandleFunc("/frontegg/identity/resources/groups/v1/", handleSCIMGroupsParamRequest) - http.HandleFunc("/frontegg/directory/resources/v1/configurations/scim2", handleSCIM2ConfigurationsRequest) - http.HandleFunc("/frontegg/directory/resources/v1/configurations/scim2/", handleSCIMConfigurationByID) - - fmt.Println("Mock Frontegg server is running at http://localhost:3000") - log.Fatal(http.ListenAndServe(":3000", nil)) -} - -func handleUserRequest(w http.ResponseWriter, r *http.Request) { - logRequest(r) - switch r.Method { - case http.MethodGet: - getUser(w, r) - case http.MethodDelete: - deleteUser(w, r) - case http.MethodPost: - createUser(w, r) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -func getUser(w http.ResponseWriter, r *http.Request) { - userID := strings.TrimPrefix(r.URL.Path, "/identity/resources/users/v1/") - if userID == "" { - http.Error(w, "User ID is required", http.StatusBadRequest) - return - } - - user, ok := users[userID] - if !ok { - http.Error(w, "User not found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -func createUser(w http.ResponseWriter, r *http.Request) { - var newUser struct { - User - RoleIDs []string `json:"roleIds"` - SkipInviteEmail bool `json:"skipInviteEmail"` - } - - if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - userID := generateUserID() - newUser.ID = userID - - // Map role IDs to role names and update the newUser.Roles slice - for _, roleID := range newUser.RoleIDs { - var roleName string - switch roleID { - case "1": - roleName = "Organization Admin" - case "2": - roleName = "Organization Member" - } - - if roleName != "" { - newUser.Roles = append(newUser.Roles, FronteggRole{ID: roleID, Name: roleName}) - } - } - - mutex.Lock() - users[userID] = newUser.User - mutex.Unlock() - - w.WriteHeader(http.StatusCreated) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(newUser.User) -} - -func generateUserID() string { - return fmt.Sprintf("user-%d", time.Now().UnixNano()) +// App struct to hold dependencies +type App struct { + Router *mux.Router + Store *DataStore + Logger *log.Logger } -func generateConfigID() string { - return fmt.Sprintf("config-%d", time.Now().UnixNano()) +// DataStore holds all the in-memory data +type DataStore struct { + Mu sync.RWMutex + AppPasswords map[string]AppPassword + TenantAppPasswords map[string]TenantApiTokenResponse + Users map[string]User + SSOConfigs map[string]SSOConfig + ScimConfigurations map[string]SCIM2Configuration + Groups map[string]ScimGroup } -func deleteUser(w http.ResponseWriter, r *http.Request) { - userID := strings.TrimPrefix(r.URL.Path, "/identity/resources/users/v1/") - if userID == "" { - http.Error(w, "User ID is required", http.StatusBadRequest) - return - } - - _, ok := users[userID] - if !ok { - http.Error(w, "User not found", http.StatusNotFound) - return - } - - delete(users, userID) - w.WriteHeader(http.StatusOK) -} - -func handleTokenRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - +// Main function to start the server +func main() { + app := &App{ + Router: mux.NewRouter(), + Store: newDataStore(), + Logger: log.New(os.Stdout, "MOCK-SERVICE: ", log.LstdFlags), + } + + app.routes() + + app.Logger.Println("Mock Frontegg server is running at http://localhost:3000") + log.Fatal(http.ListenAndServe(":3000", app.Router)) +} + +// DataStore constructor to initialize the in-memory data +func newDataStore() *DataStore { + return &DataStore{ + AppPasswords: make(map[string]AppPassword), + TenantAppPasswords: make(map[string]TenantApiTokenResponse), + Users: make(map[string]User), + SSOConfigs: make(map[string]SSOConfig), + ScimConfigurations: make(map[string]SCIM2Configuration), + Groups: make(map[string]ScimGroup), + } +} + +// Routes setup to handle different endpoints +func (app *App) routes() { + app.Router.HandleFunc("/identity/resources/auth/v1/api-token", app.handleTokenRequest).Methods("POST") + app.Router.HandleFunc("/identity/resources/users/api-tokens/v1", app.handleAppPasswords).Methods("GET", "POST") + app.Router.HandleFunc("/identity/resources/users/api-tokens/v1/{id}", app.handleAppPasswordsDelete).Methods("DELETE") + app.Router.HandleFunc("/identity/resources/tenants/api-tokens/v1", app.handleTenantAppPasswords).Methods("GET", "POST") + app.Router.HandleFunc("/identity/resources/tenants/api-tokens/v1/{id}", app.handleTenantAppPasswordsDelete).Methods("DELETE") + app.Router.HandleFunc("/identity/resources/users/v1/{id}", app.handleUserRequest).Methods("GET", "DELETE") + app.Router.HandleFunc("/identity/resources/users/v2", app.handleUserRequest).Methods("POST") + app.Router.HandleFunc("/identity/resources/roles/v2", app.handleRolesRequest).Methods("GET") + app.Router.HandleFunc("/identity/resources/users/v3", app.handleUserV3Request).Methods("GET") + app.Router.HandleFunc("/frontegg/team/resources/sso/v1/configurations", app.handleSSOConfigRequest).Methods("GET", "POST") + app.Router.HandleFunc("/frontegg/team/resources/sso/v1/configurations/{id}", app.handleSSOConfigAndDomainRequest).Methods("GET", "PATCH", "DELETE") + app.Router.HandleFunc("/frontegg/team/resources/sso/v1/configurations/{id}/domains", app.handleDomainRequests).Methods("GET", "POST") + app.Router.HandleFunc("/frontegg/team/resources/sso/v1/configurations/{id}/domains/{domainId}", app.handleDomainRequests).Methods("GET", "PATCH", "DELETE") + app.Router.HandleFunc("/frontegg/team/resources/sso/v1/configurations/{id}/groups", app.handleGroupMappingRequests).Methods("GET", "POST") + app.Router.HandleFunc("/frontegg/team/resources/sso/v1/configurations/{id}/groups/{groupId}", app.handleGroupMappingRequests).Methods("GET", "PATCH", "DELETE") + app.Router.HandleFunc("/frontegg/team/resources/sso/v1/configurations/{id}/roles", app.handleDefaultRolesRequests).Methods("GET", "PUT", "DELETE") + app.Router.HandleFunc("/frontegg/identity/resources/groups/v1", app.handleSCIMGroupsRequest).Methods("GET", "POST") + app.Router.HandleFunc("/frontegg/identity/resources/groups/v1/{id}", app.handleSCIMGroupsParamRequest).Methods("GET", "PATCH", "DELETE") + app.Router.HandleFunc("/frontegg/identity/resources/groups/v1/{id}/roles", app.handleAddRolesToGroup).Methods("POST", "DELETE") + app.Router.HandleFunc("/frontegg/identity/resources/groups/v1/{id}/users", app.handleAddUsersToGroup).Methods("POST", "DELETE") + app.Router.HandleFunc("/frontegg/directory/resources/v1/configurations/scim2", app.handleSCIM2ConfigurationsRequest).Methods("GET", "POST") + app.Router.HandleFunc("/frontegg/directory/resources/v1/configurations/scim2/{id}", app.handleSCIMConfigurationByID).Methods("DELETE") +} + +// Handler methods +func (app *App) handleTokenRequest(w http.ResponseWriter, r *http.Request) { var payload struct { ClientId string `json:"clientId"` Secret string `json:"secret"` @@ -319,54 +251,74 @@ func handleTokenRequest(w http.ResponseWriter, r *http.Request) { "accessToken": mockToken, "email": "mz_system", } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + sendJSONResponse(w, http.StatusOK, response) } else { http.Error(w, "Invalid credentials", http.StatusUnauthorized) } } -func createMockJWTToken() string { - header := base64UrlEncode([]byte(`{"alg":"HS256","typ":"JWT"}`)) - payload := base64UrlEncode([]byte(`{"email":"mz_system","exp":1700000000}`)) - signature := base64UrlEncode([]byte(`signature`)) - return fmt.Sprintf("%s.%s.%s", header, payload, signature) +func (app *App) handleAppPasswords(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + app.createAppPassword(w, r) + case http.MethodGet: + app.listAppPasswords(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } } -func base64UrlEncode(input []byte) string { - encoded := base64.StdEncoding.EncodeToString(input) - encoded = strings.ReplaceAll(encoded, "+", "-") - encoded = strings.ReplaceAll(encoded, "/", "_") - encoded = strings.TrimRight(encoded, "=") - return encoded +func (app *App) handleAppPasswordsDelete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clientID := vars["id"] + + app.Store.Mu.Lock() + delete(app.Store.AppPasswords, clientID) + app.Store.Mu.Unlock() + + w.WriteHeader(http.StatusOK) } -func handleAppPasswords(w http.ResponseWriter, r *http.Request) { - logRequest(r) +func (app *App) handleTenantAppPasswords(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: - createAppPassword(w, r) + app.createTenantAppPassword(w, r) case http.MethodGet: - listAppPasswords(w, r) - case http.MethodDelete: - deleteAppPassword(w, r) + app.listTenantAppPasswords(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func handleAppPasswordsDelete(w http.ResponseWriter, r *http.Request) { - logRequest(r) +func (app *App) handleTenantAppPasswordsDelete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clientID := vars["id"] + + app.Store.Mu.Lock() + delete(app.Store.TenantAppPasswords, clientID) + app.Store.Mu.Unlock() + + w.WriteHeader(http.StatusOK) +} + +func (app *App) handleUserRequest(w http.ResponseWriter, r *http.Request) { switch r.Method { + case http.MethodGet: + app.getUser(w, r) case http.MethodDelete: - deleteAppPassword(w, r) + app.deleteUser(w, r) + case http.MethodPost: + app.createUser(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func handleRolesRequest(w http.ResponseWriter, r *http.Request) { - logRequest(r) +func (app *App) handleUserV3Request(w http.ResponseWriter, r *http.Request) { + app.getUsersV3(w, r) +} + +func (app *App) handleRolesRequest(w http.ResponseWriter, r *http.Request) { roles := []FronteggRole{ {ID: "1", Name: "Organization Admin"}, {ID: "2", Name: "Organization Member"}, @@ -383,715 +335,382 @@ func handleRolesRequest(w http.ResponseWriter, r *http.Request) { }, } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -func createAppPassword(w http.ResponseWriter, r *http.Request) { - logRequest(r) - var req struct { - Description string `json:"description"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Generate a new app password - newAppPassword := AppPassword{ - ClientID: generateClientID(), - Secret: generateSecret(), - Description: req.Description, - Owner: "mockOwner", - CreatedAt: time.Now(), - } - - // Store the new app password - mutex.Lock() - appPasswords[newAppPassword.ClientID] = newAppPassword - mutex.Unlock() - - // Send the response back - sendResponse(w, http.StatusCreated, newAppPassword) -} - -func listAppPasswords(w http.ResponseWriter, r *http.Request) { - mutex.Lock() - passwords := make([]AppPassword, 0, len(appPasswords)) - for _, password := range appPasswords { - passwords = append(passwords, password) - } - mutex.Unlock() - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(passwords) -} - -func deleteAppPassword(w http.ResponseWriter, r *http.Request) { - clientID := strings.TrimPrefix(r.URL.Path, "/identity/resources/users/api-tokens/v1/") - if clientID == "" { - http.Error(w, "Client ID is required", http.StatusBadRequest) - return - } - - mutex.Lock() - delete(appPasswords, clientID) - mutex.Unlock() - - w.WriteHeader(http.StatusOK) + sendJSONResponse(w, http.StatusOK, response) } -// HandleTenantAppPasswords provides a single entry point for POST and GET methods. -func handleTenantAppPasswords(w http.ResponseWriter, r *http.Request) { - logRequest(r) +func (app *App) handleSSOConfigRequest(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: - createTenantAppPassword(w, r) + app.createSSOConfig(w, r) case http.MethodGet: - listTenantAppPasswords(w, r) + app.listSSOConfigs(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -// HandleTenantAppPasswordsDelete handles the DELETE method. -func handleTenantAppPasswordsDelete(w http.ResponseWriter, r *http.Request) { - logRequest(r) - if r.Method == http.MethodDelete { - deleteTenantAppPassword(w, r) - } else { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } -} +func (app *App) handleSSOConfigAndDomainRequest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + configID := vars["id"] -// Create a new tenant app password -func createTenantAppPassword(w http.ResponseWriter, r *http.Request) { - logRequest(r) - var req TenantApiTokenRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Simulate token creation logic - newToken := TenantApiTokenResponse{ - ClientID: generateClientID(), - Secret: generateSecret(), - Description: req.Description, - CreatedByUserId: "mockUser", - CreatedAt: time.Now(), - Metadata: req.Metadata, - RoleIDs: req.RoleIDs, - } - - mutex.Lock() - tenantAppPasswords[newToken.ClientID] = newToken - mutex.Unlock() - - sendResponse(w, http.StatusCreated, newToken) -} - -// List all tenant app passwords -func listTenantAppPasswords(w http.ResponseWriter, r *http.Request) { - mutex.Lock() - passwords := make([]TenantApiTokenResponse, 0, len(tenantAppPasswords)) - for _, password := range tenantAppPasswords { - passwords = append(passwords, password) - } - mutex.Unlock() - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(passwords) -} - -// Delete a tenant app password -func deleteTenantAppPassword(w http.ResponseWriter, r *http.Request) { - clientID := strings.TrimPrefix(r.URL.Path, "/identity/resources/tenants/api-tokens/v1"+"/") - if clientID == "" { - http.Error(w, "Client ID is required", http.StatusBadRequest) - return - } - - mutex.Lock() - delete(tenantAppPasswords, clientID) - mutex.Unlock() - - w.WriteHeader(http.StatusOK) -} - -// generateClientID generates a unique client ID. -func generateClientID() string { - return fmt.Sprintf("client-%d", time.Now().UnixNano()) -} - -// generateSecret generates a secret. -func generateSecret() string { - return fmt.Sprintf("secret-%d", time.Now().UnixNano()) -} - -func sendResponse(w http.ResponseWriter, statusCode int, payload interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - if payload != nil { - responseBytes, _ := json.Marshal(payload) - fmt.Printf("Response body: %s\n", string(responseBytes)) - w.Write(responseBytes) - } -} - -func logRequest(r *http.Request) { - fmt.Printf("Received request: %s %s\n", r.Method, r.URL.Path) - if r.Body != nil { - bodyBytes, err := io.ReadAll(r.Body) - if err == nil { - fmt.Printf("Request body: %s\n", string(bodyBytes)) - // Important: Restore the body for further reading - r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) - } - } -} - -func handleSSOConfigRequest(w http.ResponseWriter, r *http.Request) { - logRequest(r) switch r.Method { - case http.MethodPost: - createSSOConfig(w, r) case http.MethodGet: - listSSOConfigs(w, r) + app.getSSOConfig(w, r, configID) + case http.MethodPatch: + app.updateSSOConfig(w, r, configID) + case http.MethodDelete: + app.deleteSSOConfig(w, r, configID) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func handleSSOConfigAndDomainRequest(w http.ResponseWriter, r *http.Request) { - logRequest(r) - parts := strings.Split(r.URL.Path, "/") - if len(parts) < 8 { - http.Error(w, "Invalid URL", http.StatusBadRequest) - return - } - - ssoConfigID := parts[7] - - if len(parts) > 8 { - switch parts[8] { - case "domains": - handleDomainRequests(w, r, ssoConfigID, parts) - case "groups": - handleGroupMappingRequests(w, r, ssoConfigID, parts) - case "roles": - handleDefaultRolesRequests(w, r, ssoConfigID) - default: - http.Error(w, "Invalid request", http.StatusBadRequest) - } - } else { - switch r.Method { - case http.MethodGet: - getSSOConfig(w, r, ssoConfigID) - case http.MethodPatch: - updateSSOConfig(w, r, ssoConfigID) - case http.MethodDelete: - deleteSSOConfig(w, r, ssoConfigID) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } - } -} - -func handleDomainRequests(w http.ResponseWriter, r *http.Request, ssoConfigID string, parts []string) { - domainID := "" - if len(parts) > 9 { - domainID = parts[9] - } +func (app *App) handleDomainRequests(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + configID := vars["id"] + domainID := vars["domainId"] switch r.Method { case http.MethodPost: - createDomain(w, r, ssoConfigID) + app.createDomain(w, r, configID) case http.MethodGet: if domainID == "" { - listDomains(w, ssoConfigID) + app.listDomains(w, configID) } else { - getDomain(w, ssoConfigID, domainID) + app.getDomain(w, configID, domainID) } case http.MethodPatch: - updateDomain(w, r, ssoConfigID, domainID) + app.updateDomain(w, r, configID, domainID) case http.MethodDelete: - deleteDomain(w, ssoConfigID, domainID) + app.deleteDomain(w, configID, domainID) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func handleGroupMappingRequests(w http.ResponseWriter, r *http.Request, ssoConfigID string, parts []string) { - groupMappingID := "" - if len(parts) > 9 { - groupMappingID = parts[9] - } +func (app *App) handleGroupMappingRequests(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + configID := vars["id"] + groupID := vars["groupId"] switch r.Method { case http.MethodPost: - createGroupMapping(w, r, ssoConfigID) + app.createGroupMapping(w, r, configID) case http.MethodGet: - if groupMappingID == "" { - listGroupMappings(w, ssoConfigID) + if groupID == "" { + app.listGroupMappings(w, configID) } else { - getGroupMapping(w, ssoConfigID, groupMappingID) + app.getGroupMapping(w, configID, groupID) } case http.MethodPatch: - updateGroupMapping(w, r, ssoConfigID, groupMappingID) + app.updateGroupMapping(w, r, configID, groupID) case http.MethodDelete: - deleteGroupMapping(w, ssoConfigID, groupMappingID) + app.deleteGroupMapping(w, configID, groupID) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func handleSCIMGroupsParamRequest(w http.ResponseWriter, r *http.Request) { - logRequest(r) - // Extract the group ID and potential action from the URL path - trimmedPath := strings.TrimPrefix(r.URL.Path, "/frontegg/identity/resources/groups/v1/") - trimmedPath = strings.Split(trimmedPath, "?")[0] - parts := strings.Split(trimmedPath, "/") - groupID := "" - if len(parts) > 0 { - groupID = parts[0] - } +func (app *App) handleDefaultRolesRequests(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + configID := vars["id"] switch r.Method { - case http.MethodPost: - if strings.Contains(trimmedPath, "/roles") && groupID != "" { - // Add roles to a group - handleAddRolesToGroup(w, r, groupID) - } else if strings.Contains(trimmedPath, "/users") && groupID != "" { - // Add users to a group - handleAddUsersToGroup(w, r, groupID) - } else { - http.Error(w, "Invalid request for POST method", http.StatusBadRequest) - } - case http.MethodPatch: - if groupID != "" { - // Update a group - handleUpdateScimGroup(w, r, groupID) - } else { - http.Error(w, "Group ID is required for PATCH method", http.StatusBadRequest) - } - case http.MethodDelete: - if strings.Contains(trimmedPath, "/roles") && groupID != "" { - // Remove roles from a group - handleRemoveRolesFromGroup(w, r, groupID) - } else if strings.Contains(trimmedPath, "/users") && groupID != "" { - // Remove users from a group - handleRemoveUsersFromGroup(w, r, groupID) - } else if groupID != "" { - // Delete a group - handleDeleteScimGroup(w, r, groupID) - } else { - http.Error(w, "Group ID is required for DELETE method", http.StatusBadRequest) - } + case http.MethodPut: + app.setDefaultRoles(w, r, configID) case http.MethodGet: - if groupID != "" { - // Get a specific group by ID - handleGetScimGroupByID(w, r, groupID) - } else { - // List all groups - listSCIMGroups(w, r) - } + app.getDefaultRoles(w, configID) + case http.MethodDelete: + app.clearDefaultRoles(w, configID) default: - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -// Handle scim groups create and list -func handleSCIMGroupsRequest(w http.ResponseWriter, r *http.Request) { +func (app *App) handleSCIMGroupsRequest(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - listSCIMGroups(w, r) + app.listSCIMGroups(w, r) case http.MethodPost: - handleCreateScimGroup(w, r) + app.createScimGroup(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func handleSCIM2ConfigurationsRequest(w http.ResponseWriter, r *http.Request) { +func (app *App) handleSCIMGroupsParamRequest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + groupID := vars["id"] + switch r.Method { case http.MethodGet: - listSCIMConfigurations(w) - case http.MethodPost: - createSCIMConfiguration(w, r) + app.getScimGroupByID(w, r, groupID) + case http.MethodPatch: + app.updateScimGroup(w, r, groupID) + case http.MethodDelete: + app.deleteScimGroup(w, r, groupID) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func listSCIMGroups(w http.ResponseWriter, r *http.Request) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) handleAddRolesToGroup(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + groupID := vars["id"] - allGroups := make([]ScimGroup, 0, len(groups)) - for _, group := range groups { - allGroups = append(allGroups, group) + switch r.Method { + case http.MethodPost: + app.addRolesToGroup(w, r, groupID) + case http.MethodDelete: + app.removeRolesFromGroup(w, r, groupID) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } - - // Respond with all the groups encoded as JSON - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(SCIMGroupsResponse{Groups: allGroups}) } -func handleCreateScimGroup(w http.ResponseWriter, r *http.Request) { - var params GroupCreateParams - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } +func (app *App) handleAddUsersToGroup(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + groupID := vars["id"] - // Create a new group with the provided parameters - newGroup := ScimGroup{ - ID: uuid.New().String(), - Name: params.Name, - Description: params.Description, - Metadata: params.Metadata, - Roles: []ScimRole{}, - Users: []ScimUser{}, + switch r.Method { + case http.MethodPost: + app.addUsersToGroup(w, r, groupID) + case http.MethodDelete: + app.removeUsersFromGroup(w, r, groupID) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } - - // Store the new group in the mock data store - mutex.Lock() - groups[newGroup.ID] = newGroup - mutex.Unlock() - - // Respond with the newly created group - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(newGroup) } -func handleUpdateScimGroup(w http.ResponseWriter, r *http.Request, groupID string) { - var params GroupUpdateParams - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return +func (app *App) handleSCIM2ConfigurationsRequest(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + app.listSCIMConfigurations(w) + case http.MethodPost: + app.createSCIMConfiguration(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } +} - // Lock the mutex before accessing the shared resource - mutex.Lock() - group, exists := groups[groupID] - mutex.Unlock() +func (app *App) handleSCIMConfigurationByID(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + configID := vars["id"] + app.deleteSCIMConfiguration(w, r, configID) +} - if !exists { - http.Error(w, "Group not found", http.StatusNotFound) - return - } +// Handler functions for different routes - // Update the group's attributes if they are provided in the request - if params.Name != "" { - group.Name = params.Name - } - if params.Description != "" { - group.Description = params.Description - } - if params.Color != "" { - group.Color = params.Color +func (app *App) createAppPassword(w http.ResponseWriter, r *http.Request) { + var req struct { + Description string `json:"description"` } - if params.Metadata != "" { - group.Metadata = params.Metadata + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return } - // Update the group in the mock data store - mutex.Lock() - groups[groupID] = group - mutex.Unlock() - - // Respond with the updated group data - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(group) -} - -func handleDeleteScimGroup(w http.ResponseWriter, r *http.Request, groupID string) { - // Lock the mutex before accessing the shared resource - mutex.Lock() - defer mutex.Unlock() - - // Check if the group exists - _, exists := groups[groupID] - if !exists { - http.Error(w, "Group not found", http.StatusNotFound) - return + newAppPassword := AppPassword{ + ClientID: generateID(), + Secret: generateID(), + Description: req.Description, + Owner: "mockOwner", + CreatedAt: time.Now(), } - // Delete the group from the mock data store - delete(groups, groupID) + app.Store.Mu.Lock() + app.Store.AppPasswords[newAppPassword.ClientID] = newAppPassword + app.Store.Mu.Unlock() - // Respond with a 200 OK status to indicate successful deletion - w.WriteHeader(http.StatusOK) + sendJSONResponse(w, http.StatusCreated, newAppPassword) } -func handleGetScimGroupByID(w http.ResponseWriter, r *http.Request, groupID string) { - // Lock the mutex before accessing the shared resource - mutex.Lock() - group, exists := groups[groupID] - mutex.Unlock() - - if !exists { - // If the group does not exist, return a 404 Not Found status - http.Error(w, "Group not found", http.StatusNotFound) - return +func (app *App) listAppPasswords(w http.ResponseWriter, r *http.Request) { + app.Store.Mu.RLock() + passwords := make([]AppPassword, 0, len(app.Store.AppPasswords)) + for _, password := range app.Store.AppPasswords { + passwords = append(passwords, password) } + app.Store.Mu.RUnlock() - // If the group exists, encode it to JSON and return it with a 200 OK status - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(group) + sendJSONResponse(w, http.StatusOK, passwords) } -func handleSCIMConfigurationByID(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodDelete: - deleteSCIMConfiguration(w, r) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -func handleAddRolesToGroup(w http.ResponseWriter, r *http.Request, groupID string) { - logRequest(r) - var params AddRolesToGroupParams - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { +func (app *App) createTenantAppPassword(w http.ResponseWriter, r *http.Request) { + var req TenantApiTokenRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - // Lock the mutex before accessing the shared resource - mutex.Lock() - defer mutex.Unlock() - - group, exists := groups[groupID] - if !exists { - // If the group does not exist, return a 404 Not Found status - http.Error(w, "Group not found", http.StatusNotFound) - return + newToken := TenantApiTokenResponse{ + ClientID: generateID(), + Secret: generateID(), + Description: req.Description, + CreatedByUserId: "mockUser", + CreatedAt: time.Now(), + Metadata: req.Metadata, + RoleIDs: req.RoleIDs, } - for _, roleID := range params.RoleIds { - // Check if the role is already in the group to prevent duplicates - found := false - for _, role := range group.Roles { - if role.ID == roleID { - found = true - break - } - } + app.Store.Mu.Lock() + app.Store.TenantAppPasswords[newToken.ClientID] = newToken + app.Store.Mu.Unlock() - // If the role is not found, add it to the group with a specific name based on its ID - if !found { - roleName := "" - switch roleID { - case "1": - roleName = "Organization Admin" - case "2": - roleName = "Organization Member" - } - group.Roles = append(group.Roles, ScimRole{ID: roleID, Name: roleName}) - } - } + sendJSONResponse(w, http.StatusCreated, newToken) +} - // Update the group in the mock data store - groups[groupID] = group +func (app *App) listTenantAppPasswords(w http.ResponseWriter, r *http.Request) { + app.Store.Mu.RLock() + passwords := make([]TenantApiTokenResponse, 0, len(app.Store.TenantAppPasswords)) + for _, password := range app.Store.TenantAppPasswords { + passwords = append(passwords, password) + } + app.Store.Mu.RUnlock() - // Respond with a 201 Created status - w.WriteHeader(http.StatusCreated) + sendJSONResponse(w, http.StatusOK, passwords) } -func handleRemoveRolesFromGroup(w http.ResponseWriter, r *http.Request, groupID string) { - var params AddRolesToGroupParams - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } +func (app *App) getUser(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + userID := vars["id"] - // Lock the mutex before accessing the shared resource - mutex.Lock() - defer mutex.Unlock() + app.Store.Mu.RLock() + user, ok := app.Store.Users[userID] + app.Store.Mu.RUnlock() - group, exists := groups[groupID] - if !exists { - // If the group does not exist, return a 404 Not Found status - http.Error(w, "Group not found", http.StatusNotFound) + if !ok { + http.Error(w, "User not found", http.StatusNotFound) return } - // Remove the specified roles from the group - for _, roleID := range params.RoleIds { - for i, role := range group.Roles { - if role.ID == roleID { - // Remove the role from the slice - group.Roles = append(group.Roles[:i], group.Roles[i+1:]...) - break - } - } - } + sendJSONResponse(w, http.StatusOK, user) +} + +func (app *App) deleteUser(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + userID := vars["id"] - // Update the group in the mock data store - groups[groupID] = group + app.Store.Mu.Lock() + delete(app.Store.Users, userID) + app.Store.Mu.Unlock() - // Respond with a 200 OK status to indicate successful removal w.WriteHeader(http.StatusOK) } -func handleAddUsersToGroup(w http.ResponseWriter, r *http.Request, groupID string) { - var params struct { - UserIds []string `json:"userIds"` +func (app *App) createUser(w http.ResponseWriter, r *http.Request) { + var newUser struct { + User + RoleIDs []string `json:"roleIds"` + SkipInviteEmail bool `json:"skipInviteEmail"` } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + + if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - // Lock the mutex before accessing the shared resource - mutex.Lock() - defer mutex.Unlock() - - group, exists := groups[groupID] - if !exists { - // If the group does not exist, return a 404 Not Found status - http.Error(w, "Group not found", http.StatusNotFound) - return - } + newUser.ID = generateID() - // Add the specified users to the group, avoiding duplicates - for _, userID := range params.UserIds { - // Check if the user is already in the group - found := false - for _, user := range group.Users { - if user.ID == userID { - found = true - break - } + for _, roleID := range newUser.RoleIDs { + var roleName string + switch roleID { + case "1": + roleName = "Organization Admin" + case "2": + roleName = "Organization Member" } - // If the user is not found, add them to the group - if !found { - group.Users = append(group.Users, ScimUser{ID: userID}) + if roleName != "" { + newUser.Roles = append(newUser.Roles, FronteggRole{ID: roleID, Name: roleName}) } } - // Update the group in the mock data store - groups[groupID] = group + app.Store.Mu.Lock() + app.Store.Users[newUser.ID] = newUser.User + app.Store.Mu.Unlock() - // Respond with a 201 Created status - w.WriteHeader(http.StatusCreated) + sendJSONResponse(w, http.StatusCreated, newUser.User) } -func handleRemoveUsersFromGroup(w http.ResponseWriter, r *http.Request, groupID string) { - var params struct { - UserIds []string `json:"userIds"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } +func (app *App) getUsersV3(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + email := query.Get("_email") + limit, _ := strconv.Atoi(query.Get("_limit")) + offset, _ := strconv.Atoi(query.Get("_offset")) + ids := query.Get("ids") + sortBy := query.Get("_sortBy") + order := query.Get("_order") - // Lock the mutex before accessing the shared resource - mutex.Lock() - defer mutex.Unlock() - - group, exists := groups[groupID] - if !exists { - // If the group does not exist, return a 404 Not Found status - http.Error(w, "Group not found", http.StatusNotFound) - return + app.Store.Mu.RLock() + var filteredUsers []User + for _, user := range app.Store.Users { + if (email == "" || user.Email == email) && + (ids == "" || strings.Contains(ids, user.ID)) { + filteredUsers = append(filteredUsers, user) + } } + app.Store.Mu.RUnlock() + + if sortBy != "" { + sort.Slice(filteredUsers, func(i, j int) bool { + var less bool + switch sortBy { + case "email": + less = filteredUsers[i].Email < filteredUsers[j].Email + case "id": + less = filteredUsers[i].ID < filteredUsers[j].ID + default: + return false + } - // Remove the specified users from the group - for _, userID := range params.UserIds { - for i, user := range group.Users { - if user.ID == userID { - // Remove the user from the group - group.Users = append(group.Users[:i], group.Users[i+1:]...) - break + if order == "desc" { + return !less } - } + return less + }) } - // Update the group in the mock data store - groups[groupID] = group - - // Respond with a 200 OK status to indicate successful removal - w.WriteHeader(http.StatusOK) -} - -func listSCIMConfigurations(w http.ResponseWriter) { - mutex.Lock() - configs := make([]SCIM2Configuration, 0, len(scimConfigurations)) - for _, config := range scimConfigurations { - configs = append(configs, config) - } - for i, config := range configs { - if config.CreatedAt.IsZero() { - configs[i].CreatedAt = time.Now() + totalItems := len(filteredUsers) + if offset >= totalItems { + filteredUsers = []User{} + } else { + end := offset + limit + if end > totalItems { + end = totalItems } + filteredUsers = filteredUsers[offset:end] } - mutex.Unlock() - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(configs) -} - -func createSCIMConfiguration(w http.ResponseWriter, r *http.Request) { - var newConfig SCIM2Configuration - if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - newConfig.ID = generateMockUUID() - newConfig.Token = generateMockUUID() - newConfig.TenantID = "mockTenantID" - newConfig.CreatedAt = time.Now() - - // Log the configuration - fmt.Printf("Received SCIM 2.0 configuration: %+v\n", newConfig) - - mutex.Lock() - scimConfigurations[newConfig.ID] = newConfig - mutex.Unlock() - - // log response - responseBytes, _ := json.Marshal(newConfig) - fmt.Printf("Response body: %s\n", string(responseBytes)) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(newConfig) -} - -func deleteSCIMConfiguration(w http.ResponseWriter, r *http.Request) { - id := r.URL.Path[len("/frontegg/directory/resources/v1/configurations/scim2/"):] - - mutex.Lock() - if _, exists := scimConfigurations[id]; !exists { - mutex.Unlock() - http.Error(w, "Configuration not found", http.StatusNotFound) - return + response := struct { + Items []User `json:"items"` + Metadata struct { + TotalItems int `json:"totalItems"` + } `json:"_metadata"` + }{ + Items: filteredUsers, + Metadata: struct { + TotalItems int `json:"totalItems"` + }{ + TotalItems: totalItems, + }, } - delete(scimConfigurations, id) - mutex.Unlock() - - w.WriteHeader(http.StatusNoContent) + sendJSONResponse(w, http.StatusOK, response) } -func createSSOConfig(w http.ResponseWriter, r *http.Request) { +func (app *App) createSSOConfig(w http.ResponseWriter, r *http.Request) { var newConfig SSOConfig if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - // Adjusting fields to match production data - newConfig.Id = generateConfigID() + newConfig.Id = generateID() newConfig.PublicCertificate = base64.StdEncoding.EncodeToString([]byte(newConfig.PublicCertificate)) newConfig.CreatedAt = time.Now() newConfig.UpdatedAt = newConfig.CreatedAt - newConfig.GeneratedVerification = generateMockUUID() + newConfig.GeneratedVerification = generateID() newConfig.ConfigMetadata = nil newConfig.OverrideActiveTenant = true newConfig.SubAccountAccessLimit = 0 @@ -1099,150 +718,128 @@ func createSSOConfig(w http.ResponseWriter, r *http.Request) { newConfig.RoleIds = newConfig.DefaultRoles.RoleIds - for i, group := range newConfig.Groups { - group.Enabled = true - newConfig.Groups[i] = group + for i := range newConfig.Groups { + newConfig.Groups[i].Enabled = true } - mutex.Lock() - ssoConfigs[newConfig.Id] = newConfig - mutex.Unlock() + app.Store.Mu.Lock() + app.Store.SSOConfigs[newConfig.Id] = newConfig + app.Store.Mu.Unlock() - sendResponse(w, http.StatusCreated, newConfig) + sendJSONResponse(w, http.StatusCreated, newConfig) } -func listSSOConfigs(w http.ResponseWriter, r *http.Request) { - mutex.Lock() - configs := make([]SSOConfig, 0, len(ssoConfigs)) - for _, config := range ssoConfigs { - // Ensure that Domains and Groups are not nil +func (app *App) listSSOConfigs(w http.ResponseWriter, r *http.Request) { + app.Store.Mu.RLock() + configs := make([]SSOConfig, 0, len(app.Store.SSOConfigs)) + for _, config := range app.Store.SSOConfigs { if config.Domains == nil { config.Domains = []Domain{} } if config.Groups == nil { config.Groups = []GroupMapping{} } - // Initialize RoleIds if it's nil if config.RoleIds == nil { config.RoleIds = []string{} } configs = append(configs, config) } - mutex.Unlock() - - responseBytes, err := json.Marshal(configs) - if err != nil { - fmt.Printf("Error marshaling response: %v\n", err) - sendResponse(w, http.StatusInternalServerError, "Internal server error") - return - } + app.Store.Mu.RUnlock() - fmt.Printf("Response body: %s\n", string(responseBytes)) - sendResponse(w, http.StatusOK, configs) + sendJSONResponse(w, http.StatusOK, configs) } -func getSSOConfig(w http.ResponseWriter, r *http.Request, configID string) { - var updatedConfig SSOConfig - if err := json.NewDecoder(r.Body).Decode(&updatedConfig); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - updatedConfig.UpdatedAt = time.Now() - mutex.Lock() - config, ok := ssoConfigs[configID] - mutex.Unlock() +func (app *App) getSSOConfig(w http.ResponseWriter, r *http.Request, configID string) { + app.Store.Mu.RLock() + config, ok := app.Store.SSOConfigs[configID] + app.Store.Mu.RUnlock() if !ok { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } - sendResponse(w, http.StatusOK, config) + sendJSONResponse(w, http.StatusOK, config) } -func updateSSOConfig(w http.ResponseWriter, r *http.Request, configID string) { +func (app *App) updateSSOConfig(w http.ResponseWriter, r *http.Request, configID string) { var updatedConfig SSOConfig if err := json.NewDecoder(r.Body).Decode(&updatedConfig); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - mutex.Lock() - if _, ok := ssoConfigs[configID]; !ok { - mutex.Unlock() + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() + + if _, ok := app.Store.SSOConfigs[configID]; !ok { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } updatedConfig.Id = configID updatedConfig.PublicCertificate = base64.StdEncoding.EncodeToString([]byte(updatedConfig.PublicCertificate)) - ssoConfigs[configID] = updatedConfig - mutex.Unlock() + updatedConfig.UpdatedAt = time.Now() + app.Store.SSOConfigs[configID] = updatedConfig - sendResponse(w, http.StatusOK, updatedConfig) + sendJSONResponse(w, http.StatusOK, updatedConfig) } -func deleteSSOConfig(w http.ResponseWriter, r *http.Request, configID string) { - mutex.Lock() - if _, ok := ssoConfigs[configID]; !ok { - mutex.Unlock() +func (app *App) deleteSSOConfig(w http.ResponseWriter, r *http.Request, configID string) { + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() + + if _, ok := app.Store.SSOConfigs[configID]; !ok { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } - delete(ssoConfigs, configID) - mutex.Unlock() - + delete(app.Store.SSOConfigs, configID) w.WriteHeader(http.StatusOK) } -func createDomain(w http.ResponseWriter, r *http.Request, ssoConfigID string) { +func (app *App) createDomain(w http.ResponseWriter, r *http.Request, ssoConfigID string) { var newDomain Domain if err := json.NewDecoder(r.Body).Decode(&newDomain); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - mutex.Lock() - defer mutex.Unlock() + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } - newDomain.ID = generateDomainID() + newDomain.ID = generateID() newDomain.SsoConfigId = ssoConfigID config.Domains = append(config.Domains, newDomain) - ssoConfigs[ssoConfigID] = config - - sendResponse(w, http.StatusCreated, newDomain) -} + app.Store.SSOConfigs[ssoConfigID] = config -func generateDomainID() string { - return fmt.Sprintf("domain-%d", time.Now().UnixNano()) + sendJSONResponse(w, http.StatusCreated, newDomain) } -func listDomains(w http.ResponseWriter, ssoConfigID string) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) listDomains(w http.ResponseWriter, ssoConfigID string) { + app.Store.Mu.RLock() + config, exists := app.Store.SSOConfigs[ssoConfigID] + app.Store.Mu.RUnlock() - config, exists := ssoConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } - sendResponse(w, http.StatusOK, config.Domains) + sendJSONResponse(w, http.StatusOK, config.Domains) } -func getDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) getDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { + app.Store.Mu.RLock() + config, exists := app.Store.SSOConfigs[ssoConfigID] + app.Store.Mu.RUnlock() - config, exists := ssoConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return @@ -1250,7 +847,7 @@ func getDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { for _, domain := range config.Domains { if domain.ID == domainID { - sendResponse(w, http.StatusOK, domain) + sendJSONResponse(w, http.StatusOK, domain) return } } @@ -1258,17 +855,17 @@ func getDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { http.Error(w, "Domain not found", http.StatusNotFound) } -func updateDomain(w http.ResponseWriter, r *http.Request, ssoConfigID string, domainID string) { +func (app *App) updateDomain(w http.ResponseWriter, r *http.Request, ssoConfigID string, domainID string) { var updatedDomain Domain if err := json.NewDecoder(r.Body).Decode(&updatedDomain); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - mutex.Lock() - defer mutex.Unlock() + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return @@ -1278,8 +875,8 @@ func updateDomain(w http.ResponseWriter, r *http.Request, ssoConfigID string, do if domain.ID == domainID { updatedDomain.ID = domainID config.Domains[i] = updatedDomain - ssoConfigs[ssoConfigID] = config - sendResponse(w, http.StatusOK, updatedDomain) + app.Store.SSOConfigs[ssoConfigID] = config + sendJSONResponse(w, http.StatusOK, updatedDomain) return } } @@ -1287,11 +884,11 @@ func updateDomain(w http.ResponseWriter, r *http.Request, ssoConfigID string, do http.Error(w, "Domain not found", http.StatusNotFound) } -func deleteDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) deleteDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return @@ -1300,7 +897,7 @@ func deleteDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { for i, domain := range config.Domains { if domain.ID == domainID { config.Domains = append(config.Domains[:i], config.Domains[i+1:]...) - ssoConfigs[ssoConfigID] = config + app.Store.SSOConfigs[ssoConfigID] = config w.WriteHeader(http.StatusOK) return } @@ -1309,52 +906,48 @@ func deleteDomain(w http.ResponseWriter, ssoConfigID string, domainID string) { http.Error(w, "Domain not found", http.StatusNotFound) } -func createGroupMapping(w http.ResponseWriter, r *http.Request, ssoConfigID string) { +func (app *App) createGroupMapping(w http.ResponseWriter, r *http.Request, ssoConfigID string) { var newGroupMapping GroupMapping if err := json.NewDecoder(r.Body).Decode(&newGroupMapping); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - mutex.Lock() - defer mutex.Unlock() + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } - newGroupMapping.ID = generateGroupMappingID() + newGroupMapping.ID = generateID() newGroupMapping.SsoConfigId = ssoConfigID config.Groups = append(config.Groups, newGroupMapping) - ssoConfigs[ssoConfigID] = config - - sendResponse(w, http.StatusCreated, newGroupMapping) -} + app.Store.SSOConfigs[ssoConfigID] = config -func generateGroupMappingID() string { - return fmt.Sprintf("groupmap-%d", time.Now().UnixNano()) + sendJSONResponse(w, http.StatusCreated, newGroupMapping) } -func listGroupMappings(w http.ResponseWriter, ssoConfigID string) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) listGroupMappings(w http.ResponseWriter, ssoConfigID string) { + app.Store.Mu.RLock() + config, exists := app.Store.SSOConfigs[ssoConfigID] + app.Store.Mu.RUnlock() - config, exists := ssoConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } - sendResponse(w, http.StatusOK, config.Groups) + sendJSONResponse(w, http.StatusOK, config.Groups) } -func getGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingID string) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) getGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingID string) { + app.Store.Mu.RLock() + config, exists := app.Store.SSOConfigs[ssoConfigID] + app.Store.Mu.RUnlock() - config, exists := ssoConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return @@ -1362,7 +955,7 @@ func getGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingID s for _, mapping := range config.Groups { if mapping.ID == groupMappingID { - sendResponse(w, http.StatusOK, mapping) + sendJSONResponse(w, http.StatusOK, mapping) return } } @@ -1370,17 +963,17 @@ func getGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingID s http.Error(w, "Group mapping not found", http.StatusNotFound) } -func updateGroupMapping(w http.ResponseWriter, r *http.Request, ssoConfigID string, groupMappingID string) { +func (app *App) updateGroupMapping(w http.ResponseWriter, r *http.Request, ssoConfigID string, groupMappingID string) { var updatedMapping GroupMapping if err := json.NewDecoder(r.Body).Decode(&updatedMapping); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - mutex.Lock() - defer mutex.Unlock() + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return @@ -1390,8 +983,8 @@ func updateGroupMapping(w http.ResponseWriter, r *http.Request, ssoConfigID stri if mapping.ID == groupMappingID { updatedMapping.ID = groupMappingID config.Groups[i] = updatedMapping - ssoConfigs[ssoConfigID] = config - sendResponse(w, http.StatusOK, updatedMapping) + app.Store.SSOConfigs[ssoConfigID] = config + sendJSONResponse(w, http.StatusOK, updatedMapping) return } } @@ -1399,11 +992,11 @@ func updateGroupMapping(w http.ResponseWriter, r *http.Request, ssoConfigID stri http.Error(w, "Group mapping not found", http.StatusNotFound) } -func deleteGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingID string) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) deleteGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingID string) { + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return @@ -1412,7 +1005,7 @@ func deleteGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingI for i, mapping := range config.Groups { if mapping.ID == groupMappingID { config.Groups = append(config.Groups[:i], config.Groups[i+1:]...) - ssoConfigs[ssoConfigID] = config + app.Store.SSOConfigs[ssoConfigID] = config w.WriteHeader(http.StatusOK) return } @@ -1421,30 +1014,17 @@ func deleteGroupMapping(w http.ResponseWriter, ssoConfigID string, groupMappingI http.Error(w, "Group mapping not found", http.StatusNotFound) } -func handleDefaultRolesRequests(w http.ResponseWriter, r *http.Request, ssoConfigID string) { - switch r.Method { - case http.MethodPut: - setDefaultRoles(w, r, ssoConfigID) - case http.MethodGet: - getDefaultRoles(w, ssoConfigID) - case http.MethodDelete: - clearDefaultRoles(w, ssoConfigID) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -func setDefaultRoles(w http.ResponseWriter, r *http.Request, ssoConfigID string) { +func (app *App) setDefaultRoles(w http.ResponseWriter, r *http.Request, ssoConfigID string) { var roles DefaultRoles if err := json.NewDecoder(r.Body).Decode(&roles); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - mutex.Lock() - defer mutex.Unlock() + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return @@ -1453,40 +1033,345 @@ func setDefaultRoles(w http.ResponseWriter, r *http.Request, ssoConfigID string) config.DefaultRoles = roles config.RoleIds = roles.RoleIds - ssoConfigs[ssoConfigID] = config + app.Store.SSOConfigs[ssoConfigID] = config - sendResponse(w, http.StatusCreated, roles) + sendJSONResponse(w, http.StatusCreated, roles) } -func getDefaultRoles(w http.ResponseWriter, ssoConfigID string) { - mutex.Lock() - config, exists := ssoConfigs[ssoConfigID] - mutex.Unlock() +func (app *App) getDefaultRoles(w http.ResponseWriter, ssoConfigID string) { + app.Store.Mu.RLock() + config, exists := app.Store.SSOConfigs[ssoConfigID] + app.Store.Mu.RUnlock() if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } - sendResponse(w, http.StatusOK, config.DefaultRoles) + sendJSONResponse(w, http.StatusOK, config.DefaultRoles) } -func clearDefaultRoles(w http.ResponseWriter, ssoConfigID string) { - mutex.Lock() - defer mutex.Unlock() +func (app *App) clearDefaultRoles(w http.ResponseWriter, ssoConfigID string) { + app.Store.Mu.Lock() + defer app.Store.Mu.Unlock() - config, exists := ssoConfigs[ssoConfigID] + config, exists := app.Store.SSOConfigs[ssoConfigID] if !exists { http.Error(w, "SSO configuration not found", http.StatusNotFound) return } config.DefaultRoles = DefaultRoles{RoleIds: []string{}} - ssoConfigs[ssoConfigID] = config + app.Store.SSOConfigs[ssoConfigID] = config + + w.WriteHeader(http.StatusOK) +} + +func (app *App) listSCIMGroups(w http.ResponseWriter, r *http.Request) { + app.Store.Mu.RLock() + allGroups := make([]ScimGroup, 0, len(app.Store.Groups)) + for _, group := range app.Store.Groups { + allGroups = append(allGroups, group) + } + app.Store.Mu.RUnlock() + + sendJSONResponse(w, http.StatusOK, SCIMGroupsResponse{Groups: allGroups}) +} + +func (app *App) createScimGroup(w http.ResponseWriter, r *http.Request) { + var params GroupCreateParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + newGroup := ScimGroup{ + ID: generateID(), + Name: params.Name, + Description: params.Description, + Metadata: params.Metadata, + Roles: []ScimRole{}, + Users: []ScimUser{}, + } + + app.Store.Mu.Lock() + app.Store.Groups[newGroup.ID] = newGroup + app.Store.Mu.Unlock() + + sendJSONResponse(w, http.StatusCreated, newGroup) +} + +func (app *App) getScimGroupByID(w http.ResponseWriter, r *http.Request, groupID string) { + app.Store.Mu.RLock() + group, exists := app.Store.Groups[groupID] + app.Store.Mu.RUnlock() + + if !exists { + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + sendJSONResponse(w, http.StatusOK, group) +} + +func (app *App) updateScimGroup(w http.ResponseWriter, r *http.Request, groupID string) { + var params GroupUpdateParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + app.Store.Mu.Lock() + group, exists := app.Store.Groups[groupID] + if !exists { + app.Store.Mu.Unlock() + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + if params.Name != "" { + group.Name = params.Name + } + if params.Description != "" { + group.Description = params.Description + } + if params.Color != "" { + group.Color = params.Color + } + if params.Metadata != "" { + group.Metadata = params.Metadata + } + + app.Store.Groups[groupID] = group + app.Store.Mu.Unlock() + + sendJSONResponse(w, http.StatusOK, group) +} + +func (app *App) deleteScimGroup(w http.ResponseWriter, r *http.Request, groupID string) { + app.Store.Mu.Lock() + _, exists := app.Store.Groups[groupID] + if !exists { + app.Store.Mu.Unlock() + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + delete(app.Store.Groups, groupID) + app.Store.Mu.Unlock() + + w.WriteHeader(http.StatusOK) +} + +func (app *App) addRolesToGroup(w http.ResponseWriter, r *http.Request, groupID string) { + var params AddRolesToGroupParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + app.Store.Mu.Lock() + group, exists := app.Store.Groups[groupID] + if !exists { + app.Store.Mu.Unlock() + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + for _, roleID := range params.RoleIds { + found := false + for _, role := range group.Roles { + if role.ID == roleID { + found = true + break + } + } + + if !found { + roleName := "" + switch roleID { + case "1": + roleName = "Organization Admin" + case "2": + roleName = "Organization Member" + } + group.Roles = append(group.Roles, ScimRole{ID: roleID, Name: roleName}) + } + } + + app.Store.Groups[groupID] = group + app.Store.Mu.Unlock() + + w.WriteHeader(http.StatusCreated) +} + +func (app *App) addUsersToGroup(w http.ResponseWriter, r *http.Request, groupID string) { + var params struct { + UserIds []string `json:"userIds"` + } + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + app.Store.Mu.Lock() + group, exists := app.Store.Groups[groupID] + if !exists { + app.Store.Mu.Unlock() + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + for _, userID := range params.UserIds { + found := false + for _, user := range group.Users { + if user.ID == userID { + found = true + break + } + } + + if !found { + group.Users = append(group.Users, ScimUser{ID: userID}) + } + } + + app.Store.Groups[groupID] = group + app.Store.Mu.Unlock() + + w.WriteHeader(http.StatusCreated) +} + +func (app *App) removeRolesFromGroup(w http.ResponseWriter, r *http.Request, groupID string) { + var params AddRolesToGroupParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + app.Store.Mu.Lock() + group, exists := app.Store.Groups[groupID] + if !exists { + app.Store.Mu.Unlock() + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + for _, roleID := range params.RoleIds { + for i, role := range group.Roles { + if role.ID == roleID { + group.Roles = append(group.Roles[:i], group.Roles[i+1:]...) + break + } + } + } + + app.Store.Groups[groupID] = group + app.Store.Mu.Unlock() + + w.WriteHeader(http.StatusOK) +} + +func (app *App) removeUsersFromGroup(w http.ResponseWriter, r *http.Request, groupID string) { + var params struct { + UserIds []string `json:"userIds"` + } + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + app.Store.Mu.Lock() + group, exists := app.Store.Groups[groupID] + if !exists { + app.Store.Mu.Unlock() + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + for _, userID := range params.UserIds { + for i, user := range group.Users { + if user.ID == userID { + group.Users = append(group.Users[:i], group.Users[i+1:]...) + break + } + } + } + + app.Store.Groups[groupID] = group + app.Store.Mu.Unlock() w.WriteHeader(http.StatusOK) } -func generateMockUUID() string { +func (app *App) listSCIMConfigurations(w http.ResponseWriter) { + app.Store.Mu.RLock() + configs := make([]SCIM2Configuration, 0, len(app.Store.ScimConfigurations)) + for _, config := range app.Store.ScimConfigurations { + configs = append(configs, config) + } + app.Store.Mu.RUnlock() + + sendJSONResponse(w, http.StatusOK, configs) +} + +func (app *App) createSCIMConfiguration(w http.ResponseWriter, r *http.Request) { + var newConfig SCIM2Configuration + if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + newConfig.ID = generateID() + newConfig.Token = generateID() + newConfig.TenantID = "mockTenantID" + newConfig.CreatedAt = time.Now() + + app.Store.Mu.Lock() + app.Store.ScimConfigurations[newConfig.ID] = newConfig + app.Store.Mu.Unlock() + + sendJSONResponse(w, http.StatusCreated, newConfig) +} + +func (app *App) deleteSCIMConfiguration(w http.ResponseWriter, r *http.Request, configID string) { + app.Store.Mu.Lock() + _, exists := app.Store.ScimConfigurations[configID] + if !exists { + app.Store.Mu.Unlock() + http.Error(w, "Configuration not found", http.StatusNotFound) + return + } + + delete(app.Store.ScimConfigurations, configID) + app.Store.Mu.Unlock() + + w.WriteHeader(http.StatusNoContent) +} + +// Helper functions + +func createMockJWTToken() string { + header := base64UrlEncode([]byte(`{"alg":"HS256","typ":"JWT"}`)) + payload := base64UrlEncode([]byte(`{"email":"mz_system","exp":1700000000}`)) + signature := base64UrlEncode([]byte(`signature`)) + return fmt.Sprintf("%s.%s.%s", header, payload, signature) +} + +func base64UrlEncode(input []byte) string { + encoded := base64.StdEncoding.EncodeToString(input) + encoded = strings.ReplaceAll(encoded, "+", "-") + encoded = strings.ReplaceAll(encoded, "/", "_") + encoded = strings.TrimRight(encoded, "=") + return encoded +} + +func generateID() string { return uuid.New().String() } + +func sendJSONResponse(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(data) +} diff --git a/provider/cmd/pulumi-resource-materialize/schema.json b/provider/cmd/pulumi-resource-materialize/schema.json index 3f208c0..386a4d9 100644 --- a/provider/cmd/pulumi-resource-materialize/schema.json +++ b/provider/cmd/pulumi-resource-materialize/schema.json @@ -10303,7 +10303,7 @@ } }, "materialize:index/user:User": { - "description": "{{% examples %}}\n## Example Usage\n{{% example %}}\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as materialize from \"@pulumi/materialize\";\n\nconst exampleUser = new materialize.User(\"exampleUser\", {\n email: \"example-user@example.com\",\n roles: [\n \"Member\",\n \"Admin\",\n ],\n});\n```\n```python\nimport pulumi\nimport pulumi_materialize as materialize\n\nexample_user = materialize.User(\"exampleUser\",\n email=\"example-user@example.com\",\n roles=[\n \"Member\",\n \"Admin\",\n ])\n```\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing Materialize = Pulumi.Materialize;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var exampleUser = new Materialize.User(\"exampleUser\", new()\n {\n Email = \"example-user@example.com\",\n Roles = new[]\n {\n \"Member\",\n \"Admin\",\n },\n });\n\n});\n```\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-materialize/sdk/go/materialize\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := materialize.NewUser(ctx, \"exampleUser\", \u0026materialize.UserArgs{\n\t\t\tEmail: pulumi.String(\"example-user@example.com\"),\n\t\t\tRoles: pulumi.StringArray{\n\t\t\t\tpulumi.String(\"Member\"),\n\t\t\t\tpulumi.String(\"Admin\"),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.materialize.User;\nimport com.pulumi.materialize.UserArgs;\nimport java.util.List;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n var exampleUser = new User(\"exampleUser\", UserArgs.builder() \n .email(\"example-user@example.com\")\n .roles( \n \"Member\",\n \"Admin\")\n .build());\n\n }\n}\n```\n```yaml\nresources:\n exampleUser:\n type: materialize:User\n properties:\n email: example-user@example.com\n roles:\n - Member\n - Admin\n```\n{{% /example %}}\n{{% /examples %}}\n\n## Import\n\nUsers can be imported using the user id\n\n```sh\n $ pulumi import materialize:index/user:User example_user \u003cuser_id\u003e\n```\n\n ", + "description": "The user resource allows you to invite and delete users in your Materialize organization.\n\n{{% examples %}}\n## Example Usage\n{{% example %}}\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as materialize from \"@pulumi/materialize\";\n\nconst exampleUser = new materialize.User(\"exampleUser\", {\n email: \"example-user@example.com\",\n roles: [\n \"Member\",\n \"Admin\",\n ],\n});\n```\n```python\nimport pulumi\nimport pulumi_materialize as materialize\n\nexample_user = materialize.User(\"exampleUser\",\n email=\"example-user@example.com\",\n roles=[\n \"Member\",\n \"Admin\",\n ])\n```\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing Materialize = Pulumi.Materialize;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var exampleUser = new Materialize.User(\"exampleUser\", new()\n {\n Email = \"example-user@example.com\",\n Roles = new[]\n {\n \"Member\",\n \"Admin\",\n },\n });\n\n});\n```\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-materialize/sdk/go/materialize\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := materialize.NewUser(ctx, \"exampleUser\", \u0026materialize.UserArgs{\n\t\t\tEmail: pulumi.String(\"example-user@example.com\"),\n\t\t\tRoles: pulumi.StringArray{\n\t\t\t\tpulumi.String(\"Member\"),\n\t\t\t\tpulumi.String(\"Admin\"),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.materialize.User;\nimport com.pulumi.materialize.UserArgs;\nimport java.util.List;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n var exampleUser = new User(\"exampleUser\", UserArgs.builder() \n .email(\"example-user@example.com\")\n .roles( \n \"Member\",\n \"Admin\")\n .build());\n\n }\n}\n```\n```yaml\nresources:\n exampleUser:\n type: materialize:User\n properties:\n email: example-user@example.com\n roles:\n - Member\n - Admin\n```\n{{% /example %}}\n{{% /examples %}}\n\n## Import\n\nUsers can be imported using the user id. The user id can be retrieved by using the `materialize_user` data source.\n\n```sh\n $ pulumi import materialize:index/user:User example_user \u003cuser_id\u003e\n```\n\n ", "properties": { "authProvider": { "type": "string", @@ -11402,6 +11402,45 @@ ] } }, + "materialize:index/getUsers:GetUsers": { + "description": "The user data source allows you to retrieve information about a user in your Materialize organization.\n\n{{% examples %}}\n## Example Usage\n{{% example %}}\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as materialize from \"@pulumi/materialize\";\n\nconst exampleUser = materialize.GetUsers({\n email: \"example@example.com\",\n});\n```\n```python\nimport pulumi\nimport pulumi_materialize as materialize\n\nexample_user = materialize.get_users(email=\"example@example.com\")\n```\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing Materialize = Pulumi.Materialize;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var exampleUser = Materialize.GetUsers.Invoke(new()\n {\n Email = \"example@example.com\",\n });\n\n});\n```\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-materialize/sdk/go/materialize\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := materialize.GetUsers(ctx, \u0026materialize.GetUsersArgs{\n\t\t\tEmail: \"example@example.com\",\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.materialize.MaterializeFunctions;\nimport com.pulumi.materialize.inputs.GetUsersArgs;\nimport java.util.List;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var exampleUser = MaterializeFunctions.GetUsers(GetUsersArgs.builder()\n .email(\"example@example.com\")\n .build());\n\n }\n}\n```\n```yaml\nvariables:\n exampleUser:\n fn::invoke:\n Function: materialize:GetUsers\n Arguments:\n email: example@example.com\n```\n{{% /example %}}\n{{% /examples %}}", + "inputs": { + "description": "A collection of arguments for invoking GetUsers.\n", + "properties": { + "email": { + "type": "string", + "description": "The email address of the user to retrieve.\n" + } + }, + "type": "object", + "required": [ + "email" + ] + }, + "outputs": { + "description": "A collection of values returned by GetUsers.\n", + "properties": { + "email": { + "type": "string", + "description": "The email address of the user to retrieve.\n" + }, + "id": { + "type": "string", + "description": "The unique (UUID) identifier of the user.\n" + }, + "verified": { + "type": "boolean", + "description": "Whether the user's email address has been verified.\n" + } + }, + "type": "object", + "required": [ + "email", + "id", + "verified" + ] + } + }, "materialize:index/getViews:GetViews": { "description": "{{% examples %}}\n## Example Usage\n{{% example %}}\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as materialize from \"@pulumi/materialize\";\n\nconst all = materialize.GetViews({});\nconst materialize = materialize.GetViews({\n databaseName: \"materialize\",\n});\nconst materializeSchema = materialize.GetViews({\n databaseName: \"materialize\",\n schemaName: \"schema\",\n});\n```\n```python\nimport pulumi\nimport pulumi_materialize as materialize\n\nall = materialize.get_views()\nmaterialize = materialize.get_views(database_name=\"materialize\")\nmaterialize_schema = materialize.get_views(database_name=\"materialize\",\n schema_name=\"schema\")\n```\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing Materialize = Pulumi.Materialize;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var all = Materialize.GetViews.Invoke();\n\n var materialize = Materialize.GetViews.Invoke(new()\n {\n DatabaseName = \"materialize\",\n });\n\n var materializeSchema = Materialize.GetViews.Invoke(new()\n {\n DatabaseName = \"materialize\",\n SchemaName = \"schema\",\n });\n\n});\n```\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-materialize/sdk/go/materialize\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := materialize.GetViews(ctx, nil, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = materialize.GetViews(ctx, \u0026materialize.GetViewsArgs{\n\t\t\tDatabaseName: pulumi.StringRef(\"materialize\"),\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = materialize.GetViews(ctx, \u0026materialize.GetViewsArgs{\n\t\t\tDatabaseName: pulumi.StringRef(\"materialize\"),\n\t\t\tSchemaName: pulumi.StringRef(\"schema\"),\n\t\t}, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n```\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.materialize.MaterializeFunctions;\nimport com.pulumi.materialize.inputs.GetViewsArgs;\nimport java.util.List;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n final var all = MaterializeFunctions.GetViews();\n\n final var materialize = MaterializeFunctions.GetViews(GetViewsArgs.builder()\n .databaseName(\"materialize\")\n .build());\n\n final var materializeSchema = MaterializeFunctions.GetViews(GetViewsArgs.builder()\n .databaseName(\"materialize\")\n .schemaName(\"schema\")\n .build());\n\n }\n}\n```\n```yaml\nvariables:\n all:\n fn::invoke:\n Function: materialize:GetViews\n Arguments: {}\n materialize:\n fn::invoke:\n Function: materialize:GetViews\n Arguments:\n databaseName: materialize\n materializeSchema:\n fn::invoke:\n Function: materialize:GetViews\n Arguments:\n databaseName: materialize\n schemaName: schema\n```\n{{% /example %}}\n{{% /examples %}}", "inputs": { diff --git a/provider/go.mod b/provider/go.mod index aef2bb9..f64ad77 100644 --- a/provider/go.mod +++ b/provider/go.mod @@ -5,7 +5,7 @@ go 1.20 replace github.com/hashicorp/terraform-plugin-sdk/v2 => github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20240520223432-0c0bf0d65f10 require ( - github.com/MaterializeInc/terraform-provider-materialize v0.8.2 + github.com/MaterializeInc/terraform-provider-materialize v0.8.3 github.com/pulumi/pulumi-terraform-bridge/v3 v3.59.0 github.com/pulumi/pulumi/sdk/v3 v3.81.0 ) diff --git a/provider/go.sum b/provider/go.sum index f32ccfa..aa3e3b2 100644 --- a/provider/go.sum +++ b/provider/go.sum @@ -1257,8 +1257,8 @@ github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/MaterializeInc/terraform-provider-materialize v0.8.2 h1:nv+ILjLZcOl5OKwp0RGDNj5qGb+PcgShmig24EmAkBA= -github.com/MaterializeInc/terraform-provider-materialize v0.8.2/go.mod h1:LUEZUKVCP+Zy0V0uSD7SIRLDdW92HvExCu2BHo1xnQk= +github.com/MaterializeInc/terraform-provider-materialize v0.8.3 h1:GwozIdqFBDjA6BTO9dkAjyWcc1SNWBcvXc4NFln7LmQ= +github.com/MaterializeInc/terraform-provider-materialize v0.8.3/go.mod h1:LUEZUKVCP+Zy0V0uSD7SIRLDdW92HvExCu2BHo1xnQk= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= diff --git a/provider/resources.go b/provider/resources.go index a38e36a..7bec93b 100644 --- a/provider/resources.go +++ b/provider/resources.go @@ -168,6 +168,7 @@ func Provider() tfbridge.ProviderInfo { "materialize_table": {Tok: tfbridge.MakeDataSource(mainPkg, mainMod, "GetTables")}, "materialize_type": {Tok: tfbridge.MakeDataSource(mainPkg, mainMod, "GetTypes")}, "materialize_view": {Tok: tfbridge.MakeDataSource(mainPkg, mainMod, "GetViews")}, + "materialize_user": {Tok: tfbridge.MakeDataSource(mainPkg, mainMod, "GetUsers")}, }, JavaScript: &tfbridge.JavaScriptInfo{ // List any npm dependencies and their versions