Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Authz support for Azure #252

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ endif
### These variables should not need tweaking.
###

SRC_PKGS := *.go auth commands docs installer server util
SRC_PKGS := *.go auth authz commands docs installer server util
SRC_DIRS := $(SRC_PKGS) test hack/gendocs # directories which hold app source (not vendored)

DOCKER_PLATFORMS := linux/amd64 linux/arm linux/arm64
Expand All @@ -67,7 +67,7 @@ TAG := $(VERSION)_$(OS)_$(ARCH)
TAG_PROD := $(TAG)
TAG_DBG := $(VERSION)-dbg_$(OS)_$(ARCH)

GO_VERSION ?= 1.14.2
GO_VERSION ?= 1.14
BUILD_IMAGE ?= appscode/golang-dev:$(GO_VERSION)

OUTBIN = bin/$(BIN)-$(OS)-$(ARCH)
Expand Down Expand Up @@ -325,7 +325,7 @@ lint: $(BUILD_DIRS)
--env HTTP_PROXY=$(HTTP_PROXY) \
--env HTTPS_PROXY=$(HTTPS_PROXY) \
--env GO111MODULE=on \
--env GOFLAGS="-mod=vendor" \
--env GOFLAGS="-mod=vendor" \
$(BUILD_IMAGE) \
golangci-lint run --enable $(ADDTL_LINTERS) --deadline=10m --skip-files="generated.*\.go$\" --skip-dirs-use-default --skip-dirs=client,vendor

Expand Down
7 changes: 6 additions & 1 deletion auth/providers/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (s Authenticator) Check(token string) (*authv1.UserInfo, error) {
if err != nil {
return nil, err
}

if s.Options.ResolveGroupMembershipOnlyOnOverageClaim {
groups, skipGraphAPI, err := getGroupsAndCheckOverage(claims)
if err != nil {
Expand Down Expand Up @@ -285,7 +286,11 @@ func (c claims) getUserInfo(usernameClaim, userObjectIDClaim string) (*authv1.Us
return nil, errors.Wrap(err, "unable to get username claim")
}

return &authv1.UserInfo{Username: username}, nil
useroid, _ := c.string(userObjectIDClaim)

return &authv1.UserInfo{
Username: username,
Extra: map[string]authv1.ExtraValue{"oid": {useroid}}}, nil
}

// String gets a string value from claims given a key. Returns error if
Expand Down
10 changes: 4 additions & 6 deletions auth/providers/github/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,9 @@ func getTeamRespFunc(teamSize int) teamRespFunc {
return http.StatusOK, string(resp)
}

return http.StatusBadRequest, fmt.Sprint("List user teams request: query parameter per_page not provide")

} else {
return http.StatusBadRequest, fmt.Sprint("List user teams request: query parameter page not provide")
return http.StatusBadRequest, "List user teams request: query parameter per_page not provide"
}
return http.StatusBadRequest, "List user teams request: query parameter page not provide"
}
}

Expand Down Expand Up @@ -213,7 +211,7 @@ func githubServerSetup(githubOrg string, memberResp string, memberStatusCode int
_, _ = w.Write([]byte(memberResp))
}))

m.Get(fmt.Sprintf("/user/memberships/orgs/"), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.Get("/user/memberships/orgs/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write(getErrorMessage(errors.New("Authorization: invalid token")))
}))
Expand Down Expand Up @@ -358,7 +356,7 @@ func TestTeamListErrorAtDifferentPage(t *testing.T) {
return http.StatusInternalServerError, errMsg
}
} else {
return http.StatusBadRequest, fmt.Sprint("List user teams request: query parameter page not provide")
return http.StatusBadRequest, "List user teams request: query parameter page not provide"
}
})
defer srv.Close()
Expand Down
12 changes: 7 additions & 5 deletions auth/providers/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ func (g Authenticator) UID() string {
}

func (g *Authenticator) Check(token string) (*authv1.UserInfo, error) {
client := gitlab.NewClient(nil, token)
var opts []gitlab.ClientOptionFunc
if g.opts.BaseUrl != "" {
err := client.SetBaseURL(g.opts.BaseUrl)
if err != nil {
return nil, err
}
opts = append(opts, gitlab.WithBaseURL(g.opts.BaseUrl))
}

client, err := gitlab.NewClient(token, opts...)
if err != nil {
return nil, err
}

user, _, err := client.Users.CurrentUser()
Expand Down
8 changes: 3 additions & 5 deletions auth/providers/gitlab/gitlab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,9 @@ func gitlabGetGroupResp(groupSize int) gitlabGroupRespFunc {
return http.StatusOK, string(resp)
}

return http.StatusBadRequest, fmt.Sprint("List user groups request: query parameter per_page not provide")

} else {
return http.StatusBadRequest, fmt.Sprint("List user groups request: query parameter page not provide")
return http.StatusBadRequest, "List user groups request: query parameter per_page not provide"
}
return http.StatusBadRequest, "List user groups request: query parameter page not provide"
}
}

Expand Down Expand Up @@ -314,7 +312,7 @@ func TestGroupListErrorInDifferentPage(t *testing.T) {
return http.StatusInternalServerError, errMsg
}
} else {
return http.StatusBadRequest, fmt.Sprint("List user groups request: query parameter page not provide")
return http.StatusBadRequest, "List user groups request: query parameter page not provide"
}
})
defer srv.Close()
Expand Down
4 changes: 2 additions & 2 deletions auth/providers/ldap/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,13 @@ func (o Options) Apply(d *apps.Deployment) (extraObjs []runtime.Object, err erro
args = append(args, "--ldap.start-tls")
}
if o.CaCertFile != "" {
args = append(args, fmt.Sprintf("--ldap.ca-cert-file=/etc/guard/auth/ldap/ca.crt"))
args = append(args, "--ldap.ca-cert-file=/etc/guard/auth/ldap/ca.crt")
}
if o.ServiceAccountName != "" {
args = append(args, fmt.Sprintf("--ldap.service-account=%s", o.ServiceAccountName))
}
if o.KeytabFile != "" {
args = append(args, fmt.Sprintf("--ldap.keytab-file=/etc/guard/auth/ldap/krb5.keytab"))
args = append(args, "--ldap.keytab-file=/etc/guard/auth/ldap/krb5.keytab")
}
args = append(args, fmt.Sprintf("--ldap.auth-choice=%v", o.AuthenticationChoice))

Expand Down
125 changes: 125 additions & 0 deletions authz/providers/azure/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
Copyright The Guard 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 azure

import (
"strings"

"github.com/Azure/go-autorest/autorest/azure"
auth "github.com/appscode/guard/auth/providers/azure"
"github.com/appscode/guard/authz"
"github.com/appscode/guard/authz/providers/azure/rbac"
"github.com/golang/glog"
"github.com/pkg/errors"
authzv1 "k8s.io/api/authorization/v1"
)

const (
OrgType = "azure"
)

func init() {
authz.SupportedOrgs = append(authz.SupportedOrgs, OrgType)
}

type Authorizer struct {
rbacClient *rbac.AccessInfo
}

type authzInfo struct {
AADEndpoint string
ARMEndPoint string
}

func New(opts Options, authopts auth.Options, dataStore authz.Store) (authz.Interface, error) {
c := &Authorizer{}

authzInfoVal, err := getAuthzInfo(authopts.Environment)
if err != nil {
return nil, errors.Wrap(err, "Error in getAuthzInfo %s")
}

switch opts.AuthzMode {
case ARCAuthzMode:
c.rbacClient, err = rbac.New(authopts.ClientID, authopts.ClientSecret, authopts.TenantID, authzInfoVal.AADEndpoint, authzInfoVal.ARMEndPoint, opts.AuthzMode, opts.ResourceId, opts.ARMCallLimit, dataStore, opts.SkipAuthzCheck, opts.AuthzResolveGroupMemberships, opts.SkipAuthzForNonAADUsers)
case AKSAuthzMode:
c.rbacClient, err = rbac.NewWithAKS(opts.AKSAuthzURL, authopts.TenantID, authzInfoVal.ARMEndPoint, opts.AuthzMode, opts.ResourceId, opts.ARMCallLimit, dataStore, opts.SkipAuthzCheck, opts.AuthzResolveGroupMemberships, opts.SkipAuthzForNonAADUsers)
}

if err != nil {
return nil, errors.Wrap(err, "failed to create ms rbac client")
}
return c, nil
}

func (s Authorizer) Check(request *authzv1.SubjectAccessReviewSpec) (*authzv1.SubjectAccessReviewStatus, error) {
if request == nil {
return nil, errors.New("subject access review is nil")
}

// check if user is service account
if strings.HasPrefix(strings.ToLower(request.User), "system") {
glog.V(3).Infof("returning no op to service accounts")
return &authzv1.SubjectAccessReviewStatus{Allowed: false, Reason: rbac.NoOpinionVerdict}, nil
}

if _, ok := request.Extra["oid"]; !ok {
if s.rbacClient.ShouldSkipAuthzCheckForNonAADUsers() {
glog.V(3).Infof("Skip RBAC is set for non AAD users. Returning no opinion for user %s. You may observe this for AAD users for 'can-i' requests.", request.User)
return &authzv1.SubjectAccessReviewStatus{Allowed: false, Reason: rbac.NoOpinionVerdict}, nil
} else {
glog.V(3).Infof("Skip RBAC for non AAD user is not set. Returning deny access for non AAD user %s. You may observe this for AAD users for 'can-i' requests.", request.User)
return &authzv1.SubjectAccessReviewStatus{Allowed: false, Denied: true, Reason: rbac.NotAllowedForNonAADUsers}, nil
}
}

if s.rbacClient.SkipAuthzCheck(request) {
glog.V(3).Infof("user %s is part of skip authz list. returning no op.", request.User)
return &authzv1.SubjectAccessReviewStatus{Allowed: false, Reason: rbac.NoOpinionVerdict}, nil
}

exist, result := s.rbacClient.GetResultFromCache(request)
if exist {
if result {
glog.V(3).Infof("cache hit: returning allowed to user")
return &authzv1.SubjectAccessReviewStatus{Allowed: result, Reason: rbac.AccessAllowedVerdict}, nil
} else {
glog.V(3).Infof("cache hit: returning denied to user")
return &authzv1.SubjectAccessReviewStatus{Allowed: result, Denied: true, Reason: rbac.AccessNotAllowedVerdict}, nil
}
}

if s.rbacClient.IsTokenExpired() {
s.rbacClient.RefreshToken()
}
return s.rbacClient.CheckAccess(request)
}

func getAuthzInfo(environment string) (*authzInfo, error) {
var err error
env := azure.PublicCloud
if environment != "" {
env, err = azure.EnvironmentFromName(environment)
if err != nil {
return nil, errors.Wrap(err, "failed to parse environment for azure")
}
}

return &authzInfo{
AADEndpoint: env.ActiveDirectoryEndpoint,
ARMEndPoint: env.ResourceManagerEndpoint,
}, nil
}
122 changes: 122 additions & 0 deletions authz/providers/azure/azure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Copyright The Guard 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 azure

import (
"net"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/appscode/guard/authz/providers/azure/data"
"github.com/appscode/guard/authz/providers/azure/rbac"
"github.com/appscode/pat"
"github.com/stretchr/testify/assert"
authzv1 "k8s.io/api/authorization/v1"
)

const (
loginResp = `{ "token_type": "Bearer", "expires_in": 8459, "access_token": "%v"}`
)

func clientSetup(serverUrl, mode string) (*Authorizer, error) {
c := &Authorizer{}

var testOptions = data.Options{
HardMaxCacheSize: 1,
Shards: 1,
LifeWindow: 1 * time.Minute,
CleanWindow: 1 * time.Minute,
MaxEntriesInWindow: 10,
MaxEntrySize: 5,
Verbose: false,
}
dataStore, err := data.NewDataStore(testOptions)
if err != nil {
return nil, err
}

c.rbacClient, err = rbac.New("client_id", "client_secret", "tenant_id", serverUrl+"/login/", serverUrl+"/arm/", mode, "resourceId", 2000, dataStore, []string{"alpha, tango, charlie"}, true, true)
if err != nil {
return nil, err
}

return c, nil
}

func serverSetup(loginResp, checkaccessResp string, loginStatus, checkaccessStatus int) (*httptest.Server, error) {
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
return nil, err
}

m := pat.New()

m.Post("/login/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(loginStatus)
_, _ = w.Write([]byte(loginResp))
}))

m.Post("/arm/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(checkaccessStatus)
_, _ = w.Write([]byte(checkaccessResp))
}))

srv := &httptest.Server{
Listener: listener,
Config: &http.Server{Handler: m},
}
srv.Start()

return srv, nil
}

func getServerAndClient(t *testing.T, loginResp, checkaccessResp string) (*httptest.Server, *Authorizer) {
srv, err := serverSetup(loginResp, checkaccessResp, http.StatusOK, http.StatusOK)
if err != nil {
t.Fatalf("Error when creating server, reason: %v", err)
}

client, err := clientSetup(srv.URL, "arc")
if err != nil {
t.Fatalf("Error when creatidng azure client. reason : %v", err)
}
return srv, client
}

func TestCheck(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
var validBody = `[{"accessDecision":"Allowed",
"actionId":"Microsoft.Kubernetes/connectedClusters/pods/delete",
"isDataAction":true,"roleAssignment":null,"denyAssignment":null,"timeToLiveInMs":300000}]`

srv, client := getServerAndClient(t, loginResp, validBody)
defer srv.Close()

request := &authzv1.SubjectAccessReviewSpec{
User: "beta@bing.com",
ResourceAttributes: &authzv1.ResourceAttributes{Namespace: "dev", Group: "", Resource: "pods",
Subresource: "status", Version: "v1", Name: "test", Verb: "delete"}, Extra: map[string]authzv1.ExtraValue{"oid": {"00000000-0000-0000-0000-000000000000"}}}

resp, err := client.Check(request)
assert.Nilf(t, err, "Should not have got error")
assert.NotNil(t, resp)
assert.Equal(t, resp.Allowed, true)
assert.Equal(t, resp.Denied, false)
})
}
Loading