feat(bingocloud): add bingoiam for oauth2 (#17083)

Co-authored-by: 李锐 <lirui@bingosoft.net>
This commit is contained in:
gzericlee
2023-05-20 02:13:01 +08:00
committed by GitHub
co-authored by 李锐
parent 6788a154c8
commit 312c7584bd
9 changed files with 541 additions and 0 deletions
+1
View File
@@ -61,6 +61,7 @@ func init() {
"default",
"k8s",
"monitor",
"bingocloud",
}
const (
+2
View File
@@ -102,6 +102,7 @@ const (
IdpTemplateDingtalk = "dingtalk_oauth2"
IdpTemplateFeishu = "feishu_oauth2"
IdpTemplateQywechat = "qywechat_oauth2"
IdpTemplateBingoIAM = "bingoiam_oauth2"
)
var (
@@ -123,6 +124,7 @@ var (
IdpTemplateDingtalk: IdentityDriverOAuth2,
IdpTemplateWechat: IdentityDriverOAuth2,
IdpTemplateQywechat: IdentityDriverOAuth2,
IdpTemplateBingoIAM: IdentityDriverOAuth2,
}
)
@@ -0,0 +1,185 @@
// Copyright 2019 Yunion
//
// Licensed 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 bingoiam
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/keystone/driver/oauth2"
"yunion.io/x/onecloud/pkg/keystone/models"
)
type SBingoIAMOAuth2Driver struct {
oauth2.SOAuth2BaseDriver
idp *models.SIdentityProvider
accessToken string
domains map[string]*models.SDomain
projects map[string]*models.SProject
}
func NewBingoIAMOAuth2Driver(appId string, secret string) oauth2.IOAuth2Driver {
drv := &SBingoIAMOAuth2Driver{
domains: map[string]*models.SDomain{},
projects: map[string]*models.SProject{},
SOAuth2BaseDriver: oauth2.SOAuth2BaseDriver{
AppId: appId,
Secret: secret,
},
}
return drv
}
func (drv *SBingoIAMOAuth2Driver) getIAMEndpoint(ctx context.Context) string {
endpoint := ""
if config, isOk := ctx.Value("config").(identity.TConfigs); isOk {
endpoint = strings.Trim(config["oauth2"]["iam_endpoint"].String(), `"`)
}
return endpoint
}
func (drv *SBingoIAMOAuth2Driver) getIAMApiEndpoint(ctx context.Context) string {
endpoint := ""
if config, isOk := ctx.Value("config").(identity.TConfigs); isOk {
endpoint = strings.Trim(config["oauth2"]["iam_api_endpoint"].String(), `"`)
}
return endpoint
}
func (drv *SBingoIAMOAuth2Driver) GetSsoRedirectUri(ctx context.Context, callbackUrl, state string) (string, error) {
req := map[string]string{
"client_id": drv.AppId,
"redirect_uri": callbackUrl,
"response_type": "code id_token",
"scope": "openid",
"state": state,
}
authUrl := drv.getIAMEndpoint(ctx)
urlStr := fmt.Sprintf("%s/oauth2/authorize?%s", authUrl, jsonutils.Marshal(req).QueryString())
return urlStr, nil
}
func (drv *SBingoIAMOAuth2Driver) Authenticate(ctx context.Context, code string) (map[string][]string, error) {
accessToken, err := drv.fetchAccessToken(ctx, code)
if err != nil {
return nil, errors.Wrap(err, "fetch bingo iam accessToken")
}
userInfo, err := drv.fetchUserInfo(ctx, accessToken)
if err != nil {
return nil, errors.Wrap(err, "fetch bingo iam userInfo")
}
attrs := make(map[string][]string)
attrs["name"] = []string{fmt.Sprintf("%s", userInfo["preferred_username"])}
attrs["display_name"] = []string{fmt.Sprintf("%s", userInfo["name"])}
attrs["user_id"] = []string{fmt.Sprintf("%s", userInfo["sub"])}
attrs["name_en"] = []string{fmt.Sprintf("%s", userInfo["preferred_username"])}
attrs["email"] = []string{fmt.Sprintf("%s", userInfo["email"])}
attrs["org_id"] = []string{fmt.Sprintf("%s", userInfo["org_id"])}
attrs["mobile"] = []string{fmt.Sprintf("%s", userInfo["phone_number"])}
attrs["tenant_id"] = []string{fmt.Sprintf("%s", userInfo["tenant_id"])}
attrs["tenant_name"] = []string{fmt.Sprintf("%s", userInfo["tenant_name"])}
return attrs, nil
}
func (drv *SBingoIAMOAuth2Driver) Sync(ctx context.Context, idpId string) error {
var err error
drv.idp, err = models.IdentityProviderManager.FetchIdentityProviderById(idpId)
if err != nil {
return err
}
//err = drv.syncTenants(ctx, drv.idp)
//if err != nil {
// return err
//}
//err = drv.syncProjects(ctx, drv.idp)
//if err != nil {
// return err
//}
//err = drv.syncUsers(ctx, drv.idp)
//if err != nil {
// return err
//}
return nil
}
func (drv *SBingoIAMOAuth2Driver) getAccessToken(ctx context.Context) (string, error) {
authUrl := drv.getIAMEndpoint(ctx)
authUrl = fmt.Sprintf("%s/oauth2/token?grant_type=client_credentials", authUrl)
headers := http.Header{}
headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", drv.AppId, drv.Secret))))
httpclient := httputils.GetDefaultClient()
_, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.POST, authUrl, headers, nil, true)
if err != nil {
return "", err
}
var data map[string]interface{}
err = resp.Unmarshal(&data)
if err != nil {
return "", err
}
drv.accessToken = data["access_token"].(string)
return drv.accessToken, nil
}
func (drv *SBingoIAMOAuth2Driver) fetchAccessToken(ctx context.Context, code string) (string, error) {
authUrl := drv.getIAMEndpoint(ctx)
authUrl = fmt.Sprintf("%s/oauth2/token?grant_type=authorization_code&code=%s", authUrl, code)
headers := http.Header{}
headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", drv.AppId, drv.Secret))))
httpclient := httputils.GetDefaultClient()
_, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.POST, authUrl, headers, nil, true)
if err != nil {
return "", err
}
var data map[string]interface{}
err = resp.Unmarshal(&data)
if err != nil {
return "", err
}
return data["access_token"].(string), nil
}
func (drv *SBingoIAMOAuth2Driver) fetchUserInfo(ctx context.Context, accessToken string) (map[string]interface{}, error) {
authUrl := drv.getIAMEndpoint(ctx)
authUrl = fmt.Sprintf("%s/oauth2/userinfo?access_token=%s", authUrl, accessToken)
headers := http.Header{}
headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", drv.AppId, drv.Secret))))
httpclient := httputils.GetDefaultClient()
_, resp, err := httputils.JSONRequest(httpclient, ctx, httputils.GET, authUrl, headers, nil, true)
if err != nil {
return nil, err
}
var data map[string]interface{}
err = resp.Unmarshal(&data)
if err != nil {
return nil, err
}
return data, nil
}
@@ -0,0 +1,15 @@
// Copyright 2019 Yunion
//
// Licensed 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 bingoiam // import "yunion.io/x/onecloud/pkg/keystone/driver/oauth2/bingo"
@@ -0,0 +1,50 @@
// Copyright 2019 Yunion
//
// Licensed 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 bingoiam
import (
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/keystone/driver/oauth2"
)
type SBingoIAMDriverFactory struct{}
func (drv SBingoIAMDriverFactory) NewDriver(appId string, secret string) oauth2.IOAuth2Driver {
return NewBingoIAMOAuth2Driver(appId, secret)
}
func (drv SBingoIAMDriverFactory) TemplateName() string {
return api.IdpTemplateBingoIAM
}
func (drv SBingoIAMDriverFactory) IdpAttributeOptions() api.SIdpAttributeOptions {
return api.SIdpAttributeOptions{
DomainNameAttribute: "tenant_name",
DomainIdAttribute: "tenant_id",
UserNameAttribute: "name",
UserIdAttribute: "user_id",
UserDisplaynameAttribtue: "display_name",
ProjectAttribute: "",
DefaultProjectId: "",
}
}
func (drv SBingoIAMDriverFactory) ValidateConfig(conf api.SOAuth2IdpConfigOptions) error {
return nil
}
func init() {
oauth2.Register(&SBingoIAMDriverFactory{})
}
+273
View File
@@ -0,0 +1,273 @@
package bingoiam
import (
"context"
"database/sql"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
json "yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/keystone/models"
)
type SBase struct {
Id string `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Email string `json:"email"`
Mobile string `json:"mobile"`
ParentId string `json:"parentId"`
ExternalId string `json:"externalId"`
Active bool `json:"active"`
IsDeleted bool `json:"isDeleted"`
CreatedAt time.Time `json:"createdAt"`
CreatedBy string `json:"createdBy"`
UpdatedAt time.Time `json:"updatedAt"`
UpdatedBy string `json:"updatedBy"`
}
type STenant struct {
SBase
}
type SOrganization struct {
SBase
TenantId string `json:"tenantId"`
}
type SUser struct {
SBase
OrgId string `json:"orgId"`
UserName string `json:"userName"`
TenantId string `json:"tenantId"`
}
type SApp struct {
SBase
Name string `json:"title"`
TenantId string `json:"tenantId"`
AppType string `json:"appType"`
ProjectId string `json:"projectId"`
Description string `json:"description"`
}
type SProject struct {
Id string `json:"id"`
Name string `json:"name"`
TenantId string `json:"tenantId"`
}
func (drv *SBingoIAMOAuth2Driver) syncTenants(ctx context.Context, idp *models.SIdentityProvider) error {
var count, page = 0, 0
for {
page++
total, tenants, err := drv.getTenants(ctx, page, 500)
if err != nil {
return err
}
for _, tenant := range tenants {
domain, err := idp.SyncOrCreateDomain(ctx, tenant.Id, tenant.Name, fmt.Sprintf("Sync from %s", idp.Name), false)
if err != nil {
return errors.Wrap(err, "idp.SyncOrCreateDomain")
}
drv.domains[tenant.Id] = domain
}
count += len(tenants)
if count >= total {
break
}
time.Sleep(time.Millisecond * 200)
}
return nil
}
func (drv *SBingoIAMOAuth2Driver) syncOrganizations(ctx context.Context, idp *models.SIdentityProvider) error {
var loader func(filters string, orgs chan []*SOrganization)
loader = func(filters string, orgs chan []*SOrganization) {
var page, count = 0, 0
var childOrgs []*SOrganization
for {
page++
total, orgs, err := drv.getOrganizations(ctx, filters, page, 500)
if err != nil {
log.Errorf("get organizations %s fail %s", filters, err)
return
}
childOrgs = append(childOrgs, orgs...)
count += len(orgs)
if count >= total {
break
}
time.Sleep(time.Millisecond * 200)
}
if len(childOrgs) > 0 {
orgs <- childOrgs
}
for _, org := range childOrgs {
loader("parentId eq "+org.Id, orgs)
}
}
var allOrgs = make(chan []*SOrganization)
go func() {
for orgs := range allOrgs {
for _, org := range orgs {
domain := drv.domains[org.TenantId]
if domain == nil {
continue
}
//TODO save to db
}
}
}()
loader("parentId is null", allOrgs)
close(allOrgs)
return nil
}
func (drv *SBingoIAMOAuth2Driver) syncProjects(ctx context.Context, idp *models.SIdentityProvider) error {
var count, page = 0, 0
for {
page++
total, projects, err := drv.getProjects(ctx, page, 500)
if err != nil {
return err
}
for _, project := range projects {
domain := drv.domains[project.TenantId]
if domain == nil {
continue
}
ret, err := models.ProjectManager.FetchProject("", project.Name, domain.Id, "")
if err != nil {
log.Errorf("fetch project %s fail %s", project.Name, err)
if errors.Cause(err) == sql.ErrNoRows {
ret, err = models.ProjectManager.NewProject(ctx, project.Name, fmt.Sprintf("Sync from %s", idp.Name), domain.Id)
if err != nil {
continue
}
}
}
drv.projects[project.Id] = ret
}
count += len(projects)
if count >= total {
break
}
time.Sleep(time.Millisecond * 200)
}
return nil
}
func (drv *SBingoIAMOAuth2Driver) syncUsers(ctx context.Context, idp *models.SIdentityProvider) error {
var count, page = 0, 0
for {
page++
total, users, err := drv.getUsers(ctx, page, 500)
if err != nil {
return err
}
for _, user := range users {
domain := drv.domains[user.TenantId]
if domain == nil {
continue
}
_, err = idp.SyncOrCreateUser(ctx, user.Id, user.UserName, domain.Id, true, func(u *models.SUser) {
u.Id = user.Id
u.Name = user.UserName
u.Displayname = user.Name
u.Description = fmt.Sprintf("Sync from %s", idp.Name)
u.Email = user.Email
u.DomainId = domain.Id
u.Mobile = user.Mobile
u.Enabled = tristate.True
u.SEnabledIdentityBaseResource.Enabled = tristate.True
})
if err != nil {
return errors.Wrap(err, "idp.SyncOrCreateDomain")
}
}
count += len(users)
if count >= total {
break
}
time.Sleep(time.Millisecond * 200)
}
return nil
}
func (drv *SBingoIAMOAuth2Driver) getTenants(ctx context.Context, page, pageSize int) (int, []*STenant, error) {
var accessToken = drv.accessToken
if accessToken == "" {
accessToken, _ = drv.getAccessToken(ctx)
}
urlStrStr := fmt.Sprintf("%v/tenant?total=true&page_size=%v&page=%v", drv.getIAMApiEndpoint(ctx), pageSize, page)
headers := http.Header{}
headers.Set("Authorization", "Bearer "+accessToken)
return doRequest[[]*STenant](ctx, urlStrStr, httputils.GET, headers, nil)
}
func (drv *SBingoIAMOAuth2Driver) getOrganizations(ctx context.Context, filters string, page, pageSize int) (int, []*SOrganization, error) {
var accessToken = drv.accessToken
if accessToken == "" {
accessToken, _ = drv.getAccessToken(ctx)
}
urlStr := fmt.Sprintf("%v/organization?total=true&page_size=%v&page=%v&filters=%v", drv.getIAMApiEndpoint(ctx), pageSize, page, url.QueryEscape(filters))
headers := http.Header{}
headers.Set("Authorization", "Bearer "+accessToken)
return doRequest[[]*SOrganization](ctx, urlStr, httputils.GET, headers, nil)
}
func (drv *SBingoIAMOAuth2Driver) getUsers(ctx context.Context, page, pageSize int) (int, []*SUser, error) {
var accessToken = drv.accessToken
if accessToken == "" {
accessToken, _ = drv.getAccessToken(ctx)
}
urlStr := fmt.Sprintf("%v/user?total=true&page_size=%v&page=%v", drv.getIAMApiEndpoint(ctx), pageSize, page)
headers := http.Header{}
headers.Set("Authorization", "Bearer "+accessToken)
return doRequest[[]*SUser](ctx, urlStr, httputils.GET, headers, nil)
}
func (drv *SBingoIAMOAuth2Driver) getProjects(ctx context.Context, page, pageSize int) (int, []*SProject, error) {
var accessToken = drv.accessToken
if accessToken == "" {
accessToken, _ = drv.getAccessToken(ctx)
}
urlStr := fmt.Sprintf("%v/project?total=true&page_size=%v&page=%v", drv.getIAMApiEndpoint(ctx), pageSize, page)
headers := http.Header{}
headers.Set("Authorization", "Bearer "+accessToken)
return doRequest[[]*SProject](ctx, urlStr, httputils.GET, headers, nil)
}
func doRequest[Result any](ctx context.Context, urlStr string, method httputils.THttpMethod, headers http.Header, body json.JSONObject) (total int, result Result, err error) {
httpclient := httputils.GetDefaultClient()
repsHeaders, resp, err := httputils.JSONRequest(httpclient, ctx, method, urlStr, headers, body, true)
if err != nil {
return 0, result, err
}
total, _ = strconv.Atoi(repsHeaders.Get("X-Total-Count"))
err = resp.Unmarshal(&result)
return
}
+10
View File
@@ -71,6 +71,7 @@ func (self *SOAuth2Driver) GetSsoRedirectUri(ctx context.Context, callbackUrl, s
return "", errors.Wrapf(httperrors.ErrNotSupported, "template %s not supported", self.Template)
}
driver := factory.NewDriver(self.oauth2Config.AppId, self.oauth2Config.Secret)
ctx = context.WithValue(ctx, "config", self.SBaseIdentityDriver.Config)
return driver.GetSsoRedirectUri(ctx, callbackUrl, state)
}
@@ -82,6 +83,7 @@ func (self *SOAuth2Driver) Authenticate(ctx context.Context, ident mcclient.SAut
options := self.oauth2Config.SIdpAttributeOptions
options.Update(factory.IdpAttributeOptions())
driver := factory.NewDriver(self.oauth2Config.AppId, self.oauth2Config.Secret)
ctx = context.WithValue(ctx, "config", self.SBaseIdentityDriver.Config)
attrs, err := driver.Authenticate(ctx, ident.OAuth2.Code)
if err != nil {
return nil, errors.Wrapf(err, "driver %s Authenticate", self.Template)
@@ -120,6 +122,14 @@ func (self *SOAuth2Driver) Authenticate(ctx context.Context, ident mcclient.SAut
}
func (self *SOAuth2Driver) Sync(ctx context.Context) error {
factory := findDriverFactory(self.Template)
if factory == nil {
return nil
}
if driver, isOk := factory.NewDriver(self.oauth2Config.AppId, self.oauth2Config.Secret).(IOAuth2Synchronizer); isOk {
ctx = context.WithValue(ctx, "config", self.SBaseIdentityDriver.Config)
return driver.Sync(ctx, self.IdpId)
}
return nil
}
+4
View File
@@ -32,6 +32,10 @@ type IOAuth2Driver interface {
GetSsoRedirectUri(ctx context.Context, callbackUrl, state string) (string, error)
}
type IOAuth2Synchronizer interface {
Sync(ctx context.Context, idpId string) error
}
type SOAuth2BaseDriver struct {
AppId string
Secret string
+1
View File
@@ -19,6 +19,7 @@ import (
_ "yunion.io/x/onecloud/pkg/keystone/driver/ldap"
_ "yunion.io/x/onecloud/pkg/keystone/driver/oauth2"
_ "yunion.io/x/onecloud/pkg/keystone/driver/oauth2/alipay"
_ "yunion.io/x/onecloud/pkg/keystone/driver/oauth2/bingoiam"
_ "yunion.io/x/onecloud/pkg/keystone/driver/oauth2/dingtalk"
_ "yunion.io/x/onecloud/pkg/keystone/driver/oauth2/feishu"
_ "yunion.io/x/onecloud/pkg/keystone/driver/oauth2/qywechat"