From c8ef4b8230a14fd2ded7263ceb25396bedb6d36b Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 1 Oct 2014 17:54:11 -0400 Subject: [PATCH] Add simple Bearer authenticator filter for Kube * Default file based implementation * Define some simple interfaces * Add -token_auth_file to apiserver that will start the apiserver with a request filter for tokens --- cmd/apiserver/apiserver.go | 15 +++ .../authenticator/bearertoken/bearertoken.go | 47 ++++++++ .../bearertoken/bearertoken_test.go | 86 +++++++++++++ pkg/auth/authenticator/interfaces.go | 53 ++++++++ pkg/auth/authenticator/tokenfile/tokenfile.go | 70 +++++++++++ .../authenticator/tokenfile/tokenfile_test.go | 113 ++++++++++++++++++ pkg/auth/handlers/handlers.go | 95 +++++++++++++++ pkg/auth/handlers/handlers_test.go | 102 ++++++++++++++++ pkg/auth/user/doc.go | 19 +++ pkg/auth/user/user.go | 43 +++++++ 10 files changed, 643 insertions(+) create mode 100644 pkg/auth/authenticator/bearertoken/bearertoken.go create mode 100644 pkg/auth/authenticator/bearertoken/bearertoken_test.go create mode 100644 pkg/auth/authenticator/interfaces.go create mode 100644 pkg/auth/authenticator/tokenfile/tokenfile.go create mode 100644 pkg/auth/authenticator/tokenfile/tokenfile_test.go create mode 100644 pkg/auth/handlers/handlers.go create mode 100644 pkg/auth/handlers/handlers_test.go create mode 100644 pkg/auth/user/doc.go create mode 100644 pkg/auth/user/user.go diff --git a/cmd/apiserver/apiserver.go b/cmd/apiserver/apiserver.go index 289ea6137a..6339d35934 100644 --- a/cmd/apiserver/apiserver.go +++ b/cmd/apiserver/apiserver.go @@ -29,6 +29,9 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/tokenfile" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" @@ -50,6 +53,7 @@ var ( minionPort = flag.Uint("minion_port", 10250, "The port at which kubelet will be listening on the minions.") healthCheckMinions = flag.Bool("health_check_minions", true, "If true, health check minions and filter unhealthy ones. Default true") minionCacheTTL = flag.Duration("minion_cache_ttl", 30*time.Second, "Duration of time to cache minion information. Default 30 seconds") + tokenAuthFile = flag.String("token_auth_file", "", "If set, the file that will be used to secure the API server via token authentication") etcdServerList util.StringList machineList util.StringList corsAllowedOriginList util.StringList @@ -170,6 +174,7 @@ func main() { apiserver.InstallSupport(mux) handler := http.Handler(mux) + if len(corsAllowedOriginList) > 0 { allowedOriginRegexps, err := util.CompileRegexps(corsAllowedOriginList) if err != nil { @@ -177,6 +182,16 @@ func main() { } handler = apiserver.CORS(handler, allowedOriginRegexps, nil, nil, "true") } + + if len(*tokenAuthFile) != 0 { + auth, err := tokenfile.New(*tokenAuthFile) + if err != nil { + glog.Fatalf("Unable to load the token authentication file '%s': %v", *tokenAuthFile, err) + } + userContexts := handlers.NewUserRequestContext() + handler = handlers.NewRequestAuthenticator(userContexts, bearertoken.New(auth), handlers.Unauthorized, handler) + } + handler = apiserver.RecoverPanics(handler) s := &http.Server{ diff --git a/pkg/auth/authenticator/bearertoken/bearertoken.go b/pkg/auth/authenticator/bearertoken/bearertoken.go new file mode 100644 index 0000000000..7a5d245890 --- /dev/null +++ b/pkg/auth/authenticator/bearertoken/bearertoken.go @@ -0,0 +1,47 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 bearertoken + +import ( + "net/http" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +type Authenticator struct { + auth authenticator.Token +} + +func New(auth authenticator.Token) *Authenticator { + return &Authenticator{auth} +} + +func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + auth := strings.TrimSpace(req.Header.Get("Authorization")) + if auth == "" { + return nil, false, nil + } + parts := strings.Split(auth, " ") + if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" { + return nil, false, nil + } + + token := parts[1] + return a.auth.AuthenticateToken(token) +} diff --git a/pkg/auth/authenticator/bearertoken/bearertoken_test.go b/pkg/auth/authenticator/bearertoken/bearertoken_test.go new file mode 100644 index 0000000000..276b55af36 --- /dev/null +++ b/pkg/auth/authenticator/bearertoken/bearertoken_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 bearertoken + +import ( + "errors" + "net/http" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +func TestAuthenticateRequest(t *testing.T) { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + if token != "token" { + t.Errorf("unexpected token: %s", token) + } + return &user.DefaultInfo{Name: "user"}, true, nil + })) + user, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if !ok || user == nil || err != nil { + t.Errorf("expected valid user") + } +} + +func TestAuthenticateRequestTokenInvalid(t *testing.T) { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + return nil, false, nil + })) + user, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || user != nil || err != nil { + t.Errorf("expected not authenticated user") + } +} + +func TestAuthenticateRequestTokenError(t *testing.T) { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + return nil, false, errors.New("error") + })) + user, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || user != nil || err == nil { + t.Errorf("expected error") + } +} + +func TestAuthenticateRequestBadValue(t *testing.T) { + testCases := []struct { + Req *http.Request + }{ + {Req: &http.Request{}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer"}}}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"bear token"}}}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer: token"}}}}, + } + for i, testCase := range testCases { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + t.Errorf("authentication should not have been called") + return nil, false, nil + })) + user, ok, err := auth.AuthenticateRequest(testCase.Req) + if ok || user != nil || err != nil { + t.Errorf("%d: expected not authenticated (no token)", i) + } + } +} diff --git a/pkg/auth/authenticator/interfaces.go b/pkg/auth/authenticator/interfaces.go new file mode 100644 index 0000000000..8c9cbe73f3 --- /dev/null +++ b/pkg/auth/authenticator/interfaces.go @@ -0,0 +1,53 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 authenticator + +import ( + "net/http" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +// Token checks a string value against a backing authentication store and returns +// information about the current user and true if successful, false if not successful, +// or an error if the token could not be checked. +type Token interface { + AuthenticateToken(token string) (user.Info, bool, error) +} + +// Request attempts to extract authentication information from a request and returns +// information about the current user and true if successful, false if not successful, +// or an error if the token could not be checked. +type Request interface { + AuthenticateRequest(req *http.Request) (user.Info, bool, error) +} + +// TokenFunc is a function that implements the Token interface. +type TokenFunc func(token string) (user.Info, bool, error) + +// AuthenticateToken implements authenticator.Token. +func (f TokenFunc) AuthenticateToken(token string) (user.Info, bool, error) { + return f(token) +} + +// RequestFunc is a function that implements the Request interface. +type RequestFunc func(req *http.Request) (user.Info, bool, error) + +// AuthenticateRequest implements authenticator.Request. +func (f RequestFunc) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + return f(req) +} diff --git a/pkg/auth/authenticator/tokenfile/tokenfile.go b/pkg/auth/authenticator/tokenfile/tokenfile.go new file mode 100644 index 0000000000..dcd6851111 --- /dev/null +++ b/pkg/auth/authenticator/tokenfile/tokenfile.go @@ -0,0 +1,70 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 tokenfile + +import ( + "encoding/csv" + "fmt" + "io" + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +type TokenAuthenticator struct { + tokens map[string]*user.DefaultInfo +} + +func New(path string) (*TokenAuthenticator, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + tokens := make(map[string]*user.DefaultInfo) + reader := csv.NewReader(file) + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if len(record) < 3 { + return nil, fmt.Errorf("token file '%s' must have at least 3 columns (token, user name, user uid), found %d", path, len(record)) + } + obj := &user.DefaultInfo{ + Name: record[1], + UID: record[2], + } + tokens[record[0]] = obj + } + + return &TokenAuthenticator{ + tokens: tokens, + }, nil +} + +func (a *TokenAuthenticator) AuthenticateToken(value string) (user.Info, bool, error) { + user, ok := a.tokens[value] + if !ok { + return nil, false, nil + } + return user, true, nil +} diff --git a/pkg/auth/authenticator/tokenfile/tokenfile_test.go b/pkg/auth/authenticator/tokenfile/tokenfile_test.go new file mode 100644 index 0000000000..bd9a4a2f7e --- /dev/null +++ b/pkg/auth/authenticator/tokenfile/tokenfile_test.go @@ -0,0 +1,113 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 tokenfile + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +func TestTokenFile(t *testing.T) { + auth, err := newWithContents(t, ` +token1,user1,uid1 +token2,user2,uid2 +`) + if err != nil { + t.Fatalf("unable to read tokenfile: %v", err) + } + + testCases := []struct { + Token string + User *user.DefaultInfo + Ok bool + Err bool + }{ + { + Token: "token1", + User: &user.DefaultInfo{Name: "user1", UID: "uid1"}, + Ok: true, + }, + { + Token: "token2", + User: &user.DefaultInfo{Name: "user2", UID: "uid2"}, + Ok: true, + }, + { + Token: "token3", + }, + { + Token: "token4", + }, + } + for i, testCase := range testCases { + user, ok, err := auth.AuthenticateToken(testCase.Token) + if testCase.User == nil { + if user != nil { + t.Errorf("%d: unexpected non-nil user %#v", i, user) + } + } else if !reflect.DeepEqual(testCase.User, user) { + t.Errorf("%d: expected user %#v, got %#v", i, testCase.User, user) + } + if testCase.Ok != ok { + t.Errorf("%d: expected auth %f, got %f", i, testCase.Ok, ok) + } + switch { + case err == nil && testCase.Err: + t.Errorf("%d: unexpected nil error", i) + case err != nil && !testCase.Err: + t.Errorf("%d: unexpected error: %v", i, err) + } + } +} + +func TestBadTokenFile(t *testing.T) { + _, err := newWithContents(t, ` +token1,user1,uid1 +token2,user2,uid2 +token3,user3 +token4 +`) + if err == nil { + t.Fatalf("unexpected non error") + } +} + +func TestInsufficientColumnsTokenFile(t *testing.T) { + _, err := newWithContents(t, "token4\n") + if err == nil { + t.Fatalf("unexpected non error") + } +} + +func newWithContents(t *testing.T, contents string) (auth *TokenAuthenticator, err error) { + f, err := ioutil.TempFile("", "tokenfile_test") + if err != nil { + t.Fatalf("unexpected error creating tokenfile: %v", err) + } + f.Close() + defer os.Remove(f.Name()) + + if err := ioutil.WriteFile(f.Name(), []byte(contents), 0700); err != nil { + t.Fatalf("unexpected error writing tokenfile: %v", err) + } + + return New(f.Name()) +} diff --git a/pkg/auth/handlers/handlers.go b/pkg/auth/handlers/handlers.go new file mode 100644 index 0000000000..3e4e603097 --- /dev/null +++ b/pkg/auth/handlers/handlers.go @@ -0,0 +1,95 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 handlers + +import ( + "net/http" + "sync" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" + "github.com/golang/glog" +) + +// RequestContext is the interface used to associate a user with an http Request. +type RequestContext interface { + Set(*http.Request, user.Info) + Remove(*http.Request) +} + +// NewRequestAuthenticator creates an http handler that tries to authenticate the given request as a user, and then +// stores any such user found onto the provided context for the request. If authentication fails or returns an error +// the failed handler is used. On success, handler is invoked to serve the request. +func NewRequestAuthenticator(context RequestContext, auth authenticator.Request, failed http.Handler, handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + user, ok, err := auth.AuthenticateRequest(req) + if err != nil || !ok { + if err != nil { + glog.Errorf("Unable to authenticate the request due to an error: %v", err) + } + failed.ServeHTTP(w, req) + return + } + + context.Set(req, user) + defer context.Remove(req) + + handler.ServeHTTP(w, req) + }) +} + +var Unauthorized http.HandlerFunc = unauthorized + +// unauthorized serves an unauthorized message to clients. +func unauthorized(w http.ResponseWriter, req *http.Request) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) +} + +// UserRequestContext allows different levels of a call stack to store/retrieve info about the +// current user associated with an http.Request. +type UserRequestContext struct { + requests map[*http.Request]user.Info + lock sync.Mutex +} + +// NewUserRequestContext provides a map for storing and retrieving users associated with requests. +// Be sure to pair each `context.Set(req, user)` call with a `defer context.Remove(req)` call or +// you will leak requests. It implements the RequestContext interface. +func NewUserRequestContext() *UserRequestContext { + return &UserRequestContext{ + requests: make(map[*http.Request]user.Info), + } +} + +func (c *UserRequestContext) Get(req *http.Request) (user.Info, bool) { + c.lock.Lock() + defer c.lock.Unlock() + user, ok := c.requests[req] + return user, ok +} + +func (c *UserRequestContext) Set(req *http.Request, user user.Info) { + c.lock.Lock() + defer c.lock.Unlock() + c.requests[req] = user +} + +func (c *UserRequestContext) Remove(req *http.Request) { + c.lock.Lock() + defer c.lock.Unlock() + delete(c.requests, req) +} diff --git a/pkg/auth/handlers/handlers_test.go b/pkg/auth/handlers/handlers_test.go new file mode 100644 index 0000000000..cc856ea2d6 --- /dev/null +++ b/pkg/auth/handlers/handlers_test.go @@ -0,0 +1,102 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 handlers + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +func TestAuthenticateRequest(t *testing.T) { + success := make(chan struct{}) + context := NewUserRequestContext() + auth := NewRequestAuthenticator( + context, + authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + return &user.DefaultInfo{Name: "user"}, true, nil + }), + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + t.Errorf("unexpected call to failed") + }), + http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + if user, ok := context.Get(req); user == nil || !ok { + t.Errorf("no user stored on context: %#v", context) + } + close(success) + }), + ) + + auth.ServeHTTP(httptest.NewRecorder(), &http.Request{}) + + <-success + if len(context.requests) > 0 { + t.Errorf("context should have no stored requests", context) + } +} + +func TestAuthenticateRequestFailed(t *testing.T) { + failed := make(chan struct{}) + context := NewUserRequestContext() + auth := NewRequestAuthenticator( + context, + authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + return nil, false, nil + }), + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + close(failed) + }), + http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + t.Errorf("unexpected call to handler") + }), + ) + + auth.ServeHTTP(httptest.NewRecorder(), &http.Request{}) + + <-failed + if len(context.requests) > 0 { + t.Errorf("context should have no stored requests", context) + } +} + +func TestAuthenticateRequestError(t *testing.T) { + failed := make(chan struct{}) + context := NewUserRequestContext() + auth := NewRequestAuthenticator( + context, + authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + return nil, false, errors.New("failure") + }), + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + close(failed) + }), + http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + t.Errorf("unexpected call to handler") + }), + ) + + auth.ServeHTTP(httptest.NewRecorder(), &http.Request{}) + + <-failed + if len(context.requests) > 0 { + t.Errorf("context should have no stored requests", context) + } +} diff --git a/pkg/auth/user/doc.go b/pkg/auth/user/doc.go new file mode 100644 index 0000000000..4647b4ae52 --- /dev/null +++ b/pkg/auth/user/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 user contains utilities for dealing with simple user exchange in the auth +// packages. The user.Info interface defines an interface for exchanging that info. +package user diff --git a/pkg/auth/user/user.go b/pkg/auth/user/user.go new file mode 100644 index 0000000000..0b52b9c330 --- /dev/null +++ b/pkg/auth/user/user.go @@ -0,0 +1,43 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 user + +// UserInfo describes a user that has been authenticated to the system. +type Info interface { + // GetName returns the name that uniquely identifies this user among all + // other active users. + GetName() string + // GetUID returns a unique value for a particular user that will change + // if the user is removed from the system and another user is added with + // the same name. + GetUID() string +} + +// DefaultInfo provides a simple user information exchange object +// for components that implement the UserInfo interface. +type DefaultInfo struct { + Name string + UID string +} + +func (i *DefaultInfo) GetName() string { + return i.Name +} + +func (i *DefaultInfo) GetUID() string { + return i.UID +}