diff --git a/auth/auth_test.go b/auth/auth_test.go index c13e1daa..fe40ec60 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -453,13 +453,13 @@ func (suite *AuthTestSuite) TestAuth() { tm := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) // Test Create - CreateUser(suite.ctx, "uuid12", "johndoe", "firstdoe", "lastdoe", "orgdoe", "descdoe", []ProjectRoles{ProjectRoles{Project: "ARGO", Roles: []string{"consumer"}}}, "johndoe@fake.email.foo", "TOK3N", []string{"service_admin"}, tm, "", store) + CreateUser(suite.ctx, "uuid12", "johndoe", "firstdoe", "lastdoe", "orgdoe", "descdoe", []ProjectRoles{ProjectRoles{Project: "ARGO", Roles: []string{"consumer"}}}, "johndoe@fake.email.foo", "TOK3N", []string{"service_admin"}, "", "", tm, "", store) usrs, _ := FindUsers(suite.ctx, "", "uuid12", "", true, store) usrJSON, _ := usrs.List[0].ExportJSON() suite.Equal(expUsrJSON, usrJSON) // Test Create with empty project list - CreateUser(suite.ctx, "uuid13", "empty-proj", "", "", "", "", []ProjectRoles{{Project: "", Roles: []string{"consumer"}}}, "TOK3N", "johndoe@fake.email.foo", []string{"service_admin"}, tm, "", store) + CreateUser(suite.ctx, "uuid13", "empty-proj", "", "", "", "", []ProjectRoles{{Project: "", Roles: []string{"consumer"}}}, "TOK3N", "johndoe@fake.email.foo", []string{"service_admin"}, "", "", tm, "", store) usrs2, _ := FindUsers(suite.ctx, "", "uuid13", "", true, store) expusrs2 := Users{List: []User{{UUID: "uuid13", Projects: []ProjectRoles{}, Name: "empty-proj", Token: "TOK3N", Email: "johndoe@fake.email.foo", ServiceRoles: []string{"service_admin"}, CreatedOn: "2009-11-10T23:00:00Z", ModifiedOn: "2009-11-10T23:00:00Z", CreatedBy: ""}}} suite.Equal(expusrs2, usrs2) @@ -491,18 +491,18 @@ func (suite *AuthTestSuite) TestAuth() { "created_on": "2009-11-10T23:00:00Z", "modified_on": "2009-11-10T23:00:00Z" }` - UpdateUser(suite.ctx, "uuid12", "firstdoe2", "lastdoe2", "orgdoe2", "descdoe2", "johnny_doe", nil, "", []string{"consumer", "producer"}, tm, false, store) + UpdateUser(suite.ctx, "uuid12", "firstdoe2", "lastdoe2", "orgdoe2", "descdoe2", "johnny_doe", nil, "", []string{"consumer", "producer"}, "", "", tm, false, store) usrUpd, _ := FindUsers(suite.ctx, "", "uuid12", "", true, store) usrUpdJSON, _ := usrUpd.List[0].ExportJSON() suite.Equal(expUpdate, usrUpdJSON) // reflect obj true - usrUpd2, _ := UpdateUser(suite.ctx, "uuid12", "", "", "", "", "johnny_doe", nil, "", []string{"consumer", "producer"}, tm, true, store) + usrUpd2, _ := UpdateUser(suite.ctx, "uuid12", "", "", "", "", "johnny_doe", nil, "", []string{"consumer", "producer"}, "", "", tm, true, store) usrUpdJSON2, _ := usrUpd2.ExportJSON() suite.Equal(expUpdate, usrUpdJSON2) // Test update with empty project - UpdateUser(suite.ctx, "uuid13", "", "", "", "", "empty-proj", []ProjectRoles{{Project: "", Roles: []string{"consumer"}}}, "johndoe@fake.email.foo", []string{"service_admin"}, tm, false, store) + UpdateUser(suite.ctx, "uuid13", "", "", "", "", "empty-proj", []ProjectRoles{{Project: "", Roles: []string{"consumer"}}}, "johndoe@fake.email.foo", []string{"service_admin"}, "", "", tm, false, store) usrs2, _ = FindUsers(suite.ctx, "", "uuid13", "", true, store) expusrs2 = Users{List: []User{{UUID: "uuid13", Projects: []ProjectRoles{}, Name: "empty-proj", Token: "TOK3N", Email: "johndoe@fake.email.foo", ServiceRoles: []string{"service_admin"}, CreatedOn: "2009-11-10T23:00:00Z", ModifiedOn: "2009-11-10T23:00:00Z", CreatedBy: ""}}} suite.Equal(expusrs2, usrs2) @@ -517,7 +517,7 @@ func (suite *AuthTestSuite) TestAuth() { modified := "2009-11-10T23:00:00Z" var qUsers1 []User - qUsers1 = append(qUsers1, User{"uuid8", []ProjectRoles{{"ARGO2", []string{"consumer", "publisher"}, []string{}, []string{}}}, "UserZ", "", "", "", "", "S3CR3T1", "foo-email", []string{}, created, modified, ""}) + qUsers1 = append(qUsers1, User{"uuid8", []ProjectRoles{{"ARGO2", []string{"consumer", "publisher"}, []string{}, []string{}}}, "UserZ", "", "", "", "", "S3CR3T1", "foo-email", []string{}, "", "", created, modified, ""}) qUsers1 = append(qUsers1, User{ UUID: "uuid7", Name: "push_worker_0", @@ -529,19 +529,19 @@ func (suite *AuthTestSuite) TestAuth() { ServiceRoles: []string{"push_worker"}, CreatedOn: created, ModifiedOn: modified, CreatedBy: "", }) - qUsers1 = append(qUsers1, User{"same_uuid", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{}, []string{}}}, "UserSame2", "", "", "", "", "S3CR3T42", "foo-email", []string{}, created, modified, "UserA"}) - qUsers1 = append(qUsers1, User{"same_uuid", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{}, []string{}}}, "UserSame1", "", "", "", "", "S3CR3T41", "foo-email", []string{}, created, modified, "UserA"}) - qUsers1 = append(qUsers1, User{"uuid4", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic2"}, []string{"sub3", "sub4"}}}, "UserZ", "", "", "", "", "S3CR3T4", "foo-email", []string{}, created, modified, "UserA"}) - qUsers1 = append(qUsers1, User{"uuid3", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic3"}, []string{"sub2"}}}, "UserX", "", "", "", "", "S3CR3T3", "foo-email", []string{}, created, modified, "UserA"}) - qUsers1 = append(qUsers1, User{"uuid2", []ProjectRoles{{"ARGO", []string{"consumer", "publisher"}, []string{"topic1", "topic2"}, []string{"sub1", "sub3", "sub4"}}}, "UserB", "", "", "", "", "S3CR3T2", "foo-email", []string{}, created, modified, "UserA"}) - qUsers1 = append(qUsers1, User{"uuid1", []ProjectRoles{{"ARGO", []string{"consumer", "publisher"}, []string{"topic1", "topic2"}, []string{"sub1", "sub2", "sub3"}}}, "UserA", "FirstA", "LastA", "OrgA", "DescA", "S3CR3T1", "foo-email", []string{}, created, modified, ""}) - qUsers1 = append(qUsers1, User{"uuid0", []ProjectRoles{{"ARGO", []string{"consumer", "publisher"}, []string{}, []string{}}}, "Test", "", "", "", "", "S3CR3T", "Test@test.com", []string{}, created, modified, ""}) + qUsers1 = append(qUsers1, User{"same_uuid", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{}, []string{}}}, "UserSame2", "", "", "", "", "S3CR3T42", "foo-email", []string{}, "", "", created, modified, "UserA"}) + qUsers1 = append(qUsers1, User{"same_uuid", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{}, []string{}}}, "UserSame1", "", "", "", "", "S3CR3T41", "foo-email", []string{}, "", "", created, modified, "UserA"}) + qUsers1 = append(qUsers1, User{"uuid4", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic2"}, []string{"sub3", "sub4"}}}, "UserZ", "", "", "", "", "S3CR3T4", "foo-email", []string{}, "", "", created, modified, "UserA"}) + qUsers1 = append(qUsers1, User{"uuid3", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic3"}, []string{"sub2"}}}, "UserX", "", "", "", "", "S3CR3T3", "foo-email", []string{}, "", "", created, modified, "UserA"}) + qUsers1 = append(qUsers1, User{"uuid2", []ProjectRoles{{"ARGO", []string{"consumer", "publisher"}, []string{"topic1", "topic2"}, []string{"sub1", "sub3", "sub4"}}}, "UserB", "", "", "", "", "S3CR3T2", "foo-email", []string{}, "", "", created, modified, "UserA"}) + qUsers1 = append(qUsers1, User{"uuid1", []ProjectRoles{{"ARGO", []string{"consumer", "publisher"}, []string{"topic1", "topic2"}, []string{"sub1", "sub2", "sub3"}}}, "UserA", "FirstA", "LastA", "OrgA", "DescA", "S3CR3T1", "foo-email", []string{}, "", "", created, modified, ""}) + qUsers1 = append(qUsers1, User{"uuid0", []ProjectRoles{{"ARGO", []string{"consumer", "publisher"}, []string{}, []string{}}}, "Test", "", "", "", "", "S3CR3T", "Test@test.com", []string{}, "", "", created, modified, ""}) // return all users pu1, e1 := PaginatedFindUsers(suite.ctx, "", 0, "", true, true, store2) suite.NoError(e1) var qUsers2 []User - qUsers2 = append(qUsers2, User{"uuid8", []ProjectRoles{{"ARGO2", []string{"consumer", "publisher"}, []string{}, []string{}}}, "UserZ", "", "", "", "", "S3CR3T1", "foo-email", []string{}, created, modified, ""}) + qUsers2 = append(qUsers2, User{"uuid8", []ProjectRoles{{"ARGO2", []string{"consumer", "publisher"}, []string{}, []string{}}}, "UserZ", "", "", "", "", "S3CR3T1", "foo-email", []string{}, "", "", created, modified, ""}) qUsers2 = append(qUsers2, User{ UUID: "uuid7", Name: "push_worker_0", @@ -553,15 +553,15 @@ func (suite *AuthTestSuite) TestAuth() { ServiceRoles: []string{"push_worker"}, CreatedOn: created, ModifiedOn: modified, CreatedBy: "", }) - qUsers2 = append(qUsers2, User{"same_uuid", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{}, []string{}}}, "UserSame2", "", "", "", "", "S3CR3T42", "foo-email", []string{}, created, modified, "UserA"}) + qUsers2 = append(qUsers2, User{"same_uuid", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{}, []string{}}}, "UserSame2", "", "", "", "", "S3CR3T42", "foo-email", []string{}, "", "", created, modified, "UserA"}) // return the first page with 2 users pu2, e2 := PaginatedFindUsers(suite.ctx, "", 3, "", true, true, store2) suite.NoError(e2) var qUsers3 []User - qUsers3 = append(qUsers3, User{"uuid4", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic2"}, []string{"sub3", "sub4"}}}, "UserZ", "", "", "", "", "S3CR3T4", "foo-email", []string{}, created, modified, "UserA"}) - qUsers3 = append(qUsers3, User{"uuid3", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic3"}, []string{"sub2"}}}, "UserX", "", "", "", "", "S3CR3T3", "foo-email", []string{}, created, modified, "UserA"}) + qUsers3 = append(qUsers3, User{"uuid4", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic2"}, []string{"sub3", "sub4"}}}, "UserZ", "", "", "", "", "S3CR3T4", "foo-email", []string{}, "", "", created, modified, "UserA"}) + qUsers3 = append(qUsers3, User{"uuid3", []ProjectRoles{{"ARGO", []string{"publisher", "consumer"}, []string{"topic3"}, []string{"sub2"}}}, "UserX", "", "", "", "", "S3CR3T3", "foo-email", []string{}, "", "", created, modified, "UserA"}) // return the next 2 users pu3, e3 := PaginatedFindUsers(suite.ctx, "NA==", 2, "", true, true, store2) @@ -575,7 +575,7 @@ func (suite *AuthTestSuite) TestAuth() { // check user list by project and with unprivileged mode (token redacted) var qUsersC []User - qUsersC = append(qUsersC, User{"uuid8", []ProjectRoles{{"ARGO2", []string{"consumer", "publisher"}, []string{}, []string{}}}, "UserZ", "", "", "", "", "", "foo-email", []string{}, created, modified, ""}) + qUsersC = append(qUsersC, User{"uuid8", []ProjectRoles{{"ARGO2", []string{"consumer", "publisher"}, []string{}, []string{}}}, "UserZ", "", "", "", "", "", "foo-email", []string{}, "", "", created, modified, ""}) // check for non detailed view var ndUser []User @@ -850,7 +850,7 @@ func (suite *AuthTestSuite) TestGetPushWorker() { // normal case of push enabled true and correct push worker token u1, err1 := GetPushWorker(suite.ctx, "push_token", store) - suite.Equal(User{"uuid7", []ProjectRoles{}, "push_worker_0", "", "", "", "", "push_token", "foo-email", []string{"push_worker"}, "2009-11-10T23:00:00Z", "2009-11-10T23:00:00Z", ""}, u1) + suite.Equal(User{"uuid7", []ProjectRoles{}, "push_worker_0", "", "", "", "", "push_token", "foo-email", []string{"push_worker"}, "", "", "2009-11-10T23:00:00Z", "2009-11-10T23:00:00Z", ""}, u1) suite.Nil(err1) // incorrect push worker token diff --git a/auth/users.go b/auth/users.go index 09cfced2..f3814150 100644 --- a/auth/users.go +++ b/auth/users.go @@ -24,19 +24,21 @@ const ( // User is the struct that holds user information type User struct { - UUID string `json:"uuid"` - Projects []ProjectRoles `json:"projects,omitempty"` - Name string `json:"name"` - FirstName string `json:"first_name,omitempty"` - LastName string `json:"last_name,omitempty"` - Organization string `json:"organization,omitempty"` - Description string `json:"description,omitempty"` - Token string `json:"token,omitempty"` - Email string `json:"email"` - ServiceRoles []string `json:"service_roles"` - CreatedOn string `json:"created_on,omitempty"` - ModifiedOn string `json:"modified_on,omitempty"` - CreatedBy string `json:"created_by,omitempty"` + UUID string `json:"uuid"` + Projects []ProjectRoles `json:"projects,omitempty"` + Name string `json:"name"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Organization string `json:"organization,omitempty"` + Description string `json:"description,omitempty"` + Token string `json:"token,omitempty"` + Email string `json:"email"` + ServiceRoles []string `json:"service_roles"` + Component string `json:"component,omitempty"` + ComponentProject string `json:"component_project,omitempty"` + CreatedOn string `json:"created_on,omitempty"` + ModifiedOn string `json:"modified_on,omitempty"` + CreatedBy string `json:"created_by,omitempty"` } // ProjectRoles is the struct that hold project and role information of the user @@ -241,22 +243,24 @@ func UpdateUserRegistration(ctx context.Context, regUUID, status, declineComment } // NewUser accepts parameters and creates a new user -func NewUser(uuid string, projects []ProjectRoles, name string, fname string, lname string, org string, desc string, token string, email string, serviceRoles []string, createdOn time.Time, modifiedOn time.Time, createdBy string) User { +func NewUser(uuid string, projects []ProjectRoles, name string, fname string, lname string, org string, desc string, token string, email string, serviceRoles []string, comp string, compProject string, createdOn time.Time, modifiedOn time.Time, createdBy string) User { zuluForm := "2006-01-02T15:04:05Z" return User{ - UUID: uuid, - Projects: projects, - Name: name, - FirstName: fname, - LastName: lname, - Organization: org, - Description: desc, - Token: token, - Email: email, - ServiceRoles: serviceRoles, - CreatedOn: createdOn.Format(zuluForm), - ModifiedOn: modifiedOn.Format(zuluForm), - CreatedBy: createdBy} + UUID: uuid, + Projects: projects, + Name: name, + FirstName: fname, + LastName: lname, + Organization: org, + Description: desc, + Token: token, + Email: email, + ServiceRoles: serviceRoles, + Component: comp, + ComponentProject: compProject, + CreatedOn: createdOn.Format(zuluForm), + ModifiedOn: modifiedOn.Format(zuluForm), + CreatedBy: createdBy} } // GetPushWorker returns a push worker user by token @@ -318,7 +322,7 @@ func GetUserByToken(ctx context.Context, token string, store stores.Store) (User curUser := NewUser(user.UUID, pRoles, user.Name, user.FirstName, user.LastName, user.Organization, user.Description, user.Token, user.Email, - user.ServiceRoles, user.CreatedOn.UTC(), user.ModifiedOn.UTC(), usernameC) + user.ServiceRoles, user.Component, user.ComponentProject, user.CreatedOn.UTC(), user.ModifiedOn.UTC(), usernameC) result = curUser @@ -385,7 +389,7 @@ func FindUsers(ctx context.Context, projectUUID string, uuid string, name string } curUser := NewUser(item.UUID, pRoles, item.Name, item.FirstName, item.LastName, - item.Organization, item.Description, token, item.Email, serviceRoles, + item.Organization, item.Description, token, item.Email, serviceRoles, item.Component, item.ComponentProject, item.CreatedOn.UTC(), item.ModifiedOn.UTC(), usernameC) result.List = append(result.List, curUser) @@ -474,7 +478,7 @@ func PaginatedFindUsers(ctx context.Context, pageToken string, pageSize int64, p } curUser := NewUser(item.UUID, pRoles, item.Name, item.FirstName, item.LastName, - item.Organization, item.Description, token, item.Email, serviceRoles, + item.Organization, item.Description, token, item.Email, serviceRoles, item.Component, item.ComponentProject, item.CreatedOn.UTC(), item.ModifiedOn.UTC(), usernameC) result.Users = append(result.Users, curUser) @@ -580,7 +584,7 @@ func GetUserByUUID(ctx context.Context, uuid string, store stores.Store) (User, curUser := NewUser(user.UUID, pRoles, user.Name, user.FirstName, user.LastName, user.Organization, user.Description, user.Token, user.Email, - user.ServiceRoles, user.CreatedOn.UTC(), user.ModifiedOn.UTC(), usernameC) + user.ServiceRoles, user.Component, user.ComponentProject, user.CreatedOn.UTC(), user.ModifiedOn.UTC(), usernameC) result = curUser @@ -600,6 +604,18 @@ func GetUUIDByName(ctx context.Context, name string, store stores.Store) string return result } +// GetUUIDByComponent queries user by component info and returns the corresponding UUID +func GetUUIDByComponent(ctx context.Context, comp string, compProject string, store stores.Store) string { + result := "" + user, err := store.GetComponentUser(ctx, comp, compProject) + + if err == nil && user.UUID != "" { + result = user.UUID + } + + return result +} + // UpdateUserToken updates an existing user's token func UpdateUserToken(ctx context.Context, uuid string, token string, store stores.Store) (User, error) { if err := store.UpdateUserToken(ctx, uuid, token); err != nil { @@ -636,7 +652,7 @@ func AppendToUserProjects(ctx context.Context, userUUID string, projectUUID stri // UpdateUser updates an existing user's information // IF the function caller needs to have a view on the updated user object it can set the reflectObj to true -func UpdateUser(ctx context.Context, uuid, firstName, lastName, organization, description string, name string, projectList []ProjectRoles, email string, serviceRoles []string, modifiedOn time.Time, reflectObj bool, store stores.Store) (User, error) { +func UpdateUser(ctx context.Context, uuid, firstName, lastName, organization, description string, name string, projectList []ProjectRoles, email string, serviceRoles []string, comp string, compProject string, modifiedOn time.Time, reflectObj bool, store stores.Store) (User, error) { prList := []stores.QProjectRoles{} @@ -689,7 +705,7 @@ func UpdateUser(ctx context.Context, uuid, firstName, lastName, organization, de } } - if err := store.UpdateUser(ctx, uuid, firstName, lastName, organization, description, prList, name, email, serviceRoles, modifiedOn); err != nil { + if err := store.UpdateUser(ctx, uuid, firstName, lastName, organization, description, prList, name, email, serviceRoles, comp, compProject, modifiedOn); err != nil { return User{}, err } @@ -703,7 +719,7 @@ func UpdateUser(ctx context.Context, uuid, firstName, lastName, organization, de } // CreateUser creates a new user -func CreateUser(ctx context.Context, uuid string, name string, fname string, lname string, org string, desc string, projectList []ProjectRoles, token string, email string, serviceRoles []string, createdOn time.Time, createdBy string, store stores.Store) (User, error) { +func CreateUser(ctx context.Context, uuid string, name string, fname string, lname string, org string, desc string, projectList []ProjectRoles, token string, email string, serviceRoles []string, comp string, compProject string, createdOn time.Time, createdBy string, store stores.Store) (User, error) { // check if project with the same name exists if ExistsWithName(ctx, name, store) { return User{}, errors.New("exists") @@ -755,7 +771,7 @@ func CreateUser(ctx context.Context, uuid string, name string, fname string, lna } } - if err := store.InsertUser(ctx, uuid, prList, name, fname, lname, org, desc, token, email, serviceRoles, createdOn, createdOn, createdBy); err != nil { + if err := store.InsertUser(ctx, uuid, prList, name, fname, lname, org, desc, token, email, serviceRoles, comp, compProject, createdOn, createdOn, createdBy); err != nil { return User{}, errors.New("backend error") } diff --git a/handlers/handlers.go b/handlers/handlers.go index 5bfbf425..54b96aaa 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -146,6 +146,7 @@ func WrapLog(hfn http.Handler, name string) http.HandlerFunc { func WrapAuthenticate(hfn http.Handler, extractToken RequestTokenExtractStrategy) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { traceID := gorillaContext.Get(r, "trace_id").(string) + rCTX := context.WithValue(context.Background(), TraceIDContextKey, traceID) urlVars := mux.Vars(r) @@ -364,6 +365,12 @@ func respondOK(w http.ResponseWriter, output []byte) { w.Write(output) } +// respondAny is used to finalize reponse writer with any code and output +func respondAny(w http.ResponseWriter, code int, output []byte) { + w.WriteHeader(code) + w.Write(output) +} + // respondErr is used to finalize response writer with proper error codes and error output func respondErr(ctx context.Context, w http.ResponseWriter, apiErr APIErrorRoot) { log.WithFields( @@ -432,3 +439,23 @@ type PushServerInfo struct { Endpoint string `json:"endpoint"` Status string `json:"status"` } + +// Component response is used when a component admin asks for a token refresh on an account tied to a component under a specific project +type ComponentResponse struct { + Status struct { + Message string `json:"message"` + Code string `json:"code"` + } `json:"status"` + Data *struct { + APIKey string `json:"api_key"` + } `json:"data,omitempty"` +} + +// ExportJSON for the component response +func (resp *ComponentResponse) ExportJSON() (string, error) { + out, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return "", err + } + return string(out), nil +} diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index bf337507..f35f6a3c 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -241,7 +241,7 @@ func (suite *HandlerTestSuite) TestListVersion() { cfgKafka.PushWorkerToken = "missing" brk := brokers.MockBroker{} str := stores.NewMockStore("whatever", "argo_mgs") - str.UserList = append(str.UserList, stores.QUser{8, "uuid8", nil, "UserZ", "", "", "", "", "st", "foo-email", []string{"service_admin"}, time.Now(), time.Now(), ""}) + str.UserList = append(str.UserList, stores.QUser{8, "uuid8", nil, "UserZ", "", "", "", "", "st", "foo-email", []string{"service_admin"}, "", "", time.Now(), time.Now(), ""}) router := mux.NewRouter().StrictSlash(true) pc := new(push.MockClient) diff --git a/handlers/metrics_test.go b/handlers/metrics_test.go index 170c0a75..fc2c2919 100644 --- a/handlers/metrics_test.go +++ b/handlers/metrics_test.go @@ -784,7 +784,7 @@ func (suite *MetricsHandlersTestSuite) TestUserUsageProfile() { }, }, "UserA", "FirstA", "LastA", "OrgA", - "DescA", "S3CR3T1T", "foo-email", []string{}, + "DescA", "S3CR3T1T", "foo-email", []string{}, "", "", time.Now(), time.Now(), ""}) router := mux.NewRouter().StrictSlash(true) w := httptest.NewRecorder() diff --git a/handlers/projects.go b/handlers/projects.go index 2b4ab677..6c5faa0e 100644 --- a/handlers/projects.go +++ b/handlers/projects.go @@ -382,7 +382,7 @@ func ProjectUserCreate(w http.ResponseWriter, r *http.Request) { created := time.Now().UTC() // Get Result Object - res, err := auth.CreateUser(rCTX, uuid, urlUser, "", "", "", "", postBody.Projects, token, postBody.Email, postBody.ServiceRoles, created, refUserUUID, refStr) + res, err := auth.CreateUser(rCTX, uuid, urlUser, "", "", "", "", postBody.Projects, token, postBody.Email, postBody.ServiceRoles, "", "", created, refUserUUID, refStr) if err != nil { if err.Error() == "exists" { @@ -521,7 +521,7 @@ func ProjectUserUpdate(w http.ResponseWriter, r *http.Request) { userOrg := u.One().Organization userDesc := u.One().Description - _, err = auth.UpdateUser(rCTX, userUUID, userFN, userLN, userOrg, userDesc, userName, userProjects, userEmail, userSRoles, modified, false, refStr) + _, err = auth.UpdateUser(rCTX, userUUID, userFN, userLN, userOrg, userDesc, userName, userProjects, userEmail, userSRoles, "", "", modified, false, refStr) if err != nil { @@ -638,7 +638,7 @@ func ProjectUserRemove(w http.ResponseWriter, r *http.Request) { userOrg := u.One().Organization userDesc := u.One().Description - _, err = auth.UpdateUser(rCTX, userUUID, userFN, userLN, userOrg, userDesc, userName, userProjects, userEmail, userSRoles, modified, false, refStr) + _, err = auth.UpdateUser(rCTX, userUUID, userFN, userLN, userOrg, userDesc, userName, userProjects, userEmail, userSRoles, "", "", modified, false, refStr) if err != nil { @@ -741,7 +741,7 @@ func ProjectUserAdd(w http.ResponseWriter, r *http.Request) { Roles: data.Roles, }) - _, err = auth.UpdateUser(rCTX, userUUID, userFN, userLN, userOrg, userDesc, userName, userProjects, userEmail, userSRoles, modified, false, refStr) + _, err = auth.UpdateUser(rCTX, userUUID, userFN, userLN, userOrg, userDesc, userName, userProjects, userEmail, userSRoles, "", "", modified, false, refStr) if err != nil { @@ -765,7 +765,6 @@ func ProjectUserAdd(w http.ResponseWriter, r *http.Request) { // Write response privileged := auth.IsServiceAdmin(refRoles) - fmt.Println(privileged) results, err := auth.FindUsers(rCTX, refProjUUID, "", urlUser, privileged, refStr) if err != nil { diff --git a/handlers/registrations.go b/handlers/registrations.go index f2365780..48fd5cd6 100644 --- a/handlers/registrations.go +++ b/handlers/registrations.go @@ -123,7 +123,7 @@ func AcceptRegisterUser(w http.ResponseWriter, r *http.Request) { created := time.Now().UTC() // Get Result Object res, err := auth.CreateUser(rCTX, userUUID, ru.Name, ru.FirstName, ru.LastName, ru.Organization, ru.Description, - []auth.ProjectRoles{}, token, ru.Email, []string{}, created, refUserUUID, refStr) + []auth.ProjectRoles{}, token, ru.Email, []string{}, "", "", created, refUserUUID, refStr) if err != nil { if err.Error() == "exists" { diff --git a/handlers/subscriptions_test.go b/handlers/subscriptions_test.go index 3e7a6604..224bc624 100644 --- a/handlers/subscriptions_test.go +++ b/handlers/subscriptions_test.go @@ -3,7 +3,6 @@ package handlers import ( "bytes" "context" - "fmt" "io" "net/http" "net/http/httptest" @@ -1359,7 +1358,6 @@ func (suite *SubscriptionsHandlersTestSuite) TestSubCreate() { router.HandleFunc("/v1/projects/{project}/subscriptions/{subscription}", WrapMockAuthConfig(SubCreate, cfgKafka, &brk, str, nil)) router.ServeHTTP(w, req) sub, _ := str.QueryOneSub(suite.ctx, "argo_uuid", "subNew") - fmt.Println(sub) expResp = strings.Replace(expResp, "{{CON}}", sub.CreatedOn.Format("2006-01-02T15:04:05Z"), 1) suite.Equal(200, w.Code) suite.Equal(expResp, w.Body.String()) diff --git a/handlers/users.go b/handlers/users.go index f61f9159..26f61cad 100644 --- a/handlers/users.go +++ b/handlers/users.go @@ -118,6 +118,70 @@ func RefreshToken(w http.ResponseWriter, r *http.Request) { respondOK(w, []byte(resJSON)) } +// RefreshComponentToken (POST) refreshes user's token based on component info +func RefreshComponentToken(w http.ResponseWriter, r *http.Request) { + traceID := gorillaContext.Get(r, "trace_id").(string) + rCTX := context.WithValue(context.Background(), TraceIDContextKey, traceID) + + // Add content type header to the response + contentType := "application/json" + charset := "utf-8" + w.Header().Add("Content-Type", fmt.Sprintf("%s; charset=%s", contentType, charset)) + + // Grab url path variables + urlVars := mux.Vars(r) + urlComp := urlVars["component"] + urlProject := urlVars["project"] + + // Grab context references + refStr := gorillaContext.Get(r, "str").(stores.Store) + + // Get Result Object + userUUID := auth.GetUUIDByComponent(rCTX, urlComp, urlProject, refStr) + token, err := auth.GenToken() // generate a new user token + if err != nil { + respondErr(rCTX, w, APIErrGenericInternal(err.Error())) + return + } + + res, err := auth.UpdateUserToken(rCTX, userUUID, token, refStr) + + compResp := &ComponentResponse{} + + if err != nil { + if err.Error() == "not found" { + compResp.Status.Code = "404" + compResp.Status.Message = fmt.Sprintf("Component %s not configured for project %s", urlComp, urlProject) + compResp.ExportJSON() + resJSON, err := compResp.ExportJSON() + if err != nil { + respondErr(rCTX, w, APIErrGenericInternal(err.Error())) + return + } + respondAny(w, http.StatusNotFound, []byte(resJSON)) + return + } + err := APIErrGenericInternal(err.Error()) + respondErr(rCTX, w, err) + return + } + + // Output result to JSON + compResp.Status.Code = "200" + compResp.Status.Message = "Component api key succesfully renewed" + compResp.Data = &struct { + APIKey string `json:"api_key"` + }{APIKey: res.Token} + resJSON, err := compResp.ExportJSON() + if err != nil { + err := APIErrExportJSON() + respondErr(rCTX, w, err) + return + } + + respondOK(w, []byte(resJSON)) +} + // UserUpdate (PUT) updates the user information func UserUpdate(w http.ResponseWriter, r *http.Request) { traceID := gorillaContext.Get(r, "trace_id").(string) @@ -154,8 +218,21 @@ func UserUpdate(w http.ResponseWriter, r *http.Request) { // Get Result Object userUUID := auth.GetUUIDByName(rCTX, urlUser, refStr) modified := time.Now().UTC() + + // check component info changes to avoid duplicate + if postBody.Component != "" && postBody.ComponentProject != "" { + existingUUID := auth.GetUUIDByComponent(rCTX, postBody.Component, postBody.ComponentProject, refStr) + if existingUUID != "" && existingUUID != userUUID { + respondErr(rCTX, w, APIErrorConflict( + fmt.Sprintf("A user registered for component '%s' in project '%s'", + postBody.Component, postBody.ComponentProject), + )) + return + } + } + res, err := auth.UpdateUser(rCTX, userUUID, postBody.FirstName, postBody.LastName, postBody.Organization, postBody.Description, - postBody.Name, postBody.Projects, postBody.Email, postBody.ServiceRoles, modified, true, refStr) + postBody.Name, postBody.Projects, postBody.Email, postBody.ServiceRoles, postBody.Component, postBody.ComponentProject, modified, true, refStr) if err != nil { @@ -229,6 +306,20 @@ func UserCreate(w http.ResponseWriter, r *http.Request) { return } + // if component information is provided check if duplicate exists already + if postBody.Component != "" && postBody.ComponentProject != "" { + existingUUID := auth.GetUUIDByComponent(rCTX, postBody.Component, postBody.ComponentProject, refStr) + + if existingUUID != "" { + + respondErr(rCTX, w, APIErrorConflict( + fmt.Sprintf("A user registered for component '%s' in project '%s'", + postBody.Component, postBody.ComponentProject), + )) + return + } + } + uuid := uuid.NewV4().String() // generate a new uuid to attach to the new project token, err := auth.GenToken() // generate a new user token if err != nil { @@ -238,7 +329,7 @@ func UserCreate(w http.ResponseWriter, r *http.Request) { created := time.Now().UTC() // Get Result Object res, err := auth.CreateUser(rCTX, uuid, urlUser, postBody.FirstName, postBody.LastName, postBody.Organization, postBody.Description, - postBody.Projects, token, postBody.Email, postBody.ServiceRoles, created, refUserUUID, refStr) + postBody.Projects, token, postBody.Email, postBody.ServiceRoles, postBody.Component, postBody.ComponentProject, created, refUserUUID, refStr) if err != nil { if err.Error() == "exists" { diff --git a/handlers/users_test.go b/handlers/users_test.go index f33ba16c..3b94c68f 100644 --- a/handlers/users_test.go +++ b/handlers/users_test.go @@ -2,6 +2,12 @@ package handlers import ( "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + "github.com/ARGOeu/argo-messaging/auth" "github.com/ARGOeu/argo-messaging/brokers" "github.com/ARGOeu/argo-messaging/config" @@ -9,10 +15,6 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" - "io" - "net/http" - "net/http/httptest" - "testing" ) type UsersHandlersTestSuite struct { @@ -227,6 +229,95 @@ func (suite *UsersHandlersTestSuite) TestUserCreate() { suite.Equal("desc-1", usrOut.Description) } +func (suite *UsersHandlersTestSuite) TestComponentUser() { + + postJSON := `{ + "email":"email@foo.com", + "first_name": "fname-1", + "last_name": "lname-1", + "organization": "org-1", + "description": "desc-1", + "projects":[{"project_uuid":"argo_uuid","roles":["admin","viewer"]}], + "component": "monbox", + "component_project": "ARGO" +}` + + req, err := http.NewRequest("POST", "http://localhost:8080/v1/users/USERNEW2", bytes.NewBuffer([]byte(postJSON))) + if err != nil { + log.Fatal(err) + } + + cfgKafka := config.NewAPICfg() + cfgKafka.LoadStrJSON(suite.cfgStr) + brk := brokers.MockBroker{} + str := stores.NewMockStore("whatever", "argo_mgs") + router := mux.NewRouter().StrictSlash(true) + + w := httptest.NewRecorder() + router.HandleFunc("/v1/users/{user}", WrapMockAuthConfig(UserCreate, cfgKafka, &brk, str, nil)) + router.ServeHTTP(w, req) + suite.Equal(200, w.Code) + usrOut, _ := auth.GetUserFromJSON(w.Body.Bytes()) + + suite.Equal("USERNEW2", usrOut.Name) + // Check if the mock authenticated userA has been marked as the creator + suite.Equal("email@foo.com", usrOut.Email) + //suite.Equal([]string{"admin", "viewer"}, usrOut.Projects[0].Role) + suite.Equal("fname-1", usrOut.FirstName) + suite.Equal("lname-1", usrOut.LastName) + suite.Equal("org-1", usrOut.Organization) + suite.Equal("desc-1", usrOut.Description) + suite.Equal("monbox", usrOut.Component) + suite.Equal("ARGO", usrOut.ComponentProject) + + // try to post a second user with same component info - find duplicate + + postJSON2 := `{ + "email":"email@foo.com", + "first_name": "fname-1", + "last_name": "lname-1", + "organization": "org-1", + "description": "desc-1", + "projects":[{"project_uuid":"argo_uuid","roles":["admin","viewer"]}], + "component": "monbox", + "component_project": "ARGO" +}` + + req2, err := http.NewRequest("POST", "http://localhost:8080/v1/users/USERNEW3", bytes.NewBuffer([]byte(postJSON2))) + if err != nil { + log.Fatal(err) + } + + router2 := mux.NewRouter().StrictSlash(true) + + w2 := httptest.NewRecorder() + router2.HandleFunc("/v1/users/{user}", WrapMockAuthConfig(UserCreate, cfgKafka, &brk, str, nil)) + router2.ServeHTTP(w2, req2) + suite.Equal(409, w2.Code) + + // try to refresh token + + req3, err := http.NewRequest("POST", "http://localhost:8080/v1/integrations/component/monbox/by-project-name/ARGO/refresh", bytes.NewBuffer(nil)) + if err != nil { + log.Fatal(err) + } + + router3 := mux.NewRouter().StrictSlash(true) + + resp3 := ComponentResponse{} + + w3 := httptest.NewRecorder() + router3.HandleFunc("/v1/integrations/component/{component}/by-project-name/{project}/refresh", WrapMockAuthConfig(RefreshComponentToken, cfgKafka, &brk, str, nil)) + router3.ServeHTTP(w3, req3) + suite.Equal(200, w3.Code) + suite.Require().NoError(json.Unmarshal(w3.Body.Bytes(), &resp3)) + + suite.Equal("Component api key succesfully renewed", resp3.Status.Message) + suite.Equal("200", resp3.Status.Code) + suite.NotEmpty(resp3.Data.APIKey) + +} + func (suite *UsersHandlersTestSuite) TestUserCreateDuplicateRef() { postJSON := `{ diff --git a/routing.go b/routing.go index f620d3d9..e37afd07 100644 --- a/routing.go +++ b/routing.go @@ -91,6 +91,7 @@ var defaultRoutes = []APIRoute{ {"users:usageReport", "GET", "/users/usageReport", handlers.UserUsageReport}, {"users:show", "GET", "/users/{user}", handlers.UserListOne}, {"users:refreshToken", "POST", "/users/{user}:refreshToken", handlers.RefreshToken}, + {"users:componentRefreshToken", "POST", "/integrations/{component}/by-project-name/{project}/refresh", handlers.RefreshComponentToken}, {"users:create", "POST", "/users/{user}", handlers.UserCreate}, {"users:update", "PUT", "/users/{user}", handlers.UserUpdate}, {"users:delete", "DELETE", "/users/{user}", handlers.UserDelete}, diff --git a/stores/mock.go b/stores/mock.go index 8c5e47d2..961a2af0 100644 --- a/stores/mock.go +++ b/stores/mock.go @@ -146,21 +146,23 @@ func (mk *MockStore) Close() { } // InsertUser inserts a new user to the store -func (mk *MockStore) InsertUser(ctx context.Context, uuid string, projects []QProjectRoles, name string, fname string, lname string, org string, desc string, token string, email string, serviceRoles []string, createdOn time.Time, modifiedOn time.Time, createdBy string) error { +func (mk *MockStore) InsertUser(ctx context.Context, uuid string, projects []QProjectRoles, name string, fname string, lname string, org string, desc string, token string, email string, serviceRoles []string, comp string, compProject string, createdOn time.Time, modifiedOn time.Time, createdBy string) error { user := QUser{ - UUID: uuid, - Name: name, - Email: email, - Token: token, - FirstName: fname, - LastName: lname, - Organization: org, - Description: desc, - Projects: projects, - ServiceRoles: serviceRoles, - CreatedOn: createdOn, - ModifiedOn: modifiedOn, - CreatedBy: createdBy, + UUID: uuid, + Name: name, + Email: email, + Token: token, + FirstName: fname, + LastName: lname, + Organization: org, + Description: desc, + Projects: projects, + ServiceRoles: serviceRoles, + Component: comp, + ComponentProject: compProject, + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + CreatedBy: createdBy, } mk.UserList = append(mk.UserList, user) return nil @@ -331,7 +333,7 @@ func (mk *MockStore) AppendToUserProjects(ctx context.Context, userUUID string, } // UpdateUser updates user information -func (mk *MockStore) UpdateUser(ctx context.Context, uuid, fname, lname, org, desc string, projects []QProjectRoles, name string, email string, serviceRoles []string, modifiedOn time.Time) error { +func (mk *MockStore) UpdateUser(ctx context.Context, uuid, fname, lname, org, desc string, projects []QProjectRoles, name string, email string, serviceRoles []string, comp string, compProject string, modifiedOn time.Time) error { for i, item := range mk.UserList { if item.UUID == uuid { @@ -364,6 +366,14 @@ func (mk *MockStore) UpdateUser(ctx context.Context, uuid, fname, lname, org, de mk.UserList[i].Description = desc } + if comp != "" { + mk.UserList[i].Component = comp + } + + if compProject != "" { + mk.UserList[i].ComponentProject = compProject + } + mk.UserList[i].ModifiedOn = modifiedOn return nil @@ -946,20 +956,20 @@ func (mk *MockStore) Initialize() { // populate Users qRole := []QProjectRoles{QProjectRoles{"argo_uuid", []string{"consumer", "publisher"}}} qRoleB := []QProjectRoles{QProjectRoles{"argo_uuid2", []string{"consumer", "publisher"}}} - qUsr := QUser{0, "uuid0", qRole, "Test", "", "", "", "", "S3CR3T", "Test@test.com", []string{}, created, modified, ""} + qUsr := QUser{0, "uuid0", qRole, "Test", "", "", "", "", "S3CR3T", "Test@test.com", []string{}, "", "", created, modified, ""} mk.UserList = append(mk.UserList, qUsr) qRoleConsumerPub := []QProjectRoles{QProjectRoles{"argo_uuid", []string{"publisher", "consumer"}}} - mk.UserList = append(mk.UserList, QUser{1, "uuid1", qRole, "UserA", "FirstA", "LastA", "OrgA", "DescA", "S3CR3T1", "foo-email", []string{}, created, modified, ""}) - mk.UserList = append(mk.UserList, QUser{2, "uuid2", qRole, "UserB", "", "", "", "", "S3CR3T2", "foo-email", []string{}, created, modified, "uuid1"}) - mk.UserList = append(mk.UserList, QUser{3, "uuid3", qRoleConsumerPub, "UserX", "", "", "", "", "S3CR3T3", "foo-email", []string{}, created, modified, "uuid1"}) - mk.UserList = append(mk.UserList, QUser{4, "uuid4", qRoleConsumerPub, "UserZ", "", "", "", "", "S3CR3T4", "foo-email", []string{}, created, modified, "uuid1"}) - mk.UserList = append(mk.UserList, QUser{5, "same_uuid", qRoleConsumerPub, "UserSame1", "", "", "", "", "S3CR3T41", "foo-email", []string{}, created, modified, "uuid1"}) - mk.UserList = append(mk.UserList, QUser{6, "same_uuid", qRoleConsumerPub, "UserSame2", "", "", "", "", "S3CR3T42", "foo-email", []string{}, created, modified, "uuid1"}) - mk.UserList = append(mk.UserList, QUser{7, "uuid7", []QProjectRoles{}, "push_worker_0", "", "", "", "", "push_token", "foo-email", []string{"push_worker"}, created, modified, ""}) - mk.UserList = append(mk.UserList, QUser{8, "uuid8", qRoleB, "UserZ", "", "", "", "", "S3CR3T1", "foo-email", []string{}, created, modified, ""}) + mk.UserList = append(mk.UserList, QUser{1, "uuid1", qRole, "UserA", "FirstA", "LastA", "OrgA", "DescA", "S3CR3T1", "foo-email", []string{}, "", "", created, modified, ""}) + mk.UserList = append(mk.UserList, QUser{2, "uuid2", qRole, "UserB", "", "", "", "", "S3CR3T2", "foo-email", []string{}, "", "", created, modified, "uuid1"}) + mk.UserList = append(mk.UserList, QUser{3, "uuid3", qRoleConsumerPub, "UserX", "", "", "", "", "S3CR3T3", "foo-email", []string{}, "", "", created, modified, "uuid1"}) + mk.UserList = append(mk.UserList, QUser{4, "uuid4", qRoleConsumerPub, "UserZ", "", "", "", "", "S3CR3T4", "foo-email", []string{}, "", "", created, modified, "uuid1"}) + mk.UserList = append(mk.UserList, QUser{5, "same_uuid", qRoleConsumerPub, "UserSame1", "", "", "", "", "S3CR3T41", "foo-email", []string{}, "", "", created, modified, "uuid1"}) + mk.UserList = append(mk.UserList, QUser{6, "same_uuid", qRoleConsumerPub, "UserSame2", "", "", "", "", "S3CR3T42", "foo-email", []string{}, "", "", created, modified, "uuid1"}) + mk.UserList = append(mk.UserList, QUser{7, "uuid7", []QProjectRoles{}, "push_worker_0", "", "", "", "", "push_token", "foo-email", []string{"push_worker"}, "", "", created, modified, ""}) + mk.UserList = append(mk.UserList, QUser{8, "uuid8", qRoleB, "UserZ", "", "", "", "", "S3CR3T1", "foo-email", []string{}, "", "", created, modified, ""}) qRole1 := QRole{"topics:list_all", []string{"admin", "reader", "publisher"}} qRole2 := QRole{"topics:publish", []string{"admin", "publisher"}} @@ -1096,6 +1106,20 @@ func (mk *MockStore) GetUserFromToken(ctx context.Context, token string) (QUser, } +// GetComponentUser retrieves specific user that has attached component information +func (mk *MockStore) GetComponentUser(ctx context.Context, comp string, compProject string) (QUser, error) { + for _, item := range mk.UserList { + + if item.Component == comp && item.ComponentProject == compProject { + return item, nil + + } + } + + return QUser{}, errors.New("not found") + +} + // GetUserRoles returns the roles of a user in a project func (mk *MockStore) GetUserRoles(ctx context.Context, projectUUID string, token string) ([]string, string) { for _, item := range mk.UserList { diff --git a/stores/mongo_official_driver.go b/stores/mongo_official_driver.go index b8332c4a..ebcc21cd 100644 --- a/stores/mongo_official_driver.go +++ b/stores/mongo_official_driver.go @@ -4,13 +4,14 @@ import ( "context" "errors" "fmt" + "time" + log "github.com/sirupsen/logrus" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" - "time" ) const ( @@ -1170,7 +1171,7 @@ func (store *MongoStoreWithOfficialDriver) QueryUsers(ctx context.Context, proje // UpdateUser updates user information func (store *MongoStoreWithOfficialDriver) UpdateUser(ctx context.Context, uuid, fname, lname, org, - desc string, projects []QProjectRoles, name string, email string, serviceRoles []string, modifiedOn time.Time) error { + desc string, projects []QProjectRoles, name string, email string, serviceRoles []string, comp string, compProject string, modifiedOn time.Time) error { doc := bson.M{"uuid": uuid} results, err := store.QueryUsers(ctx, "", uuid, "") @@ -1218,6 +1219,14 @@ func (store *MongoStoreWithOfficialDriver) UpdateUser(ctx context.Context, uuid, curUsr.ServiceRoles = serviceRoles } + if comp != "" { + curUsr.Component = comp + } + + if compProject != "" { + curUsr.ComponentProject = compProject + } + curUsr.ModifiedOn = modifiedOn change := bson.M{"$set": curUsr} @@ -1270,21 +1279,23 @@ func (store *MongoStoreWithOfficialDriver) RemoveUser(ctx context.Context, uuid // InsertUser inserts a new user to the store func (store *MongoStoreWithOfficialDriver) InsertUser(ctx context.Context, uuid string, projects []QProjectRoles, - name string, firstName string, lastName string, org string, desc string, token string, email string, serviceRoles []string, createdOn time.Time, modifiedOn time.Time, createdBy string) error { + name string, firstName string, lastName string, org string, desc string, token string, email string, serviceRoles []string, comp string, compProject string, createdOn time.Time, modifiedOn time.Time, createdBy string) error { user := QUser{ - UUID: uuid, - Name: name, - Email: email, - Token: token, - FirstName: firstName, - LastName: lastName, - Organization: org, - Description: desc, - Projects: projects, - ServiceRoles: serviceRoles, - CreatedOn: createdOn, - ModifiedOn: modifiedOn, - CreatedBy: createdBy, + UUID: uuid, + Name: name, + Email: email, + Token: token, + FirstName: firstName, + LastName: lastName, + Organization: org, + Description: desc, + Projects: projects, + ServiceRoles: serviceRoles, + Component: comp, + ComponentProject: compProject, + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + CreatedBy: createdBy, } _, err := store.usersCollection.InsertOne(ctx, user) if err != nil { @@ -1324,6 +1335,38 @@ func (store *MongoStoreWithOfficialDriver) GetUserFromToken(ctx context.Context, return results[0], err } +// GetComponentUser returns specific user with attached component info +func (store *MongoStoreWithOfficialDriver) GetComponentUser(ctx context.Context, comp string, compProject string) (QUser, error) { + + query := bson.M{"component": comp, "component_project": compProject} + results, err := store.usersFindQueryProcessor.execute(ctx, query) + + if err != nil { + store.logErrorAndCrash(ctx, "GetUserFromToken", err) + return QUser{}, err + } + + if len(results) == 0 { + return QUser{}, DocNotFound{} + } + + if len(results) > 1 { + log.WithFields( + log.Fields{ + "type": "backend_log", + "trace_id": ctx.Value("trace_id"), + "component": comp, + "component_project": compProject, + "backend_service": "mongo", + "backend_hosts": store.Server, + }, + ).Warning("Multiple users with the same component info") + } + + // Search the found user for project roles + return results[0], err +} + // UsersCount returns the amount of users created in the given time period per project func (store *MongoStoreWithOfficialDriver) UsersCount(ctx context.Context, startDate, endDate time.Time, projectUUIDs []string) (map[string]int64, error) { diff --git a/stores/query_models.go b/stores/query_models.go index abc5fae4..7f91269f 100644 --- a/stores/query_models.go +++ b/stores/query_models.go @@ -93,20 +93,22 @@ type QUserRegistration struct { // QUser are the results of the QUser query type QUser struct { - ID interface{} `bson:"_id,omitempty"` - UUID string `bson:"uuid"` - Projects []QProjectRoles `bson:"projects"` - Name string `bson:"name"` - FirstName string `bson:"first_name,omitempty"` - LastName string `bson:"last_name,omitempty"` - Organization string `bson:"organization,omitempty"` - Description string `bson:"description,omitempty"` - Token string `bson:"token"` - Email string `bson:"email"` - ServiceRoles []string `bson:"service_roles"` - CreatedOn time.Time `bson:"created_on"` - ModifiedOn time.Time `bson:"modified_on"` - CreatedBy string `bson:"created_by"` + ID interface{} `bson:"_id,omitempty"` + UUID string `bson:"uuid"` + Projects []QProjectRoles `bson:"projects"` + Name string `bson:"name"` + FirstName string `bson:"first_name,omitempty"` + LastName string `bson:"last_name,omitempty"` + Organization string `bson:"organization,omitempty"` + Description string `bson:"description,omitempty"` + Token string `bson:"token"` + Email string `bson:"email"` + ServiceRoles []string `bson:"service_roles"` + Component string `bson:"component,omitempty"` + ComponentProject string `bson:"component_project,omitepmty"` + CreatedOn time.Time `bson:"created_on"` + ModifiedOn time.Time `bson:"modified_on"` + CreatedBy string `bson:"created_by"` } // QProjectRoles include information about projects and roles that user has diff --git a/stores/store.go b/stores/store.go index 00465cfc..0a98d1fe 100644 --- a/stores/store.go +++ b/stores/store.go @@ -51,12 +51,13 @@ type Store interface { HasUsers(ctx context.Context, projectUUID string, users []string) (bool, []string) PaginatedQueryUsers(ctx context.Context, pageToken string, pageSize int64, projectUUID string) ([]QUser, int64, string, error) QueryUsers(ctx context.Context, projectUUID string, uuid string, name string) ([]QUser, error) - UpdateUser(ctx context.Context, uuid, fname, lname, org, desc string, projects []QProjectRoles, name string, email string, serviceRoles []string, modifiedOn time.Time) error + UpdateUser(ctx context.Context, uuid, fname, lname, org, desc string, projects []QProjectRoles, name string, email string, serviceRoles []string, comp string, compProject string, modifiedOn time.Time) error AppendToUserProjects(ctx context.Context, userUUID string, projectUUID string, pRoles ...string) error UpdateUserToken(ctx context.Context, uuid string, token string) error RemoveUser(ctx context.Context, uuid string) error - InsertUser(ctx context.Context, uuid string, projects []QProjectRoles, name string, firstName string, lastName string, org string, desc string, token string, email string, serviceRoles []string, createdOn time.Time, modifiedOn time.Time, createdBy string) error + InsertUser(ctx context.Context, uuid string, projects []QProjectRoles, name string, firstName string, lastName string, org string, desc string, token string, email string, serviceRoles []string, comp string, compProject string, createdOn time.Time, modifiedOn time.Time, createdBy string) error GetUserFromToken(ctx context.Context, token string) (QUser, error) + GetComponentUser(ctx context.Context, comp string, compProject string) (QUser, error) UsersCount(ctx context.Context, startDate, endDate time.Time, projectUUIDs []string) (map[string]int64, error) GetUserRoles(ctx context.Context, projectUUID string, token string) ([]string, string) diff --git a/stores/store_test.go b/stores/store_test.go index 27aa94c5..217dfdc8 100644 --- a/stores/store_test.go +++ b/stores/store_test.go @@ -474,8 +474,8 @@ func (suite *StoreTestSuite) TestMockStore() { qRoles := []QProjectRoles{QProjectRoles{"argo_uuid", []string{"admin"}}, QProjectRoles{"argo_uuid2", []string{"admin", "viewer"}}} expUsr10 := QUser{UUID: "user_uuid10", Projects: qRoleAdmin1, Name: "newUser1", FirstName: "fname", LastName: "lname", Organization: "org1", Description: "desc1", Token: "A3B94A94V3A", Email: "fake@email.com", ServiceRoles: []string{}, CreatedOn: created, ModifiedOn: modified, CreatedBy: "uuid1"} expUsr11 := QUser{UUID: "user_uuid11", Projects: qRoles, Name: "newUser2", Token: "BX312Z34NLQ", Email: "fake@email.com", ServiceRoles: []string{}, CreatedOn: created, ModifiedOn: modified, CreatedBy: "uuid1"} - store.InsertUser(ctx, "user_uuid10", qRoleAdmin1, "newUser1", "fname", "lname", "org1", "desc1", "A3B94A94V3A", "fake@email.com", []string{}, created, modified, "uuid1") - store.InsertUser(ctx, "user_uuid11", qRoles, "newUser2", "", "", "", "", "BX312Z34NLQ", "fake@email.com", []string{}, created, modified, "uuid1") + store.InsertUser(ctx, "user_uuid10", qRoleAdmin1, "newUser1", "fname", "lname", "org1", "desc1", "A3B94A94V3A", "fake@email.com", []string{}, "", "", created, modified, "uuid1") + store.InsertUser(ctx, "user_uuid11", qRoles, "newUser2", "", "", "", "", "BX312Z34NLQ", "fake@email.com", []string{}, "", "", created, modified, "uuid1") usr10, _ := store.QueryUsers(ctx, "argo_uuid", "user_uuid10", "") usr11, _ := store.QueryUsers(ctx, "argo_uuid", "", "newUser2") @@ -492,7 +492,7 @@ func (suite *StoreTestSuite) TestMockStore() { // Test Update User usrUpdated := QUser{UUID: "user_uuid11", Projects: qRoles, Name: "updated_name", Token: "BX312Z34NLQ", Email: "fake@email.com", ServiceRoles: []string{"service_admin"}, CreatedOn: created, ModifiedOn: modified, CreatedBy: "uuid1"} - store.UpdateUser(ctx, "user_uuid11", "", "", "", "", nil, "updated_name", "", []string{"service_admin"}, modified) + store.UpdateUser(ctx, "user_uuid11", "", "", "", "", nil, "updated_name", "", []string{"service_admin"}, "", "", modified) usr11, _ = store.QueryUsers(ctx, "", "user_uuid11", "") suite.Equal(usrUpdated, usr11[0]) diff --git a/website/docs/api_advanced/api_integrations.md b/website/docs/api_advanced/api_integrations.md new file mode 100644 index 00000000..5007a92a --- /dev/null +++ b/website/docs/api_advanced/api_integrations.md @@ -0,0 +1,70 @@ +--- +id: api_integrations +title: Integrations/Components +sidebar_position: 10 +--- + +Calls for components that integrate with argo-web-api and access tenant data. + +## Refresh Access Token for a Component Integration + +Component administrators can request to refresh access keys for their assigned components within a specific tenant. + +``` +POST /integrations/components/{COMPONENT}/by-project-name/{PROJECT}/refresh +``` + +Where `{COMPONENT}` the name of the component account supported, example: +- monbox + +Where `{PROJECT}` the name of the project + +### Request headers + +``` +Accept: application/json +x-api-key: component-admin-s3cr3t +``` + +### Response +Headers: `Status: 200 OK` + +### Response Body + +Json Response example: +```json +{ + "status": { + "message": "component api key succesfully renewed", + "code": "200" + }, + "data": { + "api_key": "..." + } +} +``` + +## Errors + + +If a component access account has not yet been set up for a specific tenant, then the response will be: + +```json +{ + "status": { + "message": "Component monbox not configured for project FOO", + "code": "404" + } +} + +``` + +### Types of components + +Types of component accounts supported: + +- engine +- poem-admin +- poem-viewer +- monbox +- probe \ No newline at end of file diff --git a/website/docs/api_advanced/api_users.md b/website/docs/api_advanced/api_users.md index 6b85b233..10f6e7d3 100644 --- a/website/docs/api_advanced/api_users.md +++ b/website/docs/api_advanced/api_users.md @@ -714,6 +714,11 @@ POST "/v1/users/{user_name}" - service_roles: A list of service-wide roles. An example of service-wide role is `service_admin` which can manage projects or other users +### Optional component fields see more [here](#custom-id) +When we want a user account to be attached to a specific component that integrates with Messaging we use the following optional fields: +- component: the name of the component that will use this account +- component_project: the name of the project under which the component will use this account + ##### Available Roles ARGO Messaging Service has the following predefined project roles: @@ -728,7 +733,8 @@ and the following service-wide role: | Role | Description | |---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| service_admin | Users with `service_admin` role operate service wide. They are able to create, modify and delete projects. Also they are able to create, modify and delete users and assign them to projects. | +| service_admin | Users with `service_admin` role operate service wide. They are able to create, modify and delete projects. Also they are able to create, modify and delete users and assign them to projects. | +| component_admin | Users with `component_admin` role operate service wide for only component related calls. They are able to retrieve access tokens for component accounts under specific projects | ### Example request @@ -947,3 +953,52 @@ Success Response ### Errors Please refer to section [Errors](/api_basic/api_errors.md) to see all possible Errors + + +## Accounts tied to components {#comp} + +When integrating with external components certain user accounts can be used per component integration. To declare a user account that will be used by a specific account we need to specify the `component` and `component_project` fields during user creation like below: + +To create a publisher account for the component monbox for project PROJECT-101 we issue: +## [POST] Manage Users - Create new user + +This request creates a new user in a project + +### Request + +``` +POST "/v1/users/{user_name}" +``` + +### Post body: + +```json +{ + "projects": [ + { + "project": "ARGO", + "roles": [ + "project_admin" + ] + } + ], + "email": "foo-email", + "first_name": "fname-1", + "last_name": "lname-1", + "organization": "org-1", + "description": "desc-1", + "service_roles": [], + "component": "monbox", + "component_project": "PROJECT-101" +} +``` + +### Where + +- user_name: Name of the user +- projects: A list of Projects & associated roles that the user has on those projects +- email: User's email +- service_roles: A list of service-wide roles. An example of service-wide role is `service_admin` which can manage + projects or other users +- component: the name of the component that will use this account +- component_project: the name of the project under which the component will use this account \ No newline at end of file diff --git a/website/static/openapi/ams.yml b/website/static/openapi/ams.yml index 3ffb6983..199f91bc 100644 --- a/website/static/openapi/ams.yml +++ b/website/static/openapi/ams.yml @@ -1657,7 +1657,7 @@ paths: 404: $ref: "#/responses/404" 409: - $ref: "#/responses/409_no_offset_for_time" + $ref: "#/responses/409" 500: $ref: "#/responses/500" @@ -1700,7 +1700,7 @@ paths: 404: $ref: "#/responses/404" 409: - $ref: "#/responses/409_no_topic" + $ref: "#/responses/409" 500: $ref: "#/responses/500" @@ -1820,7 +1820,7 @@ paths: 404: $ref: "#/responses/404" 409: - $ref: "#/responses/409_action_conflict" + $ref: "#/responses/409" 500: $ref: "#/responses/500" @@ -2282,6 +2282,44 @@ paths: schema: $ref: "#/definitions/Version" + /integrations/components/{COMPONENT}/by-project-name/{PROJECT}/refresh: + post: + tags: + - Integrations + summary: Renew an account access key for a component that integrates with a specific project + description: For a specific project, renew the access key of a specific integration component + operationId: integrations.components.refresh + parameters: + - name: PROJECT + in: path + description: Name of the project + required: true + type: string + - name: COMPONENT + in: path + description: Name of the component + required: true + type: string + responses: + 200: + description: A response containing the refreshed api key + schema: + $ref: '#/definitions/ComponentApiKey' + 409: + description: Conflict if component account is not configured + schema: + $ref: '#/definitions/ComponentStatus' + 400: + $ref: "#/responses/400" + 401: + $ref: "#/responses/401" + 403: + $ref: "#/responses/403" + 404: + $ref: "#/responses/404" + 500: + $ref: "#/responses/500" + parameters: Details: name: details @@ -2371,46 +2409,36 @@ parameters: default: all of the registered projects responses: - 400: + "400": description: Invalid argument used schema: $ref: '#/definitions/ErrorMsg' - 401: + "401": description: Unauthorized user based on key schema: $ref: '#/definitions/ErrorMsg' - 403: + "403": description: Access Forbidden for the user on the resource schema: $ref: '#/definitions/ErrorMsg' - 404: + "404": description: Item not found schema: $ref: '#/definitions/ErrorMsg' - 408: + "408": description: Server timed out waiting for the request schema: $ref: '#/definitions/ErrorMsg' - 409: - description: Item already exists! - schema: - $ref: '#/definitions/ErrorMsg' - 409_action_conflict: - description: A conflict occurred due to the user's action! + "409": + description: | + Conflict. Possible causes: + * Item already exists + * A conflict occurred due to the user's action + * Subscription's topic doesn't exist + * Timestamp is out of bounds for the subscription's topic/partition schema: $ref: '#/definitions/ErrorMsg' - - 409_no_topic: - description: Subscription's topic doesn't exist - schema: - $ref: '#/definitions/ErrorMsg' - - 409_no_offset_for_time: - description: Timestamp is out of bounds for the subscription's topic/partition - schema: - $ref: '#/definitions/ErrorMsg' - - 500: + "500": description: Internal Error schema: $ref: '#/definitions/ErrorMsg' @@ -2538,6 +2566,34 @@ definitions: items: $ref: '#/definitions/Metric' + ComponentApiKey: + type: object + properties: + status: + type: object + properties: + message: + type: string + code: + type: string + data: + type: object + properties: + api_key: + type: string + ComponentStatus: + type: object + properties: + status: + type: object + properties: + message: + type: string + code: + type: string + + + Metric: type: object properties: @@ -2834,6 +2890,10 @@ definitions: type: array items: type: string + component: + type: string + component_project: + type: string created_on: type: string modified_on: