Skip to content

Commit

Permalink
feat(server): Enable RBAC for SSO. Closes argoproj#3525
Browse files Browse the repository at this point in the history
  • Loading branch information
alexec committed Aug 20, 2020
1 parent da43086 commit 721958a
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 51 deletions.
13 changes: 12 additions & 1 deletion docs/workflow-controller-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,18 @@ data:
# be in the form <argo-server-root-url>/oauth2/callback. It must be
# browser-accessible.
redirectUrl: https://argo-server/oauth2/callback
# Enable RBAC. >= v2.11
rbac:
# Rules in order of precedence. Maybe empty.
rules:
- anyOf:
- my-group
# Use this service account if any of the groups match.
serviceAccountRef:
name: my-service-account
# Use this default service account if none of the rules match. Typically either read-only, or no permissions at all.
defaultServiceAccountRef:
name: my-default-service-account
# workflowRequirements restricts the Workflows that the controller will process.
# Current options:
# referenceOnly: Only Workflows using "workflowTemplateRef" will be processed. This allows the administrator of the controller
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
apiVersion: v1
data:
sso : |
sso: |
issuer: http:https://dex:5556/dex
clientId:
name: argo-server-sso
Expand All @@ -9,6 +9,14 @@ data:
name: argo-server-sso
key: clientSecret
redirectUrl: http:https://localhost:2746/oauth2/callback
rbac:
rules:
- anyOf:
- authors
serviceAccountRef:
name: argo-server
defaultServiceAccountRef:
name: argo-server
kind: ConfigMap
metadata:
name: workflow-controller-configmap
2 changes: 1 addition & 1 deletion pkg/apiclient/argo-kube-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func newArgoKubeClient(clientConfig clientcmd.ClientConfig, instanceIDService in
if err != nil {
return nil, nil, err
}
gatekeeper, err := auth.NewGatekeeper(auth.Modes{auth.Server: true}, wfClient, kubeClient, restConfig, nil)
gatekeeper, err := auth.NewGatekeeper(auth.Modes{auth.Server: true}, wfClient, kubeClient, restConfig, nil, "unused")
if err != nil {
return nil, nil, err
}
Expand Down
118 changes: 87 additions & 31 deletions pkg/apiclient/info/info.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/apiclient/info/info.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ message GetUserInfoRequest {
message GetUserInfoResponse {
string issuer = 1;
string subject = 2;
repeated string groups = 3;
}

service InfoService {
Expand Down
2 changes: 1 addition & 1 deletion server/apiserver/argoserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func NewArgoServer(opts ArgoServerOpts) (*argoServer, error) {
} else {
log.Info("SSO disabled")
}
gatekeeper, err := auth.NewGatekeeper(opts.AuthModes, opts.WfClientSet, opts.KubeClientset, opts.RestConfig, ssoIf)
gatekeeper, err := auth.NewGatekeeper(opts.AuthModes, opts.WfClientSet, opts.KubeClientset, opts.RestConfig, ssoIf, opts.Namespace)
if err != nil {
return nil, err
}
Expand Down
39 changes: 36 additions & 3 deletions server/auth/gatekeeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"

Expand Down Expand Up @@ -41,13 +42,15 @@ type gatekeeper struct {
kubeClient kubernetes.Interface
restConfig *rest.Config
ssoIf sso.Interface
// The namespace the server is installed in.
namespace string
}

func NewGatekeeper(modes Modes, wfClient versioned.Interface, kubeClient kubernetes.Interface, restConfig *rest.Config, ssoIf sso.Interface) (Gatekeeper, error) {
func NewGatekeeper(modes Modes, wfClient versioned.Interface, kubeClient kubernetes.Interface, restConfig *rest.Config, ssoIf sso.Interface, namespace string) (Gatekeeper, error) {
if len(modes) == 0 {
return nil, fmt.Errorf("must specify at least one auth mode")
}
return &gatekeeper{modes, wfClient, kubeClient, restConfig, ssoIf}, nil
return &gatekeeper{modes, wfClient, kubeClient, restConfig, ssoIf, namespace}, nil
}

func (s *gatekeeper) UnaryServerInterceptor() grpc.UnaryServerInterceptor {
Expand Down Expand Up @@ -145,7 +148,37 @@ func (s gatekeeper) getClients(ctx context.Context) (versioned.Interface, kubern
if err != nil {
return nil, nil, nil, status.Error(codes.Unauthenticated, err.Error())
}
return s.wfClient, s.kubeClient, claimSet, nil
serviceAccount, err := s.ssoIf.GetServiceAccount(claimSet.Groups)
if err != nil {
return nil, nil, nil, status.Errorf(codes.Unauthenticated, "failed to get SSO RBAC service account ref: %v", err)
} else if serviceAccount != nil {
serviceAccount, err := s.kubeClient.CoreV1().ServiceAccounts(s.namespace).Get(serviceAccount.Name, metav1.GetOptions{})
if err != nil {
return nil, nil, nil, status.Errorf(codes.Unauthenticated, "failed to get SSO RBAC service account: %v", err)
}
if len(serviceAccount.Secrets) == 0 {
return nil, nil, nil, status.Errorf(codes.Unauthenticated, "expected at least one secret for SSO RBAC service account: %v", err)
}
secret, err := s.kubeClient.CoreV1().Secrets(s.namespace).Get(serviceAccount.Secrets[0].Name, metav1.GetOptions{})
if err != nil {
return nil, nil, nil, status.Errorf(codes.Unauthenticated, "failed to get SSO RBAC service account secret: %v", err)
}
restConfig, err := kubeconfig.GetRestConfig("Bearer " + string(secret.Data["token"]))
if err != nil {
return nil, nil, nil, status.Errorf(codes.Unauthenticated, "failed to create SSO RBAC REST config: %v", err)
}
wfClient, err := versioned.NewForConfig(restConfig)
if err != nil {
return nil, nil, nil, status.Errorf(codes.Unauthenticated, "failure to create SSO RBAC wfClientset with ClientConfig: %v", err)
}
kubeClient, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, nil, nil, status.Errorf(codes.Unauthenticated, "failure to create SSO RBAC kubeClientset with ClientConfig: %v", err)
}
return wfClient, kubeClient, claimSet, nil
} else {
return s.wfClient, s.kubeClient, claimSet, nil
}
default:
panic("this should never happen")
}
Expand Down
47 changes: 38 additions & 9 deletions server/auth/gatekeeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc/metadata"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest"

Expand All @@ -17,28 +19,39 @@ import (

func TestServer_GetWFClient(t *testing.T) {
wfClient := &fakewfclientset.Clientset{}
kubeClient := &fake.Clientset{}
kubeClient := fake.NewSimpleClientset(
&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{Name: "my-sa", Namespace: "my-ns"},
Secrets: []corev1.ObjectReference{{Name: "my-secret"}},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "my-ns"},
Data: map[string][]byte{
"token": {},
},
},
)
t.Run("None", func(t *testing.T) {
_, err := NewGatekeeper(Modes{}, wfClient, kubeClient, nil, nil)
_, err := NewGatekeeper(Modes{}, wfClient, kubeClient, nil, nil, "")
assert.Error(t, err)
})
t.Run("Invalid", func(t *testing.T) {
g, err := NewGatekeeper(Modes{Client: true}, wfClient, kubeClient, nil, nil)
g, err := NewGatekeeper(Modes{Client: true}, wfClient, kubeClient, nil, nil, "")
if assert.NoError(t, err) {
_, err := g.Context(x("invalid"))
assert.Error(t, err)
}
})
t.Run("NotAllowed", func(t *testing.T) {
g, err := NewGatekeeper(Modes{SSO: true}, wfClient, kubeClient, nil, nil)
g, err := NewGatekeeper(Modes{SSO: true}, wfClient, kubeClient, nil, nil, "")
if assert.NoError(t, err) {
_, err := g.Context(x("Bearer "))
assert.Error(t, err)
}
})
// not possible to unit test client auth today
t.Run("Server", func(t *testing.T) {
g, err := NewGatekeeper(Modes{Server: true}, wfClient, kubeClient, &rest.Config{Username: "my-username"}, nil)
g, err := NewGatekeeper(Modes{Server: true}, wfClient, kubeClient, &rest.Config{Username: "my-username"}, nil, "")
assert.NoError(t, err)
ctx, err := g.Context(x(""))
if assert.NoError(t, err) {
Expand All @@ -49,14 +62,30 @@ func TestServer_GetWFClient(t *testing.T) {
})
t.Run("SSO", func(t *testing.T) {
ssoIf := &mocks.Interface{}
ssoIf.On("Authorize", mock.Anything, mock.Anything).Return(&jws.ClaimSet{}, nil)
g, err := NewGatekeeper(Modes{SSO: true}, wfClient, kubeClient, nil, ssoIf)
ssoIf.On("Authorize", mock.Anything, mock.Anything).Return(&jws.ClaimSet{Sub: "my-sub"}, nil)
ssoIf.On("GetServiceAccount", mock.Anything).Return(nil, nil)
g, err := NewGatekeeper(Modes{SSO: true}, wfClient, kubeClient, nil, ssoIf, "my-ns")
if assert.NoError(t, err) {
ctx, err := g.Context(x("Bearer id_token:whatever"))
ctx, err := g.Context(x("Bearer id_token:"))
if assert.NoError(t, err) {
assert.Equal(t, wfClient, GetWfClient(ctx))
assert.Equal(t, kubeClient, GetKubeClient(ctx))
assert.NotNil(t, GetClaimSet(ctx))
if assert.NotNil(t, GetClaimSet(ctx)) {
assert.Equal(t, "my-sub", GetClaimSet(ctx).Sub)
}
}
}
})
t.Run("SSO+RBAC", func(t *testing.T) {
ssoIf := &mocks.Interface{}
ssoIf.On("Authorize", mock.Anything, mock.Anything).Return(&jws.ClaimSet{Groups: []string{"my-group"}}, nil)
ssoIf.On("GetServiceAccount", []string{"my-group"}).Return(&corev1.LocalObjectReference{Name: "my-sa"}, nil)
g, err := NewGatekeeper(Modes{SSO: true}, wfClient, kubeClient, nil, ssoIf, "my-ns")
if assert.NoError(t, err) {
ctx, err := g.Context(x("Bearer id_token:"))
if assert.NoError(t, err) {
assert.NotEqual(t, wfClient, GetWfClient(ctx))
assert.NotEqual(t, kubeClient, GetKubeClient(ctx))
}
}
})
Expand Down
5 changes: 3 additions & 2 deletions server/auth/jws/claim_set.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jws

type ClaimSet struct {
Iss string `json:"iss"`
Sub string `json:"sub,omitempty"`
Iss string `json:"iss"`
Sub string `json:"sub,omitempty"`
Groups []string `json:"groups,omitempty"`
}
Loading

0 comments on commit 721958a

Please sign in to comment.