From a26683e01dc7e161e735b1b387d1633bc32da2fe Mon Sep 17 00:00:00 2001 From: Cory Bennett Date: Sun, 23 Feb 2020 23:59:39 -0800 Subject: [PATCH] update all usage of user.name to user.accountId for privacy migration: https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-user-privacy-api-migration-guide/ --- _t/100basic.t | 66 ++++++++++++------------ _t/101default.t | 4 +- _t/110basic-worklog.t | 2 +- _t/120custom-commands.t | 2 +- _t/130basic-epics.t | 6 +-- _t/140basic-attach.t | 12 ++--- _t/200scrum.t | 56 ++++++++++----------- _t/300kanban.t | 56 ++++++++++----------- _t/400project.t | 60 +++++++++++----------- _t/500process.t | 56 ++++++++++----------- _t/600task.t | 56 ++++++++++----------- go.mod | 2 +- issue.go | 28 +++++++++++ jiracli/cli.go | 11 +++- jiracli/templates.go | 48 ++++++++++++------ jiracmd/assign.go | 29 ++++++++++- jiracmd/create.go | 15 ++++++ jiracmd/edit.go | 108 ++++++++++++++++++++++++++++++++++++++++ jiracmd/subtask.go | 15 ++++++ jiracmd/transition.go | 14 ++++++ jiracmd/watch.go | 24 +++++++++ jiradata/ServerInfo.go | 13 +++++ serverinfo.go | 22 ++++++++ users.go | 58 +++++++++++++++++++++ 24 files changed, 557 insertions(+), 206 deletions(-) create mode 100644 jiradata/ServerInfo.go create mode 100644 serverinfo.go create mode 100644 users.go diff --git a/_t/100basic.t b/_t/100basic.t index 51de422..4d460dc 100755 --- a/_t/100basic.t +++ b/_t/100basic.t @@ -35,8 +35,8 @@ status: To Do summary: summary project: BASIC issuetype: Bug -assignee: gojira -reporter: gojira +assignee: GoJira +reporter: GoJira priority: Medium votes: 0 description: | @@ -69,7 +69,7 @@ DIFF <" -}} {{- end -}} {{- if .fields.assignee -}} - {{- cell .fields.assignee.name -}} + {{- cell .fields.assignee.displayName -}} {{- else -}} {{- cell "" -}} {{- end -}} @@ -357,7 +357,7 @@ const defaultAttachListTemplate = `{{/* attach list template */ -}} {{- cell .id -}} {{- cell .filename -}} {{- cell .size -}} - {{- cell .author.name -}} + {{- cell .author.displayName -}} {{- cell (.created | age) -}} {{- end -}} ` @@ -379,11 +379,11 @@ components: {{ range .fields.components }}{{ .name }} {{end}} issuetype: {{ .fields.issuetype.name }} {{end -}} {{if .fields.assignee -}} -assignee: {{ .fields.assignee.name }} +assignee: {{ .fields.assignee.displayName }} {{end -}} -reporter: {{ if .fields.reporter }}{{ .fields.reporter.name }}{{end}} +reporter: {{ if .fields.reporter }}{{ .fields.reporter.displayName }}{{end}} {{if .fields.customfield_10110 -}} -watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{end}} +watchers: {{ range .fields.customfield_10110 }}{{ .displayName }} {{end}} {{end -}} {{if .fields.issuelinks -}} blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}[{{.outwardIssue.fields.status.name}}]{{end}}{{end}} @@ -402,7 +402,7 @@ description: | {{ or .fields.description "" | indent 2 }} {{if .fields.comment.comments}} comments: -{{ range .fields.comment.comments }} - | # {{.author.name}}, {{.created | age}} ago +{{ range .fields.comment.comments }} - | # {{.author.displayName}}, {{.created | age}} ago {{ or .body "" | indent 4}} {{end}} {{end -}} @@ -421,9 +421,15 @@ fields: components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}} - name: {{.}}{{end}}{{else}}{{ range .fields.components }} - name: {{ .name }}{{end}}{{end}}{{end}} -{{- if .meta.fields.assignee}} +{{- if .meta.fields.assignee }} + {{- if .overrides.assignee }} assignee: - name: {{ if .overrides.assignee }}{{.overrides.assignee}}{{else}}{{if .fields.assignee }}{{ .fields.assignee.name }}{{end}}{{end}}{{end}} + name: {{ .overrides.assignee }} + {{- else if .fields.assignee }} + assignee: {{if .fields.assignee.name}} + name: {{ or .fields.assignee.name}} + {{- else }} + displayName: {{.fields.assignee.displayName}}{{end}}{{end}}{{end}} {{- if .meta.fields.reporter}} reporter: name: {{ if .overrides.reporter }}{{ .overrides.reporter }}{{else if .fields.reporter}}{{ .fields.reporter.name }}{{end}}{{end}} @@ -439,7 +445,7 @@ fields: {{ or .overrides.description .fields.description "" | indent 4 }} # votes: {{ .fields.votes.votes }} # comments: -# {{ range .fields.comment.comments }} - | # {{.author.name}}, {{.created | age}} ago +# {{ range .fields.comment.comments }} - | # {{.author.displayName}}, {{.created | age}} ago # {{ or .body "" | indent 4 | comment}} # {{end}} ` @@ -546,9 +552,15 @@ update: {{ or .overrides.comment "" | indent 10 }} {{- end -}} fields: -{{- if .meta.fields.assignee}} +{{- if .meta.fields.assignee }} + {{- if .overrides.assignee }} assignee: - name: {{if .overrides.assignee}}{{.overrides.assignee}}{{else}}{{if .fields.assignee}}{{.fields.assignee.name}}{{end}}{{end}} + name: {{ .overrides.assignee }} + {{- else if .fields.assignee }} + assignee: {{if .fields.assignee.name}} + name: {{ or .fields.assignee.name}} + {{- else }} + displayName: {{.fields.assignee.displayName}}{{end}}{{end}} {{- end -}} {{if .meta.fields.components}} components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}} @@ -579,9 +591,15 @@ fields: priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}} name: {{ or .overrides.priority "unassigned" }} {{- end -}} -{{if .meta.fields.reporter}} +{{- if .meta.fields.reporter }} + {{- if .overrides.reporter }} reporter: - name: {{if .overrides.reporter}}{{.overrides.reporter}}{{else}}{{if .fields.reporter}}{{.fields.reporter.name}}{{end}}{{end}} + name: {{ .overrides.reporter }} + {{- else if .fields.reporter }} + reporter: {{if .fields.reporter.name}} + name: {{ or .fields.reporter.name}} + {{- else }} + displayName: {{.fields.reporter.displayName}}{{end}}{{end}} {{- end -}} {{if .meta.fields.resolution}} resolution: # Values: {{ range .meta.fields.resolution.allowedValues }}{{.name}}, {{end}} @@ -610,7 +628,7 @@ started: {{ or .started "" }} ` const defaultWorklogsTemplate = `{{/* worklogs template */ -}} -{{ range .worklogs }}- # {{.author.name}}, {{.created | age}} ago +{{ range .worklogs }}- # {{.author.displayName}}, {{.created | age}} ago comment: {{ or .comment "" }} started: {{ .started }} timeSpent: {{ .timeSpent }} diff --git a/jiracmd/assign.go b/jiracmd/assign.go index a33531e..0be938f 100644 --- a/jiracmd/assign.go +++ b/jiracmd/assign.go @@ -2,6 +2,7 @@ package jiracmd import ( "fmt" + "strings" "github.com/coryb/figtree" "github.com/coryb/oreo" @@ -48,7 +49,33 @@ func CmdAssignUsage(cmd *kingpin.CmdClause, opts *AssignOptions) error { // CmdAssign will assign an issue to a user func CmdAssign(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AssignOptions) error { - err := jira.IssueAssign(o, globals.Endpoint.Value, opts.Issue, opts.Assignee) + if globals.JiraDeploymentType.Value == "" { + serverInfo, err := jira.ServerInfo(o, globals.Endpoint.Value) + if err != nil { + return err + } + globals.JiraDeploymentType.Value = strings.ToLower(serverInfo.DeploymentType) + } + + assignFunc := jira.IssueAssign + if globals.JiraDeploymentType.Value == jiracli.CloudDeploymentType { + if opts.Assignee != "" && opts.Assignee != "-1" { + users, err := jira.UserSearch(o, globals.Endpoint.Value, &jira.UserSearchOptions{ + Username: opts.Assignee, + }) + if err != nil { + return err + } + if len(users) > 1 { + return fmt.Errorf("Found %d accounts for users with username %q", len(users), opts.Assignee) + } else if len(users) == 1 { + opts.Assignee = users[0].AccountID + } + } + assignFunc = jira.IssueAssignAccountID + } + + err := assignFunc(o, globals.Endpoint.Value, opts.Issue, opts.Assignee) if err != nil { return err } diff --git a/jiracmd/create.go b/jiracmd/create.go index e7232e3..ca5c661 100644 --- a/jiracmd/create.go +++ b/jiracmd/create.go @@ -3,6 +3,7 @@ package jiracmd import ( "fmt" "os" + "strings" "github.com/coryb/figtree" "github.com/coryb/oreo" @@ -62,6 +63,14 @@ func CmdCreateUsage(cmd *kingpin.CmdClause, opts *CreateOptions) error { // CmdCreate sends the create-metadata to the "create" template for editing, then // will parse the edited document as YAML and submit the document to jira. func CmdCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateOptions) error { + if globals.JiraDeploymentType.Value == "" { + serverInfo, err := jira.ServerInfo(o, globals.Endpoint.Value) + if err != nil { + return err + } + globals.JiraDeploymentType.Value = strings.ToLower(serverInfo.DeploymentType) + } + type templateInput struct { Meta *jiradata.IssueType `yaml:"meta" json:"meta"` Overrides map[string]string `yaml:"overrides" json:"overrides"` @@ -86,6 +95,12 @@ func CmdCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateOptio var issueResp *jiradata.IssueCreateResponse err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error { + if globals.JiraDeploymentType.Value == jiracli.CloudDeploymentType { + err := fixGDPRUserFields(o, globals.Endpoint.Value, createMeta.Fields, issueUpdate.Fields) + if err != nil { + return err + } + } issueResp, err = jira.CreateIssue(o, globals.Endpoint.Value, &issueUpdate) return err }) diff --git a/jiracmd/edit.go b/jiracmd/edit.go index f7b5c7c..9855067 100644 --- a/jiracmd/edit.go +++ b/jiracmd/edit.go @@ -2,6 +2,7 @@ package jiracmd import ( "fmt" + "strings" "github.com/coryb/figtree" "github.com/coryb/oreo" @@ -71,6 +72,14 @@ func CmdEditUsage(cmd *kingpin.CmdClause, opts *EditOptions, fig *figtree.FigTre // Edit will get issue data and send to "edit" template func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions) error { + if globals.JiraDeploymentType.Value == "" { + serverInfo, err := jira.ServerInfo(o, globals.Endpoint.Value) + if err != nil { + return err + } + globals.JiraDeploymentType.Value = strings.ToLower(serverInfo.DeploymentType) + } + type templateInput struct { *jiradata.Issue `yaml:",inline"` Meta *jiradata.EditMeta `yaml:"meta" json:"meta"` @@ -93,6 +102,12 @@ func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions) Overrides: opts.Overrides, } err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error { + if globals.JiraDeploymentType.Value == jiracli.CloudDeploymentType { + err := fixGDPRUserFields(o, globals.Endpoint.Value, editMeta.Fields, issueUpdate.Fields) + if err != nil { + return err + } + } return jira.EditIssue(o, globals.Endpoint.Value, opts.Issue, &issueUpdate) }) if err != nil { @@ -123,6 +138,12 @@ func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions) Overrides: opts.Overrides, } err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error { + if globals.JiraDeploymentType.Value == jiracli.CloudDeploymentType { + err := fixGDPRUserFields(o, globals.Endpoint.Value, editMeta.Fields, issueUpdate.Fields) + if err != nil { + return err + } + } return jira.EditIssue(o, globals.Endpoint.Value, issueData.Key, &issueUpdate) }) if err == jiracli.EditLoopAbort && len(results.Issues) > i+1 { @@ -152,3 +173,90 @@ func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions) } return nil } + +func fixUserField(ua jira.HttpClient, endpoint string, userField map[string]interface{}) error { + if _, ok := userField["accountId"].(string); ok { + // this field is already GDPR ready + return nil + } + + if username, ok := userField["name"].(string); ok { + users, err := jira.UserSearch(ua, endpoint, &jira.UserSearchOptions{ + Username: username, + }) + if err != nil { + return err + } + if len(users) != 1 { + return fmt.Errorf("Found %d accounts for username %q", len(users), username) + } + userField["accountId"] = users[0].AccountID + return nil + } + + queryName, ok := userField["displayName"].(string) + if !ok { + queryName, ok = userField["emailAddress"].(string) + if !ok { + // no fields to search on, skip user lookup + return nil + } + } + users, err := jira.UserSearch(ua, endpoint, &jira.UserSearchOptions{ + // Query field will search users displayName and emailAddress + Query: queryName, + }) + if err != nil { + return err + } + if len(users) != 1 { + return fmt.Errorf("Found %d accounts for users with query %q", len(users), queryName) + } + userField["accountId"] = users[0].AccountID + return nil +} + +func fixGDPRUserFields(ua jira.HttpClient, endpoint string, meta jiradata.FieldMetaMap, fields map[string]interface{}) error { + for fieldName, fieldMeta := range meta { + // check to see if meta-field is in fields data, otherwise skip + if _, ok := fields[fieldName]; !ok { + continue + } + if fieldMeta.Schema.Type == "user" { + userField, ok := fields[fieldName].(map[string]interface{}) + if !ok { + // for some reason the field seems to be the wrong type in the data + // even though the schema is a "user" + continue + } + err := fixUserField(ua, endpoint, userField) + if err != nil { + return err + } + fields[fieldName] = userField + } + if fieldMeta.Schema.Type == "array" && fieldMeta.Schema.Items == "user" { + listUserField, ok := fields[fieldName].([]interface{}) + if !ok { + // for some reason the field seems to be the wrong type in the data + // even though the schema is a list of "user" + continue + } + for i, userFieldItem := range listUserField { + userField, ok := userFieldItem.(map[string]interface{}) + if !ok { + // for some reason the field seems to be the wrong type in the data + // even though the schema is a "user" + continue + } + err := fixUserField(ua, endpoint, userField) + if err != nil { + return err + } + listUserField[i] = userField + } + fields[fieldName] = listUserField + } + } + return nil +} diff --git a/jiracmd/subtask.go b/jiracmd/subtask.go index 75875f1..869cca8 100644 --- a/jiracmd/subtask.go +++ b/jiracmd/subtask.go @@ -2,6 +2,7 @@ package jiracmd import ( "fmt" + "strings" "github.com/coryb/figtree" "github.com/coryb/oreo" @@ -63,6 +64,14 @@ func CmdSubtaskUsage(cmd *kingpin.CmdClause, opts *SubtaskOptions) error { // CmdSubtask sends the subtask-metadata to the "subtask" template for editing, then // will parse the edited document as YAML and submit the document to jira. func CmdSubtask(o *oreo.Client, globals *jiracli.GlobalOptions, opts *SubtaskOptions) error { + if globals.JiraDeploymentType.Value == "" { + serverInfo, err := jira.ServerInfo(o, globals.Endpoint.Value) + if err != nil { + return err + } + globals.JiraDeploymentType.Value = strings.ToLower(serverInfo.DeploymentType) + } + type templateInput struct { Meta *jiradata.IssueType `yaml:"meta" json:"meta"` Overrides map[string]string `yaml:"overrides" json:"overrides"` @@ -101,6 +110,12 @@ func CmdSubtask(o *oreo.Client, globals *jiracli.GlobalOptions, opts *SubtaskOpt var issueResp *jiradata.IssueCreateResponse err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error { + if globals.JiraDeploymentType.Value == jiracli.CloudDeploymentType { + err := fixGDPRUserFields(o, globals.Endpoint.Value, createMeta.Fields, issueUpdate.Fields) + if err != nil { + return err + } + } issueResp, err = jira.CreateIssue(o, globals.Endpoint.Value, &issueUpdate) return err }) diff --git a/jiracmd/transition.go b/jiracmd/transition.go index 85200de..57b10e4 100644 --- a/jiracmd/transition.go +++ b/jiracmd/transition.go @@ -86,6 +86,14 @@ func defaultResolution(transMeta *jiradata.Transition) string { // CmdTransition will move state of the given issue to the given transtion func CmdTransition(o *oreo.Client, globals *jiracli.GlobalOptions, opts *TransitionOptions) error { + if globals.JiraDeploymentType.Value == "" { + serverInfo, err := jira.ServerInfo(o, globals.Endpoint.Value) + if err != nil { + return err + } + globals.JiraDeploymentType.Value = strings.ToLower(serverInfo.DeploymentType) + } + issueData, err := jira.GetIssue(o, globals.Endpoint.Value, opts.Issue, nil) if err != nil { return jiracli.CliError(err) @@ -151,6 +159,12 @@ func CmdTransition(o *oreo.Client, globals *jiracli.GlobalOptions, opts *Transit Overrides: opts.Overrides, } err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error { + if globals.JiraDeploymentType.Value == jiracli.CloudDeploymentType { + err := fixGDPRUserFields(o, globals.Endpoint.Value, transMeta.Fields, issueUpdate.Fields) + if err != nil { + return err + } + } return jira.TransitionIssue(o, globals.Endpoint.Value, opts.Issue, &issueUpdate) }) if err != nil { diff --git a/jiracmd/watch.go b/jiracmd/watch.go index 27616c5..f24cc3a 100644 --- a/jiracmd/watch.go +++ b/jiracmd/watch.go @@ -2,6 +2,7 @@ package jiracmd import ( "fmt" + "strings" "github.com/coryb/figtree" "github.com/coryb/oreo" @@ -62,6 +63,29 @@ func CmdWatch(o *oreo.Client, globals *jiracli.GlobalOptions, opts *WatchOptions if opts.Watcher == "" { opts.Watcher = globals.User.Value } + + if globals.JiraDeploymentType.Value == "" { + serverInfo, err := jira.ServerInfo(o, globals.Endpoint.Value) + if err != nil { + return err + } + globals.JiraDeploymentType.Value = strings.ToLower(serverInfo.DeploymentType) + } + + if globals.JiraDeploymentType.Value == jiracli.CloudDeploymentType { + users, err := jira.UserSearch(o, globals.Endpoint.Value, &jira.UserSearchOptions{ + Username: opts.Watcher, + }) + if err != nil { + return err + } + if len(users) > 1 { + return fmt.Errorf("Found %d accounts for users with username %q", len(users), opts.Watcher) + } else if len(users) == 1 { + opts.Watcher = users[0].AccountID + } + } + if opts.Action == WatcherAdd { if err := jira.IssueAddWatcher(o, globals.Endpoint.Value, opts.Issue, opts.Watcher); err != nil { return err diff --git a/jiradata/ServerInfo.go b/jiradata/ServerInfo.go new file mode 100644 index 0000000..0f3b184 --- /dev/null +++ b/jiradata/ServerInfo.go @@ -0,0 +1,13 @@ +package jiradata + +type ServerInfo struct { + BaseURL string `json:"baseUrl,omitempty" yaml:"baseUrl,omitempty"` + BuildDate string `json:"buildDate,omitempty" yaml:"buildDate,omitempty"` + BuildNumber int `json:"buildNumber,omitempty" yaml:"buildNumber,omitempty"` + DeploymentType string `json:"deploymentType,omitempty" yaml:"deploymentType,omitempty"` + SCMInfo string `json:"scmInfo,omitempty" yaml:"scmInfo,omitempty"` + ServerTime string `json:"serverTime,omitempty" yaml:"serverTime,omitempty"` + ServerTitle string `json:"serverTitle,omitempty" yaml:"serverTitle,omitempty"` + Version string `json:"version,omitempty" yaml:"version,omitempty"` + VersionNumbers []int `json:"versionNumbers,omitempty" yaml:"versionNumbers,omitempty"` +} diff --git a/serverinfo.go b/serverinfo.go new file mode 100644 index 0000000..93c88a7 --- /dev/null +++ b/serverinfo.go @@ -0,0 +1,22 @@ +package jira + +import ( + "encoding/json" + + "github.com/go-jira/jira/jiradata" +) + +func ServerInfo(ua HttpClient, endpoint string) (*jiradata.ServerInfo, error) { + uri := URLJoin(endpoint, "rest/api/2/serverInfo") + resp, err := ua.GetJSON(uri) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + results := jiradata.ServerInfo{} + return &results, json.NewDecoder(resp.Body).Decode(&results) + } + return nil, responseError(resp) +} diff --git a/users.go b/users.go new file mode 100644 index 0000000..46b8934 --- /dev/null +++ b/users.go @@ -0,0 +1,58 @@ +package jira + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/go-jira/jira/jiradata" +) + +type UserSearchOptions struct { + Query string `yaml:"query,omitempty" json:"query,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + AccountID string `yaml:"accountId,omitempty" json:"accountId,omitempty"` + StartAt int `yaml:"startAt,omitempty" json:"startAt,omitempty"` + MaxResults int `yaml:"max-results,omitempty" json:"max-results,omitempty"` + Property string `yaml:"property,omitempty" json:"property,omitempty"` +} + +// https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-search-get + +func UserSearch(ua HttpClient, endpoint string, opts *UserSearchOptions) ([]*jiradata.User, error) { + uri := URLJoin(endpoint, "rest/api/2/user/search") + params := []string{} + if opts.Query != "" { + params = append(params, "query="+url.QueryEscape(opts.Query)) + } + if opts.Username != "" { + params = append(params, "username="+url.QueryEscape(opts.Username)) + } + if opts.AccountID != "" { + params = append(params, "accountId="+url.QueryEscape(opts.AccountID)) + } + if opts.StartAt != 0 { + params = append(params, fmt.Sprintf("startAt=%d", opts.StartAt)) + } + if opts.MaxResults != 0 { + params = append(params, fmt.Sprintf("maxResults=%d", opts.MaxResults)) + } + if opts.Property != "" { + params = append(params, "property="+url.QueryEscape(opts.Property)) + } + if len(params) > 0 { + uri += "?" + strings.Join(params, "&") + } + resp, err := ua.GetJSON(uri) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + results := []*jiradata.User{} + return results, json.NewDecoder(resp.Body).Decode(&results) + } + return nil, responseError(resp) +}