From fde9d2e7cdd7bc515b868cdaf65cfb1db33042b1 Mon Sep 17 00:00:00 2001 From: littlejian Date: Thu, 13 Jul 2023 10:44:59 +0800 Subject: [PATCH] feat: support gitlab identity provider (#5836) --- .../identityprovider/gitlab/gitlab.go | 177 ++++++++++++++++++ .../identityprovider/gitlab/gitlab_test.go | 82 ++++++++ pkg/apiserver/authentication/options.go | 1 + 3 files changed, 260 insertions(+) create mode 100644 pkg/apiserver/authentication/identityprovider/gitlab/gitlab.go create mode 100644 pkg/apiserver/authentication/identityprovider/gitlab/gitlab_test.go diff --git a/pkg/apiserver/authentication/identityprovider/gitlab/gitlab.go b/pkg/apiserver/authentication/identityprovider/gitlab/gitlab.go new file mode 100644 index 000000000..95e4e526f --- /dev/null +++ b/pkg/apiserver/authentication/identityprovider/gitlab/gitlab.go @@ -0,0 +1,177 @@ +/* +Copyright 2020 The KubeSphere Authors. + +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 gitlab + +import ( + "context" + "crypto/tls" + "encoding/json" + "io" + "net/http" + "strconv" + + "github.com/mitchellh/mapstructure" + "golang.org/x/oauth2" + + "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" + "kubesphere.io/kubesphere/pkg/server/options" +) + +const ( + userInfoURL = "https://gitlab.com/api/v4/user" + authURL = "https://gitlab.com/oauth/authorize" + tokenURL = "https://gitlab.com/oauth/token" +) + +func init() { + identityprovider.RegisterOAuthProvider(&gitlabProviderFactory{}) +} + +type gitlab struct { + // ClientID is the application's ID. + ClientID string `json:"clientID" yaml:"clientID"` + + // ClientSecret is the application's secret. + ClientSecret string `json:"-" yaml:"clientSecret"` + + // Endpoint contains the resource server's token endpoint + // URLs. These are constants specific to each server and are + // often available via site-specific packages, such as + // google.Endpoint or sso.endpoint. + Endpoint endpoint `json:"endpoint" yaml:"endpoint"` + + // RedirectURL is the URL to redirect users going through + // the OAuth flow, after the resource owner's URLs. + RedirectURL string `json:"redirectURL" yaml:"redirectURL"` + + // Used to turn off TLS certificate checks + InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"` + + // Scope specifies optional requested permissions. + Scopes []string `json:"scopes" yaml:"scopes"` + + Config *oauth2.Config `json:"-" yaml:"-"` +} + +// endpoint represents an OAuth 2.0 provider's authorization and token +// endpoint URLs. +type endpoint struct { + AuthURL string `json:"authURL" yaml:"authURL"` + TokenURL string `json:"tokenURL" yaml:"tokenURL"` + UserInfoURL string `json:"userInfoURL" yaml:"userInfoURL"` +} + +type gitlabIdentity struct { + ID int64 `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` +} + +type gitlabProviderFactory struct { +} + +func (g *gitlabProviderFactory) Type() string { + return "GitlabIdentityProvider" +} + +func (g *gitlabProviderFactory) Create(opts options.DynamicOptions) (identityprovider.OAuthProvider, error) { + var gitlab gitlab + if err := mapstructure.Decode(opts, &gitlab); err != nil { + return nil, err + } + + if gitlab.Endpoint.AuthURL == "" { + gitlab.Endpoint.AuthURL = authURL + } + if gitlab.Endpoint.TokenURL == "" { + gitlab.Endpoint.TokenURL = tokenURL + } + if gitlab.Endpoint.UserInfoURL == "" { + gitlab.Endpoint.UserInfoURL = userInfoURL + } + // fixed options + opts["endpoint"] = options.DynamicOptions{ + "authURL": gitlab.Endpoint.AuthURL, + "tokenURL": gitlab.Endpoint.TokenURL, + "userInfoURL": gitlab.Endpoint.UserInfoURL, + } + gitlab.Config = &oauth2.Config{ + ClientID: gitlab.ClientID, + ClientSecret: gitlab.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: gitlab.Endpoint.AuthURL, + TokenURL: gitlab.Endpoint.TokenURL, + }, + RedirectURL: gitlab.RedirectURL, + Scopes: gitlab.Scopes, + } + return &gitlab, nil +} + +func (g gitlabIdentity) GetUserID() string { + return strconv.FormatInt(g.ID, 10) +} + +func (g gitlabIdentity) GetUsername() string { + return g.Username +} + +func (g gitlabIdentity) GetEmail() string { + return g.Email +} + +func (g *gitlab) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) { + // OAuth2 callback, see also https://tools.ietf.org/html/rfc6749#section-4.1.2 + code := req.URL.Query().Get("code") + ctx := req.Context() + if g.InsecureSkipVerify { + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + ctx = context.WithValue(ctx, oauth2.HTTPClient, client) + } + token, err := g.Config.Exchange(ctx, code) + if err != nil { + return nil, err + } + resp, err := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)).Get(g.Endpoint.UserInfoURL) + if err != nil { + return nil, err + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var gitlabIdentity gitlabIdentity + err = json.Unmarshal(data, &gitlabIdentity) + if err != nil { + return nil, err + } + + return gitlabIdentity, nil +} diff --git a/pkg/apiserver/authentication/identityprovider/gitlab/gitlab_test.go b/pkg/apiserver/authentication/identityprovider/gitlab/gitlab_test.go new file mode 100644 index 000000000..a04241aea --- /dev/null +++ b/pkg/apiserver/authentication/identityprovider/gitlab/gitlab_test.go @@ -0,0 +1,82 @@ +package gitlab + +import ( + "reflect" + "testing" + + "golang.org/x/oauth2" + "gopkg.in/yaml.v3" + + "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" + "kubesphere.io/kubesphere/pkg/server/options" +) + +func Test_gitlabProviderFactory_Create(t *testing.T) { + type args struct { + opts options.DynamicOptions + } + + mustUnmarshalYAML := func(data string) options.DynamicOptions { + var dynamicOptions options.DynamicOptions + _ = yaml.Unmarshal([]byte(data), &dynamicOptions) + return dynamicOptions + } + + tests := []struct { + name string + args args + want identityprovider.OAuthProvider + wantErr bool + }{ + { + name: "should create successfully", + args: args{opts: mustUnmarshalYAML(` +clientID: 035c18fc229c686e4652d7034 +clientSecret: 75c82b42e54aaf25186140f5 +endpoint: + userInfoUrl: "https://gitlab.com/api/v4/user" + authURL: "https://gitlab.com/oauth/authorize" + tokenURL: "https://gitlab.com/oauth/token" +redirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/gitlab" +scopes: +- read +`)}, + want: &gitlab{ + ClientID: "035c18fc229c686e4652d7034", + ClientSecret: "75c82b42e54aaf25186140f5", + Endpoint: endpoint{ + AuthURL: "https://gitlab.com/oauth/authorize", + TokenURL: "https://gitlab.com/oauth/token", + UserInfoURL: "https://gitlab.com/api/v4/user", + }, + RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/gitlab", + Scopes: []string{"read"}, + Config: &oauth2.Config{ + ClientID: "035c18fc229c686e4652d7034", + ClientSecret: "75c82b42e54aaf25186140f5", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://gitlab.com/oauth/authorize", + TokenURL: "https://gitlab.com/oauth/token", + AuthStyle: oauth2.AuthStyleAutoDetect, + }, + RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/gitlab", + Scopes: []string{"read"}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &gitlabProviderFactory{} + got, err := g.Create(tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Create() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/apiserver/authentication/options.go b/pkg/apiserver/authentication/options.go index 0bba53fea..f969e37b5 100644 --- a/pkg/apiserver/authentication/options.go +++ b/pkg/apiserver/authentication/options.go @@ -28,6 +28,7 @@ import ( _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/aliyunidaas" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/cas" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/github" + _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/gitlab" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/ldap" _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/oidc" "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"