From b61944458ab49a9a0a1ce6b7e05f9af55ba7436b Mon Sep 17 00:00:00 2001 From: "Yuan (Bob) Gong" Date: Tue, 28 Apr 2020 07:42:04 +0800 Subject: [PATCH] [Backend] Authorization service (#3627) * Authorization service proto * implement auth service * Add unit tests --- backend/api/BUILD.bazel | 1 + backend/api/auth.proto | 86 ++++++ backend/api/go_client/BUILD.bazel | 2 + backend/api/go_client/auth.pb.go | 260 ++++++++++++++++++ backend/api/go_client/auth.pb.gw.go | 129 +++++++++ backend/api/swagger/auth.swagger.json | 140 ++++++++++ backend/src/apiserver/main.go | 2 + backend/src/apiserver/server/BUILD.bazel | 2 + backend/src/apiserver/server/auth_server.go | 52 ++++ .../src/apiserver/server/auth_server_test.go | 98 +++++++ backend/src/apiserver/server/util.go | 16 ++ 11 files changed, 788 insertions(+) create mode 100644 backend/api/auth.proto create mode 100755 backend/api/go_client/auth.pb.go create mode 100755 backend/api/go_client/auth.pb.gw.go create mode 100755 backend/api/swagger/auth.swagger.json create mode 100644 backend/src/apiserver/server/auth_server.go create mode 100644 backend/src/apiserver/server/auth_server_test.go diff --git a/backend/api/BUILD.bazel b/backend/api/BUILD.bazel index 053903f449e..73af36c2245 100644 --- a/backend/api/BUILD.bazel +++ b/backend/api/BUILD.bazel @@ -5,6 +5,7 @@ load("@com_github_grpc_ecosystem_grpc_gateway//protoc-gen-swagger:defs.bzl", "pr proto_library( name = "go_client_proto", srcs = [ + "auth.proto", "error.proto", "experiment.proto", "filter.proto", diff --git a/backend/api/auth.proto b/backend/api/auth.proto new file mode 100644 index 00000000000..28a27b3aa43 --- /dev/null +++ b/backend/api/auth.proto @@ -0,0 +1,86 @@ +// Copyright 2020 Google LLC +// +// 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. + +syntax = "proto3"; + +option go_package = "github.com/kubeflow/pipelines/backend/api/go_client"; +package api; + +import "google/api/annotations.proto"; +import "google/protobuf/empty.proto"; +import "backend/api/error.proto"; +import "protoc-gen-swagger/options/annotations.proto"; + +option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + responses: { + key: "default"; + value: { + schema: { + json_schema: { + ref: ".api.Status"; + } + } + } + } + // Use bearer token for authorizing access to job service. + // Kubernetes client library(https://kubernetes.io/docs/reference/using-api/client-libraries/) + // uses bearer token as default for authorization. The section below + // ensures security definition object is generated in the swagger definition. + // For more details see https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#securityDefinitionsObject + security_definitions: { + security: { + key: "Bearer"; + value: { + type: TYPE_API_KEY; + in: IN_HEADER; + name: "authorization"; + } + } + } + security: { + security_requirement: { + key: "Bearer"; + value: {}; + } + } +}; + +service AuthService { + rpc Authorize(AuthorizeRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + get: "/apis/v1beta1/auth" + }; + } +} + +// Ask for authorization of an access by providing resource's namespace, type +// and verb. User identity is not part of the message, because it is expected +// to be parsed from request headers. Caller should proxy user request's headers. +message AuthorizeRequest { + // Type of resources in pipelines system. + enum Resources { + UNASSIGNED_RESOURCES = 0; + VIEWERS = 1; + } + // Type of verbs that act on the resources. + enum Verb { + UNASSIGNED_VERB = 0; + CREATE = 1; + GET = 2; + DELETE = 3; + } + string namespace = 1; // Namespace the resource belongs to. + Resources resources = 2; // Resource type asking for authorization. + Verb verb = 3; // Verb on the resource asking for authorization. +} diff --git a/backend/api/go_client/BUILD.bazel b/backend/api/go_client/BUILD.bazel index 07c7677beb2..58da7ef4764 100644 --- a/backend/api/go_client/BUILD.bazel +++ b/backend/api/go_client/BUILD.bazel @@ -3,6 +3,8 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ + "auth.pb.go", + "auth.pb.gw.go", "error.pb.go", "experiment.pb.go", "experiment.pb.gw.go", diff --git a/backend/api/go_client/auth.pb.go b/backend/api/go_client/auth.pb.go new file mode 100755 index 00000000000..c434d1bf924 --- /dev/null +++ b/backend/api/go_client/auth.pb.go @@ -0,0 +1,260 @@ +// Copyright 2020 Google LLC +// +// 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: backend/api/auth.proto + +package go_client // import "github.com/kubeflow/pipelines/backend/api/go_client" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import empty "github.com/golang/protobuf/ptypes/empty" +import _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options" +import _ "google.golang.org/genproto/googleapis/api/annotations" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type AuthorizeRequest_Resources int32 + +const ( + AuthorizeRequest_UNASSIGNED_RESOURCES AuthorizeRequest_Resources = 0 + AuthorizeRequest_VIEWERS AuthorizeRequest_Resources = 1 +) + +var AuthorizeRequest_Resources_name = map[int32]string{ + 0: "UNASSIGNED_RESOURCES", + 1: "VIEWERS", +} +var AuthorizeRequest_Resources_value = map[string]int32{ + "UNASSIGNED_RESOURCES": 0, + "VIEWERS": 1, +} + +func (x AuthorizeRequest_Resources) String() string { + return proto.EnumName(AuthorizeRequest_Resources_name, int32(x)) +} +func (AuthorizeRequest_Resources) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_auth_b463ef3269931e86, []int{0, 0} +} + +type AuthorizeRequest_Verb int32 + +const ( + AuthorizeRequest_UNASSIGNED_VERB AuthorizeRequest_Verb = 0 + AuthorizeRequest_CREATE AuthorizeRequest_Verb = 1 + AuthorizeRequest_GET AuthorizeRequest_Verb = 2 + AuthorizeRequest_DELETE AuthorizeRequest_Verb = 3 +) + +var AuthorizeRequest_Verb_name = map[int32]string{ + 0: "UNASSIGNED_VERB", + 1: "CREATE", + 2: "GET", + 3: "DELETE", +} +var AuthorizeRequest_Verb_value = map[string]int32{ + "UNASSIGNED_VERB": 0, + "CREATE": 1, + "GET": 2, + "DELETE": 3, +} + +func (x AuthorizeRequest_Verb) String() string { + return proto.EnumName(AuthorizeRequest_Verb_name, int32(x)) +} +func (AuthorizeRequest_Verb) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_auth_b463ef3269931e86, []int{0, 1} +} + +type AuthorizeRequest struct { + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Resources AuthorizeRequest_Resources `protobuf:"varint,2,opt,name=resources,proto3,enum=api.AuthorizeRequest_Resources" json:"resources,omitempty"` + Verb AuthorizeRequest_Verb `protobuf:"varint,3,opt,name=verb,proto3,enum=api.AuthorizeRequest_Verb" json:"verb,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AuthorizeRequest) Reset() { *m = AuthorizeRequest{} } +func (m *AuthorizeRequest) String() string { return proto.CompactTextString(m) } +func (*AuthorizeRequest) ProtoMessage() {} +func (*AuthorizeRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_auth_b463ef3269931e86, []int{0} +} +func (m *AuthorizeRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AuthorizeRequest.Unmarshal(m, b) +} +func (m *AuthorizeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AuthorizeRequest.Marshal(b, m, deterministic) +} +func (dst *AuthorizeRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_AuthorizeRequest.Merge(dst, src) +} +func (m *AuthorizeRequest) XXX_Size() int { + return xxx_messageInfo_AuthorizeRequest.Size(m) +} +func (m *AuthorizeRequest) XXX_DiscardUnknown() { + xxx_messageInfo_AuthorizeRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_AuthorizeRequest proto.InternalMessageInfo + +func (m *AuthorizeRequest) GetNamespace() string { + if m != nil { + return m.Namespace + } + return "" +} + +func (m *AuthorizeRequest) GetResources() AuthorizeRequest_Resources { + if m != nil { + return m.Resources + } + return AuthorizeRequest_UNASSIGNED_RESOURCES +} + +func (m *AuthorizeRequest) GetVerb() AuthorizeRequest_Verb { + if m != nil { + return m.Verb + } + return AuthorizeRequest_UNASSIGNED_VERB +} + +func init() { + proto.RegisterType((*AuthorizeRequest)(nil), "api.AuthorizeRequest") + proto.RegisterEnum("api.AuthorizeRequest_Resources", AuthorizeRequest_Resources_name, AuthorizeRequest_Resources_value) + proto.RegisterEnum("api.AuthorizeRequest_Verb", AuthorizeRequest_Verb_name, AuthorizeRequest_Verb_value) +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// AuthServiceClient is the client API for AuthService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type AuthServiceClient interface { + Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*empty.Empty, error) +} + +type authServiceClient struct { + cc *grpc.ClientConn +} + +func NewAuthServiceClient(cc *grpc.ClientConn) AuthServiceClient { + return &authServiceClient{cc} +} + +func (c *authServiceClient) Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/api.AuthService/Authorize", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServiceServer is the server API for AuthService service. +type AuthServiceServer interface { + Authorize(context.Context, *AuthorizeRequest) (*empty.Empty, error) +} + +func RegisterAuthServiceServer(s *grpc.Server, srv AuthServiceServer) { + s.RegisterService(&_AuthService_serviceDesc, srv) +} + +func _AuthService_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthorizeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).Authorize(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.AuthService/Authorize", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).Authorize(ctx, req.(*AuthorizeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _AuthService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "api.AuthService", + HandlerType: (*AuthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Authorize", + Handler: _AuthService_Authorize_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "backend/api/auth.proto", +} + +func init() { proto.RegisterFile("backend/api/auth.proto", fileDescriptor_auth_b463ef3269931e86) } + +var fileDescriptor_auth_b463ef3269931e86 = []byte{ + // 460 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0xc1, 0x6e, 0xd3, 0x40, + 0x14, 0x8c, 0x9d, 0x2a, 0xc1, 0x2f, 0x94, 0x9a, 0x6d, 0x29, 0x91, 0x09, 0x6a, 0x94, 0x53, 0x0f, + 0xd4, 0x56, 0xd3, 0x2b, 0x1c, 0x92, 0x76, 0x55, 0x55, 0x82, 0x22, 0xad, 0xd3, 0x20, 0xf5, 0x52, + 0xad, 0xdd, 0x17, 0x67, 0x55, 0xc7, 0x6b, 0xd6, 0xeb, 0x54, 0x70, 0x44, 0xe2, 0x03, 0x80, 0x4f, + 0xe3, 0x17, 0xf8, 0x10, 0xe4, 0x4d, 0x9a, 0x46, 0x90, 0xd3, 0x6a, 0xdf, 0xcc, 0x9b, 0x19, 0x69, + 0x1e, 0xec, 0x47, 0x3c, 0xbe, 0xc3, 0xec, 0x36, 0xe0, 0xb9, 0x08, 0x78, 0xa9, 0xa7, 0x7e, 0xae, + 0xa4, 0x96, 0xa4, 0xce, 0x73, 0xe1, 0x75, 0x12, 0x29, 0x93, 0x14, 0x17, 0x58, 0x96, 0x49, 0xcd, + 0xb5, 0x90, 0x59, 0xb1, 0xa0, 0x78, 0xaf, 0x96, 0xa8, 0xf9, 0x45, 0xe5, 0x24, 0xc0, 0x59, 0xae, + 0xbf, 0x2c, 0xc1, 0x97, 0xeb, 0xba, 0xa8, 0x94, 0x54, 0x4b, 0xe0, 0x8d, 0x79, 0xe2, 0xa3, 0x04, + 0xb3, 0xa3, 0xe2, 0x9e, 0x27, 0x09, 0xaa, 0x40, 0xe6, 0x46, 0xf7, 0x7f, 0x8f, 0xde, 0x0f, 0x1b, + 0xdc, 0x41, 0xa9, 0xa7, 0x52, 0x89, 0xaf, 0xc8, 0xf0, 0x73, 0x89, 0x85, 0x26, 0x1d, 0x70, 0x32, + 0x3e, 0xc3, 0x22, 0xe7, 0x31, 0xb6, 0xad, 0xae, 0x75, 0xe8, 0xb0, 0xc7, 0x01, 0x79, 0x07, 0x8e, + 0xc2, 0x42, 0x96, 0x2a, 0xc6, 0xa2, 0x6d, 0x77, 0xad, 0xc3, 0x67, 0xfd, 0x03, 0x9f, 0xe7, 0xc2, + 0xff, 0x57, 0xc7, 0x67, 0x0f, 0x34, 0xf6, 0xb8, 0x41, 0x7c, 0xd8, 0x9a, 0xa3, 0x8a, 0xda, 0x75, + 0xb3, 0xe9, 0x6d, 0xde, 0x1c, 0xa3, 0x8a, 0x98, 0xe1, 0xf5, 0xfa, 0xe0, 0xac, 0x74, 0x48, 0x1b, + 0xf6, 0xae, 0x2e, 0x07, 0x61, 0x78, 0x71, 0x7e, 0x49, 0xcf, 0x6e, 0x18, 0x0d, 0x3f, 0x5e, 0xb1, + 0x53, 0x1a, 0xba, 0x35, 0xd2, 0x82, 0xe6, 0xf8, 0x82, 0x7e, 0xa2, 0x2c, 0x74, 0xad, 0xde, 0x5b, + 0xd8, 0xaa, 0x14, 0xc8, 0x2e, 0xec, 0xac, 0xd1, 0xc7, 0x94, 0x0d, 0xdd, 0x1a, 0x01, 0x68, 0x9c, + 0x32, 0x3a, 0x18, 0x51, 0xd7, 0x22, 0x4d, 0xa8, 0x9f, 0xd3, 0x91, 0x6b, 0x57, 0xc3, 0x33, 0xfa, + 0x9e, 0x8e, 0xa8, 0x5b, 0xef, 0x23, 0xb4, 0xaa, 0x40, 0x21, 0xaa, 0xb9, 0x88, 0x91, 0x8c, 0xc1, + 0x59, 0xe5, 0x23, 0x2f, 0x36, 0xe6, 0xf5, 0xf6, 0xfd, 0x45, 0x57, 0xfe, 0x43, 0x57, 0x3e, 0xad, + 0xba, 0xea, 0x79, 0xdf, 0x7e, 0xff, 0xf9, 0x65, 0xef, 0x11, 0x52, 0xd5, 0x54, 0x04, 0xf3, 0xe3, + 0x08, 0x35, 0x3f, 0x36, 0x77, 0x30, 0xfc, 0x6e, 0xfd, 0x1c, 0x7c, 0x60, 0x1d, 0x68, 0xde, 0xe2, + 0x84, 0x97, 0xa9, 0x26, 0xcf, 0xc9, 0x0e, 0x6c, 0x7b, 0x2d, 0xe3, 0x10, 0x6a, 0xae, 0xcb, 0xe2, + 0xfa, 0x00, 0x5e, 0x43, 0x63, 0x88, 0x5c, 0xa1, 0x22, 0xbb, 0x4f, 0x6c, 0x6f, 0x9b, 0x2f, 0x9d, + 0x4d, 0x89, 0x5d, 0x3b, 0x7a, 0x0a, 0xb0, 0x22, 0xd4, 0xae, 0x4f, 0x12, 0xa1, 0xa7, 0x65, 0xe4, + 0xc7, 0x72, 0x16, 0xdc, 0x95, 0x11, 0x4e, 0x52, 0x79, 0x1f, 0xe4, 0x22, 0xc7, 0x54, 0x64, 0x58, + 0x04, 0xeb, 0x27, 0x93, 0xc8, 0x9b, 0x38, 0x15, 0x98, 0xe9, 0xa8, 0x61, 0x32, 0x9f, 0xfc, 0x0d, + 0x00, 0x00, 0xff, 0xff, 0x8e, 0x47, 0x2d, 0x41, 0xaa, 0x02, 0x00, 0x00, +} diff --git a/backend/api/go_client/auth.pb.gw.go b/backend/api/go_client/auth.pb.gw.go new file mode 100755 index 00000000000..41119c97f80 --- /dev/null +++ b/backend/api/go_client/auth.pb.gw.go @@ -0,0 +1,129 @@ +// Copyright 2020 Google LLC +// +// 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. + +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: backend/api/auth.proto + +/* +Package go_client is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package go_client + +import ( + "context" + "io" + "net/http" + + "github.com/golang/protobuf/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/grpc-ecosystem/grpc-gateway/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/status" +) + +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray + +var ( + filter_AuthService_Authorize_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_AuthService_Authorize_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AuthorizeRequest + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_AuthService_Authorize_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Authorize(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +// RegisterAuthServiceHandlerFromEndpoint is same as RegisterAuthServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterAuthServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterAuthServiceHandler(ctx, mux, conn) +} + +// RegisterAuthServiceHandler registers the http handlers for service AuthService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterAuthServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterAuthServiceHandlerClient(ctx, mux, NewAuthServiceClient(conn)) +} + +// RegisterAuthServiceHandlerClient registers the http handlers for service AuthService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "AuthServiceClient" to call the correct interceptors. +func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthServiceClient) error { + + mux.Handle("GET", pattern_AuthService_Authorize_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_Authorize_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_AuthService_Authorize_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_AuthService_Authorize_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"apis", "v1beta1", "auth"}, "")) +) + +var ( + forward_AuthService_Authorize_0 = runtime.ForwardResponseMessage +) diff --git a/backend/api/swagger/auth.swagger.json b/backend/api/swagger/auth.swagger.json new file mode 100755 index 00000000000..5b8e5beb0e6 --- /dev/null +++ b/backend/api/swagger/auth.swagger.json @@ -0,0 +1,140 @@ +{ + "swagger": "2.0", + "info": { + "title": "backend/api/auth.proto", + "version": "version not set" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/apis/v1beta1/auth": { + "get": { + "operationId": "Authorize", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "properties": {} + } + }, + "default": { + "description": "", + "schema": { + "$ref": "#/definitions/apiStatus" + } + } + }, + "parameters": [ + { + "name": "namespace", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "resources", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "UNASSIGNED_RESOURCES", + "VIEWERS" + ], + "default": "UNASSIGNED_RESOURCES" + }, + { + "name": "verb", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "UNASSIGNED_VERB", + "CREATE", + "GET", + "DELETE" + ], + "default": "UNASSIGNED_VERB" + } + ], + "tags": [ + "AuthService" + ] + } + } + }, + "definitions": { + "AuthorizeRequestResources": { + "type": "string", + "enum": [ + "UNASSIGNED_RESOURCES", + "VIEWERS" + ], + "default": "UNASSIGNED_RESOURCES", + "description": "Type of resources in pipelines system." + }, + "AuthorizeRequestVerb": { + "type": "string", + "enum": [ + "UNASSIGNED_VERB", + "CREATE", + "GET", + "DELETE" + ], + "default": "UNASSIGNED_VERB", + "description": "Type of verbs that act on the resources." + }, + "apiStatus": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "code": { + "type": "integer", + "format": "int32" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "type_url": { + "type": "string", + "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." + }, + "value": { + "type": "string", + "format": "byte", + "description": "Must be a valid serialized protocol buffer of the above specified type." + } + }, + "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" + } + }, + "securityDefinitions": { + "Bearer": { + "type": "apiKey", + "name": "authorization", + "in": "header" + } + }, + "security": [ + { + "Bearer": [] + } + ] +} diff --git a/backend/src/apiserver/main.go b/backend/src/apiserver/main.go index b072943a6b4..12e3ba0ddf1 100644 --- a/backend/src/apiserver/main.go +++ b/backend/src/apiserver/main.go @@ -101,6 +101,7 @@ func startRpcServer(resourceManager *resource.ResourceManager) { common.GetStringConfig(visualizationServiceHost), common.GetStringConfig(visualizationServicePort), )) + api.RegisterAuthServiceServer(s, server.NewAuthServer(resourceManager)) // Register reflection service on gRPC server. reflection.Register(s) @@ -125,6 +126,7 @@ func startHttpProxy(resourceManager *resource.ResourceManager) { registerHttpHandlerFromEndpoint(api.RegisterRunServiceHandlerFromEndpoint, "RunService", ctx, mux) registerHttpHandlerFromEndpoint(api.RegisterReportServiceHandlerFromEndpoint, "ReportService", ctx, mux) registerHttpHandlerFromEndpoint(api.RegisterVisualizationServiceHandlerFromEndpoint, "Visualization", ctx, mux) + registerHttpHandlerFromEndpoint(api.RegisterAuthServiceHandlerFromEndpoint, "AuthService", ctx, mux) // Create a top level mux to include both pipeline upload server and gRPC servers. topMux := http.NewServeMux() diff --git a/backend/src/apiserver/server/BUILD.bazel b/backend/src/apiserver/server/BUILD.bazel index 1b076e831b6..b26a2794b30 100644 --- a/backend/src/apiserver/server/BUILD.bazel +++ b/backend/src/apiserver/server/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "api_converter.go", + "auth_server.go", "experiment_server.go", "job_server.go", "list_request_util.go", @@ -46,6 +47,7 @@ go_test( name = "go_default_test", srcs = [ "api_converter_test.go", + "auth_server_test.go", "experiment_server_test.go", "job_server_test.go", "list_request_util_test.go", diff --git a/backend/src/apiserver/server/auth_server.go b/backend/src/apiserver/server/auth_server.go new file mode 100644 index 00000000000..0cc6750c6f1 --- /dev/null +++ b/backend/src/apiserver/server/auth_server.go @@ -0,0 +1,52 @@ +package server + +import ( + "context" + + "github.com/golang/protobuf/ptypes/empty" + api "github.com/kubeflow/pipelines/backend/api/go_client" + "github.com/kubeflow/pipelines/backend/src/apiserver/resource" + "github.com/kubeflow/pipelines/backend/src/common/util" +) + +type AuthServer struct { + resourceManager *resource.ResourceManager +} + +func (s *AuthServer) Authorize(ctx context.Context, request *api.AuthorizeRequest) ( + *empty.Empty, error) { + err := ValidateAuthorizeRequest(request) + if err != nil { + return nil, util.Wrap(err, "Authorize request is not valid") + } + + // TODO: when KFP changes authorization implementation to have more + // granularity, we need to start using resources and verb info in the + // request. + err = CanAccessNamespace(s.resourceManager, ctx, request.Namespace) + if err != nil { + return nil, util.Wrap(err, "Failed to authorize the request") + } + + return &empty.Empty{}, nil +} + +func ValidateAuthorizeRequest(request *api.AuthorizeRequest) error { + if request == nil { + return util.NewInvalidInputError("request object is empty.") + } + if len(request.Namespace) == 0 { + return util.NewInvalidInputError("Namespace is empty. Please specify a valid namespace.") + } + if request.Resources == api.AuthorizeRequest_UNASSIGNED_RESOURCES { + return util.NewInvalidInputError("Resources not specified. Please specify a valid resources.") + } + if request.Verb == api.AuthorizeRequest_UNASSIGNED_VERB { + return util.NewInvalidInputError("Verb not specified. Please specify a valid verb.") + } + return nil +} + +func NewAuthServer(resourceManager *resource.ResourceManager) *AuthServer { + return &AuthServer{resourceManager: resourceManager} +} diff --git a/backend/src/apiserver/server/auth_server_test.go b/backend/src/apiserver/server/auth_server_test.go new file mode 100644 index 00000000000..b6797d28b95 --- /dev/null +++ b/backend/src/apiserver/server/auth_server_test.go @@ -0,0 +1,98 @@ +package server + +import ( + "context" + "testing" + + api "github.com/kubeflow/pipelines/backend/api/go_client" + "github.com/kubeflow/pipelines/backend/src/apiserver/client" + "github.com/kubeflow/pipelines/backend/src/apiserver/common" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/metadata" +) + +func TestAuthorizeRequest_SingleUserMode(t *testing.T) { + clients, manager, _ := initWithExperiment(t) + defer clients.Close() + authServer := AuthServer{resourceManager: manager} + clients.KfamClientFake = client.NewFakeKFAMClientUnauthorized() + + md := metadata.New(map[string]string{}) + ctx := metadata.NewIncomingContext(context.Background(), md) + + request := &api.AuthorizeRequest{ + Namespace: "ns1", + Resources: api.AuthorizeRequest_VIEWERS, + Verb: api.AuthorizeRequest_GET, + } + + _, err := authServer.Authorize(ctx, request) + // Authz is completely skipped without checking anything. + assert.Nil(t, err) +} + +func TestAuthorizeRequest_InvalidRequest(t *testing.T) { + viper.Set(common.MultiUserMode, "true") + defer viper.Set(common.MultiUserMode, "false") + + clients, manager, _ := initWithExperiment(t) + defer clients.Close() + authServer := AuthServer{resourceManager: manager} + + md := metadata.New(map[string]string{}) + ctx := metadata.NewIncomingContext(context.Background(), md) + + request := &api.AuthorizeRequest{ + Namespace: "", + Resources: api.AuthorizeRequest_UNASSIGNED_RESOURCES, + Verb: api.AuthorizeRequest_UNASSIGNED_VERB, + } + + _, err := authServer.Authorize(ctx, request) + assert.Error(t, err) + assert.EqualError(t, err, "Authorize request is not valid: Invalid input error: Namespace is empty. Please specify a valid namespace.") +} + +func TestAuthorizeRequest_Authorized(t *testing.T) { + viper.Set(common.MultiUserMode, "true") + defer viper.Set(common.MultiUserMode, "false") + + clients, manager, _ := initWithExperiment(t) + defer clients.Close() + authServer := AuthServer{resourceManager: manager} + + md := metadata.New(map[string]string{common.GoogleIAPUserIdentityHeader: "accounts.google.com:user@google.com"}) + ctx := metadata.NewIncomingContext(context.Background(), md) + + request := &api.AuthorizeRequest{ + Namespace: "ns1", + Resources: api.AuthorizeRequest_VIEWERS, + Verb: api.AuthorizeRequest_GET, + } + + _, err := authServer.Authorize(ctx, request) + assert.Nil(t, err) +} + +func TestAuthorizeRequest_Unauthorized(t *testing.T) { + viper.Set(common.MultiUserMode, "true") + defer viper.Set(common.MultiUserMode, "false") + + clients, manager, _ := initWithExperiment_KFAM_Unauthorized(t) + defer clients.Close() + authServer := AuthServer{resourceManager: manager} + + md := metadata.New(map[string]string{common.GoogleIAPUserIdentityHeader: "accounts.google.com:user@google.com"}) + ctx := metadata.NewIncomingContext(context.Background(), md) + + request := &api.AuthorizeRequest{ + Namespace: "ns1", + Resources: api.AuthorizeRequest_VIEWERS, + Verb: api.AuthorizeRequest_GET, + } + + _, err := authServer.Authorize(ctx, request) + assert.Error(t, err) + assert.EqualError(t, err, "Failed to authorize the request: Failed to authorize namespace: BadRequestError: Unauthorized access for user@google.com to namespace ns1: Unauthorized access for user@google.com to namespace ns1") +} diff --git a/backend/src/apiserver/server/util.go b/backend/src/apiserver/server/util.go index 8029e0210bd..c97405fa7b3 100644 --- a/backend/src/apiserver/server/util.go +++ b/backend/src/apiserver/server/util.go @@ -347,6 +347,22 @@ func CanAccessNamespaceInResourceReferences(resourceManager *resource.ResourceMa return nil } +func CanAccessNamespace(resourceManager *resource.ResourceManager, ctx context.Context, namespace string) error { + if common.IsMultiUserMode() == false { + // Skip authz if not multi-user mode. + return nil + } + + if len(namespace) == 0 { + return util.NewBadRequestError(errors.New("Namespace required for authorization."), "Namespace required for authorization.") + } + err := isAuthorized(resourceManager, ctx, namespace) + if err != nil { + return util.Wrap(err, "Failed to authorize namespace") + } + return nil +} + // isAuthorized verified whether the user identity, which is contains in the context object, // can access the target namespace. If the returned error is nil, the authorization passes. // Otherwise, Authorization fails with a non-nil error.