Merge pull request #9351 from ioito/hotfix/qx-aliyun-saml-auth

feat(cloudid): support aliyun saml auth
This commit is contained in:
Zexi Li
2020-12-13 12:13:38 +08:00
committed by GitHub
12 changed files with 391 additions and 20 deletions
+4 -1
View File
@@ -142,8 +142,11 @@ func (manager *SSamluserManager) ValidateCreateData(ctx context.Context, userCre
return input, err
}
account := _account.(*SCloudaccount)
if account.SAMLAuth.IsFalse() {
return input, httperrors.NewNotSupportedError("cloudaccount %s not enable saml auth", account.Name)
}
if account.Provider != group.Provider {
return input, httperrors.NewConflictError("account %s and group %s not with same provider %s", account.Name, group.Name)
return input, httperrors.NewConflictError("account %s and group %s not with same provider", account.Name, group.Name)
}
input.Status = api.SAML_USER_STATUS_AVAILABLE
return input, nil
+34 -4
View File
@@ -16,23 +16,53 @@ package aliyun
import (
"context"
"database/sql"
"fmt"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/cloudid/models"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/samlutils"
"yunion.io/x/onecloud/pkg/util/samlutils/idp"
)
func (d *SAliyunSAMLDriver) GetIdpInitiatedLoginData(ctx context.Context, userCred mcclient.TokenCredential, cloudAccountId string, sp *idp.SSAMLServiceProvider) (samlutils.SSAMLIdpInitiatedLoginData, error) {
// TODO
data := samlutils.SSAMLIdpInitiatedLoginData{}
data.NameId = "ecsossreadonly"
_account, err := models.CloudaccountManager.FetchById(cloudAccountId)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return data, httperrors.NewResourceNotFoundError2("cloudaccount", cloudAccountId)
}
return data, httperrors.NewGeneralError(err)
}
account := _account.(*models.SCloudaccount)
if account.Provider != api.CLOUD_PROVIDER_ALIYUN {
return data, httperrors.NewClientError("cloudaccount %s is %s not %s", account.Id, account.Provider, api.CLOUD_PROVIDER_ALIYUN)
}
if account.SAMLAuth.IsFalse() {
return data, httperrors.NewNotSupportedError("cloudaccount %s not open saml auth", account.Id)
}
SAMLProvider, valid := account.IsSAMLProviderValid()
if !valid {
return data, httperrors.NewResourceNotReadyError("SAMLProvider for account %s not ready", account.Id)
}
role, err := account.SyncRole(userCred.GetUserId())
if err != nil {
return data, httperrors.NewGeneralError(errors.Wrapf(err, "SyncRole"))
}
data.NameId = userCred.GetUserName()
data.NameIdFormat = samlutils.NAME_ID_FORMAT_PERSISTENT
data.AudienceRestriction = sp.GetEntityId()
for k, v := range map[string]string{
"https://www.aliyun.com/SAML-Role/Attributes/Role": "acs:ram::1123247935774897:role/administrator,acs:ram::1123247935774897:saml-provider/saml.yunion.io",
"https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName": "ecsossreadonly",
"https://www.aliyun.com/SAML-Role/Attributes/Role": fmt.Sprintf("%s,%s", role.ExternalId, SAMLProvider.ExternalId),
"https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName": userCred.GetUserId(),
"https://www.aliyun.com/SAML-Role/Attributes/SessionDuration": "1800",
} {
data.Attributes = append(data.Attributes, samlutils.SSAMLResponseAttribute{
+1 -1
View File
@@ -35,7 +35,7 @@ func (d *SAWSSAMLDriver) GetIdpInitiatedLoginData(ctx context.Context, userCred
_account, err := models.CloudaccountManager.FetchById(cloudAccountId)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return data, httperrors.NewResourceNotFoundError("cloudaccount", cloudAccountId)
return data, httperrors.NewResourceNotFoundError2("cloudaccount", cloudAccountId)
}
return data, httperrors.NewGeneralError(err)
}
+1 -1
View File
@@ -34,7 +34,7 @@ func (d *SAWSCNSAMLDriver) GetIdpInitiatedLoginData(ctx context.Context, userCre
_account, err := models.CloudaccountManager.FetchById(cloudAccountId)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return data, httperrors.NewResourceNotFoundError("cloudaccount", cloudAccountId)
return data, httperrors.NewResourceNotFoundError2("cloudaccount", cloudAccountId)
}
return data, httperrors.NewGeneralError(err)
}
+1 -1
View File
@@ -41,7 +41,7 @@ func (d *SHuaweiSAMLDriver) GetSpInitiatedLoginData(ctx context.Context, userCre
_account, err := models.CloudaccountManager.FetchById(cloudAccountId)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return data, httperrors.NewResourceNotFoundError("cloudaccount", cloudAccountId)
return data, httperrors.NewResourceNotFoundError2("cloudaccount", cloudAccountId)
}
return data, httperrors.NewGeneralError(err)
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (d *SQcloudSAMLDriver) GetIdpInitiatedLoginData(ctx context.Context, userCr
_account, err := models.CloudaccountManager.FetchById(cloudAccountId)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return data, httperrors.NewResourceNotFoundError("cloudaccount", cloudAccountId)
return data, httperrors.NewResourceNotFoundError2("cloudaccount", cloudAccountId)
}
return data, httperrors.NewGeneralError(err)
}
+10
View File
@@ -61,6 +61,7 @@ const (
ALIYUN_ALIDNS_API_VERSION = "2015-01-09"
ALIYUN_CBN_API_VERSION = "2017-09-12"
ALIYUN_CDN_API_VERSION = "2018-05-10"
ALIYUN_IMS_API_VERSION = "2019-08-15"
)
var (
@@ -243,6 +244,14 @@ func (self *SAliyunClient) getSdkClient(regionId string) (*sdk.Client, error) {
return client, err
}
func (self *SAliyunClient) imsRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
cli, err := self.getDefaultClient()
if err != nil {
return nil, err
}
return jsonRequest(cli, "ims.aliyuncs.com", ALIYUN_IMS_API_VERSION, apiName, params, self.debug)
}
func (self *SAliyunClient) rmRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
cli, err := self.getDefaultClient()
if err != nil {
@@ -538,6 +547,7 @@ func (region *SAliyunClient) GetCapabilities() []string {
cloudprovider.CLOUD_CAPABILITY_CLOUDID,
cloudprovider.CLOUD_CAPABILITY_DNSZONE,
cloudprovider.CLOUD_CAPABILITY_INTERVPCNETWORK,
cloudprovider.CLOUD_CAPABILITY_SAML_AUTH,
}
return caps
}
@@ -16,6 +16,7 @@ package provider
import (
"context"
"fmt"
"strings"
"yunion.io/x/jsonutils"
@@ -56,6 +57,10 @@ func (factory *SAliyunProviderFactory) IsSystemCloudpolicyUnified() bool {
return false
}
func (factory *SAliyunProviderFactory) IsSupportSAMLAuth() bool {
return true
}
func (self *SAliyunProviderFactory) GetSupportedDnsZoneTypes() []cloudprovider.TDnsZoneType {
return []cloudprovider.TDnsZoneType{
cloudprovider.PublicZone,
@@ -447,3 +452,40 @@ func (self *SAliyunProvider) CreateICloudInterVpcNetwork(opts *cloudprovider.SIn
func (self *SAliyunProvider) GetCloudRegionExternalIdPrefix() string {
return self.client.GetAccessEnv() + "/"
}
func (self *SAliyunProvider) GetICloudroles() ([]cloudprovider.ICloudrole, error) {
return self.client.GetICloudroles()
}
func (self *SAliyunProvider) GetICloudroleById(id string) (cloudprovider.ICloudrole, error) {
info := strings.Split(id, "role/")
if len(info) == 2 {
role, err := self.client.GetRole(info[1])
if err != nil {
return nil, errors.Wrapf(err, "GetRole(%s)", info[1])
}
return role, nil
}
return nil, fmt.Errorf("invalid role id %s", id)
}
func (self *SAliyunProvider) CreateICloudrole(opts *cloudprovider.SRoleCreateOptions) (cloudprovider.ICloudrole, error) {
stetement := fmt.Sprintf(`{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Federated":["%s"]},"Condition":{"StringEquals":{"saml:recipient":"https://signin.aliyun.com/saml-role/sso"}}}],"Version":"1"}`, opts.SAMLProvider)
role, err := self.client.CreateRole(opts.Name, stetement, opts.Desc)
if err != nil {
return nil, errors.Wrapf(err, "CreateRole")
}
return role, nil
}
func (self *SAliyunProvider) GetICloudSAMLProviders() ([]cloudprovider.ICloudSAMLProvider, error) {
return self.client.GetICloudSAMLProviders()
}
func (self *SAliyunProvider) CreateICloudSAMLProvider(opts *cloudprovider.SAMLProviderCreateOptions) (cloudprovider.ICloudSAMLProvider, error) {
sp, err := self.client.CreateSAMLProvider(opts.Name, opts.Metadata.String(), "")
if err != nil {
return nil, errors.Wrapf(err, "CreateSAMLProvider")
}
return sp, nil
}
+89 -2
View File
@@ -18,7 +18,10 @@ import (
"fmt"
"time"
"github.com/pkg/errors"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/cloudprovider"
)
type sRoles struct {
@@ -43,6 +46,91 @@ type SRole struct {
AssumeRolePolicyDocument string
}
func (self *SRole) GetGlobalId() string {
return self.Arn
}
func (self *SRole) GetName() string {
return self.RoleName
}
func (self *SRole) GetICloudpolicies() ([]cloudprovider.ICloudpolicy, error) {
policies, err := self.client.ListPoliciesForRole(self.RoleName)
if err != nil {
return nil, errors.Wrapf(err, "ListPoliciesForRole")
}
ret := []cloudprovider.ICloudpolicy{}
for i := range policies {
policies[i].client = self.client
ret = append(ret, &policies[i])
}
return ret, nil
}
func (self *SRole) AttachPolicy(policyName string) error {
return self.client.AttachPolicy2Role("System", policyName, self.RoleName)
}
func (self *SRole) DetachPolicy(policyName string) error {
return self.client.DetachPolicyFromRole("System", policyName, self.RoleName)
}
func (self *SRole) Delete() error {
return self.client.DeleteRole(self.RoleName)
}
func (self *SRole) GetDocument() *jsonutils.JSONDict {
role, err := self.client.GetRole(self.RoleName)
if err != nil {
return nil
}
documet, err := jsonutils.Parse([]byte(role.AssumeRolePolicyDocument))
if err != nil {
return nil
}
return documet.(*jsonutils.JSONDict)
}
func (self *SRole) GetSAMLProvider() string {
document := self.GetDocument()
if document != nil {
statement, err := document.GetArray("Statement")
if err == nil {
for i := range statement {
if action, _ := statement[i].GetString("Action"); action == "sts:AssumeRole" {
sp, _ := statement[i].GetString("Principal", "Federated")
if len(sp) > 0 {
return sp
}
}
}
}
}
return ""
}
func (self *SAliyunClient) GetICloudroles() ([]cloudprovider.ICloudrole, error) {
roles := []SRole{}
marker := ""
for {
part, err := self.ListRoles(marker, 1000)
if err != nil {
return nil, errors.Wrapf(err, "ListRoles(%s)", marker)
}
roles = append(roles, part.Roles.Role...)
if len(part.Marker) == 0 {
break
}
marker = part.Marker
}
ret := []cloudprovider.ICloudrole{}
for i := range roles {
roles[i].client = self
ret = append(ret, &roles[i])
}
return ret, nil
}
func (self *SAliyunClient) ListRoles(offset string, limit int) (*SRoles, error) {
if limit < 0 || limit > 1000 {
limit = 1000
@@ -66,7 +154,6 @@ func (self *SAliyunClient) ListRoles(offset string, limit int) (*SRoles, error)
if err != nil {
return nil, errors.Wrap(err, "body.Unmarshal(&")
}
return roles, nil
}
+167 -6
View File
@@ -15,23 +15,184 @@
package aliyun
import (
"yunion.io/x/log"
"encoding/base64"
"fmt"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis/cloudid"
api "yunion.io/x/onecloud/pkg/apis/cloudid"
"yunion.io/x/onecloud/pkg/cloudprovider"
"yunion.io/x/onecloud/pkg/compute/options"
"yunion.io/x/onecloud/pkg/multicloud"
"yunion.io/x/onecloud/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/util/samlutils"
)
type SAMLProvider struct {
multicloud.SResourceBase
client *SAliyunClient
Arn string
CreateDate time.Time
Description string
SAMLProviderName string
UpdateDate time.Time
// base64
EncodedSAMLMetadataDocument string
}
func (self *SAliyunClient) ListSAMLProviders() ([]SAMLProvider, error) {
func (self *SAMLProvider) GetName() string {
return self.SAMLProviderName
}
func (self *SAMLProvider) GetGlobalId() string {
return self.Arn
}
func (self *SAMLProvider) GetId() string {
return self.Arn
}
func (self *SAMLProvider) GetAuthUrl() string {
input := samlutils.SIdpInitiatedLoginInput{
EntityID: cloudprovider.SAML_ENTITY_ID_ALIYUN_ROLE,
IdpId: self.client.cpcfg.AccountId,
}
return httputils.JoinPath(options.Options.ApiServer, cloudid.SAML_IDP_PREFIX, fmt.Sprintf("sso?%s", jsonutils.Marshal(input).QueryString()))
}
func (self *SAMLProvider) Delete() error {
return self.client.DeleteSAMLProvider(self.SAMLProviderName)
}
func (self *SAMLProvider) GetStatus() string {
return api.SAML_PROVIDER_STATUS_AVAILABLE
}
func (self *SAliyunClient) ListSAMLProviders(marker string, maxItems int) ([]SAMLProvider, string, error) {
if maxItems < 1 || maxItems > 100 {
maxItems = 100
}
params := map[string]string{
"MaxItems": fmt.Sprintf("%d", maxItems),
}
if len(marker) > 0 {
params["Marker"] = marker
}
resp, err := self.imsRequest("ListSAMLProviders", params)
if err != nil {
return nil, "", errors.Wrapf(err, "ListSAMLProviders")
}
result := []SAMLProvider{}
resp, err := self.ramRequest("ListSAMLProviders", map[string]string{})
err = resp.Unmarshal(&result, "SAMLProviders", "SAMLProvider")
if err != nil {
return nil, "", errors.Wrapf(err, "resp.Unmarshal")
}
marker, _ = resp.GetString("Marker")
return result, marker, nil
}
func (self *SAliyunClient) DeleteSAMLProvider(name string) error {
params := map[string]string{
"SAMLProviderName": name,
}
_, err := self.imsRequest("DeleteSAMLProvider", params)
if err != nil {
return errors.Wrapf(err, "DeleteSAMLProvider")
}
return nil
}
func (self *SAMLProvider) GetMetadataDocument() (*samlutils.EntityDescriptor, error) {
sp, err := self.client.GetSAMLProvider(self.SAMLProviderName)
if err != nil {
return nil, errors.Wrapf(err, "GetSAMLProvider(%s)", self.SAMLProviderName)
}
metadata, err := base64.StdEncoding.DecodeString(sp.EncodedSAMLMetadataDocument)
if err != nil {
return nil, errors.Wrapf(err, "DecodeString")
}
ret, err := samlutils.ParseMetadata(metadata)
if err != nil {
return nil, errors.Wrapf(err, "ListSAMLProviders")
return nil, errors.Wrapf(err, "ParseMetadata")
}
return &ret, nil
}
func (self *SAMLProvider) UpdateMetadata(metadata samlutils.EntityDescriptor) error {
return self.client.UpdateSAMLProvider(self.SAMLProviderName, metadata.String())
}
func (self *SAliyunClient) GetSAMLProvider(name string) (*SAMLProvider, error) {
params := map[string]string{
"SAMLProviderName": name,
}
resp, err := self.imsRequest("GetSAMLProvider", params)
if err != nil {
return nil, errors.Wrapf(err, "GetSAMLProvider")
}
sp := &SAMLProvider{client: self}
err = resp.Unmarshal(sp, "SAMLProvider")
if err != nil {
return nil, errors.Wrapf(err, "resp.Unmarshal")
}
return sp, nil
}
func (self *SAliyunClient) UpdateSAMLProvider(name string, metadata string) error {
params := map[string]string{
"SAMLProviderName": name,
"NewEncodedSAMLMetadataDocument": base64.StdEncoding.EncodeToString([]byte(metadata)),
}
_, err := self.imsRequest("UpdateSAMLProvider", params)
if err != nil {
return errors.Wrapf(err, "GetSAMLProvider")
}
return nil
}
func (self *SAliyunClient) CreateSAMLProvider(name, metadata, desc string) (*SAMLProvider, error) {
params := map[string]string{
"EncodedSAMLMetadataDocument": base64.StdEncoding.EncodeToString([]byte(metadata)),
"SAMLProviderName": name,
}
if len(desc) > 0 {
params["Description"] = desc
}
resp, err := self.imsRequest("CreateSAMLProvider", params)
if err != nil {
return nil, errors.Wrapf(err, "CreateSAMLProvider")
}
sp := &SAMLProvider{client: self}
err = resp.Unmarshal(sp, "SAMLProvider")
if err != nil {
return nil, errors.Wrapf(err, "resp.Unmarshal")
}
return sp, nil
}
func (self *SAliyunClient) GetICloudSAMLProviders() ([]cloudprovider.ICloudSAMLProvider, error) {
marker := ""
sps := []SAMLProvider{}
for {
part, _marker, err := self.ListSAMLProviders(marker, 100)
if err != nil {
return nil, errors.Wrapf(err, "ListSAMLProviders")
}
sps = append(sps, part...)
if len(_marker) == 0 {
break
}
marker = _marker
}
ret := []cloudprovider.ICloudSAMLProvider{}
for i := range sps {
sps[i].client = self
ret = append(ret, &sps[i])
}
log.Errorf("result: %s", resp.PrettyString())
return result, nil
return ret, nil
}
+15
View File
@@ -45,6 +45,21 @@ func init() {
return nil
})
type RoleCreateOptions struct {
NAME string
DOCUMENT string
Desc string
}
shellutils.R(&RoleCreateOptions{}, "cloud-role-create", "Create ram role", func(cli *aliyun.SRegion, args *RoleCreateOptions) error {
role, err := cli.GetClient().CreateRole(args.NAME, args.DOCUMENT, args.Desc)
if err != nil {
return err
}
printObject(role)
return nil
})
type RolePolicyOptions struct {
ROLENAME string
POLICYNAME string
+26 -3
View File
@@ -21,11 +21,11 @@ import (
func init() {
type SamlProviderListOptions struct {
Limit int `help:"page size"`
Offset int `help:"page offset"`
Marker string `help:"Marker"`
MaxItems int `help:"Max items"`
}
shellutils.R(&SamlProviderListOptions{}, "saml-provider-list", "List saml provider", func(cli *aliyun.SRegion, args *SamlProviderListOptions) error {
result, err := cli.GetClient().ListSAMLProviders()
result, _, err := cli.GetClient().ListSAMLProviders(args.Marker, args.MaxItems)
if err != nil {
return err
}
@@ -33,4 +33,27 @@ func init() {
return nil
})
type SamlProviderDeleteOptions struct {
NAME string `help:"SAML Provider Name"`
}
shellutils.R(&SamlProviderDeleteOptions{}, "saml-provider-delete", "Delete saml provider", func(cli *aliyun.SRegion, args *SamlProviderDeleteOptions) error {
return cli.GetClient().DeleteSAMLProvider(args.NAME)
})
type SAMLProviderCreateOptions struct {
NAME string
METADAT string
Desc string
}
shellutils.R(&SAMLProviderCreateOptions{}, "saml-provider-create", "Create saml provider", func(cli *aliyun.SRegion, args *SAMLProviderCreateOptions) error {
sp, err := cli.GetClient().CreateSAMLProvider(args.NAME, args.METADAT, args.Desc)
if err != nil {
return err
}
printObject(sp)
return nil
})
}