mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-18 22:57:38 +01:00
populate group list when doing token review
This commit fixes #9869 It has some refactor to make sure the group is populated when user is authenticated via tokenreview workflow. Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
parent
6d382bdb18
commit
3664bf36d2
@ -34,7 +34,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/core/auth"
|
"github.com/goharbor/harbor/src/core/auth"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/pkg/authproxy"
|
"github.com/goharbor/harbor/src/pkg/authproxy"
|
||||||
k8s_api_v1beta1 "k8s.io/api/authentication/v1beta1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const refreshDuration = 2 * time.Second
|
const refreshDuration = 2 * time.Second
|
||||||
@ -101,31 +100,12 @@ func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
|||||||
s := session{}
|
s := session{}
|
||||||
err = json.Unmarshal(data, &s)
|
err = json.Unmarshal(data, &s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to read session %v", err)
|
return nil, auth.NewErrAuth(fmt.Sprintf("failed to read session %v", err))
|
||||||
}
|
}
|
||||||
|
if err := a.tokenReview(s.SessionID, user); err != nil {
|
||||||
reviewResponse, err := a.tokenReview(s.SessionID)
|
return nil, auth.NewErrAuth(err.Error())
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if reviewResponse == nil {
|
|
||||||
return nil, auth.ErrAuth{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach user group ID information
|
|
||||||
ugList := reviewResponse.Status.User.Groups
|
|
||||||
log.Debugf("user groups %+v", ugList)
|
|
||||||
if len(ugList) > 0 {
|
|
||||||
userGroups := models.UserGroupsFromName(ugList, common.HTTPGroupType)
|
|
||||||
groupIDList, err := group.PopulateGroup(userGroups)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Debugf("current user's group ID list is %+v", groupIDList)
|
|
||||||
user.GroupIDs = groupIDList
|
|
||||||
}
|
}
|
||||||
return user, nil
|
return user, nil
|
||||||
|
|
||||||
} else if resp.StatusCode == http.StatusUnauthorized {
|
} else if resp.StatusCode == http.StatusUnauthorized {
|
||||||
return nil, auth.ErrAuth{}
|
return nil, auth.ErrAuth{}
|
||||||
} else {
|
} else {
|
||||||
@ -134,17 +114,24 @@ func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
|||||||
log.Warningf("Failed to read response body, error: %v", err)
|
log.Warningf("Failed to read response body, error: %v", err)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to authenticate, status code: %d, text: %s", resp.StatusCode, string(data))
|
return nil, fmt.Errorf("failed to authenticate, status code: %d, text: %s", resp.StatusCode, string(data))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) tokenReview(sessionID string) (*k8s_api_v1beta1.TokenReview, error) {
|
func (a *Auth) tokenReview(sessionID string, user *models.User) error {
|
||||||
httpAuthProxySetting, err := config.HTTPAuthProxySetting()
|
httpAuthProxySetting, err := config.HTTPAuthProxySetting()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
return authproxy.TokenReview(sessionID, httpAuthProxySetting)
|
reviewStatus, err := authproxy.TokenReview(sessionID, httpAuthProxySetting)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u2, err := authproxy.UserFromReviewStatus(reviewStatus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.GroupIDs = u2.GroupIDs
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnBoardUser delegates to dao pkg to insert/update data in DB.
|
// OnBoardUser delegates to dao pkg to insert/update data in DB.
|
||||||
|
@ -325,16 +325,16 @@ func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||||||
log.Errorf("fail to get auth proxy settings, %v", err)
|
log.Errorf("fail to get auth proxy settings, %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
tokenReviewResponse, err := authproxy.TokenReview(proxyPwd, httpAuthProxyConf)
|
tokenReviewStatus, err := authproxy.TokenReview(proxyPwd, httpAuthProxyConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("fail to review token, %v", err)
|
log.Errorf("fail to review token, %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if rawUserName != tokenReviewStatus.User.Username {
|
||||||
if !tokenReviewResponse.Status.Authenticated {
|
log.Errorf("user name doesn't match with token: %s", rawUserName)
|
||||||
log.Errorf("fail to auth user: %s", rawUserName)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := dao.GetUser(models.User{
|
user, err := dao.GetUser(models.User{
|
||||||
Username: rawUserName,
|
Username: rawUserName,
|
||||||
})
|
})
|
||||||
@ -346,11 +346,12 @@ func (ap *authProxyReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||||||
log.Errorf("User: %s has not been on boarded yet.", rawUserName)
|
log.Errorf("User: %s has not been on boarded yet.", rawUserName)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if rawUserName != tokenReviewResponse.Status.User.Username {
|
u2, err := authproxy.UserFromReviewStatus(tokenReviewStatus)
|
||||||
log.Errorf("user name doesn't match with token: %s", rawUserName)
|
if err != nil {
|
||||||
|
log.Errorf("Failed to get user information from token review status, error: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
user.GroupIDs = u2.GroupIDs
|
||||||
pm := config.GlobalProjectMgr
|
pm := config.GlobalProjectMgr
|
||||||
log.Debug("creating local database security context for auth proxy...")
|
log.Debug("creating local database security context for auth proxy...")
|
||||||
securCtx := local.NewSecurityContext(user, pm)
|
securCtx := local.NewSecurityContext(user, pm)
|
||||||
|
@ -2,6 +2,9 @@ package authproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao/group"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
k8s_api_v1beta1 "k8s.io/api/authentication/v1beta1"
|
k8s_api_v1beta1 "k8s.io/api/authentication/v1beta1"
|
||||||
@ -13,8 +16,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TokenReview ...
|
// TokenReview ...
|
||||||
func TokenReview(sessionID string, authProxyConfig *models.HTTPAuthProxy) (*k8s_api_v1beta1.TokenReview, error) {
|
func TokenReview(rawToken string, authProxyConfig *models.HTTPAuthProxy) (k8s_api_v1beta1.TokenReviewStatus, error) {
|
||||||
|
|
||||||
|
emptyStatus := k8s_api_v1beta1.TokenReviewStatus{}
|
||||||
// Init auth client with the auth proxy endpoint.
|
// Init auth client with the auth proxy endpoint.
|
||||||
authClientCfg := &rest.Config{
|
authClientCfg := &rest.Config{
|
||||||
Host: authProxyConfig.TokenReviewEndpoint,
|
Host: authProxyConfig.TokenReviewEndpoint,
|
||||||
@ -22,14 +26,14 @@ func TokenReview(sessionID string, authProxyConfig *models.HTTPAuthProxy) (*k8s_
|
|||||||
GroupVersion: &schema.GroupVersion{},
|
GroupVersion: &schema.GroupVersion{},
|
||||||
NegotiatedSerializer: serializer.DirectCodecFactory{CodecFactory: scheme.Codecs},
|
NegotiatedSerializer: serializer.DirectCodecFactory{CodecFactory: scheme.Codecs},
|
||||||
},
|
},
|
||||||
BearerToken: sessionID,
|
BearerToken: rawToken,
|
||||||
TLSClientConfig: rest.TLSClientConfig{
|
TLSClientConfig: rest.TLSClientConfig{
|
||||||
Insecure: !authProxyConfig.VerifyCert,
|
Insecure: !authProxyConfig.VerifyCert,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
authClient, err := rest.RESTClientFor(authClientCfg)
|
authClient, err := rest.RESTClientFor(authClientCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return emptyStatus, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do auth with the token.
|
// Do auth with the token.
|
||||||
@ -39,27 +43,49 @@ func TokenReview(sessionID string, authProxyConfig *models.HTTPAuthProxy) (*k8s_
|
|||||||
APIVersion: "authentication.k8s.io/v1beta1",
|
APIVersion: "authentication.k8s.io/v1beta1",
|
||||||
},
|
},
|
||||||
Spec: k8s_api_v1beta1.TokenReviewSpec{
|
Spec: k8s_api_v1beta1.TokenReviewSpec{
|
||||||
Token: sessionID,
|
Token: rawToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
res := authClient.Post().Body(tokenReviewRequest).Do()
|
res := authClient.Post().Body(tokenReviewRequest).Do()
|
||||||
err = res.Error()
|
err = res.Error()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("fail to POST auth request, %v", err)
|
log.Errorf("fail to POST auth request, %v", err)
|
||||||
return nil, err
|
return emptyStatus, err
|
||||||
}
|
}
|
||||||
resRaw, err := res.Raw()
|
resRaw, err := res.Raw()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("fail to get raw data of token review, %v", err)
|
log.Errorf("fail to get raw data of token review, %v", err)
|
||||||
return nil, err
|
return emptyStatus, err
|
||||||
}
|
}
|
||||||
// Parse the auth response, check the user name and authenticated status.
|
// Parse the auth response, check the user name and authenticated status.
|
||||||
tokenReviewResponse := &k8s_api_v1beta1.TokenReview{}
|
tokenReviewResponse := &k8s_api_v1beta1.TokenReview{}
|
||||||
err = json.Unmarshal(resRaw, &tokenReviewResponse)
|
err = json.Unmarshal(resRaw, tokenReviewResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("fail to decode token review, %v", err)
|
log.Errorf("fail to decode token review, %v", err)
|
||||||
return nil, err
|
return emptyStatus, err
|
||||||
}
|
}
|
||||||
return tokenReviewResponse, nil
|
return tokenReviewResponse.Status, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserFromReviewStatus transform a review status to a user model.
|
||||||
|
// Group entries will be populated if needed.
|
||||||
|
func UserFromReviewStatus(status k8s_api_v1beta1.TokenReviewStatus) (*models.User, error) {
|
||||||
|
if !status.Authenticated {
|
||||||
|
return nil, fmt.Errorf("failed to authenticate the token, error in status: %s", status.Error)
|
||||||
|
}
|
||||||
|
user := &models.User{
|
||||||
|
Username: status.User.Username,
|
||||||
|
}
|
||||||
|
if len(status.User.Groups) > 0 {
|
||||||
|
userGroups := models.UserGroupsFromName(status.User.Groups, common.HTTPGroupType)
|
||||||
|
groupIDList, err := group.PopulateGroup(userGroups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugf("current user's group ID list is %+v", groupIDList)
|
||||||
|
user.GroupIDs = groupIDList
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
87
src/pkg/authproxy/http_test.go
Normal file
87
src/pkg/authproxy/http_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package authproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao/group"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/api/authentication/v1beta1"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
dao.PrepareTestForPostgresSQL()
|
||||||
|
result := m.Run()
|
||||||
|
if result != 0 {
|
||||||
|
os.Exit(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserFromReviewStatus(t *testing.T) {
|
||||||
|
type result struct {
|
||||||
|
hasErr bool
|
||||||
|
username string
|
||||||
|
groupLen int
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
input v1beta1.TokenReviewStatus
|
||||||
|
expect result
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: v1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: false,
|
||||||
|
Error: "connection error",
|
||||||
|
},
|
||||||
|
expect: result{
|
||||||
|
hasErr: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: v1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: v1beta1.UserInfo{
|
||||||
|
Username: "jack",
|
||||||
|
UID: "u-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: result{
|
||||||
|
hasErr: false,
|
||||||
|
username: "jack",
|
||||||
|
groupLen: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: v1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: v1beta1.UserInfo{
|
||||||
|
Username: "daniel",
|
||||||
|
Groups: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
Error: "",
|
||||||
|
},
|
||||||
|
expect: result{
|
||||||
|
hasErr: false,
|
||||||
|
username: "daniel",
|
||||||
|
groupLen: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
u, err := UserFromReviewStatus(c.input)
|
||||||
|
if c.expect.hasErr == true {
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, c.expect.username, u.Username)
|
||||||
|
assert.Equal(t, c.expect.groupLen, len(u.GroupIDs))
|
||||||
|
}
|
||||||
|
if u != nil {
|
||||||
|
for _, gid := range u.GroupIDs {
|
||||||
|
t.Logf("Deleting group %d", gid)
|
||||||
|
if err := group.DeleteUserGroup(gid); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user