Merge pull request #6820 from wy65701436/robot-service

Add robot account authn & authz implementation
This commit is contained in:
Daniel Jiang 2019-01-29 16:08:25 +08:00 committed by GitHub
commit bf663df0e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 914 additions and 25 deletions

View File

@ -36,6 +36,7 @@ env:
- REDIS_HOST: localhost
- REG_VERSION: v2.6.2
- UI_BUILDER_VERSION: 1.6.0
- TOKEN_PRIVATE_KEY_PATH: "/home/travis/gopath/src/github.com/goharbor/harbor/tests/private_key.pem"
addons:
apt:
sources:

View File

@ -120,6 +120,8 @@ const (
DefaultPortalURL = "http://portal"
DefaultRegistryCtlURL = "http://registryctl:8080"
DefaultClairHealthCheckServerURL = "http://clair:6061"
// Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user.
RobotPrefix = "robot$"
)
// Shared variable, not allowed to modify

View File

@ -16,6 +16,7 @@ package models
import (
"github.com/astaxie/beego/validation"
"github.com/goharbor/harbor/src/common/rbac"
"time"
)
@ -48,7 +49,7 @@ type RobotReq struct {
Name string `json:"name"`
Description string `json:"description"`
Disabled bool `json:"disabled"`
Access []*ResourceActions `json:"access"`
Access []*rbac.Policy `json:"access"`
}
// Valid put request validation

View File

@ -105,7 +105,8 @@ var (
}
)
func policiesForPublicProject(namespace rbac.Namespace) []*rbac.Policy {
// PoliciesForPublicProject ...
func PoliciesForPublicProject(namespace rbac.Namespace) []*rbac.Policy {
policies := []*rbac.Policy{}
for _, policy := range publicProjectPolicies {

View File

@ -51,7 +51,7 @@ func (v *visitor) GetPolicies() []*rbac.Policy {
}
if v.namespace.IsPublic() {
return policiesForPublicProject(v.namespace)
return PoliciesForPublicProject(v.namespace)
}
return nil

View File

@ -57,13 +57,13 @@ func (suite *VisitorTestSuite) TestGetPolicies() {
suite.Nil(anonymous.GetPolicies())
anonymousForPublicProject := NewUser(anonymousCtx, publicNamespace)
suite.Equal(anonymousForPublicProject.GetPolicies(), policiesForPublicProject(publicNamespace))
suite.Equal(anonymousForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
authenticated := NewUser(authenticatedCtx, namespace)
suite.Nil(authenticated.GetPolicies())
authenticatedForPublicProject := NewUser(authenticatedCtx, publicNamespace)
suite.Equal(authenticatedForPublicProject.GetPolicies(), policiesForPublicProject(publicNamespace))
suite.Equal(authenticatedForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
systemAdmin := NewUser(sysAdminCtx, namespace)
suite.Equal(systemAdmin.GetPolicies(), GetAllPolicies(namespace))

View File

@ -0,0 +1,107 @@
// Copyright Project Harbor 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 robot
import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/core/promgr"
)
// SecurityContext implements security.Context interface based on database
type SecurityContext struct {
robot *models.Robot
pm promgr.ProjectManager
policy []*rbac.Policy
}
// NewSecurityContext ...
func NewSecurityContext(robot *models.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext {
return &SecurityContext{
robot: robot,
pm: pm,
policy: policy,
}
}
// IsAuthenticated returns true if the user has been authenticated
func (s *SecurityContext) IsAuthenticated() bool {
return s.robot != nil
}
// GetUsername returns the username of the authenticated user
// It returns null if the user has not been authenticated
func (s *SecurityContext) GetUsername() string {
if !s.IsAuthenticated() {
return ""
}
return s.robot.Name
}
// IsSysAdmin robot cannot be a system admin
func (s *SecurityContext) IsSysAdmin() bool {
return false
}
// IsSolutionUser robot cannot be a system admin
func (s *SecurityContext) IsSolutionUser() bool {
return false
}
// HasReadPerm returns whether the user has read permission to the project
func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage))
}
// HasWritePerm returns whether the user has write permission to the project
func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage))
}
// HasAllPerm returns whether the user has all permissions to the project
func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage))
}
// GetMyProjects no implementation
func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
return nil, nil
}
// GetProjectRoles no implementation
func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
return nil
}
// Can returns whether the robot can do action on resource
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
ns, err := resource.GetNamespace()
if err == nil {
switch ns.Kind() {
case "project":
projectIDOrName := ns.Identity()
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject)
robot := NewRobot(s.GetUsername(), projectNamespace, s.policy)
return rbac.HasPermission(robot, resource, action)
}
}
return false
}

View File

@ -0,0 +1,197 @@
// Copyright Project Harbor 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 robot
import (
"os"
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"strconv"
)
var (
private = &models.Project{
Name: "testrobot",
OwnerID: 1,
}
pm = promgr.NewDefaultProjectManager(local.NewDriver(), true)
)
func TestMain(m *testing.M) {
dbHost := os.Getenv("POSTGRESQL_HOST")
if len(dbHost) == 0 {
log.Fatalf("environment variable POSTGRES_HOST is not set")
}
dbUser := os.Getenv("POSTGRESQL_USR")
if len(dbUser) == 0 {
log.Fatalf("environment variable POSTGRES_USR is not set")
}
dbPortStr := os.Getenv("POSTGRESQL_PORT")
if len(dbPortStr) == 0 {
log.Fatalf("environment variable POSTGRES_PORT is not set")
}
dbPort, err := strconv.Atoi(dbPortStr)
if err != nil {
log.Fatalf("invalid POSTGRESQL_PORT: %v", err)
}
dbPassword := os.Getenv("POSTGRESQL_PWD")
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
}
database := &models.Database{
Type: "postgresql",
PostGreSQL: &models.PostGreSQL{
Host: dbHost,
Port: dbPort,
Username: dbUser,
Password: dbPassword,
Database: dbDatabase,
},
}
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
// add project
id, err := dao.AddProject(*private)
if err != nil {
log.Fatalf("failed to add project: %v", err)
}
private.ProjectID = id
defer dao.DeleteProject(id)
os.Exit(m.Run())
}
func TestIsAuthenticated(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil, nil)
assert.False(t, ctx.IsAuthenticated())
// authenticated
ctx = NewSecurityContext(&models.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
assert.True(t, ctx.IsAuthenticated())
}
func TestGetUsername(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil, nil)
assert.Equal(t, "", ctx.GetUsername())
// authenticated
ctx = NewSecurityContext(&models.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
assert.Equal(t, "test", ctx.GetUsername())
}
func TestIsSysAdmin(t *testing.T) {
// unauthenticated
ctx := NewSecurityContext(nil, nil, nil)
assert.False(t, ctx.IsSysAdmin())
// authenticated, non admin
ctx = NewSecurityContext(&models.Robot{
Name: "test",
Disabled: false,
}, nil, nil)
assert.False(t, ctx.IsSysAdmin())
}
func TestIsSolutionUser(t *testing.T) {
ctx := NewSecurityContext(nil, nil, nil)
assert.False(t, ctx.IsSolutionUser())
}
func TestHasReadPerm(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/testrobot/image",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := &models.Robot{
Name: "test_robot_1",
Description: "desc",
}
ctx := NewSecurityContext(robot, pm, policies)
assert.True(t, ctx.HasReadPerm(private.Name))
}
func TestHasWritePerm(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/testrobot/image",
Action: "push",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := &models.Robot{
Name: "test_robot_2",
Description: "desc",
}
ctx := NewSecurityContext(robot, pm, policies)
assert.True(t, ctx.HasWritePerm(private.Name))
}
func TestHasAllPerm(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/testrobot/image",
Action: "push+pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := &models.Robot{
Name: "test_robot_3",
Description: "desc",
}
ctx := NewSecurityContext(robot, pm, policies)
assert.True(t, ctx.HasAllPerm(private.Name))
}
func TestGetMyProjects(t *testing.T) {
ctx := NewSecurityContext(nil, nil, nil)
projects, err := ctx.GetMyProjects()
require.Nil(t, err)
assert.Nil(t, projects)
}
func TestGetProjectRoles(t *testing.T) {
ctx := NewSecurityContext(nil, nil, nil)
roles := ctx.GetProjectRoles("test")
assert.Nil(t, roles)
}

View File

@ -0,0 +1,42 @@
package robot
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
)
// robot implement the rbac.User interface for project robot account
type robot struct {
username string
namespace rbac.Namespace
policy []*rbac.Policy
}
// GetUserName get the robot name.
func (r *robot) GetUserName() string {
return r.username
}
// GetPolicies ...
func (r *robot) GetPolicies() []*rbac.Policy {
policies := []*rbac.Policy{}
if r.namespace.IsPublic() {
policies = append(policies, project.PoliciesForPublicProject(r.namespace)...)
}
policies = append(policies, r.policy...)
return policies
}
// GetRoles robot has no definition of role, always return nil here.
func (r *robot) GetRoles() []rbac.Role {
return nil
}
// NewRobot ...
func NewRobot(username string, namespace rbac.Namespace, policy []*rbac.Policy) rbac.User {
return &robot{
username: username,
namespace: namespace,
policy: policy,
}
}

View File

@ -0,0 +1,27 @@
package robot
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/stretchr/testify/assert"
"testing"
)
func TestGetPolicies(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
robot := robot{
username: "test",
namespace: rbac.NewProjectNamespace("library", false),
policy: policies,
}
assert.Equal(t, robot.GetUserName(), "test")
assert.NotNil(t, robot.GetPolicies())
assert.Nil(t, robot.GetRoles())
}

View File

@ -0,0 +1,29 @@
package token
import (
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/pkg/errors"
)
// RobotClaims implements the interface of jwt.Claims
type RobotClaims struct {
jwt.StandardClaims
TokenID int64 `json:"id"`
ProjectID int64 `json:"pid"`
Access []*rbac.Policy `json:"access"`
}
// Valid valid the claims "tokenID, projectID and access".
func (rc RobotClaims) Valid() error {
if rc.TokenID < 0 {
return errors.New("Token id must an valid INT")
}
if rc.ProjectID < 0 {
return errors.New("Project id must an valid INT")
}
if rc.Access == nil {
return errors.New("The access info cannot be nil")
}
return nil
}

View File

@ -0,0 +1,68 @@
package token
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/stretchr/testify/assert"
"testing"
)
func TestValid(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &RobotClaims{
TokenID: 1,
ProjectID: 2,
Access: policies,
}
assert.Nil(t, rClaims.Valid())
}
func TestUnValidTokenID(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &RobotClaims{
TokenID: -1,
ProjectID: 2,
Access: policies,
}
assert.NotNil(t, rClaims.Valid())
}
func TestUnValidProjectID(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &RobotClaims{
TokenID: 1,
ProjectID: -2,
Access: policies,
}
assert.NotNil(t, rClaims.Valid())
}
func TestUnValidPolicy(t *testing.T) {
rClaims := &RobotClaims{
TokenID: 1,
ProjectID: 2,
Access: nil,
}
assert.NotNil(t, rClaims.Valid())
}

View File

@ -0,0 +1,85 @@
package token
import (
"crypto/ecdsa"
"crypto/rsa"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"time"
)
// HToken htoken is a jwt token for harbor robot account,
// which contains the robot ID, project ID and the access permission for the project.
// It used for authn/authz for robot account in Harbor.
type HToken struct {
jwt.Token
}
// New ...
func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) {
rClaims := &RobotClaims{
TokenID: tokenID,
ProjectID: projectID,
Access: access,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(DefaultOptions.TTL).Unix(),
Issuer: DefaultOptions.Issuer,
},
}
err := rClaims.Valid()
if err != nil {
return nil, err
}
return &HToken{
Token: *jwt.NewWithClaims(DefaultOptions.SignMethod, rClaims),
}, nil
}
// Raw get the Raw string of token
func (htk *HToken) Raw() (string, error) {
key, err := DefaultOptions.GetKey()
if err != nil {
return "", nil
}
raw, err := htk.Token.SignedString(key)
if err != nil {
log.Debugf(fmt.Sprintf("failed to issue token %v", err))
return "", err
}
return raw, err
}
// ParseWithClaims ...
func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) {
key, err := DefaultOptions.GetKey()
if err != nil {
return nil, err
}
token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != DefaultOptions.SignMethod.Alg() {
return nil, errors.New("invalid signing method")
}
switch k := key.(type) {
case *rsa.PrivateKey:
return &k.PublicKey, nil
case *ecdsa.PrivateKey:
return &k.PublicKey, nil
default:
return key, nil
}
})
if err != nil {
log.Errorf(fmt.Sprintf("parse token error, %v", err))
return nil, err
}
if !token.Valid {
log.Errorf(fmt.Sprintf("invalid jwt token, %v", token))
return nil, err
}
return &HToken{
Token: *token,
}, nil
}

View File

@ -0,0 +1,77 @@
package token
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestMain(m *testing.M) {
server, err := test.NewAdminserver(nil)
if err != nil {
panic(err)
}
defer server.Close()
if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil {
panic(err)
}
if err := config.Init(); err != nil {
panic(err)
}
result := m.Run()
if result != 0 {
os.Exit(result)
}
}
func TestNew(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
tokenID := int64(123)
projectID := int64(321)
token, err := New(tokenID, projectID, policies)
assert.Nil(t, err)
assert.Equal(t, token.Header["alg"], "RS256")
assert.Equal(t, token.Header["typ"], "JWT")
}
func TestRaw(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/library/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
tokenID := int64(123)
projectID := int64(321)
token, err := New(tokenID, projectID, policies)
assert.Nil(t, err)
rawTk, err := token.Raw()
assert.Nil(t, err)
assert.NotNil(t, rawTk)
}
func TestParseWithClaims(t *testing.T) {
rawTk := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTIzLCJQcm9qZWN0SUQiOjAsIkFjY2VzcyI6W3siUmVzb3VyY2UiOiIvcHJvamVjdC9saWJyYXkvcmVwb3NpdG9yeSIsIkFjdGlvbiI6InB1bGwiLCJFZmZlY3QiOiIifV0sIlN0YW5kYXJkQ2xhaW1zIjp7ImV4cCI6MTU0ODE0MDIyOSwiaXNzIjoiaGFyYm9yLXRva2VuLWlzc3VlciJ9fQ.Jc3qSKN4SJVUzAvBvemVpRcSOZaHlu0Avqms04qzPm4ru9-r9IRIl3mnSkI6m9XkzLUeJ7Kiwyw63ghngnVKw_PupeclOGC6s3TK5Cfmo4h-lflecXjZWwyy-dtH_e7Us_ItS-R3nXDJtzSLEpsGHCcAj-1X2s93RB2qD8LNSylvYeDezVkTzqRzzfawPJheKKh9JTrz-3eUxCwQard9-xjlwvfUYULoHTn9npNAUq4-jqhipW4uE8HL-ym33AGF57la8U0RO11hmDM5K8-PiYknbqJ_oONeS3HBNym2pEFeGjtTv2co213wl4T5lemlg4SGolMBuJ03L7_beVZ0o-MKTkKDqDwJalb6_PM-7u3RbxC9IzJMiwZKIPnD3FvV10iPxUUQHaH8Jz5UZ2pFIhi_8BNnlBfT0JOPFVYATtLjHMczZelj2YvAeR1UHBzq3E0jPpjjwlqIFgaHCaN_KMwEvadTo_Fi2sEH4pNGP7M3yehU_72oLJQgF4paJarsmEoij6ZtPs6xekBz1fccVitq_8WNIz9aeCUdkUBRwI5QKw1RdW4ua-w74ld5MZStWJA8veyoLkEb_Q9eq2oAj5KWFjJbW5-ltiIfM8gxKflsrkWAidYGcEIYcuXr7UdqEKXxtPiWM0xb3B91ovYvO5402bn3f9-UGtlcestxNHA"
rClaims := &RobotClaims{}
_, _ = ParseWithClaims(rawTk, rClaims)
assert.Equal(t, int64(123), rClaims.TokenID)
assert.Equal(t, int64(0), rClaims.ProjectID)
assert.Equal(t, "/project/libray/repository", rClaims.Access[0].Resource.String())
}

View File

@ -0,0 +1,83 @@
package token
import (
"crypto/rsa"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"io/ioutil"
"time"
)
const (
ttl = 60 * time.Minute
issuer = "harbor-token-issuer"
signedMethod = "RS256"
)
var (
privateKey = config.TokenPrivateKeyPath()
// DefaultOptions ...
DefaultOptions = NewOptions()
)
// Options ...
type Options struct {
SignMethod jwt.SigningMethod
PublicKey []byte
PrivateKey []byte
TTL time.Duration
Issuer string
}
// NewOptions ...
func NewOptions() *Options {
privateKey, err := ioutil.ReadFile(privateKey)
if err != nil {
log.Errorf(fmt.Sprintf("failed to read private key %v", err))
return nil
}
opt := &Options{
SignMethod: jwt.GetSigningMethod(signedMethod),
PrivateKey: privateKey,
Issuer: issuer,
TTL: ttl,
}
return opt
}
// GetKey ...
func (o *Options) GetKey() (interface{}, error) {
var err error
var privateKey *rsa.PrivateKey
var publicKey *rsa.PublicKey
switch o.SignMethod.(type) {
case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
if len(o.PrivateKey) > 0 {
privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(o.PrivateKey)
if err != nil {
return nil, err
}
}
if len(o.PublicKey) > 0 {
publicKey, err = jwt.ParseRSAPublicKeyFromPEM(o.PublicKey)
if err != nil {
return nil, err
}
}
if privateKey == nil {
if publicKey != nil {
return publicKey, nil
}
return nil, fmt.Errorf("key is provided")
}
if publicKey != nil && publicKey.E != privateKey.E && publicKey.N.Cmp(privateKey.N) != 0 {
return nil, fmt.Errorf("the public key and private key are not match")
}
return privateKey, nil
default:
return nil, fmt.Errorf(fmt.Sprintf("unsupported sign method, %s", o.SignMethod))
}
}

View File

@ -0,0 +1,23 @@
package token
import (
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewOptions(t *testing.T) {
defaultOpt := DefaultOptions
assert.NotNil(t, defaultOpt)
assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256"))
assert.Equal(t, defaultOpt.Issuer, "harbor-token-issuer")
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
}
func TestGetKey(t *testing.T) {
defaultOpt := DefaultOptions
key, err := defaultOpt.GetKey()
assert.Nil(t, err)
assert.NotNil(t, key)
}

View File

@ -16,16 +16,14 @@ package api
import (
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/token"
"net/http"
"strconv"
)
// User this prefix to distinguish harbor user,
// The prefix contains a specific character($), so it cannot be registered as a harbor user.
const robotPrefix = "robot$"
// RobotAPI ...
type RobotAPI struct {
BaseController
@ -98,17 +96,14 @@ func (r *RobotAPI) Prepare() {
func (r *RobotAPI) Post() {
var robotReq models.RobotReq
r.DecodeJSONReq(&robotReq)
createdName := common.RobotPrefix + robotReq.Name
createdName := robotPrefix + robotReq.Name
// first to add a robot account, and get its id.
robot := models.Robot{
Name: createdName,
Description: robotReq.Description,
ProjectID: r.project.ProjectID,
// TODO: use token service to generate token per access information
Token: "this is a placeholder",
}
id, err := dao.AddRobot(&robot)
if err != nil {
if err == dao.ErrDupRows {
@ -119,11 +114,32 @@ func (r *RobotAPI) Post() {
return
}
robotRep := models.RobotRep{
Name: robot.Name,
Token: robot.Token,
// generate the token, and return it with response data.
// token is not stored in the database.
jwtToken, err := token.New(id, r.project.ProjectID, robotReq.Access)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err))
err := dao.DeleteRobot(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err))
}
return
}
rawTk, err := jwtToken.Raw()
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to sign token for robot account, %v", err))
err := dao.DeleteRobot(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err))
}
return
}
robotRep := models.RobotRep{
Name: robot.Name,
Token: rawTk,
}
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
r.Data["json"] = robotRep
r.ServeJSON()

View File

@ -17,6 +17,7 @@ package api
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"net/http"
"testing"
)
@ -27,6 +28,14 @@ var (
)
func TestRobotAPIPost(t *testing.T) {
rbacPolicy := &rbac.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies = append(policies, rbacPolicy)
cases := []*codeCheckingCase{
// 401
{
@ -42,7 +51,7 @@ func TestRobotAPIPost(t *testing.T) {
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{},
bodyJSON: &models.RobotReq{},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
@ -52,9 +61,10 @@ func TestRobotAPIPost(t *testing.T) {
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{
bodyJSON: &models.RobotReq{
Name: "test",
Description: "test desc",
Access: policies,
},
credential: projAdmin4Robot,
},
@ -65,7 +75,7 @@ func TestRobotAPIPost(t *testing.T) {
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{
bodyJSON: &models.RobotReq{
Name: "test2",
Description: "test2 desc",
},
@ -79,10 +89,10 @@ func TestRobotAPIPost(t *testing.T) {
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{
bodyJSON: &models.RobotReq{
Name: "test",
Description: "test desc",
ProjectID: 1,
Access: policies,
},
credential: projAdmin4Robot,
},

View File

@ -53,6 +53,11 @@ func TestConfig(t *testing.T) {
if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil {
t.Fatalf("failed to set env %s: %v", "KEY_PATH", err)
}
oriKeyPath := os.Getenv("TOKEN_PRIVATE_KEY_PATH")
if err := os.Setenv("TOKEN_PRIVATE_KEY_PATH", ""); err != nil {
t.Fatalf("failed to set env %s: %v", "TOKEN_PRIVATE_KEY_PATH", err)
}
defer os.Setenv("TOKEN_PRIVATE_KEY_PATH", oriKeyPath)
if err := Init(); err != nil {
t.Fatalf("failed to initialize configurations: %v", err)

View File

@ -22,18 +22,23 @@ import (
beegoctx "github.com/astaxie/beego/context"
"github.com/docker/distribution/reference"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
secstore "github.com/goharbor/harbor/src/common/secret"
"github.com/goharbor/harbor/src/common/security"
admr "github.com/goharbor/harbor/src/common/security/admiral"
"github.com/goharbor/harbor/src/common/security/admiral/authcontext"
"github.com/goharbor/harbor/src/common/security/local"
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
"github.com/goharbor/harbor/src/common/security/secret"
"github.com/goharbor/harbor/src/common/token"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/admiral"
"strings"
)
// ContextValueKey for content value
@ -95,6 +100,7 @@ func Init() {
// standalone
reqCtxModifiers = []ReqCtxModifier{
&secretReqCtxModifier{config.SecretStore},
&robotAuthReqCtxModifier{},
&basicAuthReqCtxModifier{},
&sessionReqCtxModifier{},
&unauthorizedReqCtxModifier{}}
@ -147,6 +153,47 @@ func (s *secretReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
return true
}
type robotAuthReqCtxModifier struct{}
func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
robotName, robotTk, ok := ctx.Request.BasicAuth()
if !ok {
return false
}
if !strings.HasPrefix(robotName, common.RobotPrefix) {
return false
}
rClaims := &token.RobotClaims{}
htk, err := token.ParseWithClaims(robotTk, rClaims)
if err != nil {
log.Errorf("failed to decrypt robot token, %v", err)
return false
}
// Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable.
robot, err := dao.GetRobotByID(htk.Claims.(*token.RobotClaims).TokenID)
if err != nil {
log.Errorf("failed to get robot %s: %v", robotName, err)
return false
}
if robot == nil {
log.Error("the token provided doesn't exist.")
return false
}
if robotName != robot.Name {
log.Errorf("failed to authenticate : %v", robotName)
return false
}
if robot.Disabled {
log.Errorf("the robot account %s is disabled", robot.Name)
return false
}
log.Debug("creating robot account security context...")
pm := config.GlobalProjectMgr
securCtx := robotCtx.NewSecurityContext(robot, pm, htk.Claims.(*token.RobotClaims).Access)
setSecurCtxAndPM(ctx.Request, securCtx, pm)
return true
}
type basicAuthReqCtxModifier struct{}
func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {

View File

@ -122,6 +122,23 @@ func TestSecretReqCtxModifier(t *testing.T) {
assert.NotNil(t, projectManager(ctx))
}
func TestRobotReqCtxModifier(t *testing.T) {
req, err := http.NewRequest(http.MethodGet,
"http://127.0.0.1/api/projects/", nil)
if err != nil {
t.Fatalf("failed to create request: %v", req)
}
req.SetBasicAuth("robot$test1", "Harbor12345")
ctx, err := newContext(req)
if err != nil {
t.Fatalf("failed to crate context: %v", err)
}
modifier := &robotAuthReqCtxModifier{}
modified := modifier.Modify(ctx)
assert.False(t, modified)
}
func TestBasicAuthReqCtxModifier(t *testing.T) {
req, err := http.NewRequest(http.MethodGet,
"http://127.0.0.1/api/projects/", nil)

51
tests/private_key.pem Normal file
View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----