From 2b521dfc2a3b7f4b13c4ae8a1dffbb71bd4c87f4 Mon Sep 17 00:00:00 2001 From: wang522 Date: Thu, 16 Aug 2018 11:35:39 +0800 Subject: [PATCH] add application apis and test --- ssolib/app.go | 16 + ssolib/app_test.go | 14 - ssolib/application.go | 418 +++++++++++++++++++++++ ssolib/application_test.go | 203 +++++++++++ ssolib/email.go | 109 ++++++ ssolib/models/app/app.go | 11 + ssolib/models/application/application.go | 302 ++++++++++++++++ ssolib/models/group/dag.go | 60 ++-- ssolib/models/group/member.go | 118 +++---- ssolib/server.go | 4 +- 10 files changed, 1146 insertions(+), 109 deletions(-) create mode 100644 ssolib/application.go create mode 100644 ssolib/application_test.go create mode 100644 ssolib/email.go create mode 100644 ssolib/models/application/application.go diff --git a/ssolib/app.go b/ssolib/app.go index 407632d..8a1a1cb 100644 --- a/ssolib/app.go +++ b/ssolib/app.go @@ -234,4 +234,20 @@ type App struct { Secret string `json:"secret"` RedirectUri string `json:"redirect_uri"` AdminGroup *Group `json:"admin_group"` +} + +type AppInfoResource struct { + server.BaseResource +} +func (ai AppInfoResource) Get(ctx context.Context, r *http.Request) (int, interface{}) { + err := requireLogin(ctx) + if err != nil { + return http.StatusUnauthorized, err + } + mctx := getModelContext(ctx) + Apps, err := app.ListApps(mctx) + if err != nil { + return http.StatusInternalServerError, err + } + return http.StatusOK, Apps } \ No newline at end of file diff --git a/ssolib/app_test.go b/ssolib/app_test.go index 1cad00b..90e68a0 100644 --- a/ssolib/app_test.go +++ b/ssolib/app_test.go @@ -5,8 +5,6 @@ import ( "net/http" "strings" "github.com/laincloud/sso/Godeps/_workspace/src/github.com/stretchr/testify/assert" - "github.com/laincloud/sso/ssolib/models/iuser" - "github.com/laincloud/sso/ssolib/models/testbackend" ) func TestAppResource_Put(t *testing.T) { @@ -91,16 +89,4 @@ func createApp2(th *TestHelper) (int, interface{}){ r, _ := http.NewRequest("POST", "http://sso.example.com/api/apps", strings.NewReader(`{"fullname": "app2", "redirect_uri": "https://example.com"}`)) return AppsResource{}.Post(th.Ctx, r) -} - -func createTestUserWithEmail(th *TestHelper, username string) iuser.User { - err := testBack.CreateUser(&testbackend.TestUser{ - Name: username, - PasswordHash: []byte("test"), - Email: username+"@example.com", - }, true) - assert.Nil(th.T, err) - u, err := testBack.GetUserByName(username) - assert.Nil(th.T, err) - return u } \ No newline at end of file diff --git a/ssolib/application.go b/ssolib/application.go new file mode 100644 index 0000000..f216261 --- /dev/null +++ b/ssolib/application.go @@ -0,0 +1,418 @@ +package ssolib + +import ( + "net/http" + "strconv" + + "github.com/mijia/sweb/log" + "github.com/mijia/sweb/server" + "golang.org/x/net/context" + + "github.com/laincloud/sso/ssolib/models/iuser" + "github.com/laincloud/sso/ssolib/models" + "github.com/laincloud/sso/ssolib/models/group" + "github.com/mijia/sweb/form" + "github.com/laincloud/sso/ssolib/models/application" + "github.com/laincloud/sso/ssolib/models/role" + "errors" + "github.com/laincloud/sso/ssolib/models/app" +) + +type ApplicationsResource struct { + server.BaseResource +} + +func createApplication(ctx context.Context, id int, newApp *application.Application)(*application.Application, error) { + mctx := getModelContext(ctx) + adminIds, err := group.GetAdminIdsOfGroup(mctx, id) + if err != nil { + return nil, err + } + if adminIds == nil { + return nil, errors.New("no available admins") + } + if !(newApp.TargetType == "group" || newApp.TargetType == "role") { + return nil, errors.New("no such target type") + } + adminEmails := []string{} + batchEmails := "" + //send emails and create applications + for _, id := range adminIds { + admin, err := mctx.Back.GetUser(id) + if err != nil { + return nil, err + } + adminEmail := admin.GetProfile().GetEmail() + adminEmails = append(adminEmails, adminEmail) + batchEmails = batchEmails + adminEmail + " " + } + res, err := application.CreateApplication(mctx, newApp, adminEmails) + if err != nil { + return nil, err + } + var name string + if newApp.TargetType == "group" { + name = "group: " + res.TargetContent.Name + } else { + name = "role " + res.TargetContent.AppName + " " + res.TargetContent.Name + } + user := getCurrentUser(ctx) + content := user.GetName() + "applies to join" + name + "\n" + "https://text/api/applications/" + strconv.Itoa(res.Id) + err = SendTo("new application", content, batchEmails) + if err != nil { + log.Debug(err) + } + return res, nil +} + + +type ApplicationReq struct { + Target []application.TargetContent `json:"target"` + TargetType string `json:"target_type"` + Reason string `json:"reason"` +} + + +func checkIfQualified(ctx *models.Context, gId int, targetRole string, u iuser.User) (bool, error) { + if group.CheckIfInGroup(ctx, gId, u.GetId()) { + role, err := group.GetRoleOfUser(ctx, u.GetId(), gId) + if err != nil { + return false, err + } + if role == group.NORMAL && targetRole == "admin" { + return true, nil + } + return false, nil + } + return true, nil +} + +type RespWithLen struct { + Resp []application.Application `json:"applications"` + Total int `json:"total"` +} + +func getApplicationsForCommiting(ctx context.Context, from int, to int) (int, interface{}) { + Applications := []application.Application{} + mctx := getModelContext(ctx) + currentEmail := getCurrentUser(ctx).GetProfile().GetEmail() + pendingApplications, num, err := application.GetPendingApplicationByEmail(mctx, currentEmail, from, to) + if err != nil { + return http.StatusBadRequest, err + } + for _, pa := range pendingApplications { + a, err := application.GetApplication(mctx, pa.ApplicationId) + if err != nil { + return http.StatusBadRequest, err + } + Applications = append(Applications, *a) + } + respWithLen := RespWithLen{ + Resp: Applications, + Total: num, + } + return http.StatusOK, respWithLen +} + +func parseCommitEmails(mctx * models.Context, applications []application.Application, total int) (int, interface{}) { + resp := []application.Application{} + for _,a := range applications { + operatorList := []string{} + if a.Status == "initialled" { + pendApplications, err := application.GetPendingApplicationByApplicationId(mctx, a.Id) + if err != nil { + return http.StatusBadRequest, err + } + for _, p := range pendApplications { + operatorList = append(operatorList, p.OperatorEmail) + } + a.ParseOprEmail(operatorList) + } + resp = append(resp, a) + } + respWithLen := RespWithLen{ + Resp: resp, + Total: total, + } + return http.StatusOK, respWithLen +} + + +func (ay ApplicationsResource) Get(ctx context.Context, r *http.Request) (int, interface{}) { + err := requireLogin(ctx) + if err != nil { + return http.StatusUnauthorized, err + } + currentUser := getCurrentUser(ctx) + r.ParseForm() + mctx := getModelContext(ctx) + applicantEmail := r.Form.Get("applicant_email") + status := r.Form.Get("status") + fromStr := r.Form.Get("from") + toStr := r.Form.Get("to") + from := 0 + to := 50 + applications := []application.Application{} + var total int + if fromStr != "" && toStr != "" { + start, err := strconv.Atoi(fromStr) + if err != nil { + return http.StatusBadRequest, err + } + end, err := strconv.Atoi(toStr) + if err != nil { + return http.StatusBadRequest, err + } + if !(start >= 0 && start <= end) { + from = 0 + to = 50 + } else { + from = start + to = end + } + } + currentEmail := currentUser.GetProfile().GetEmail() + commitEmail := r.Form.Get("commit_email") + if commitEmail == currentEmail { + log.Debug(commitEmail) + return getApplicationsForCommiting(ctx, from, to) + } else if commitEmail == "" { + islain := false + laingroup, err := group.GetGroupByName(mctx, "lain") + if laingroup != nil && err == nil { + islain = group.CheckIfInGroup(mctx, laingroup.Id, currentUser.GetId()) + } + //lain member checks all applications + if islain && applicantEmail == "" { + apps, num, err := application.GetAllApplications(mctx, status, from, to) + if err != nil { + return http.StatusBadRequest, err + } + applications = apps + total = num + //user checks his applications + } else if applicantEmail == "" || applicantEmail == currentEmail{ + apps, num, err := application.GetApplications(mctx, currentEmail, status, from, to) + if err != nil { + return http.StatusBadRequest, err + } + applications = apps + total = num + //lain member checks some user's applications + } else if islain && applicantEmail != currentEmail { + apps, num, err := application.GetApplications(mctx, applicantEmail, status, from, to) + if err != nil { + return http.StatusBadRequest, err + } + applications = apps + total = num + } else { + return http.StatusBadRequest, "not qualified for the operation" + } + return parseCommitEmails(mctx, applications, total) + } + return http.StatusBadRequest, "commitEmail is not vaild" +} + +func createApplicationForGroup(ctx context.Context, req ApplicationReq) (int, interface{}) { + mctx := getModelContext(ctx) + if len(req.Target) < 1 { + return http.StatusBadRequest, "no enough target" + } + groupIds := []int{} + for i := 0; i < len(req.Target); i++ { + g, err := group.GetGroup(mctx, req.Target[i].Id) + if err != nil { + if err == group.ErrGroupNotFound { + return http.StatusBadRequest, err + } + return http.StatusInternalServerError, err + } + groupIds = append(groupIds,g.Id) + req.Target[i].Name = g.Name + } + return checkQualifiedAndCreateApplications(ctx, req, groupIds) +} + +func createApplicationForRole(ctx context.Context, req ApplicationReq) (int, interface{}) { + mctx := getModelContext(ctx) + if len(req.Target) < 1 { + return http.StatusBadRequest, "no enough target" + } + roleIds := []int{} + for i := 0; i < len(req.Target); i++ { + theRole, err := role.GetRole(mctx, req.Target[i].Id) + if err != nil { + if err == role.ErrRoleNotFound { + return http.StatusBadRequest, err + } + return http.StatusInternalServerError, err + } + _, err = group.GetGroup(mctx, req.Target[i].Id) + if err != nil { + if err == group.ErrGroupNotFound { + return http.StatusBadRequest, err + } + return http.StatusInternalServerError, err + } + theApp, err := app.GetApp(mctx, theRole.AppId) + if err != nil { + if err == app.ErrAppNotFound { + return http.StatusBadRequest, err + } + return http.StatusInternalServerError, err + } + req.Target[i].Name = theRole.Name + req.Target[i].AppName = theApp.FullName + roleIds = append(roleIds, req.Target[i].Id) + } + return checkQualifiedAndCreateApplications(ctx, req, roleIds) +} + +func checkQualifiedAndCreateApplications(ctx context.Context, req ApplicationReq, targets []int) (int, interface{}) { + mctx := getModelContext(ctx) + applicant := getCurrentUser(ctx) + var applications []application.Application + for i := 0; i < len(req.Target); i++ { + qualified, err := checkIfQualified(mctx, targets[i], req.Target[i].Role, applicant) + if err != nil { + return http.StatusInternalServerError, err + } + if !qualified { + temp := application.Application{ + TargetType: req.TargetType, + TargetContent: &req.Target[i], + Status: "existed", + } + applications = append(applications, temp) + } else { + newApp :=&application.Application{ + Reason: req.Reason, + TargetContent: &req.Target[i], + TargetType: req.TargetType, + ApplicantEmail: applicant.GetProfile().GetEmail(), + } + resp, err := createApplication(ctx, targets[i], newApp) + if err == nil && resp != nil{ + applications = append(applications, *resp) + } else { + log.Debug(err) + return http.StatusBadRequest, applications + } + } + } + return http.StatusOK, applications +} + + + +func (ay ApplicationsResource) Post(ctx context.Context, r *http.Request) (int, interface{}) { + err := requireLogin(ctx) + if err != nil { + return http.StatusUnauthorized, err + } + req := ApplicationReq{} + if err := form.ParamBodyJson(r, &req); err != nil { + return http.StatusBadRequest, err + } + if req.TargetType == "role" { + return createApplicationForRole(ctx, req) + } + if req.TargetType == "group" { + return createApplicationForGroup(ctx, req) + } + return http.StatusBadRequest, "no such type" +} + + + +type ApplicationResource struct { + server.BaseResource +} + +func (ah ApplicationResource) Put(ctx context.Context, r *http.Request) (int, interface{}) { + err := requireLogin(ctx) + if err != nil { + return http.StatusUnauthorized, err + } + currentUser := getCurrentUser(ctx) + mctx := getModelContext(ctx) + aid := params(ctx, "id") + r.ParseForm() + action := r.Form.Get("action") + aId, err := strconv.Atoi(aid) + if err != nil { + return http.StatusBadRequest, err + } + currentApp, err := application.GetApplication(mctx, aId) + if err != nil { + log.Debug(err) + return http.StatusBadRequest, err + } + switch action { + case "recall": + { + if currentApp.ApplicantEmail != currentUser.GetProfile().GetEmail() { + return http.StatusBadRequest, "only applicant can delete application" + } + err = application.RecallApplication(mctx, aId) + if err != nil { + return http.StatusBadRequest, err + } + return http.StatusNoContent, "application deleted" + } + case "approve", "reject": + { + targetGroup, err := group.GetGroup(mctx, currentApp.TargetContent.Id) + if err != nil { + if err == group.ErrGroupNotFound { + return http.StatusBadRequest, err + } + return http.StatusInternalServerError, err + } + commitRole, err := group.GetRoleOfUser(mctx, currentUser.GetId(), targetGroup.Id) + if err != nil { + log.Debug(err) + return http.StatusBadRequest, err + } + if commitRole != group.ADMIN { + return http.StatusBadRequest, "not qualified for the operation" + } + applicant, err := mctx.Back.GetUserByEmail(currentApp.ApplicantEmail) + if err != nil { + return http.StatusBadRequest, err + } + if action == "approve" { + var role group.MemberRole + if currentApp.TargetContent.Role == "admin" { + role = group.ADMIN + } else { + role = group.NORMAL + } + log.Debug("adding member") + err := targetGroup.AddMember(mctx, applicant, role) + if err != nil { + return http.StatusBadRequest, err + } + log.Debug("finishing handing application") + resp, err := application.FinishApplication(mctx, currentApp.Id, "approved", currentUser.GetProfile().GetEmail()) + if err != nil { + return http.StatusBadRequest, err + } + return http.StatusOK, resp + } else if action == "reject" { + log.Debug("finishing handing application") + resp, err := application.FinishApplication(mctx, currentApp.Id, "rejected", currentUser.GetProfile().GetEmail()) + if err != nil { + return http.StatusBadRequest, err + } + return http.StatusOK, resp + } + + } + default: + return http.StatusBadRequest, "no such operation" + } + return http.StatusInternalServerError, "dummy message" +} + diff --git a/ssolib/application_test.go b/ssolib/application_test.go new file mode 100644 index 0000000..4c42823 --- /dev/null +++ b/ssolib/application_test.go @@ -0,0 +1,203 @@ +package ssolib + +import ( + "testing" + "net/http" + "strings" + "github.com/laincloud/sso/Godeps/_workspace/src/github.com/stretchr/testify/assert" + "github.com/laincloud/sso/ssolib/models/iuser" + "github.com/laincloud/sso/ssolib/models/testbackend" + "github.com/laincloud/sso/ssolib/models/group" + "github.com/laincloud/sso/ssolib/models/application" + "strconv" +) + +func TestApply_Post(t *testing.T) { + //test applying for group + th := NewTestHelper(t) + createTestUserAndTargets(th) + th.login("testuser") + code1 ,resp1 := callPostApplicationOfGroup(th) + assert.Equal(t, code1, http.StatusOK) + a, ok1 := resp1.([]application.Application) + assert.True(t, ok1) + assert.Equal(t, "testuser@creditease.cn", a[0].ApplicantEmail) + //test applying for role + code2 ,resp2 := callPostApplicationOfRole(th) + t.Log(resp2) + assert.Equal(t, http.StatusOK, code2) + b, ok2 := resp2.([]application.Application) + assert.True(t, ok2) + assert.Equal(t, application.TargetContent{Id:1, Role:"normal", Name:"role1", AppName:"app1"}, *b[0].TargetContent) + +} + + +func createTestUserAndTargets(th *TestHelper) iuser.User { + mctx := getModelContext(th.Ctx) + admin := createTestUserWithEmail(th, "testadmin") + u := createTestUserWithEmail(th, "testuser") + th.loginWithScope("testadmin", "write:app write:group write:role") + createApp(th) + createRootRole(th) + callPostGroupsWithGroupName(th, "group1") + g1 ,_:= group.GetGroupByName(mctx, "group1") + g1.AddMember(mctx, admin, 1) + callPostGroupsWithGroupName(th, "group2") + g2 ,_:= group.GetGroupByName(mctx, "group2") + g2.AddMember(mctx, admin, 1) + callPostGroupsWithGroupName(th, "lain") + g3 ,_:= group.GetGroupByName(mctx, "lain") + g3.AddMember(mctx, admin, 1) + th.logout() + return u +} + + +//create two users and three groups. one user is testuser, and the other is testadmin. +//Two normal groups and one lain groups. +//testadmin is the admin of three groups. +//testuser will apply to join in two normal groups. +func createTestUserWithEmail(th *TestHelper, username string) iuser.User { + err := testBack.CreateUser(&testbackend.TestUser{ + Name: username, + PasswordHash: []byte("test"), + Email: username+"@creditease.cn", + }, true) + assert.Nil(th.T, err) + u, err := testBack.GetUserByName(username) + assert.Nil(th.T, err) + return u +} + + +func callPostApplicationOfGroup(th *TestHelper) (int, interface{}) { + r, _ := http.NewRequest("POST", "http://sso.example.com/api/applications", + strings.NewReader(`{"target_type": "group", "reason":"testing", "target": [{"id": 2,"role":"normal"}, {"id": 3,"role":"normal"}]}`)) + return ApplicationsResource{}.Post(th.Ctx, r) +} + +func callPostApplicationOfRole(th *TestHelper) (int, interface{}) { + r, _ := http.NewRequest("POST", "http://sso.example.com/api/applications", + strings.NewReader(`{"target_type": "role", "reason":"testing", "target": [{"id": 1,"role":"normal"}]}`)) + return ApplicationsResource{}.Post(th.Ctx, r) +} + +func TestApply_Get(t *testing.T) { + th := NewTestHelper(t) + createTestUserAndTargets(th) + th.login("testuser") + callPostApplicationOfGroup(th) + code1, resp1 := callGetApplication(th) + assert.Equal(t, http.StatusOK, code1) + a, ok := resp1.(RespWithLen) + assert.True(t, ok) + assert.Equal(t, "testuser@creditease.cn", a.Resp[0].ApplicantEmail) + th.logout() + th.login("testadmin") + //lain member can see others' application + code2, resp2 :=callGetApplication(th) + assert.Equal(t, http.StatusOK, code2) + b, ok2 := resp2.(RespWithLen) + assert.True(t, ok2) + assert.Equal(t, 2 , len(b.Resp)) + //test admin's getting applications + code3, resp3 := callGetApplicationByCommit(th) + assert.Equal(t, http.StatusOK, code3) + c, ok3 := resp3.(RespWithLen) + assert.True(t, ok3) + assert.Equal(t, 2, len(c.Resp)) + code4, resp4 := callPostApplicationHandle(th, strconv.Itoa(1),"approve") + assert.Equal(t, http.StatusOK, code4) + d, ok4 := resp4.(*application.Application) + assert.True(t, ok4) + assert.Equal(t, "approved", d.Status) + //test getting by status + code5, resp5 := callGetApplicationByStatus(th) + assert.Equal(t, http.StatusOK, code5) + e, ok5 := resp5.(RespWithLen) + assert.True(t, ok5) + assert.Equal(t, 1 , len(e.Resp)) + assert.Equal(t, "approved", e.Resp[0].Status) + //test getting by time + //get 2nd application which has a initialled status + code6,resp6 := callGetApplicationByTime(th) + assert.Equal(t, http.StatusOK, code6) + f, ok6 := resp6.(RespWithLen) + assert.True(t, ok6) + assert.Equal(t, 1 , len(f.Resp)) + assert.Equal(t, 2, f.Total) + assert.Equal(t, "initialled", f.Resp[0].Status) +} + +func callGetApplication(th *TestHelper) (int, interface{}) { + r, _ := http.NewRequest("GET", "http://sso.example.com/api/applications?"+"applicant_email=testuser@creditease.cn", nil) + return ApplicationsResource{}.Get(th.Ctx, r) +} + +func callGetApplicationByTime(th *TestHelper) (int, interface{}) { + r, _ := http.NewRequest("GET", "http://sso.example.com/api/applications?"+"from=1&to=1", nil) + return ApplicationsResource{}.Get(th.Ctx, r) +} + +func callGetApplicationByCommit(th *TestHelper) (int, interface{}) { + r, _ := http.NewRequest("GET", "http://sso.example.com/api/applications?"+"commit_email=testadmin@creditease.cn", nil) + return ApplicationsResource{}.Get(th.Ctx, r) +} + +func callGetApplicationByStatus(th *TestHelper) (int, interface{}) { + r, _ := http.NewRequest("GET", "http://sso.example.com/api/applications?"+"applicant_email=testuser@creditease.cn&status=approved", nil) + return ApplicationsResource{}.Get(th.Ctx, r) +} + +func TestApplicationHandle_Post(t *testing.T) { + th := NewTestHelper(t) + mctx := getModelContext(th.Ctx) + //test handling application of group + user := createTestUserAndTargets(th) + th.login("testuser") + callPostApplicationOfGroup(th) + th.logout() + th.login("testadmin") + code1, resp1 := callPostApplicationHandle(th, strconv.Itoa(1),"approve") + assert.Equal(t, http.StatusOK, code1) + a, ok1 := resp1.(*application.Application) + assert.True(t, ok1) + assert.Equal(t, "approved", a.Status) + gs ,_ := group.GetGroupRolesOfUser(mctx, user) + assert.Equal(t, 1, len(gs)) + th.logout() + th.login("testuser") + code2, _ := callPostApplicationHandle(th, strconv.Itoa(2),"recall") + assert.Equal(t, http.StatusNoContent, code2) + code3, resp3 := callGetApplication(th) + assert.Equal(t, http.StatusOK, code3) + c, ok3 := resp3.(RespWithLen) + assert.True(t, ok3) + assert.Equal(t, 1, len(c.Resp)) + //test handling application of role + th.logout() + th.loginWithScope("testadmin", "write:app") + createApp(th) + createRootRole(th) + th.logout() + th.login("testuser") + callPostApplicationOfRole(th) + th.logout() + th.login("testadmin") + code4, _ := callPostApplicationHandle(th, strconv.Itoa(3),"approve") + assert.Equal(t, http.StatusOK, code4) + u ,_ := mctx.Back.GetUserByName("testuser") + g, _ := group.GetGroupRolesOfUser(mctx, u) + assert.Equal(t, 2, len(g)) + +} + +func callPostApplicationHandle (th *TestHelper, id string, action string) (int, interface{}) { + url := "http://sso.example.com/api/applications/" + id + "?action=" + action + r, _ := http.NewRequest("PUT", url, nil) + aMock := mockParams(th, map[string]string{ + "id": id}) + defer aMock.restore() + return ApplicationResource{}.Put(th.Ctx, r) +} \ No newline at end of file diff --git a/ssolib/email.go b/ssolib/email.go new file mode 100644 index 0000000..f26cc62 --- /dev/null +++ b/ssolib/email.go @@ -0,0 +1,109 @@ +package ssolib + +import ( + "bytes" + b64 "encoding/base64" + "fmt" + "net/mail" + "net/smtp" + "net/textproto" + "strings" +) + +type Mail struct { + From string + To string + Subject string + HTML string +} + + +type loginAuth struct { + username, password string +} + +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + } + } + return nil, nil +} + + +// Send does what it is supposed to do. +func (m *Mail) Send(host string, port int, user, pass string) error { + // validate from address + from, err := mail.ParseAddress(m.From) + if err != nil { + fmt.Println(err) + return err + } + + // validate to address + tos, err := mail.ParseAddressList(m.To) + if err != nil { + fmt.Println(err) + return err + } + + var addresses string + for _, to := range tos { + addresses = addresses + "," + to.Address + } + + // set headers for html email + header := textproto.MIMEHeader{} + header.Set(textproto.CanonicalMIMEHeaderKey("from"), from.Address) + header.Set(textproto.CanonicalMIMEHeaderKey("to"), addresses[1:]) + header.Set(textproto.CanonicalMIMEHeaderKey("content-type"), "text/html; charset=UTF-8") + header.Set(textproto.CanonicalMIMEHeaderKey("mime-version"), "1.0") + header.Set(textproto.CanonicalMIMEHeaderKey("subject"), fmt.Sprintf("=?utf-8?B?%s?=", b64.StdEncoding.EncodeToString([]byte(m.Subject)))) + + // init empty message + var buffer bytes.Buffer + + // write header + for key, value := range header { + buffer.WriteString(fmt.Sprintf("%s: %s\r\n", key, value[0])) + } + + // write body + buffer.WriteString(fmt.Sprintf("\r\n%s", m.HTML)) + + // send email + addr := fmt.Sprintf("%s:%d", host, port) + auth := LoginAuth(user, pass) + mails := strings.Split(m.To, ",") + + if mails[len(mails)-1] == "" { + mails = mails[:len(mails)-1] + } + + return smtp.SendMail(addr, auth, from.Address, mails, buffer.Bytes()) +} + + +func SendTo(subject, content, to string) error { + m := Mail{ + From: "noreply@bdp.yixin.com", + To: to, + Subject: subject, + } + m.HTML = content + + err := m.Send("mail.bdp.cc", 25, "username", "password") + return err +} diff --git a/ssolib/models/app/app.go b/ssolib/models/app/app.go index b9c39c5..bfbee7d 100644 --- a/ssolib/models/app/app.go +++ b/ssolib/models/app/app.go @@ -180,3 +180,14 @@ func AppNameExist(ctx *models.Context, appName string) (bool, error) { return true, nil } } + +type AppInfo struct { + Id int `json:"id"` + FullName string `json:"fullname"` +} + +func ListApps(ctx *models.Context) ([]AppInfo, error) { + apps := []AppInfo{} + err := ctx.DB.Select(&apps, "SELECT id, fullname FROM app") + return apps, err +} \ No newline at end of file diff --git a/ssolib/models/application/application.go b/ssolib/models/application/application.go new file mode 100644 index 0000000..0f9e834 --- /dev/null +++ b/ssolib/models/application/application.go @@ -0,0 +1,302 @@ +package application + +import ( + "github.com/mijia/sweb/log" + "github.com/laincloud/sso/ssolib/models" + "encoding/json" +) + +const createApplicationTableSQL = ` +CREATE TABLE IF NOT EXISTS application ( + id INT NOT NULL AUTO_INCREMENT, + applicant_email VARCHAR(64) NULL DEFAULT NULL, + target_type VARCHAR(64) NULL DEFAULT NULL, + target VARCHAR(128) CHARACTER SET utf8 NOT NULL, + reason VARCHAR(1024) CHARACTER SET utf8 NOT NULL, + status VARCHAR(64) NULL DEFAULT NULL, + commit_email VARCHAR(64) NULL DEFAULT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY (applicant_email, target_type, target) +) DEFAULT CHARSET=latin1` + + +const createPENDING_ApplicationStatusTableSQL = ` +CREATE TABLE IF NOT EXISTS pending_application ( + id INT NOT NULL AUTO_INCREMENT, + application_id INT NOT NULL, + operator_email VARCHAR(64) NULL DEFAULT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY (application_id,operator_email ) +) DEFAULT CHARSET=latin1` + + +func InitDatabase(ctx *models.Context) { + ctx.DB.MustExec(createApplicationTableSQL) + ctx.DB.MustExec(createPENDING_ApplicationStatusTableSQL) +} + +type Application struct { + Id int `json:"id"` + ApplicantEmail string `db:"applicant_email" json:"applicant_email"` + TargetType string `db:"target_type" json:"target_type"` + TargetStr string `db:"target"` + TargetContent *TargetContent `json:"target"` + Reason string `json:"reason"` + Status string `json:"status"` + CommitEmail string `db:"commit_email" json:"commit_email"` + Created string `json:"created"` + Updated string `json:"updated"` +} + +func (a *Application) MarshalJson() ([]byte, error) { + ret := map[string]interface{}{ + "id": a.Id, + } + return json.Marshal(ret) +} + +type TargetContent struct { + Id int `json:"id"` + Role string `json:"role"` + Name string `json:"name"` + AppName string `json:"app_name"` +} + +func (a *Application) ParseTarget() { + if a.TargetContent == nil && a.TargetStr !="" { + if a.TargetType == "role" { + var temp TargetContent + json.Unmarshal([]byte(a.TargetStr), &(temp)) + a.TargetContent= &TargetContent{ + temp.Id, + temp.Role, + temp.Name, + temp.AppName, + } + } else if a.TargetType == "group" { + var temp TargetContent + json.Unmarshal([]byte(a.TargetStr), &(temp)) + a.TargetContent= &TargetContent{ + temp.Id, + temp.Role, + temp.Name, + "", + } + } + } + a.TargetStr = "" +} + +func (a *Application) ParseOprEmail(emails []string) { + if a.Status == "initialled" { + t, err:= json.Marshal(emails) + if err != nil { + log.Debug(err) + } + a.CommitEmail = string(t) + } +} + +type PendingApplication struct { + Id int `json:"id"` + ApplicationId int `db:"application_id" json:"application_id"` + OperatorEmail string `db:"operator_email" json:"operator_email"` + Created string `json:"created"` + Updated string `json:"updated"` +} + + +func GetPendingApplicationByEmail(ctx *models.Context, oprEmail string, from int, to int) ([]PendingApplication,int, error) { + applicationstatus := []PendingApplication{} + var total []int + err := ctx.DB.Select(&total, "SELECT count(*) FROM pending_application WHERE operator_email=?", oprEmail) + if err != nil { + return nil, -1, err + } + err = ctx.DB.Select(&applicationstatus, "SELECT * FROM pending_application WHERE operator_email=? ORDER BY created DESC LIMIT ?, ?", oprEmail, from, to - from + 1) + if err != nil { + return nil, -1, err + } + return applicationstatus, total[0], nil +} + + +func GetPendingApplicationByApplicationId(ctx *models.Context, id int) ([]PendingApplication, error) { + log.Debug("start getting pending_application") + applications := []PendingApplication{} + err := ctx.DB.Select(&applications, "SELECT * FROM pending_application WHERE application_id=?", id) + if err != nil { + log.Debug(err) + return nil, err + } + return applications, nil +} + +func CreateApplication (mctx *models.Context, newApp *Application, adminEmails []string) (*Application, error) { + log.Debug("CreateApplication") + tx := mctx.DB.MustBegin() + t, err:= json.Marshal(newApp.TargetContent) + if err != nil { + return nil, err + } + result, err := tx.Exec( + "INSERT INTO application (applicant_email, target_type, target, reason, status, commit_email) VALUES(?, ?, ?, ?, ?, ?)", + newApp.ApplicantEmail, newApp.TargetType, string(t), newApp.Reason, "initialled", "NULL") + if err != nil { + tx.Rollback() + return nil, err + } + id, err := result.LastInsertId() + if err != nil { + tx.Rollback() + return nil, err + } + for _, adminEmail := range adminEmails { + _, err = tx.Exec( + "INSERT INTO pending_application (application_id, operator_email) VALUES(?, ?)", id, adminEmail) + if err != nil { + tx.Rollback() + return nil, err + } + } + err = tx.Commit() + if err != nil { + return nil, err + } + return GetApplication(mctx, int(id)) +} + + +func FinishApplication (ctx *models.Context, id int, status string, commitEmail string) (*Application, error) { + tx := ctx.DB.MustBegin() + _, err := tx.Exec( + "UPDATE application SET status=?, commit_email=? WHERE id=?", status, commitEmail, id) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = tx.Exec("DELETE FROM pending_application WHERE application_id=?", id) + if err != nil { + tx.Rollback() + return nil, err + } + if err := tx.Commit(); err != nil { + return nil, err + } + return GetApplication(ctx, id) +} + + +func GetApplications(ctx *models.Context, email string, status string, from int, to int) ([]Application, int, error) { + applications := []Application{} + var total int + if status != "" { + rows, err := ctx.DB.Query("SELECT count(*) FROM application WHERE applicant_email=? AND status=?", email, status) + if err != nil { + return nil, -1, err + } + for rows.Next() { + if err = rows.Scan(&total); err != nil { + return nil, -1, err + } + } + err = ctx.DB.Select(&applications, "SELECT * FROM application WHERE applicant_email=? AND status=? ORDER BY created DESC LIMIT ?, ?", email, status, from, to - from + 1) + if err != nil { + return nil, -1, err + } + }else { + rows, err := ctx.DB.Query("SELECT count(*) FROM application WHERE applicant_email=?", email) + if err != nil { + return nil, -1, err + } + for rows.Next() { + if err = rows.Scan(&total); err != nil { + return nil, -1, err + } + } + err = ctx.DB.Select(&applications, "SELECT * FROM application WHERE applicant_email=? ORDER BY created DESC LIMIT ?, ?", email, from, to - from + 1) + if err != nil { + return nil, -1, err + } + } + Applications := []Application{} + for _,a := range applications { + a.ParseTarget() + Applications = append(Applications, a) + } + + return Applications, total, nil +} + +func GetAllApplications(ctx *models.Context, status string, from int, to int) ([]Application, int, error) { + applications := []Application{} + var total int + if status != "" { + rows, err := ctx.DB.Query("SELECT count(*) FROM application WHERE status=?", status) + if err != nil { + return nil, -1, err + } + for rows.Next() { + if err = rows.Scan(&total); err != nil { + return nil, -1, err + } + } + err = ctx.DB.Select(&applications, "SELECT * FROM application WHERE status=? ORDER BY created DESC LIMIT ?, ?",status, from, to - from + 1) + if err != nil { + return nil, -1, err + } + }else { + rows, err := ctx.DB.Query("SELECT count(*) FROM application") + if err != nil { + return nil, -1, err + } + for rows.Next() { + if err = rows.Scan(&total); err != nil { + return nil, -1, err + } + } + err = ctx.DB.Select(&applications, "SELECT * FROM application ORDER BY created DESC LIMIT ?, ?", from, to - from + 1) + if err != nil { + return nil, -1, err + } + } + Applications := []Application{} + for _,a := range applications { + a.ParseTarget() + Applications = append(Applications, a) + } + + return Applications, total, nil +} + + +func GetApplication(ctx *models.Context, id int) (*Application, error) { + application := Application{} + err := ctx.DB.Get(&application, "SELECT * FROM application WHERE id=?", id) + if err != nil { + log.Debug(err) + return nil, err + } + application.ParseTarget() + return &application, nil +} + +func RecallApplication(ctx *models.Context, id int) (error) { + tx := ctx.DB.MustBegin() + _, err := tx.Exec("DELETE FROM application WHERE id=?", id) + if err != nil { + tx.Rollback() + return err + } + _, err = tx.Exec("DELETE FROM pending_application WHERE application_id=?", id) + if err != nil { + tx.Rollback() + return err + } + err = tx.Commit() + return err +} diff --git a/ssolib/models/group/dag.go b/ssolib/models/group/dag.go index eae57a1..09d27f3 100644 --- a/ssolib/models/group/dag.go +++ b/ssolib/models/group/dag.go @@ -507,34 +507,54 @@ func GetGroupMemberRole(ctx *models.Context, fatherId int, sonId int) (role Memb func ListFathersOfGroups(ctx *models.Context, sonIds []int) ([]int, error) { fathers := []int{} - query, args, err := sqlx.In("SELECT father_id FROM groupdag WHERE son_id IN(?)", sonIds) - if err != nil { - return nil, err - } - query = ctx.DB.Rebind(query) - err = ctx.DB.Select(&fathers, query, args...) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil + for i := 0; ; i++ { + partFathers := make([]int, 0, MAXREQUEST) + finished := i * MAXREQUEST + remain := len(sonIds) - finished + processing := finished + MAXREQUEST + if remain <= MAXREQUEST { + processing = finished + remain + } + query, args, err := sqlx.In("SELECT father_id FROM groupdag WHERE son_id IN(?)", sonIds[finished:processing]) + if err != nil { + return nil, err + } + query = ctx.DB.Rebind(query) + err = ctx.DB.Select(&partFathers, query, args...) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + fathers = append(fathers, partFathers...) + if remain <= MAXREQUEST { + break } - return nil, err } return fathers, nil } func ListAdminFathersOfGroups(ctx *models.Context, sonIds []int) ([]int, error) { fathers := []int{} - query, args, err := sqlx.In("SELECT father_id FROM groupdag WHERE son_id IN(?) AND role=?", sonIds, ADMIN) - if err != nil { - return nil, err - } - query = ctx.DB.Rebind(query) - err = ctx.DB.Select(&fathers, query, args...) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil + for i := 0; ; i++ { + partFathers := make([]int, 0, MAXREQUEST) + finished := i * MAXREQUEST + remain := len(sonIds) - finished + processing := finished + MAXREQUEST + if remain <= MAXREQUEST { + processing = finished + remain + } + query, args, err := sqlx.In("SELECT father_id FROM groupdag WHERE role=? AND son_id IN(?)", ADMIN, sonIds[finished:processing]) + if err != nil { + return nil, err + } + query = ctx.DB.Rebind(query) + err = ctx.DB.Select(&partFathers, query, args...) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + fathers = append(fathers, partFathers...) + if remain <= MAXREQUEST { + break } - return nil, err } return fathers, nil } diff --git a/ssolib/models/group/member.go b/ssolib/models/group/member.go index 5a58d9f..6db9cc6 100644 --- a/ssolib/models/group/member.go +++ b/ssolib/models/group/member.go @@ -9,6 +9,7 @@ import ( "github.com/laincloud/sso/ssolib/models" "github.com/laincloud/sso/ssolib/models/iuser" + "errors" ) const MAXREQUEST int = 100 @@ -30,12 +31,6 @@ type GroupRole struct { Role MemberRole // some user's role in the group } -type GroupUser struct { - GroupId int `db:"group_id"` - UserId int `db:"user_id"` - Role MemberRole -} - const createUserGroupTableSQL = ` CREATE TABLE IF NOT EXISTS user_group ( user_id INT NOT NULL, @@ -272,22 +267,22 @@ func GetGroupRolesDirectlyOfUser(ctx *models.Context, user iuser.User) ([]GroupR return ret, nil } -func getSSOLIBRoleDirectlyOfUser(ctx *models.Context, user iuser.User) (map[int]MemberRole, error) { - grMap := make(map[int]MemberRole) - if rows, err := ctx.DB.Query("SELECT group_id, role FROM user_group WHERE user_id=?", user.GetId()); err != nil { +func getAdminGroupDirectlyOfUser(ctx *models.Context, user iuser.User) ([]int, error) { + groups := []int{} + if rows, err := ctx.DB.Query("SELECT group_id FROM user_group WHERE user_id=? AND role=?", user.GetId(), ADMIN); err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } else { for rows.Next() { - var groupId, role int - if err := rows.Scan(&groupId, &role); err == nil { - grMap[groupId] = MemberRole(role) + var groupId int + if err := rows.Scan(&groupId); err == nil { + groups = append(groups, groupId) } } } - return grMap, nil + return groups, nil } func getSSOLIBGroupRolesDirectlyOfUser(ctx *models.Context, user iuser.User) ([]GroupRole, error) { @@ -425,44 +420,23 @@ func getGroupRolesRecursivelyOfUser(ctx *models.Context, user iuser.User, adminO } func getAdminGroupsRecursivelyOfUser(ctx *models.Context, user iuser.User) (map[int]MemberRole, error) { - preQueue := make([]int, 0, MAXREQUEST) + queue := make([]int, 0, MAXREQUEST) ret := make(map[int]MemberRole) - groupRoles, err := getSSOLIBRoleDirectlyOfUser(ctx, user) + adminGroups, err := getAdminGroupDirectlyOfUser(ctx, user) if err != nil { return nil, err } - for gId, role := range groupRoles { - if role == ADMIN { - ret[gId] = ADMIN - preQueue = append(preQueue, gId) - } + for _, gId := range adminGroups { + ret[gId] = ADMIN + queue = append(queue, gId) } for true { - if len(preQueue) == 0 { + if len(queue) == 0 { break } - rawFathers := make([]int, 0, MAXREQUEST) - //when 1 < len <= MAXREQUEST, loop one time - for i := 0; ; i++ { - offset := i * MAXREQUEST - remain := len(preQueue) - offset - if remain <= MAXREQUEST { - queue := make([]int, remain, MAXREQUEST) - copy(queue, preQueue[offset:offset + remain]) - partFathers, err := ListAdminFathersOfGroups(ctx, queue) - if err != nil { - return nil, err - } - rawFathers = append(rawFathers,partFathers...) - break - } - queue := make([]int, MAXREQUEST, MAXREQUEST) - copy(queue, preQueue[offset:offset + MAXREQUEST]) - partFathers, err := ListAdminFathersOfGroups(ctx, queue) - if err != nil { - return nil, err - } - rawFathers = append(rawFathers,partFathers...) + rawFathers, err := ListAdminFathersOfGroups(ctx, queue) + if err != nil { + return nil, err } //deduplicate fathers := make([]int, 0, MAXREQUEST) @@ -472,13 +446,13 @@ func getAdminGroupsRecursivelyOfUser(ctx *models.Context, user iuser.User) (map[ fathers = append(fathers, f) } } - preQueue = fathers + queue = fathers } return ret, nil } func getGroupsRecursivelyOfUser(ctx *models.Context, user iuser.User) (map[int]MemberRole, error) { - preQueue := make([]int, 0, MAXREQUEST) + queue := make([]int, 0, MAXREQUEST) ret := make(map[int]MemberRole) groups, err := getGroupsDirectlyOfUser(ctx, user) if err != nil { @@ -486,34 +460,15 @@ func getGroupsRecursivelyOfUser(ctx *models.Context, user iuser.User) (map[int]M } for _, v := range groups { ret[v.Id] = NORMAL - preQueue = append(preQueue, v.Id) + queue = append(queue, v.Id) } for true { - if len(preQueue) == 0 { + if len(queue) == 0 { break } - rawFathers := make([]int, 0, MAXREQUEST) - //when 1 < len <= MAXREQUEST, loop one time - for i := 0; ; i++ { - offset := i * MAXREQUEST - remain := len(preQueue) - offset - if remain <= MAXREQUEST { - queue := make([]int, remain, MAXREQUEST) - copy(queue, preQueue[offset:offset + remain]) - partFathers, err := ListFathersOfGroups(ctx, queue) - if err != nil { - return nil, err - } - rawFathers = append(rawFathers,partFathers...) - break - } - queue := make([]int, MAXREQUEST, MAXREQUEST) - copy(queue, preQueue[offset:offset + MAXREQUEST]) - partFathers, err := ListFathersOfGroups(ctx, queue) - if err != nil { - return nil, err - } - rawFathers = append(rawFathers,partFathers...) + rawFathers, err := ListFathersOfGroups(ctx, queue) + if err != nil { + return nil, err } //deduplicate fathers := make([]int, 0, MAXREQUEST) @@ -523,17 +478,32 @@ func getGroupsRecursivelyOfUser(ctx *models.Context, user iuser.User) (map[int]M fathers = append(fathers, f) } } - preQueue = fathers + queue = fathers } return ret, nil } -func GetRoleOfUser(mctx *models.Context, uId int, gId int) (MemberRole, error) { - role := GroupUser{} - err := mctx.DB.Get(&role, "SELECT * FROM user_group WHERE user_id =? AND group_id=?", +func GetRoleOfUser(mctx *models.Context, uId int, gId int) (MemberRole, error){ + Roles := []MemberRole{} + err := mctx.DB.Select(&Roles, "SELECT role FROM user_group WHERE user_id =? AND group_id=?", uId, gId) if err != nil { return NORMAL, err } - return role.Role, nil + if len(Roles) != 1 { + return NORMAL, errors.New("wrong number of roles") + } + return Roles[0], nil +} + +func CheckIfInGroup(ctx *models.Context, groupId int, uId int) (bool) { + users := []int{} + err := ctx.DB.Select(&users, "SELECT user_id FROM user_group WHERE group_id=? AND user_id=?",groupId, uId) + if err != nil { + return false + } + if len(users) == 1 { + return true + } + return false } \ No newline at end of file diff --git a/ssolib/server.go b/ssolib/server.go index 5b7e835..9a0144b 100644 --- a/ssolib/server.go +++ b/ssolib/server.go @@ -173,7 +173,9 @@ func (s *Server) ListenAndServe(addr string, addHandlers AddHandles) error { s.AddRestfulResource("/api/roles/:id", "RoleResource", RoleResource{}) s.AddRestfulResource("/api/roles/:id/members/:username", "RoleMemberResource", RoleMemberResource{}) s.AddRestfulResource("/api/roles/:id/resources", "RoleResourceResource", RoleResourceResource{}) - + s.AddRestfulResource("/api/applications", "ApplicationsResource", ApplicationsResource{}) + s.AddRestfulResource("/api/applications/:id", "ApplicationResource", ApplicationResource{}) + s.AddRestfulResource("/api/app_info", "AppInfoResource", AppInfoResource{}) puk, prk, err := loadCertAndKey(s.pubkeyfile, s.prikeyfile)