connector-basic/basic.go (216 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package basic import ( "context" "embed" "encoding/json" "fmt" "io" "regexp" "strings" "time" "unicode/utf8" "github.com/apache/answer-plugins/connector-basic/i18n" "github.com/apache/answer-plugins/util" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" "github.com/tidwall/gjson" "golang.org/x/oauth2" ) var ( replaceUsernameReg = regexp.MustCompile(`[^a-zA-Z0-9._-]+`) //go:embed info.yaml Info embed.FS ) type Connector struct { Config *ConnectorConfig } type ConnectorConfig struct { Name string `json:"name"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` AuthorizeUrl string `json:"authorize_url"` TokenUrl string `json:"token_url"` UserJsonUrl string `json:"user_json_url"` UserIDJsonPath string `json:"user_id_json_path"` UserDisplayNameJsonPath string `json:"user_display_name_json_path"` UserUsernameJsonPath string `json:"user_username_json_path"` UserEmailJsonPath string `json:"user_email_json_path"` UserAvatarJsonPath string `json:"user_avatar_json_path"` CheckEmailVerified bool `json:"check_email_verified"` EmailVerifiedJsonPath string `json:"email_verified_json_path"` Scope string `json:"scope"` LogoSVG string `json:"logo_svg"` } func init() { plugin.Register(&Connector{ Config: &ConnectorConfig{}, }) } func (g *Connector) Info() plugin.Info { info := &util.Info{} info.GetInfo(Info) return plugin.Info{ Name: plugin.MakeTranslator(i18n.InfoName), SlugName: info.SlugName, Description: plugin.MakeTranslator(i18n.InfoDescription), Author: info.Author, Version: info.Version, Link: info.Link, } } func (g *Connector) ConnectorLogoSVG() string { return g.Config.LogoSVG } func (g *Connector) ConnectorName() plugin.Translator { if len(g.Config.Name) > 0 { return plugin.MakeTranslator(g.Config.Name) } return plugin.MakeTranslator(i18n.ConnectorName) } func (g *Connector) ConnectorSlugName() string { return "basic" } func (g *Connector) ConnectorSender(ctx *plugin.GinContext, receiverURL string) (redirectURL string) { oauth2Config := &oauth2.Config{ ClientID: g.Config.ClientID, ClientSecret: g.Config.ClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: g.Config.AuthorizeUrl, TokenURL: g.Config.TokenUrl, }, RedirectURL: receiverURL, Scopes: strings.Split(g.Config.Scope, ","), } return oauth2Config.AuthCodeURL("state") } func (g *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (userInfo plugin.ExternalLoginUserInfo, err error) { code := ctx.Query("code") // Exchange code for token oauth2Config := &oauth2.Config{ ClientID: g.Config.ClientID, ClientSecret: g.Config.ClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: g.Config.AuthorizeUrl, TokenURL: g.Config.TokenUrl, AuthStyle: oauth2.AuthStyleAutoDetect, }, RedirectURL: receiverURL, } token, err := oauth2Config.Exchange(context.Background(), code) if err != nil { return userInfo, fmt.Errorf("code exchange failed: %s", err.Error()) } // Exchange token for user info client := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token.AccessToken}, )) client.Timeout = 15 * time.Second response, err := client.Get(g.Config.UserJsonUrl) if err != nil { return userInfo, fmt.Errorf("failed getting user info: %s", err.Error()) } defer response.Body.Close() data, _ := io.ReadAll(response.Body) userInfo = plugin.ExternalLoginUserInfo{ MetaInfo: string(data), } if len(g.Config.UserIDJsonPath) > 0 { userInfo.ExternalID = gjson.GetBytes(data, g.Config.UserIDJsonPath).String() } if len(userInfo.ExternalID) == 0 { log.Errorf("fail to get user id from json path: %s", g.Config.UserIDJsonPath) return userInfo, nil } if len(g.Config.UserDisplayNameJsonPath) > 0 { userInfo.DisplayName = gjson.GetBytes(data, g.Config.UserDisplayNameJsonPath).String() } if len(g.Config.UserUsernameJsonPath) > 0 { userInfo.Username = gjson.GetBytes(data, g.Config.UserUsernameJsonPath).String() } if len(g.Config.UserEmailJsonPath) > 0 { userInfo.Email = gjson.GetBytes(data, g.Config.UserEmailJsonPath).String() } if g.Config.CheckEmailVerified && len(g.Config.EmailVerifiedJsonPath) > 0 { emailVerified := gjson.GetBytes(data, g.Config.EmailVerifiedJsonPath).Bool() if !emailVerified { userInfo.Email = "" } } if len(g.Config.UserAvatarJsonPath) > 0 { userInfo.Avatar = gjson.GetBytes(data, g.Config.UserAvatarJsonPath).String() } userInfo = g.formatUserInfo(userInfo) return userInfo, nil } func (g *Connector) formatUserInfo(userInfo plugin.ExternalLoginUserInfo) ( userInfoFormatted plugin.ExternalLoginUserInfo) { userInfoFormatted = userInfo if checker.IsInvalidUsername(userInfoFormatted.Username) { userInfoFormatted.Username = replaceUsernameReg.ReplaceAllString(userInfoFormatted.Username, "_") } usernameLength := utf8.RuneCountInString(userInfoFormatted.Username) if usernameLength < 4 { userInfoFormatted.Username = userInfoFormatted.Username + strings.Repeat("_", 4-usernameLength) } else if usernameLength > 30 { userInfoFormatted.Username = string([]rune(userInfoFormatted.Username)[:30]) } return userInfoFormatted } func (g *Connector) ConfigFields() []plugin.ConfigField { fields := make([]plugin.ConfigField, 0) fields = append(fields, createTextInput("name", i18n.ConfigNameTitle, i18n.ConfigNameDescription, g.Config.Name, true)) fields = append(fields, createTextInput("client_id", i18n.ConfigClientIDTitle, i18n.ConfigClientIDDescription, g.Config.ClientID, true)) fields = append(fields, createTextInput("client_secret", i18n.ConfigClientSecretTitle, i18n.ConfigClientSecretDescription, g.Config.ClientSecret, true)) fields = append(fields, createTextInput("authorize_url", i18n.ConfigAuthorizeUrlTitle, i18n.ConfigAuthorizeUrlDescription, g.Config.AuthorizeUrl, true)) fields = append(fields, createTextInput("token_url", i18n.ConfigTokenUrlTitle, i18n.ConfigTokenUrlDescription, g.Config.TokenUrl, true)) fields = append(fields, createTextInput("user_json_url", i18n.ConfigUserJsonUrlTitle, i18n.ConfigUserJsonUrlDescription, g.Config.UserJsonUrl, true)) fields = append(fields, createTextInput("user_id_json_path", i18n.ConfigUserIDJsonPathTitle, i18n.ConfigUserIDJsonPathDescription, g.Config.UserIDJsonPath, true)) fields = append(fields, createTextInput("user_display_name_json_path", i18n.ConfigUserDisplayNameJsonPathTitle, i18n.ConfigUserDisplayNameJsonPathDescription, g.Config.UserDisplayNameJsonPath, false)) fields = append(fields, createTextInput("user_username_json_path", i18n.ConfigUserUsernameJsonPathTitle, i18n.ConfigUserUsernameJsonPathDescription, g.Config.UserUsernameJsonPath, false)) fields = append(fields, createTextInput("user_email_json_path", i18n.ConfigUserEmailJsonPathTitle, i18n.ConfigUserEmailJsonPathDescription, g.Config.UserEmailJsonPath, false)) fields = append(fields, createTextInput("user_avatar_json_path", i18n.ConfigUserAvatarJsonPathTitle, i18n.ConfigUserAvatarJsonPathDescription, g.Config.UserAvatarJsonPath, false)) fields = append(fields, plugin.ConfigField{ Name: "check_email_verified", Type: plugin.ConfigTypeSwitch, Title: plugin.MakeTranslator(i18n.ConfigCheckEmailVerifiedTitle), Value: g.Config.CheckEmailVerified, UIOptions: plugin.ConfigFieldUIOptions{ Label: plugin.MakeTranslator(i18n.ConfigCheckEmailVerifiedLabel), }, }) fields = append(fields, createTextInput("email_verified_json_path", i18n.ConfigEmailVerifiedJsonPathTitle, i18n.ConfigEmailVerifiedJsonPathDescription, g.Config.EmailVerifiedJsonPath, false)) fields = append(fields, createTextInput("scope", i18n.ConfigScopeTitle, i18n.ConfigScopeDescription, g.Config.Scope, false)) fields = append(fields, createTextInput("logo_svg", i18n.ConfigLogoSVGTitle, i18n.ConfigLogoSVGDescription, g.Config.LogoSVG, false)) return fields } func createTextInput(name, title, desc, value string, require bool) plugin.ConfigField { return plugin.ConfigField{ Name: name, Type: plugin.ConfigTypeInput, Title: plugin.MakeTranslator(title), Description: plugin.MakeTranslator(desc), Required: require, UIOptions: plugin.ConfigFieldUIOptions{ InputType: plugin.InputTypeText, }, Value: value, } } func (g *Connector) ConfigReceiver(config []byte) error { c := &ConnectorConfig{} _ = json.Unmarshal(config, c) g.Config = c return nil }