diff --git a/src/config/config.go b/src/config/config.go index d3749d6..8c23811 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -71,6 +71,23 @@ type WebhooksConfig struct { KafkaConfig KafkaConfig } +// Organization organization data type +type Organization struct { + Name string `valid:"required"` + Location string `valid:"required"` + Description string + EulaURL string +} + +type OrgType struct { + Name string `valid:"required"` +} + +type User struct { + Username string `valid:"required"` + Password string `valid:"required"` +} + // Configuration data type type Configuration struct { DataBase struct { @@ -80,6 +97,9 @@ type Configuration struct { Password string } ApplicationMode string + Organization Organization + Type OrgType + User User Iam Iam Twilio Twilio Firebase Firebase diff --git a/src/handlerv1/organization_handler.go b/src/handlerv1/organization_handler.go index 22911e5..d934d9b 100644 --- a/src/handlerv1/organization_handler.go +++ b/src/handlerv1/organization_handler.go @@ -134,16 +134,6 @@ func GetOrganizationByID(w http.ResponseWriter, r *http.Request) { w.Write(response) } -// GetOrganizationId Gets an organization Id. -func GetOrganizationId() (string, error) { - org, err := org.GetOrganization() - if err != nil { - log.Printf("Failed to get organization") - return "", err - } - return org.ID.Hex(), err -} - type orgUpdateReq struct { Name string Location string diff --git a/src/main/main.go b/src/main/main.go index badf86c..0584ff4 100644 --- a/src/main/main.go +++ b/src/main/main.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "log" "net/http" @@ -25,13 +24,6 @@ import ( "github.com/spf13/cobra" ) -func handleCommandLineArgs() (configFileName string) { - fconfigFileName := flag.String("config", "config-development.json", "configuration file") - flag.Parse() - - return *fconfigFileName -} - func main() { var rootCmd = &cobra.Command{Use: "bb-consent-api"} @@ -45,7 +37,7 @@ func main() { Run: func(cmd *cobra.Command, args []string) { configFile := "/opt/bb-consent/api/config/" + configFileName - config, err := config.Load(configFile) + loadedConfig, err := config.Load(configFile) if err != nil { log.Printf("Failed to load config file %s \n", configFile) panic(err) @@ -53,28 +45,28 @@ func main() { log.Printf("config file: %s loaded\n", configFile) - err = database.Init(config) + err = database.Init(loadedConfig) if err != nil { panic(err) } log.Println("Data base session opened") - webhooks.Init(config) + webhooks.Init(loadedConfig) log.Println("Webhooks configuration initialized") - err = kafkaUtils.Init(config) + err = kafkaUtils.Init(loadedConfig) if err != nil { panic(err) } log.Println("Kafka producer client initialised") - handler.IamInit(config) + handler.IamInit(loadedConfig) log.Println("Iam initialized") - email.Init(config) + email.Init(loadedConfig) log.Println("Email initialized") - token.Init(config) + token.Init(loadedConfig) log.Println("Token initialized") err = notifications.Init() @@ -82,10 +74,10 @@ func main() { panic(err) } - firebaseUtils.Init(config) + firebaseUtils.Init(loadedConfig) log.Println("Firebase initialized") - middleware.ApplicationModeInit(config) + middleware.ApplicationModeInit(loadedConfig) log.Println("Application mode initialized") // setup casbin auth rules @@ -94,6 +86,11 @@ func main() { panic(err) } + // If the application starts in single tenant mode then create/update organisation, type, admin logic + if loadedConfig.ApplicationMode == config.SingleTenant { + SingleTenantConfiguration(loadedConfig) + } + router := mux.NewRouter() httppathsv1.SetRoutes(router, authEnforcer) httppathsv2.SetRoutes(router, authEnforcer) @@ -114,20 +111,20 @@ func main() { configFile := "/opt/bb-consent/api/config/" + configFileName - config, err := config.Load(configFile) + loadedConfig, err := config.Load(configFile) if err != nil { log.Printf("Failed to load config file %s \n", configFile) panic(err) } log.Printf("config file: %s loaded\n", configFile) - err = database.Init(config) + err = database.Init(loadedConfig) if err != nil { panic(err) } log.Println("Data base session opened") - webhookdispatcher.WebhookDispatcherInit(config) + webhookdispatcher.WebhookDispatcherInit(loadedConfig) }, } diff --git a/src/main/single_tenant.go b/src/main/single_tenant.go new file mode 100644 index 0000000..5d5acd3 --- /dev/null +++ b/src/main/single_tenant.go @@ -0,0 +1,134 @@ +package main + +import ( + "log" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/org" + "github.com/bb-consent/api/src/orgtype" + "github.com/bb-consent/api/src/user" + "go.mongodb.org/mongo-driver/mongo" +) + +func createOrganisationAdmin(config *config.Configuration) user.User { + u, err := user.GetByEmail(config.User.Username) + if err != nil { + log.Println("Failed to get user, creating new user.") + u, err = user.RegisterUser(config.User, config.Iam) + if err != nil { + log.Println("failed to create user") + panic(err) + } + } + + return u +} + +func createOrganisationType(config *config.Configuration) orgtype.OrgType { + orgType, err := orgtype.GetFirstType() + if err != nil { + if err == mongo.ErrNoDocuments { + log.Printf("Organization type doesn't exist, creating organization type.") + orgType, err = orgtype.AddOrganizationType(config.Type) + if err != nil { + log.Println("failed to add organization") + panic(err) + } + } else { + log.Println("failed to find organization") + panic(err) + } + } + + return orgType +} + +func addOrganisationAdminRole(organisationAdminId string, organisationId string) { + _, err := user.AddRole(organisationAdminId, user.Role{RoleID: common.GetRoleID("Admin"), OrgID: organisationId}) + if err != nil { + log.Printf("Failed to update user : %v roles for org: %v", organisationAdminId, organisationId) + panic(err) + } +} + +func createOrganisation(config *config.Configuration, orgType orgtype.OrgType, organisationAdminId string) org.Organization { + organization, err := org.GetFirstOrganization() + if err != nil { + if err == mongo.ErrNoDocuments { + log.Printf("Organization doesn't exist, creating organization.") + organization, err = org.AddOrganization(config.Organization, orgType.ID.Hex(), organisationAdminId) + if err != nil { + log.Println("failed to add organization") + panic(err) + } + // Add roles to organisation admin user + addOrganisationAdminRole(organisationAdminId, organization.ID.Hex()) + + } else { + log.Println("failed to find organization") + panic(err) + } + } + + return organization +} + +func deleteAllOrganisationTypes() { + typesCount, err := orgtype.GetTypesCount() + if err != nil { + log.Println("failed to count types") + panic(err) + } + + if typesCount > 1 { + _, err := orgtype.DeleteAllTypes() + if err != nil { + log.Println("failed to delete organizations") + panic(err) + } + } +} + +func deleteAllOrganisations() { + count, err := org.GetOrganizationsCount() + if err != nil { + log.Println("failed to count organization") + panic(err) + } + if count > 1 { + _, err := org.DeleteAllOrganizations() + if err != nil { + log.Println("failed to delete organizations") + panic(err) + } + } +} + +// SingleTenantConfiguration If the application starts in single tenant mode then create/update organisation, type, admin logic +func SingleTenantConfiguration(config *config.Configuration) { + + // Following is not allowed: + // 1. Updation of organisation is not allowed + // 2. Updation of organistaion type is not allowed + // 3. Updation of organisation admin is not allowed + // Note: Database has to be cleared if new organisation, type or admin has to be added + + // If there is more than 1 organisation or type, delete all (this is a temporary and will be removed later) + deleteAllOrganisationTypes() + deleteAllOrganisations() + + // Create an organisation admin + organisationAdmin := createOrganisationAdmin(config) + organisationAdminId := organisationAdmin.ID.Hex() + + // TODO: If wrong password is provided, the application panics + user.GetOrganisationAdminToken(config.User, config.Iam) + + // Create organisation type + orgType := createOrganisationType(config) + + // Create organisation + createOrganisation(config, orgType, organisationAdminId) + +} diff --git a/src/middleware/middleware.go b/src/middleware/middleware.go index a8245e7..e8d1c37 100644 --- a/src/middleware/middleware.go +++ b/src/middleware/middleware.go @@ -8,6 +8,7 @@ import ( "github.com/bb-consent/api/src/apikey" handler "github.com/bb-consent/api/src/handlerv1" + "github.com/bb-consent/api/src/org" "github.com/bb-consent/api/src/rbac" "github.com/casbin/casbin/v2" "github.com/gorilla/mux" @@ -204,9 +205,11 @@ func Authorize(e *casbin.Enforcer) Middleware { } var ApplicationMode string +var Organization config.Organization func ApplicationModeInit(config *config.Configuration) { ApplicationMode = config.ApplicationMode + Organization = config.Organization } // SetApplicationMode sets application modes for routes to either single tenant or multi tenant @@ -218,12 +221,14 @@ func SetApplicationMode() Middleware { return func(w http.ResponseWriter, r *http.Request) { if ApplicationMode == config.SingleTenant { - organizationId, err := handler.GetOrganizationId() + + organization, err := org.GetFirstOrganization() if err != nil { m := "failed to find organization" common.HandleError(w, http.StatusBadRequest, m, err) return } + organizationId := organization.ID.Hex() r.Header.Set(config.OrganizationId, organizationId) } diff --git a/src/org/organizations.go b/src/org/organizations.go index dae6b60..82cbd25 100644 --- a/src/org/organizations.go +++ b/src/org/organizations.go @@ -4,9 +4,14 @@ import ( "context" "errors" "log" + "strings" + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" "github.com/bb-consent/api/src/database" "github.com/bb-consent/api/src/orgtype" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" @@ -259,8 +264,8 @@ func Get(organizationID string) (Organization, error) { return result, err } -// GetOrganization Gets a single organization -func GetOrganization() (Organization, error) { +// GetFirstOrganization Gets first organization +func GetFirstOrganization() (Organization, error) { var result Organization err := collection().FindOne(context.TODO(), bson.M{}).Decode(&result) @@ -660,3 +665,71 @@ func GetName(organizationID string) (string, error) { return result.Name, err } + +// GetOrganizationsCount Get organizations count +func GetOrganizationsCount() (int64, error) { + count, err := collection().CountDocuments(context.TODO(), bson.D{}) + if err != nil { + return count, err + } + + return count, err +} + +// DeleteAllOrganizations delete all organizations +func DeleteAllOrganizations() (*mongo.DeleteResult, error) { + + result, err := collection().DeleteMany(context.TODO(), bson.D{}) + if err != nil { + return result, err + } + log.Printf("Number of documents deleted: %d\n", result.DeletedCount) + + return result, err +} + +// AddOrganization Adds an organization +func AddOrganization(orgReq config.Organization, typeId string, userId string) (Organization, error) { + + // validating request payload + valid, err := govalidator.ValidateStruct(orgReq) + if !valid { + log.Printf("Missing mandatory params for adding organization") + return Organization{}, err + } + + // checking if the string contained whitespace only + if strings.TrimSpace(orgReq.Name) == "" { + log.Printf("Failed to add organization: Missing mandatory param - Name") + return Organization{}, errors.New("missing mandatory param - Name") + } + + if strings.TrimSpace(orgReq.Location) == "" { + log.Printf("Failed to add organization: Missing mandatory param - Location") + return Organization{}, errors.New("missing mandatory param - Location") + } + + orgType, err := orgtype.Get(typeId) + if err != nil { + log.Printf("Invalid organization type ID: %v", typeId) + return Organization{}, err + } + + admin := Admin{UserID: userId, RoleID: common.GetRoleID("Admin")} + + var o Organization + o.Name = orgReq.Name + o.Location = orgReq.Location + o.Type = orgType + o.Description = orgReq.Description + o.EulaURL = orgReq.EulaURL + o.Admins = append(o.Admins, admin) + + orgResp, err := Add(o) + if err != nil { + log.Printf("Failed to add organization: %v", orgReq.Name) + return orgResp, err + } + + return orgResp, err +} diff --git a/src/orgtype/orgType.go b/src/orgtype/orgType.go index fd4e612..d6cd5f1 100644 --- a/src/orgtype/orgType.go +++ b/src/orgtype/orgType.go @@ -2,7 +2,10 @@ package orgtype import ( "context" + "log" + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/config" "github.com/bb-consent/api/src/database" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -99,3 +102,55 @@ func UpdateImage(organizationTypeID string, imageID string, imageURL string) err bson.M{"$set": bson.M{"imageid": imageID, "imageurl": imageURL}}) return err } + +// GetTypesCount Gets types count +func GetTypesCount() (int64, error) { + count, err := collection().CountDocuments(context.TODO(), bson.D{}) + if err != nil { + return count, err + } + + return count, err +} + +// GetFirstOrganization Gets first type +func GetFirstType() (OrgType, error) { + + var result OrgType + err := collection().FindOne(context.TODO(), bson.M{}).Decode(&result) + + return result, err +} + +// DeleteAllTypes delete all types +func DeleteAllTypes() (*mongo.DeleteResult, error) { + + result, err := collection().DeleteMany(context.TODO(), bson.D{}) + if err != nil { + return result, err + } + log.Printf("Number of documents deleted: %d\n", result.DeletedCount) + + return result, err +} + +// AddOrganizationType Adds an organization type +func AddOrganizationType(typeReq config.OrgType) (OrgType, error) { + + // validating request payload + valid, err := govalidator.ValidateStruct(typeReq) + if !valid { + log.Printf("Failed to add organization type: Missing mandatory param - Type") + return OrgType{}, err + } + + var orgType OrgType + orgType.Type = typeReq.Name + + orgType, err = Add(orgType) + if err != nil { + log.Printf("Failed to add organization type: %v", orgType) + return OrgType{}, err + } + return orgType, err +} diff --git a/src/user/users.go b/src/user/users.go index cb69d54..76901df 100644 --- a/src/user/users.go +++ b/src/user/users.go @@ -1,10 +1,19 @@ package user import ( + "bytes" "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" "log" + "net/http" + "net/url" "time" + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/config" "github.com/bb-consent/api/src/database" "github.com/bb-consent/api/src/org" "github.com/bb-consent/api/src/orgtype" @@ -476,3 +485,266 @@ func GetAPIKey(userID string) (string, error) { return result.APIKey, err } + +type iamCredentials struct { + Type string `json:"type"` + Value string `json:"value"` +} + +type iamUserRegisterReq struct { + Username string `json:"username"` + Firstname string `json:"firstName"` + Lastname string `json:"lastName"` + Email string `json:"email"` + Enabled bool `json:"enabled"` + RealmRoles []string `json:"realmRoles"` + Credentials []iamCredentials `json:"credentials"` +} + +type iamToken struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshExpiresIn int `json:"refresh_expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` +} + +type iamError struct { + ErrorType string `json:"error"` + Error string `json:"error_description"` +} + +func getAdminToken(iamConfig config.Iam) (iamToken, int, iamError, error) { + t, status, iamErr, err := getToken(iamConfig.AdminUser, iamConfig.AdminPassword, "admin-cli", "master", iamConfig.URL) + return t, status, iamErr, err +} + +func getToken(username string, password string, clientID string, realm string, iamUrl string) (iamToken, int, iamError, error) { + var tok iamToken + var e iamError + var status = http.StatusInternalServerError + + data := url.Values{} + data.Set("username", username) + data.Add("password", password) + data.Add("client_id", clientID) + data.Add("grant_type", "password") + + resp, err := http.PostForm(iamUrl+"/realms/"+realm+"/protocol/openid-connect/token", data) + if err != nil { + return tok, status, e, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + if err != nil { + return tok, status, e, err + } + if resp.StatusCode != http.StatusOK { + var e iamError + json.Unmarshal(body, &e) + return tok, resp.StatusCode, e, errors.New("failed to get token") + } + json.Unmarshal(body, &tok) + + return tok, resp.StatusCode, e, err +} + +// registerUser Registers a new user +func registerUser(iamRegReq iamUserRegisterReq, adminToken string, iamConfig config.Iam) (int, iamError, error) { + var e iamError + var status = http.StatusInternalServerError + jsonReq, _ := json.Marshal(iamRegReq) + req, err := http.NewRequest("POST", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/users", bytes.NewBuffer(jsonReq)) + if err != nil { + return status, e, err + } + + req.Header.Add("Authorization", "Bearer "+adminToken) + req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) + + //dump, err := httputil.DumpRequest(req, true) + //dump, err := httputil.DumpRequestOut(req, true) + //log.Printf("\n %q \n", dump) + timeout := time.Duration(time.Duration(iamConfig.Timeout) * time.Second) + + client := http.Client{ + Timeout: timeout, + } + resp, err := client.Do(req) + if err != nil { + return status, e, err + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + body, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + type errorMsg struct { + ErrorMessage string `json:"errorMessage"` + } + var errMsg errorMsg + json.Unmarshal(body, &errMsg) + e.Error = errMsg.ErrorMessage + e.ErrorType = "Creation failed" + return resp.StatusCode, e, errors.New("failed to register user") + } + return resp.StatusCode, e, err +} + +func getUserIamID(userName string, adminToken string, iamConfig config.Iam) (string, int, iamError, error) { + var userID = "" + var status = http.StatusInternalServerError + var e iamError + req, err := http.NewRequest("GET", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/users"+"?username="+userName, nil) + if err != nil { + return userID, status, e, err + } + //log.Printf("token: %v", t) + req.Header.Add("Authorization", "Bearer "+adminToken) + req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) + + //dump, err := httputil.DumpRequest(req, true) + //dump, err := httputil.DumpRequestOut(req, true) + //log.Printf("\n %q \n", dump) + timeout := time.Duration(time.Duration(iamConfig.Timeout) * time.Second) + + client := http.Client{ + Timeout: timeout, + } + resp, err := client.Do(req) + if err != nil { + return userID, status, e, err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + json.Unmarshal(body, &e) + return userID, resp.StatusCode, e, errors.New("failed to register user") + } + type userDetails struct { + ID string `json:"id"` + } + var users []userDetails + json.Unmarshal(body, &users) + return users[0].ID, resp.StatusCode, e, err +} + +type rReq struct { + ClientRole bool `json:"clientRole"` + Composite bool `json:"composite"` + ContainerID string `json:"containerId"` + Description string `json:"description"` + ID string `json:"id"` + Name string `json:"name"` + ScopeParamRequired bool `json:"scopeParamRequired"` +} + +var realmRoleOrgAdmin = "70698dc8-3202-4668-a982-4d95107399d4" + +func setAdminRole(userID string, adminToken string, iamConfig config.Iam) (int, iamError, error) { + var status = http.StatusInternalServerError + var e iamError + + var iReq []rReq + iReq = append(iReq, rReq{false, false, iamConfig.Realm, "${organization.admin}", realmRoleOrgAdmin, "organization-admin", false}) + jsonReq, _ := json.Marshal(iReq) + req, err := http.NewRequest("POST", iamConfig.URL+"/admin/realms/"+iamConfig.Realm+"/users/"+userID+"/role-mappings/realm", bytes.NewBuffer(jsonReq)) + if err != nil { + return status, e, err + } + + req.Header.Add("Authorization", "Bearer "+adminToken) + req.Header.Add(config.ContentTypeHeader, config.ContentTypeJSON) + + //dump, err := httputil.DumpRequest(req, true) + //dump, err := httputil.DumpRequestOut(req, true) + //fmt.Printf("\n %q \n", dump) + timeout := time.Duration(time.Duration(iamConfig.Timeout) * time.Second) + + client := http.Client{ + Timeout: timeout, + } + resp, err := client.Do(req) + if err != nil { + log.Printf("Failed to set user roles: with status:%v", resp.StatusCode) + return status, e, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + json.Unmarshal(body, &e) + return resp.StatusCode, e, err +} + +func RegisterUser(regReq config.User, iamConfig config.Iam) (User, error) { + valid, err := govalidator.ValidateStruct(regReq) + if !valid { + log.Printf("Failed to add organization type: Missing mandatory params") + return User{}, err + } + + var iamRegReq iamUserRegisterReq + iamRegReq.Username = regReq.Username + iamRegReq.Email = regReq.Username + iamRegReq.Enabled = true + + iamRegReq.Credentials = append(iamRegReq.Credentials, iamCredentials{"password", regReq.Password}) + iamRegReq.RealmRoles = append(iamRegReq.RealmRoles, "organization-admin") + + t, status, iamErr, err := getAdminToken(iamConfig) + if err != nil { + log.Printf("Failed to get admin token, user: %v registration", regReq.Username) + return User{}, err + } + fmt.Println(status, iamErr) + + _, _, err = registerUser(iamRegReq, t.AccessToken, iamConfig) + if err != nil { + log.Printf("Failed to register user: %v err: %v", regReq.Username, err) + return User{}, err + } + + userIamID, _, _, err := getUserIamID(regReq.Username, t.AccessToken, iamConfig) + if err != nil { + log.Printf("Failed to get userID for user: %v err: %v", regReq.Username, err) + return User{}, err + } + + _, _, err = setAdminRole(userIamID, t.AccessToken, iamConfig) + if err != nil { + log.Printf("Failed to set roles for user: %v iam id: %v err: %v", regReq.Username, userIamID, err) + return User{}, err + } + + var u User + u.IamID = userIamID + u.Email = regReq.Username + u.Orgs = []Org{} + u.Roles = []Role{} + + u, err = Add(u) + if err != nil { + log.Printf("Failed to add user: %v id: %v to Db err: %v", regReq.Username, userIamID, err) + return User{}, err + } + + log.Printf("successfully registered user: %v", regReq.Username) + + return u, err + +} + +func GetOrganisationAdminToken(userConfig config.User, iamConfig config.Iam) { + _, _, iamErr, err := getToken(userConfig.Username, userConfig.Password, "igrant-ios-app", iamConfig.Realm, iamConfig.URL) + if err != nil { + log.Printf("Failed to get admin token:%v with error: %s", userConfig.Username, iamErr) + panic(err) + } +}