deprecate version 1 robot account (#15296)

1, deprecate support for version 1 robot support, the robotv1 cannot be used anymore.
2, reserve the /project/{id_or_name}/robots api.

After the PR, user cannot use the robotv1 to login, and do any interaction with Harbor,
but still can view & delete them with UI or API.

Signed-off-by: Wang Yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2021-07-13 13:39:44 +08:00 committed by GitHub
parent 3e502ec9a4
commit f7a4401dcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 55 additions and 133 deletions

View File

@ -15,60 +15,76 @@
package security package security
import ( import (
"net/http" "fmt"
"strings"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/security"
robotCtx "github.com/goharbor/harbor/src/common/security/robot" robotCtx "github.com/goharbor/harbor/src/common/security/robot"
ctl_robot "github.com/goharbor/harbor/src/controller/robot" "github.com/goharbor/harbor/src/common/utils"
robot_ctl "github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
pkg_token "github.com/goharbor/harbor/src/pkg/token" "github.com/goharbor/harbor/src/lib/q"
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot" "github.com/goharbor/harbor/src/pkg/permission/types"
"strings"
"time"
"github.com/goharbor/harbor/src/pkg/robot/model"
"net/http"
) )
type robot struct{} type robot struct{}
func (r *robot) Generate(req *http.Request) security.Context { func (r *robot) Generate(req *http.Request) security.Context {
log := log.G(req.Context()) log := log.G(req.Context())
robotName, robotTk, ok := req.BasicAuth() name, secret, ok := req.BasicAuth()
if !ok { if !ok {
return nil return nil
} }
if !strings.HasPrefix(robotName, common.RobotPrefix) { if !strings.HasPrefix(name, config.RobotPrefix(req.Context())) {
return nil return nil
} }
rClaims := &robot_claim.Claim{} // The robot name can be used as the unique identifier to locate robot as it contains the project name.
defaultOpt := pkg_token.DefaultTokenOptions() robots, err := robot_ctl.Ctl.List(req.Context(), q.New(q.KeyWords{
if defaultOpt == nil { "name": strings.TrimPrefix(name, config.RobotPrefix(req.Context())),
log.Error("failed to get default token options") }), &robot_ctl.Option{
return nil WithPermission: true,
} })
rtk, err := pkg_token.Parse(defaultOpt, robotTk, rClaims)
if err != nil { if err != nil {
log.Debugf("failed to decrypt robot token of v1 robot: %s, as: %v", robotName, err) log.Errorf("failed to list robots: %v", err)
return nil return nil
} }
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable. if len(robots) == 0 {
ctr := ctl_robot.Ctl
robot, err := ctr.Get(req.Context(), rtk.Claims.(*robot_claim.Claim).TokenID, nil)
if err != nil {
log.Errorf("failed to get robot %s: %v", robotName, err)
return nil return nil
} }
if robot == nil {
log.Error("the token provided doesn't exist.") robot := robots[0]
return nil if utils.Encrypt(secret, robot.Salt, utils.SHA256) != robot.Secret {
} log.Errorf("failed to authenticate robot account: %s", name)
if robotName != robot.Name {
log.Errorf("failed to authenticate : %v", robotName)
return nil return nil
} }
if robot.Disabled { if robot.Disabled {
log.Errorf("the robot account %s is disabled", robot.Name) log.Errorf("failed to authenticate disabled robot account: %s", name)
return nil return nil
} }
log.Debugf("a robot security context generated for request %s %s", req.Method, req.URL.Path) now := time.Now().Unix()
if robot.ExpiresAt != -1 && robot.ExpiresAt <= now {
return robotCtx.NewSecurityContext(&robot.Robot, false, rtk.Claims.(*robot_claim.Claim).Access) log.Errorf("the robot account is expired: %s", name)
return nil
}
var accesses []*types.Policy
for _, p := range robot.Permissions {
for _, a := range p.Access {
accesses = append(accesses, &types.Policy{
Action: a.Action,
Effect: a.Effect,
Resource: types.Resource(fmt.Sprintf("%s/%s", p.Scope, a.Resource)),
})
}
}
modelRobot := &model.Robot{
Name: name,
}
log.Infof("a robot security context generated for request %s %s", req.Method, req.URL.Path)
return robotCtx.NewSecurityContext(modelRobot, robot.Level == robot_ctl.LEVELSYSTEM, accesses)
} }

View File

@ -1,76 +0,0 @@
package security
import (
"fmt"
"github.com/goharbor/harbor/src/common/security"
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
"github.com/goharbor/harbor/src/common/utils"
robot_ctl "github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/permission/types"
"strings"
"time"
"github.com/goharbor/harbor/src/pkg/robot/model"
"net/http"
)
type robot2 struct{}
func (r *robot2) Generate(req *http.Request) security.Context {
log := log.G(req.Context())
name, secret, ok := req.BasicAuth()
if !ok {
return nil
}
if !strings.HasPrefix(name, config.RobotPrefix(req.Context())) {
return nil
}
// The robot name can be used as the unique identifier to locate robot as it contains the project name.
robots, err := robot_ctl.Ctl.List(req.Context(), q.New(q.KeyWords{
"name": strings.TrimPrefix(name, config.RobotPrefix(req.Context())),
}), &robot_ctl.Option{
WithPermission: true,
})
if err != nil {
log.Errorf("failed to list robots: %v", err)
return nil
}
if len(robots) == 0 {
return nil
}
robot := robots[0]
if utils.Encrypt(secret, robot.Salt, utils.SHA256) != robot.Secret {
log.Errorf("failed to authenticate robot account: %s", name)
return nil
}
if robot.Disabled {
log.Errorf("failed to authenticate disabled robot account: %s", name)
return nil
}
now := time.Now().Unix()
if robot.ExpiresAt != -1 && robot.ExpiresAt <= now {
log.Errorf("the robot account is expired: %s", name)
return nil
}
var accesses []*types.Policy
for _, p := range robot.Permissions {
for _, a := range p.Access {
accesses = append(accesses, &types.Policy{
Action: a.Action,
Effect: a.Effect,
Resource: types.Resource(fmt.Sprintf("%s/%s", p.Scope, a.Resource)),
})
}
}
modelRobot := &model.Robot{
Name: name,
}
log.Infof("a robot2 security context generated for request %s %s", req.Method, req.URL.Path)
return robotCtx.NewSecurityContext(modelRobot, robot.Level == robot_ctl.LEVELSYSTEM, accesses)
}

View File

@ -1,24 +0,0 @@
package security
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/lib/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"testing"
)
func TestRobot2(t *testing.T) {
conf := map[string]interface{}{
common.RobotNamePrefix: "robot@",
}
config.InitWithSettings(conf)
robot := &robot2{}
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
require.Nil(t, err)
req.SetBasicAuth("robot@est1", "Harbor12345")
ctx := robot.Generate(req)
assert.Nil(t, ctx)
}

View File

@ -15,6 +15,8 @@
package security package security
import ( import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/lib/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"net/http" "net/http"
@ -22,10 +24,15 @@ import (
) )
func TestRobot(t *testing.T) { func TestRobot(t *testing.T) {
conf := map[string]interface{}{
common.RobotNamePrefix: "robot@",
}
config.InitWithSettings(conf)
robot := &robot{} robot := &robot{}
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil)
require.Nil(t, err) require.Nil(t, err)
req.SetBasicAuth("robot$test1", "Harbor12345") req.SetBasicAuth("robot@est1", "Harbor12345")
ctx := robot.Generate(req) ctx := robot.Generate(req)
assert.Nil(t, ctx) assert.Nil(t, ctx)
} }

View File

@ -33,7 +33,6 @@ var (
&idToken{}, &idToken{},
&authProxy{}, &authProxy{},
&robot{}, &robot{},
&robot2{},
&basicAuth{}, &basicAuth{},
&session{}, &session{},
&proxyCacheSecret{}, &proxyCacheSecret{},