diff --git a/.proxyrc b/.proxyrc index 751cb293176..5ff93861553 100644 --- a/.proxyrc +++ b/.proxyrc @@ -1,5 +1,8 @@ { "/v1": { - "target": "http://localhost:9000/", - } + "target": "http://localhost:9001/", + }, + "/oauth2": { + "target": "http://localhost:9001/" + }, } diff --git a/api/applications/applications.proto b/api/applications/applications.proto index 0b06ec1ff7a..ec28e024f3c 100644 --- a/api/applications/applications.proto +++ b/api/applications/applications.proto @@ -174,6 +174,15 @@ service Applications { }; } + /** + * Config returns configuration information about the server + */ + rpc GetFeatureFlags(GetFeatureFlagsRequest) returns (GetFeatureFlagsResponse) { + option (google.api.http) = { + get : "/v1/featureflags" + }; + } + } // This object represents a single condition for a Kubernetes object. @@ -431,3 +440,9 @@ message ValidateProviderTokenRequest { message ValidateProviderTokenResponse { bool valid = 1; } + +message GetFeatureFlagsRequest {} + +message GetFeatureFlagsResponse { + map flags = 1; +} diff --git a/api/applications/applications.swagger.json b/api/applications/applications.swagger.json index 73407f90bac..7adb8d963c7 100644 --- a/api/applications/applications.swagger.json +++ b/api/applications/applications.swagger.json @@ -570,6 +570,29 @@ "Applications" ] } + }, + "/v1/featureflags": { + "get": { + "summary": "Config returns configuration information about the server", + "operationId": "Applications_GetFeatureFlags", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetFeatureFlagsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "Applications" + ] + } } }, "definitions": { @@ -792,6 +815,17 @@ } } }, + "v1GetFeatureFlagsResponse": { + "type": "object", + "properties": { + "flags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "v1GetGithubAuthStatusRequest": { "type": "object", "properties": { diff --git a/cmd/gitops/ui/run/cmd.go b/cmd/gitops/ui/run/cmd.go index 9383231c914..3c8a3195fe6 100644 --- a/cmd/gitops/ui/run/cmd.go +++ b/cmd/gitops/ui/run/cmd.go @@ -47,11 +47,11 @@ type Options struct { // OIDCAuthenticationOptions contains the OIDC authentication options for the // `ui run` command. type OIDCAuthenticationOptions struct { - IssuerURL string - ClientID string - ClientSecret string - RedirectURL string - CookieDuration time.Duration + IssuerURL string + ClientID string + ClientSecret string + RedirectURL string + TokenDuration time.Duration } var options Options @@ -83,7 +83,7 @@ func NewCommand() *cobra.Command { cmd.Flags().StringVar(&options.OIDC.ClientID, "oidc-client-id", "", "The client ID for the OpenID Connect client") cmd.Flags().StringVar(&options.OIDC.ClientSecret, "oidc-client-secret", "", "The client secret to use with OpenID Connect issuer") cmd.Flags().StringVar(&options.OIDC.RedirectURL, "oidc-redirect-url", "", "The OAuth2 redirect URL") - cmd.Flags().DurationVar(&options.OIDC.CookieDuration, "oidc-cookie-duration", time.Hour, "The duration of the ID token cookie. It should be set in the format: number + time unit (s,m,h) e.g., 20m") + cmd.Flags().DurationVar(&options.OIDC.TokenDuration, "oidc-token-duration", time.Hour, "The duration of the ID token. It should be set in the format: number + time unit (s,m,h) e.g., 20m") } return cmd @@ -189,29 +189,26 @@ func runCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid issuer URL: %w", err) } - redirectURL, err := url.Parse(options.OIDC.RedirectURL) + _, err = url.Parse(options.OIDC.RedirectURL) if err != nil { return fmt.Errorf("invalid redirect URL: %w", err) } - var oidcIssueSecureCookies bool - if redirectURL.Scheme == "https" { - oidcIssueSecureCookies = true + tsv, err := auth.NewHMACTokenSignerVerifier(options.OIDC.TokenDuration) + if err != nil { + return fmt.Errorf("could not create HMAC token signer: %w", err) } srv, err := auth.NewAuthServer(cmd.Context(), appConfig.Logger, http.DefaultClient, auth.AuthConfig{ OIDCConfig: auth.OIDCConfig{ - IssuerURL: options.OIDC.IssuerURL, - ClientID: options.OIDC.ClientID, - ClientSecret: options.OIDC.ClientSecret, - RedirectURL: options.OIDC.RedirectURL, - }, - CookieConfig: auth.CookieConfig{ - CookieDuration: options.OIDC.CookieDuration, - IssueSecureCookies: oidcIssueSecureCookies, + IssuerURL: options.OIDC.IssuerURL, + ClientID: options.OIDC.ClientID, + ClientSecret: options.OIDC.ClientSecret, + RedirectURL: options.OIDC.RedirectURL, + TokenDuration: options.OIDC.TokenDuration, }, - }, + }, rawClient, tsv, ) if err != nil { return fmt.Errorf("could not create auth server: %w", err) diff --git a/go.mod b/go.mod index 4aa41694282..2f8ae4a639e 100644 --- a/go.mod +++ b/go.mod @@ -72,6 +72,7 @@ require ( github.com/gofrs/flock v0.8.1 github.com/google/uuid v1.3.0 github.com/oauth2-proxy/mockoidc v0.0.0-20210703044157-382d3faf2671 + golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/yaml.v2 v2.4.0 ) @@ -211,7 +212,6 @@ require ( go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/mod v0.4.2 // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect diff --git a/package.json b/package.json index 26de5427221..f3970f89293 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "peerDependencies": { "lodash": "^4.17.21", "luxon": "^1.27.0", + "react": "^17.0.2", "react-dom": "^17.0.2", "react-toastify": "^7.0.4", - "react": "^17.0.2", "styled-components": "^5.3.0" }, "dependencies": { diff --git a/pkg/api/applications/applications.pb.go b/pkg/api/applications/applications.pb.go index 683e9b85567..9a82218e580 100644 --- a/pkg/api/applications/applications.pb.go +++ b/pkg/api/applications/applications.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc (unknown) // source: api/applications/applications.proto package applications @@ -2596,6 +2596,91 @@ func (x *ValidateProviderTokenResponse) GetValid() bool { return false } +type GetFeatureFlagsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetFeatureFlagsRequest) Reset() { + *x = GetFeatureFlagsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_applications_applications_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetFeatureFlagsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeatureFlagsRequest) ProtoMessage() {} + +func (x *GetFeatureFlagsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_applications_applications_proto_msgTypes[39] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFeatureFlagsRequest.ProtoReflect.Descriptor instead. +func (*GetFeatureFlagsRequest) Descriptor() ([]byte, []int) { + return file_api_applications_applications_proto_rawDescGZIP(), []int{39} +} + +type GetFeatureFlagsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Flags map[string]string `protobuf:"bytes,1,rep,name=flags,proto3" json:"flags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *GetFeatureFlagsResponse) Reset() { + *x = GetFeatureFlagsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_applications_applications_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetFeatureFlagsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeatureFlagsResponse) ProtoMessage() {} + +func (x *GetFeatureFlagsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_applications_applications_proto_msgTypes[40] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFeatureFlagsResponse.ProtoReflect.Descriptor instead. +func (*GetFeatureFlagsResponse) Descriptor() ([]byte, []int) { + return file_api_applications_applications_proto_rawDescGZIP(), []int{40} +} + +func (x *GetFeatureFlagsResponse) GetFlags() map[string]string { + if x != nil { + return x.Flags + } + return nil +} + var File_api_applications_applications_proto protoreflect.FileDescriptor var file_api_applications_applications_proto_rawDesc = []byte{ @@ -2914,165 +2999,184 @@ var file_api_applications_applications_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x35, 0x0a, 0x1d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x2a, - 0x29, 0x0a, 0x0e, 0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, - 0x64, 0x12, 0x0d, 0x0a, 0x09, 0x4b, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x65, 0x10, 0x00, - 0x12, 0x08, 0x0a, 0x04, 0x48, 0x65, 0x6c, 0x6d, 0x10, 0x01, 0x2a, 0x32, 0x0a, 0x0b, 0x47, 0x69, - 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, - 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x47, 0x69, 0x74, 0x48, 0x75, 0x62, - 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x47, 0x69, 0x74, 0x4c, 0x61, 0x62, 0x10, 0x02, 0x32, 0x98, - 0x11, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x86, 0x01, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x12, 0x23, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x25, 0x22, 0x20, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x3a, 0x01, 0x2a, 0x12, 0x7f, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, - 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x2e, 0x77, - 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x80, 0x01, 0x0a, 0x0e, 0x47, 0x65, - 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x77, - 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, - 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x7f, 0x0a, 0x0b, - 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x77, 0x65, - 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x23, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x12, 0x1f, 0x2f, 0x76, - 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, - 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0xa9, 0x01, - 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x64, 0x4f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, - 0x63, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, - 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x3f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x39, - 0x22, 0x34, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, - 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x6f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x84, 0x01, 0x0a, 0x0f, 0x47, 0x65, - 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x2e, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x22, + 0x18, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x46, 0x6c, 0x61, + 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x9d, 0x01, 0x0a, 0x17, 0x47, 0x65, + 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x6c, + 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x1a, + 0x38, 0x0a, 0x0a, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x29, 0x0a, 0x0e, 0x41, 0x75, 0x74, + 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0d, 0x0a, 0x09, 0x4b, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x69, 0x7a, 0x65, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x65, + 0x6c, 0x6d, 0x10, 0x01, 0x2a, 0x32, 0x0a, 0x0b, 0x47, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, + 0x12, 0x0a, 0x0a, 0x06, 0x47, 0x69, 0x74, 0x48, 0x75, 0x62, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, + 0x47, 0x69, 0x74, 0x4c, 0x61, 0x62, 0x10, 0x02, 0x32, 0x96, 0x12, 0x0a, 0x0c, 0x41, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x86, 0x01, 0x0a, 0x0c, 0x41, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x23, 0x2e, 0x77, 0x65, 0x67, + 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x24, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x22, 0x20, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2f, + 0x7b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x3a, + 0x01, 0x2a, 0x12, 0x7f, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x28, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x80, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x1a, 0x22, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x1e, 0x2f, + 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x12, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, - 0x63, 0x68, 0x69, 0x6c, 0x64, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x3a, 0x01, 0x2a, - 0x12, 0x9e, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x76, 0x31, 0x2f, 0x61, - 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x12, 0xa8, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x41, - 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2a, 0x2e, 0x77, 0x65, 0x67, 0x6f, - 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x38, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x22, 0x2d, 0x2f, 0x76, 0x31, 0x2f, - 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x75, 0x74, - 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x95, 0x01, 0x0a, - 0x10, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x41, 0x75, 0x74, 0x68, 0x55, 0x52, - 0x4c, 0x12, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x41, 0x75, 0x74, 0x68, - 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x77, 0x65, 0x67, - 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, - 0x69, 0x74, 0x6c, 0x61, 0x62, 0x41, 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x76, - 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x67, 0x69, - 0x74, 0x6c, 0x61, 0x62, 0x12, 0x9f, 0x01, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x47, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x12, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x47, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x47, 0x69, 0x74, 0x6c, 0x61, - 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x35, 0x22, 0x30, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x7c, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x41, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x64, 0x64, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x22, - 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, - 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x2e, 0x77, 0x65, 0x67, - 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x7f, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x77, 0x65, 0x67, 0x6f, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x12, 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, + 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0xa9, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x12, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x64, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x22, 0x3f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x39, 0x22, 0x34, 0x2f, 0x76, 0x31, 0x2f, + 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x75, + 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, + 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x3a, 0x01, 0x2a, 0x12, 0x84, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x68, 0x69, 0x6c, + 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x77, 0x65, + 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x43, 0x68, 0x69, 0x6c, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, + 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x1e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x5f, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x9e, 0x01, 0x0a, 0x13, 0x47, + 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, + 0x64, 0x65, 0x12, 0x2a, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, + 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2e, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x12, 0xa8, 0x01, 0x0a, 0x13, + 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x2a, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x41, 0x75, + 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2b, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x32, 0x22, 0x2d, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x95, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x47, 0x69, + 0x74, 0x6c, 0x61, 0x62, 0x41, 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x12, 0x27, 0x2e, 0x77, 0x65, + 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x47, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x41, 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x41, + 0x75, 0x74, 0x68, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2e, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x12, 0x9f, + 0x01, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x47, 0x69, 0x74, 0x6c, + 0x61, 0x62, 0x12, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x47, 0x69, 0x74, + 0x6c, 0x61, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x77, 0x65, 0x67, + 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x47, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x35, 0x22, 0x30, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x75, + 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2f, 0x67, 0x69, 0x74, + 0x6c, 0x61, 0x62, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x3a, 0x01, 0x2a, + 0x12, 0x7c, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x70, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x22, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, + 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x01, 0x2a, 0x12, 0x8c, + 0x01, 0x0a, 0x11, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x70, 0x70, 0x6c, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1c, 0x2a, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, - 0x3a, 0x01, 0x2a, 0x12, 0x8b, 0x01, 0x0a, 0x0f, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x70, 0x70, 0x6c, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, - 0x22, 0x1c, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x3a, 0x01, - 0x2a, 0x12, 0x82, 0x01, 0x0a, 0x0c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x55, - 0x52, 0x4c, 0x12, 0x23, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x55, 0x52, 0x4c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, - 0x70, 0x6f, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x12, 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x5f, 0x72, 0x65, - 0x70, 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0xa0, 0x01, 0x0a, 0x15, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x12, 0x2c, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, + 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x1c, 0x2a, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x3a, 0x01, 0x2a, 0x12, 0x8b, 0x01, + 0x0a, 0x0f, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x27, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x22, 0x1c, 0x2f, 0x76, 0x31, 0x2f, + 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x6e, 0x61, + 0x6d, 0x65, 0x7d, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x3a, 0x01, 0x2a, 0x12, 0x82, 0x01, 0x0a, 0x0c, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x55, 0x52, 0x4c, 0x12, 0x23, 0x2e, 0x77, + 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, + 0x72, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x24, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x55, 0x52, 0x4c, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x12, + 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x75, 0x72, 0x6c, + 0x12, 0xa0, 0x01, 0x0a, 0x15, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2c, 0x2e, 0x77, 0x65, 0x67, + 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x22, + 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x3a, 0x01, 0x2a, 0x12, 0x7c, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x77, 0x65, 0x67, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x22, 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x3a, 0x01, 0x2a, 0x42, 0xce, 0x01, 0x5a, 0x3a, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x77, 0x65, 0x61, 0x76, 0x65, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x2f, 0x77, 0x65, 0x61, 0x76, 0x65, 0x2d, 0x67, 0x69, 0x74, 0x6f, 0x70, 0x73, - 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x92, 0x41, 0x8e, 0x01, 0x12, 0x68, 0x0a, 0x15, - 0x57, 0x65, 0x47, 0x6f, 0x20, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x20, 0x41, 0x50, 0x49, 0x12, 0x4a, 0x54, 0x68, 0x65, 0x20, 0x57, 0x65, 0x47, 0x6f, 0x20, - 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x41, 0x50, 0x49, - 0x20, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x20, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x57, 0x65, 0x61, 0x76, 0x65, 0x20, 0x47, 0x69, - 0x74, 0x4f, 0x70, 0x73, 0x20, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x32, 0x03, 0x30, 0x2e, 0x31, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, + 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x66, 0x6c, 0x61, 0x67, + 0x73, 0x42, 0xce, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x77, 0x65, 0x61, 0x76, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x2f, 0x77, 0x65, 0x61, 0x76, + 0x65, 0x2d, 0x67, 0x69, 0x74, 0x6f, 0x70, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x92, 0x41, 0x8e, 0x01, 0x12, 0x68, 0x0a, 0x15, 0x57, 0x65, 0x47, 0x6f, 0x20, 0x41, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x41, 0x50, 0x49, 0x12, 0x4a, 0x54, + 0x68, 0x65, 0x20, 0x57, 0x65, 0x47, 0x6f, 0x20, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x41, 0x50, 0x49, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, + 0x20, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, + 0x57, 0x65, 0x61, 0x76, 0x65, 0x20, 0x47, 0x69, 0x74, 0x4f, 0x70, 0x73, 0x20, 0x41, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x03, 0x30, 0x2e, 0x31, 0x32, 0x10, + 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, + 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, + 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3088,7 +3192,7 @@ func file_api_applications_applications_proto_rawDescGZIP() []byte { } var file_api_applications_applications_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_api_applications_applications_proto_msgTypes = make([]protoimpl.MessageInfo, 39) +var file_api_applications_applications_proto_msgTypes = make([]protoimpl.MessageInfo, 42) var file_api_applications_applications_proto_goTypes = []interface{}{ (AutomationKind)(0), // 0: wego_server.v1.AutomationKind (GitProvider)(0), // 1: wego_server.v1.GitProvider @@ -3132,6 +3236,9 @@ var file_api_applications_applications_proto_goTypes = []interface{}{ (*AuthorizeGitlabResponse)(nil), // 39: wego_server.v1.AuthorizeGitlabResponse (*ValidateProviderTokenRequest)(nil), // 40: wego_server.v1.ValidateProviderTokenRequest (*ValidateProviderTokenResponse)(nil), // 41: wego_server.v1.ValidateProviderTokenResponse + (*GetFeatureFlagsRequest)(nil), // 42: wego_server.v1.GetFeatureFlagsRequest + (*GetFeatureFlagsResponse)(nil), // 43: wego_server.v1.GetFeatureFlagsResponse + nil, // 44: wego_server.v1.GetFeatureFlagsResponse.FlagsEntry } var file_api_applications_applications_proto_depIdxs = []int32{ 3, // 0: wego_server.v1.Application.source_conditions:type_name -> wego_server.v1.Condition @@ -3158,41 +3265,44 @@ var file_api_applications_applications_proto_depIdxs = []int32{ 25, // 21: wego_server.v1.GetChildObjectsRes.objects:type_name -> wego_server.v1.UnstructuredObject 1, // 22: wego_server.v1.ParseRepoURLResponse.provider:type_name -> wego_server.v1.GitProvider 1, // 23: wego_server.v1.ValidateProviderTokenRequest.provider:type_name -> wego_server.v1.GitProvider - 9, // 24: wego_server.v1.Applications.Authenticate:input_type -> wego_server.v1.AuthenticateRequest - 11, // 25: wego_server.v1.Applications.ListApplications:input_type -> wego_server.v1.ListApplicationsRequest - 13, // 26: wego_server.v1.Applications.GetApplication:input_type -> wego_server.v1.GetApplicationRequest - 22, // 27: wego_server.v1.Applications.ListCommits:input_type -> wego_server.v1.ListCommitsRequest - 26, // 28: wego_server.v1.Applications.GetReconciledObjects:input_type -> wego_server.v1.GetReconciledObjectsReq - 28, // 29: wego_server.v1.Applications.GetChildObjects:input_type -> wego_server.v1.GetChildObjectsReq - 30, // 30: wego_server.v1.Applications.GetGithubDeviceCode:input_type -> wego_server.v1.GetGithubDeviceCodeRequest - 32, // 31: wego_server.v1.Applications.GetGithubAuthStatus:input_type -> wego_server.v1.GetGithubAuthStatusRequest - 36, // 32: wego_server.v1.Applications.GetGitlabAuthURL:input_type -> wego_server.v1.GetGitlabAuthURLRequest - 38, // 33: wego_server.v1.Applications.AuthorizeGitlab:input_type -> wego_server.v1.AuthorizeGitlabRequest - 15, // 34: wego_server.v1.Applications.AddApplication:input_type -> wego_server.v1.AddApplicationRequest - 17, // 35: wego_server.v1.Applications.RemoveApplication:input_type -> wego_server.v1.RemoveApplicationRequest - 19, // 36: wego_server.v1.Applications.SyncApplication:input_type -> wego_server.v1.SyncApplicationRequest - 34, // 37: wego_server.v1.Applications.ParseRepoURL:input_type -> wego_server.v1.ParseRepoURLRequest - 40, // 38: wego_server.v1.Applications.ValidateProviderToken:input_type -> wego_server.v1.ValidateProviderTokenRequest - 10, // 39: wego_server.v1.Applications.Authenticate:output_type -> wego_server.v1.AuthenticateResponse - 12, // 40: wego_server.v1.Applications.ListApplications:output_type -> wego_server.v1.ListApplicationsResponse - 14, // 41: wego_server.v1.Applications.GetApplication:output_type -> wego_server.v1.GetApplicationResponse - 23, // 42: wego_server.v1.Applications.ListCommits:output_type -> wego_server.v1.ListCommitsResponse - 27, // 43: wego_server.v1.Applications.GetReconciledObjects:output_type -> wego_server.v1.GetReconciledObjectsRes - 29, // 44: wego_server.v1.Applications.GetChildObjects:output_type -> wego_server.v1.GetChildObjectsRes - 31, // 45: wego_server.v1.Applications.GetGithubDeviceCode:output_type -> wego_server.v1.GetGithubDeviceCodeResponse - 33, // 46: wego_server.v1.Applications.GetGithubAuthStatus:output_type -> wego_server.v1.GetGithubAuthStatusResponse - 37, // 47: wego_server.v1.Applications.GetGitlabAuthURL:output_type -> wego_server.v1.GetGitlabAuthURLResponse - 39, // 48: wego_server.v1.Applications.AuthorizeGitlab:output_type -> wego_server.v1.AuthorizeGitlabResponse - 16, // 49: wego_server.v1.Applications.AddApplication:output_type -> wego_server.v1.AddApplicationResponse - 18, // 50: wego_server.v1.Applications.RemoveApplication:output_type -> wego_server.v1.RemoveApplicationResponse - 20, // 51: wego_server.v1.Applications.SyncApplication:output_type -> wego_server.v1.SyncApplicationResponse - 35, // 52: wego_server.v1.Applications.ParseRepoURL:output_type -> wego_server.v1.ParseRepoURLResponse - 41, // 53: wego_server.v1.Applications.ValidateProviderToken:output_type -> wego_server.v1.ValidateProviderTokenResponse - 39, // [39:54] is the sub-list for method output_type - 24, // [24:39] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 44, // 24: wego_server.v1.GetFeatureFlagsResponse.flags:type_name -> wego_server.v1.GetFeatureFlagsResponse.FlagsEntry + 9, // 25: wego_server.v1.Applications.Authenticate:input_type -> wego_server.v1.AuthenticateRequest + 11, // 26: wego_server.v1.Applications.ListApplications:input_type -> wego_server.v1.ListApplicationsRequest + 13, // 27: wego_server.v1.Applications.GetApplication:input_type -> wego_server.v1.GetApplicationRequest + 22, // 28: wego_server.v1.Applications.ListCommits:input_type -> wego_server.v1.ListCommitsRequest + 26, // 29: wego_server.v1.Applications.GetReconciledObjects:input_type -> wego_server.v1.GetReconciledObjectsReq + 28, // 30: wego_server.v1.Applications.GetChildObjects:input_type -> wego_server.v1.GetChildObjectsReq + 30, // 31: wego_server.v1.Applications.GetGithubDeviceCode:input_type -> wego_server.v1.GetGithubDeviceCodeRequest + 32, // 32: wego_server.v1.Applications.GetGithubAuthStatus:input_type -> wego_server.v1.GetGithubAuthStatusRequest + 36, // 33: wego_server.v1.Applications.GetGitlabAuthURL:input_type -> wego_server.v1.GetGitlabAuthURLRequest + 38, // 34: wego_server.v1.Applications.AuthorizeGitlab:input_type -> wego_server.v1.AuthorizeGitlabRequest + 15, // 35: wego_server.v1.Applications.AddApplication:input_type -> wego_server.v1.AddApplicationRequest + 17, // 36: wego_server.v1.Applications.RemoveApplication:input_type -> wego_server.v1.RemoveApplicationRequest + 19, // 37: wego_server.v1.Applications.SyncApplication:input_type -> wego_server.v1.SyncApplicationRequest + 34, // 38: wego_server.v1.Applications.ParseRepoURL:input_type -> wego_server.v1.ParseRepoURLRequest + 40, // 39: wego_server.v1.Applications.ValidateProviderToken:input_type -> wego_server.v1.ValidateProviderTokenRequest + 42, // 40: wego_server.v1.Applications.GetFeatureFlags:input_type -> wego_server.v1.GetFeatureFlagsRequest + 10, // 41: wego_server.v1.Applications.Authenticate:output_type -> wego_server.v1.AuthenticateResponse + 12, // 42: wego_server.v1.Applications.ListApplications:output_type -> wego_server.v1.ListApplicationsResponse + 14, // 43: wego_server.v1.Applications.GetApplication:output_type -> wego_server.v1.GetApplicationResponse + 23, // 44: wego_server.v1.Applications.ListCommits:output_type -> wego_server.v1.ListCommitsResponse + 27, // 45: wego_server.v1.Applications.GetReconciledObjects:output_type -> wego_server.v1.GetReconciledObjectsRes + 29, // 46: wego_server.v1.Applications.GetChildObjects:output_type -> wego_server.v1.GetChildObjectsRes + 31, // 47: wego_server.v1.Applications.GetGithubDeviceCode:output_type -> wego_server.v1.GetGithubDeviceCodeResponse + 33, // 48: wego_server.v1.Applications.GetGithubAuthStatus:output_type -> wego_server.v1.GetGithubAuthStatusResponse + 37, // 49: wego_server.v1.Applications.GetGitlabAuthURL:output_type -> wego_server.v1.GetGitlabAuthURLResponse + 39, // 50: wego_server.v1.Applications.AuthorizeGitlab:output_type -> wego_server.v1.AuthorizeGitlabResponse + 16, // 51: wego_server.v1.Applications.AddApplication:output_type -> wego_server.v1.AddApplicationResponse + 18, // 52: wego_server.v1.Applications.RemoveApplication:output_type -> wego_server.v1.RemoveApplicationResponse + 20, // 53: wego_server.v1.Applications.SyncApplication:output_type -> wego_server.v1.SyncApplicationResponse + 35, // 54: wego_server.v1.Applications.ParseRepoURL:output_type -> wego_server.v1.ParseRepoURLResponse + 41, // 55: wego_server.v1.Applications.ValidateProviderToken:output_type -> wego_server.v1.ValidateProviderTokenResponse + 43, // 56: wego_server.v1.Applications.GetFeatureFlags:output_type -> wego_server.v1.GetFeatureFlagsResponse + 41, // [41:57] is the sub-list for method output_type + 25, // [25:41] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_api_applications_applications_proto_init() } @@ -3669,6 +3779,30 @@ func file_api_applications_applications_proto_init() { return nil } } + file_api_applications_applications_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetFeatureFlagsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_applications_applications_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetFeatureFlagsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_api_applications_applications_proto_msgTypes[19].OneofWrappers = []interface{}{} type x struct{} @@ -3677,7 +3811,7 @@ func file_api_applications_applications_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_applications_applications_proto_rawDesc, NumEnums: 3, - NumMessages: 39, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/api/applications/applications.pb.gw.go b/pkg/api/applications/applications.pb.gw.go index 9553db38ebd..735e976a9d6 100644 --- a/pkg/api/applications/applications.pb.gw.go +++ b/pkg/api/applications/applications.pb.gw.go @@ -739,6 +739,24 @@ func local_request_Applications_ValidateProviderToken_0(ctx context.Context, mar } +func request_Applications_GetFeatureFlags_0(ctx context.Context, marshaler runtime.Marshaler, client ApplicationsClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetFeatureFlagsRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetFeatureFlags(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Applications_GetFeatureFlags_0(ctx context.Context, marshaler runtime.Marshaler, server ApplicationsServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetFeatureFlagsRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetFeatureFlags(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterApplicationsHandlerServer registers the http handlers for service Applications to "mux". // UnaryRPC :call ApplicationsServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1090,6 +1108,29 @@ func RegisterApplicationsHandlerServer(ctx context.Context, mux *runtime.ServeMu }) + mux.Handle("GET", pattern_Applications_GetFeatureFlags_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/wego_server.v1.Applications/GetFeatureFlags", runtime.WithHTTPPathPattern("/v1/featureflags")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Applications_GetFeatureFlags_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Applications_GetFeatureFlags_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1431,6 +1472,26 @@ func RegisterApplicationsHandlerClient(ctx context.Context, mux *runtime.ServeMu }) + mux.Handle("GET", pattern_Applications_GetFeatureFlags_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, "/wego_server.v1.Applications/GetFeatureFlags", runtime.WithHTTPPathPattern("/v1/featureflags")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Applications_GetFeatureFlags_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Applications_GetFeatureFlags_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1464,6 +1525,8 @@ var ( pattern_Applications_ParseRepoURL_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "applications", "parse_repo_url"}, "")) pattern_Applications_ValidateProviderToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "applications", "validate_token"}, "")) + + pattern_Applications_GetFeatureFlags_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "featureflags"}, "")) ) var ( @@ -1496,4 +1559,6 @@ var ( forward_Applications_ParseRepoURL_0 = runtime.ForwardResponseMessage forward_Applications_ValidateProviderToken_0 = runtime.ForwardResponseMessage + + forward_Applications_GetFeatureFlags_0 = runtime.ForwardResponseMessage ) diff --git a/pkg/api/applications/applications_grpc.pb.go b/pkg/api/applications/applications_grpc.pb.go index 5fcf15035e8..d97f25e9c48 100644 --- a/pkg/api/applications/applications_grpc.pb.go +++ b/pkg/api/applications/applications_grpc.pb.go @@ -76,6 +76,9 @@ type ApplicationsClient interface { // // ValidateProviderToken check to see if the git provider token is still valid ValidateProviderToken(ctx context.Context, in *ValidateProviderTokenRequest, opts ...grpc.CallOption) (*ValidateProviderTokenResponse, error) + // + // Config returns configuration information about the server + GetFeatureFlags(ctx context.Context, in *GetFeatureFlagsRequest, opts ...grpc.CallOption) (*GetFeatureFlagsResponse, error) } type applicationsClient struct { @@ -221,6 +224,15 @@ func (c *applicationsClient) ValidateProviderToken(ctx context.Context, in *Vali return out, nil } +func (c *applicationsClient) GetFeatureFlags(ctx context.Context, in *GetFeatureFlagsRequest, opts ...grpc.CallOption) (*GetFeatureFlagsResponse, error) { + out := new(GetFeatureFlagsResponse) + err := c.cc.Invoke(ctx, "/wego_server.v1.Applications/GetFeatureFlags", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ApplicationsServer is the server API for Applications service. // All implementations must embed UnimplementedApplicationsServer // for forward compatibility @@ -283,6 +295,9 @@ type ApplicationsServer interface { // // ValidateProviderToken check to see if the git provider token is still valid ValidateProviderToken(context.Context, *ValidateProviderTokenRequest) (*ValidateProviderTokenResponse, error) + // + // Config returns configuration information about the server + GetFeatureFlags(context.Context, *GetFeatureFlagsRequest) (*GetFeatureFlagsResponse, error) mustEmbedUnimplementedApplicationsServer() } @@ -335,6 +350,9 @@ func (UnimplementedApplicationsServer) ParseRepoURL(context.Context, *ParseRepoU func (UnimplementedApplicationsServer) ValidateProviderToken(context.Context, *ValidateProviderTokenRequest) (*ValidateProviderTokenResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ValidateProviderToken not implemented") } +func (UnimplementedApplicationsServer) GetFeatureFlags(context.Context, *GetFeatureFlagsRequest) (*GetFeatureFlagsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetFeatureFlags not implemented") +} func (UnimplementedApplicationsServer) mustEmbedUnimplementedApplicationsServer() {} // UnsafeApplicationsServer may be embedded to opt out of forward compatibility for this service. @@ -618,6 +636,24 @@ func _Applications_ValidateProviderToken_Handler(srv interface{}, ctx context.Co return interceptor(ctx, in, info, handler) } +func _Applications_GetFeatureFlags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetFeatureFlagsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApplicationsServer).GetFeatureFlags(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/wego_server.v1.Applications/GetFeatureFlags", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApplicationsServer).GetFeatureFlags(ctx, req.(*GetFeatureFlagsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Applications_ServiceDesc is the grpc.ServiceDesc for Applications service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -685,6 +721,10 @@ var Applications_ServiceDesc = grpc.ServiceDesc{ MethodName: "ValidateProviderToken", Handler: _Applications_ValidateProviderToken_Handler, }, + { + MethodName: "GetFeatureFlags", + Handler: _Applications_GetFeatureFlags_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/applications/applications.proto", diff --git a/pkg/server/auth/auth.go b/pkg/server/auth/auth.go index 7214f9b0dda..b32a1f990c3 100644 --- a/pkg/server/auth/auth.go +++ b/pkg/server/auth/auth.go @@ -4,9 +4,8 @@ import ( "context" "crypto/rand" "encoding/base64" - "encoding/json" - "fmt" "net/http" + "net/url" ) const ( @@ -30,8 +29,12 @@ const ( // This route is called by the OIDC Provider in order to pass back state after // the authentication flow completes. func RegisterAuthServer(mux *http.ServeMux, prefix string, srv *AuthServer) { - mux.Handle(prefix+"/callback", srv) + mux.Handle(prefix+"/callback", srv.Callback()) + mux.Handle(prefix+"/sign_in", srv.SignIn()) + mux.Handle(prefix+"/userinfo", srv.UserInfo()) mux.Handle(prefix+"/logout", srv.Logout()) + // mux.Handle(prefix+"/config", srv.GetAuthConfig()) + } type principalCtxKey struct{} @@ -60,13 +63,21 @@ func WithPrincipal(ctx context.Context, p *UserPrincipal) context.Context { // WithAPIAuth middleware adds auth validation to API handlers. // // Unauthorized requests will be denied with a 401 status code. -func WithAPIAuth(next http.Handler, srv *AuthServer) http.Handler { +func WithAPIAuth(next http.Handler, srv *AuthServer, publicRoutes []string) http.Handler { + adminAuth := NewJWTAdminCookiePrincipalGetter(srv.logger, srv.tokenSignerVerifier, IDTokenCookieName) cookieAuth := NewJWTCookiePrincipalGetter(srv.logger, srv.verifier(), IDTokenCookieName) headerAuth := NewJWTAuthorizationHeaderPrincipalGetter(srv.logger, srv.verifier()) - multi := MultiAuthPrincipal{cookieAuth, headerAuth} + multi := MultiAuthPrincipal{ + adminAuth, + cookieAuth, headerAuth} return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if isPublicRoute(r.URL, publicRoutes) { + next.ServeHTTP(rw, r) + return + } + principal, err := multi.Principal(r) if err != nil { srv.logger.Error(err, "failed to get principal") @@ -87,10 +98,13 @@ func WithAPIAuth(next http.Handler, srv *AuthServer) http.Handler { // It is meant to be used with routes that serve HTML content, // not API routes. func WithWebAuth(next http.Handler, srv *AuthServer) http.Handler { + adminAuth := NewJWTAdminCookiePrincipalGetter(srv.logger, srv.tokenSignerVerifier, IDTokenCookieName) cookieAuth := NewJWTCookiePrincipalGetter(srv.logger, srv.verifier(), IDTokenCookieName) headerAuth := NewJWTAuthorizationHeaderPrincipalGetter(srv.logger, srv.verifier()) - multi := MultiAuthPrincipal{cookieAuth, headerAuth} + multi := MultiAuthPrincipal{ + adminAuth, + cookieAuth, headerAuth} return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { principal, err := multi.Principal(r) @@ -99,7 +113,7 @@ func WithWebAuth(next http.Handler, srv *AuthServer) http.Handler { } if principal == nil || err != nil { - startAuthFlow(rw, r, srv) + srv.startAuthFlow(rw, r) return } @@ -107,35 +121,6 @@ func WithWebAuth(next http.Handler, srv *AuthServer) http.Handler { }) } -func startAuthFlow(rw http.ResponseWriter, r *http.Request, srv *AuthServer) { - nonce, err := generateNonce() - if err != nil { - http.Error(rw, fmt.Sprintf("failed to generate nonce: %v", err), http.StatusInternalServerError) - return - } - - b, err := json.Marshal(SessionState{ - Nonce: nonce, - ReturnURL: r.URL.String(), - }) - if err != nil { - http.Error(rw, fmt.Sprintf("failed to marshal state to JSON: %v", err), http.StatusInternalServerError) - return - } - - state := base64.StdEncoding.EncodeToString(b) - - var scopes []string - // "openid", "offline_access", "email" and "groups" scopes added by default - scopes = append(scopes, scopeProfile) - authCodeUrl := srv.oauth2Config(scopes).AuthCodeURL(state) - - // Issue state cookie - http.SetCookie(rw, srv.createCookie(StateCookieName, state)) - - http.Redirect(rw, r, authCodeUrl, http.StatusSeeOther) -} - func generateNonce() (string, error) { b := make([]byte, 32) @@ -146,3 +131,12 @@ func generateNonce() (string, error) { return base64.StdEncoding.EncodeToString(b), nil } + +func isPublicRoute(u *url.URL, publicRoutes []string) bool { + for _, pr := range publicRoutes { + if u.Path == pr { + return true + } + } + return false +} diff --git a/pkg/server/auth/auth_test.go b/pkg/server/auth/auth_test.go index f8616cc30b3..9533cc91249 100644 --- a/pkg/server/auth/auth_test.go +++ b/pkg/server/auth/auth_test.go @@ -14,6 +14,7 @@ import ( "github.com/go-logr/logr" "github.com/oauth2-proxy/mockoidc" "github.com/weaveworks/weave-gitops/pkg/server/auth" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestWithAPIAuthReturns401ForUnauthenticatedRequests(t *testing.T) { @@ -30,20 +31,23 @@ func TestWithAPIAuthReturns401ForUnauthenticatedRequests(t *testing.T) { fake := m.Config() mux := http.NewServeMux() + fakeKubernetesClient := ctrlclient.NewClientBuilder().Build() + + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } srv, err := auth.NewAuthServer(ctx, logr.Discard(), http.DefaultClient, auth.AuthConfig{ auth.OIDCConfig{ - IssuerURL: fake.Issuer, - ClientID: fake.ClientID, - ClientSecret: fake.ClientSecret, - RedirectURL: "", + IssuerURL: fake.Issuer, + ClientID: fake.ClientID, + ClientSecret: fake.ClientSecret, + RedirectURL: "", + TokenDuration: 20 * time.Minute, }, - auth.CookieConfig{ - CookieDuration: 20 * time.Minute, - IssueSecureCookies: false, - }, - }) + }, fakeKubernetesClient, tokenSignerVerifier) if err != nil { t.Error("failed to create auth config") } @@ -58,11 +62,20 @@ func TestWithAPIAuthReturns401ForUnauthenticatedRequests(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, s.URL, nil) - auth.WithAPIAuth(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), srv).ServeHTTP(res, req) + auth.WithAPIAuth(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), srv, nil).ServeHTTP(res, req) if res.Result().StatusCode != http.StatusUnauthorized { t.Errorf("expected status of %d but got %d", http.StatusUnauthorized, res.Result().StatusCode) } + + // Test out the publicRoutes + res = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, s.URL+"/v1/featureflags", nil) + auth.WithAPIAuth(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), srv, []string{"/v1/featureflags"}).ServeHTTP(res, req) + + if res.Result().StatusCode != http.StatusOK { + t.Errorf("expected status of %d but got %d", http.StatusUnauthorized, res.Result().StatusCode) + } } func TestWithWebAuthRedirectsToOIDCIssuerForUnauthenticatedRequests(t *testing.T) { @@ -79,20 +92,23 @@ func TestWithWebAuthRedirectsToOIDCIssuerForUnauthenticatedRequests(t *testing.T fake := m.Config() mux := http.NewServeMux() + fakeKubernetesClient := ctrlclient.NewClientBuilder().Build() + + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } srv, err := auth.NewAuthServer(ctx, logr.Discard(), http.DefaultClient, auth.AuthConfig{ auth.OIDCConfig{ - IssuerURL: fake.Issuer, - ClientID: fake.ClientID, - ClientSecret: fake.ClientSecret, - RedirectURL: "", - }, - auth.CookieConfig{ - CookieDuration: 20 * time.Minute, - IssueSecureCookies: false, + IssuerURL: fake.Issuer, + ClientID: fake.ClientID, + ClientSecret: fake.ClientSecret, + RedirectURL: "", + TokenDuration: 20 * time.Minute, }, - }) + }, fakeKubernetesClient, tokenSignerVerifier) if err != nil { t.Error("failed to create auth config") } diff --git a/pkg/server/auth/jwt.go b/pkg/server/auth/jwt.go index 1d4a01b5b07..02c78005511 100644 --- a/pkg/server/auth/jwt.go +++ b/pkg/server/auth/jwt.go @@ -102,6 +102,40 @@ func parseJWTToken(ctx context.Context, verifier *oidc.IDTokenVerifier, rawIDTok return &UserPrincipal{ID: claims.Email, Groups: claims.Groups}, nil } +type JWTAdminCookiePrincipalGetter struct { + log logr.Logger + verifier TokenSignerVerifier + cookieName string +} + +func NewJWTAdminCookiePrincipalGetter(log logr.Logger, verifier TokenSignerVerifier, cookieName string) PrincipalGetter { + return &JWTAdminCookiePrincipalGetter{ + log: log, + verifier: verifier, + cookieName: cookieName, + } +} + +func (pg *JWTAdminCookiePrincipalGetter) Principal(r *http.Request) (*UserPrincipal, error) { + pg.log.Info("attempt to read token from cookie") + + cookie, err := r.Cookie(pg.cookieName) + if err == http.ErrNoCookie { + return nil, nil + } + + return parseJWTAdminToken(pg.verifier, cookie.Value) +} + +func parseJWTAdminToken(verifier TokenSignerVerifier, rawIDToken string) (*UserPrincipal, error) { + claims, err := verifier.Verify(rawIDToken) + if err != nil { + return nil, fmt.Errorf("failed to verify JWT token: %w", err) + } + + return &UserPrincipal{ID: claims.Subject, Groups: []string{}}, nil +} + // MultiAuthPrincipal looks for a principal in an array of principal getters and // if it finds an error or a principal it returns, otherwise it returns (nil,nil). type MultiAuthPrincipal []PrincipalGetter diff --git a/pkg/server/auth/server.go b/pkg/server/auth/server.go index a0aedbcf793..28a484c6db5 100644 --- a/pkg/server/auth/server.go +++ b/pkg/server/auth/server.go @@ -2,6 +2,7 @@ package auth import ( "context" + "crypto/rand" "encoding/base64" "encoding/json" "fmt" @@ -10,65 +11,88 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/go-logr/logr" + "golang.org/x/crypto/bcrypt" "golang.org/x/oauth2" + corev1 "k8s.io/api/core/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + LoginOIDC string = "oidc" + LoginUsername string = "username" ) // OIDCConfig is used to configure an AuthServer to interact with // an OIDC issuer. type OIDCConfig struct { - IssuerURL string - ClientID string - ClientSecret string - RedirectURL string -} - -// CookieConfig is used to configure the cookies that get issued -// from the OIDC issuer once the OAuth2 process flow completes. -type CookieConfig struct { - CookieDuration time.Duration - IssueSecureCookies bool + IssuerURL string + ClientID string + ClientSecret string + RedirectURL string + TokenDuration time.Duration } // AuthConfig is used to configure an AuthServer. type AuthConfig struct { OIDCConfig - CookieConfig } // AuthServer interacts with an OIDC issuer to handle the OAuth2 process flow. type AuthServer struct { - logger logr.Logger - client *http.Client - provider *oidc.Provider - config AuthConfig + logger logr.Logger + client *http.Client + provider *oidc.Provider + config AuthConfig + kubernetesClient ctrlclient.Client + tokenSignerVerifier TokenSignerVerifier +} + +// LoginRequest represents the data submitted by client when the auth flow (non-OIDC) is used. +type LoginRequest struct { + Password string `json:"password"` +} + +// UserInfo represents the response returned from the user info handler. +type UserInfo struct { + Email string `json:"email"` + Groups []string `json:"groups"` } // NewAuthServer creates a new AuthServer object. -func NewAuthServer(ctx context.Context, logger logr.Logger, client *http.Client, config AuthConfig) (*AuthServer, error) { +func NewAuthServer(ctx context.Context, logger logr.Logger, client *http.Client, config AuthConfig, kubernetesClient ctrlclient.Client, tokenSignerVerifier TokenSignerVerifier) (*AuthServer, error) { provider, err := oidc.NewProvider(ctx, config.IssuerURL) if err != nil { return nil, fmt.Errorf("could not create provider: %w", err) } + hmacSecret := make([]byte, 64) + + _, err = rand.Read(hmacSecret) + if err != nil { + return nil, fmt.Errorf("could not generate random HMAC secret: %w", err) + } + return &AuthServer{ - logger: logger, - client: client, - provider: provider, - config: config, + logger: logger, + client: client, + provider: provider, + config: config, + kubernetesClient: kubernetesClient, + tokenSignerVerifier: tokenSignerVerifier, }, nil } // SetRedirectURL is used to set the redirect URL. This is meant to be used // in unit tests only. -func (c *AuthServer) SetRedirectURL(url string) { - c.config.RedirectURL = url +func (s *AuthServer) SetRedirectURL(url string) { + s.config.RedirectURL = url } -func (c *AuthServer) verifier() *oidc.IDTokenVerifier { - return c.provider.Verifier(&oidc.Config{ClientID: c.config.ClientID}) +func (s *AuthServer) verifier() *oidc.IDTokenVerifier { + return s.provider.Verifier(&oidc.Config{ClientID: s.config.ClientID}) } -func (c *AuthServer) oauth2Config(scopes []string) *oauth2.Config { +func (s *AuthServer) oauth2Config(scopes []string) *oauth2.Config { // Ensure "openid" scope is always present. if !contains(scopes, oidc.ScopeOpenID) { scopes = append(scopes, oidc.ScopeOpenID) @@ -90,108 +114,260 @@ func (c *AuthServer) oauth2Config(scopes []string) *oauth2.Config { } return &oauth2.Config{ - ClientID: c.config.ClientID, - ClientSecret: c.config.ClientSecret, - Endpoint: c.provider.Endpoint(), - RedirectURL: c.config.RedirectURL, + ClientID: s.config.ClientID, + ClientSecret: s.config.ClientSecret, + Endpoint: s.provider.Endpoint(), + RedirectURL: s.config.RedirectURL, Scopes: scopes, } } -func (c *AuthServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - var ( - token *oauth2.Token - state SessionState - ) +func (s *AuthServer) Callback() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + var ( + token *oauth2.Token + state SessionState + ) - ctx := oidc.ClientContext(r.Context(), c.client) + if r.Method != http.MethodGet { + rw.Header().Add("Allow", "GET") + rw.WriteHeader(http.StatusMethodNotAllowed) + + return + } + + ctx := oidc.ClientContext(r.Context(), s.client) - switch r.Method { - case http.MethodGet: // Authorization redirect callback from OAuth2 auth flow. - if errMsg := r.FormValue("error"); errMsg != "" { - c.logger.Info("authz redirect callback failed", "error", errMsg, "error_description", r.FormValue("error_description")) - http.Error(rw, "", http.StatusBadRequest) + if errorCode := r.FormValue("error"); errorCode != "" { + s.logger.Info("authz redirect callback failed", "error", errorCode, "error_description", r.FormValue("error_description")) + rw.WriteHeader(http.StatusBadRequest) return } code := r.FormValue("code") if code == "" { - c.logger.Info("code value was empty") - http.Error(rw, "", http.StatusBadRequest) + s.logger.Info("code value was empty") + rw.WriteHeader(http.StatusBadRequest) return } cookie, err := r.Cookie(StateCookieName) if err != nil { - c.logger.Error(err, "cookie was not found in the request", "cookie", StateCookieName) - http.Error(rw, "", http.StatusBadRequest) + s.logger.Error(err, "cookie was not found in the request", "cookie", StateCookieName) + rw.WriteHeader(http.StatusBadRequest) return } if state := r.FormValue("state"); state != cookie.Value { - c.logger.Info("cookie value does not match state value") - http.Error(rw, "", http.StatusBadRequest) + s.logger.Info("cookie value does not match state form value") + rw.WriteHeader(http.StatusBadRequest) return } b, err := base64.StdEncoding.DecodeString(cookie.Value) if err != nil { - c.logger.Error(err, "cannot base64 decode cookie", "cookie", StateCookieName, "cookie_value", cookie.Value) - http.Error(rw, "", http.StatusInternalServerError) + s.logger.Error(err, "cannot base64 decode cookie", "cookie", StateCookieName, "cookie_value", cookie.Value) + rw.WriteHeader(http.StatusBadRequest) return } if err := json.Unmarshal(b, &state); err != nil { - c.logger.Error(err, "failed to unmarshal state to JSON") - http.Error(rw, "", http.StatusInternalServerError) + s.logger.Error(err, "failed to unmarshal state to JSON", "state", string(b)) + rw.WriteHeader(http.StatusBadRequest) return } - token, err = c.oauth2Config(nil).Exchange(ctx, code) + token, err = s.oauth2Config(nil).Exchange(ctx, code) if err != nil { - c.logger.Error(err, "failed to exchange auth code for token") - http.Error(rw, "", http.StatusInternalServerError) + s.logger.Error(err, "failed to exchange auth code for token", "code", code) + rw.WriteHeader(http.StatusInternalServerError) return } - default: - http.Error(rw, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest) - return + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + http.Error(rw, "no id_token in token response", http.StatusInternalServerError) + return + } + + _, err = s.verifier().Verify(r.Context(), rawIDToken) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to verify ID token: %v", err), http.StatusInternalServerError) + return + } + + // Issue ID token cookie + http.SetCookie(rw, s.createCookie(IDTokenCookieName, rawIDToken)) + + // Some OIDC providers may not include a refresh token + if token.RefreshToken != "" { + // Issue refresh token cookie + http.SetCookie(rw, s.createCookie(RefreshTokenCookieName, token.RefreshToken)) + } + + // Clear state cookie + http.SetCookie(rw, s.clearCookie(StateCookieName)) + + http.Redirect(rw, r, state.ReturnURL, http.StatusSeeOther) + } +} + +func (s *AuthServer) SignIn() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + rw.Header().Add("Allow", "POST") + rw.WriteHeader(http.StatusMethodNotAllowed) + + return + } + + var loginRequest LoginRequest + + err := json.NewDecoder(r.Body).Decode(&loginRequest) + if err != nil { + s.logger.Error(err, "Failed to decode from JSON") + http.Error(rw, "Failed to read request body.", http.StatusBadRequest) + + return + } + + var hashedSecret corev1.Secret + + if err := s.kubernetesClient.Get(r.Context(), ctrlclient.ObjectKey{ + Namespace: "wego-system", + Name: "admin-password-hash", + }, &hashedSecret); err != nil { + s.logger.Error(err, "Failed to query for the secret") + http.Error(rw, "Please ensure that a password has been set.", http.StatusBadRequest) + + return + } + + if err := bcrypt.CompareHashAndPassword(hashedSecret.Data["password"], []byte(loginRequest.Password)); err != nil { + s.logger.Error(err, "Failed to compare hash with password") + rw.WriteHeader(http.StatusUnauthorized) + + return + } + + signed, err := s.tokenSignerVerifier.Sign() + if err != nil { + s.logger.Error(err, "Failed to create and sign token") + rw.WriteHeader(http.StatusInternalServerError) + + return + } + + http.SetCookie(rw, s.createCookie(IDTokenCookieName, signed)) + rw.WriteHeader(http.StatusOK) + } +} + +// UserInfo inspects the cookie and attempts to verify it as an admin token. If successful, +// it returns a UserInfo object with the email set to the admin token subject. Otherwise it +// uses the token to query the OIDC provider's user info endpoint and return a UserInfo object +// back or a 401 status in any other case. +func (s *AuthServer) UserInfo() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + rw.Header().Add("Allow", "GET") + rw.WriteHeader(http.StatusMethodNotAllowed) + + return + } + + c, err := r.Cookie(IDTokenCookieName) + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + + return + } + + claims, err := s.tokenSignerVerifier.Verify(c.Value) + if err == nil { + ui := UserInfo{ + Email: claims.Subject, + } + toJson(rw, ui, s.logger) + + return + } + + info, err := s.provider.UserInfo(r.Context(), oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: c.Value, + })) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to query user info endpoint: %v", err), http.StatusUnauthorized) + return + } + + ui := UserInfo{ + Email: info.Email, + } + + toJson(rw, ui, s.logger) } +} + +// func (s *AuthServer) GetAuthConfig() (string) { +// authFlag := os.Getenv("WEAVE_GITOPS_AUTH_ENABLED") +// return authFlag +// } - rawIDToken, ok := token.Extra("id_token").(string) - if !ok { - http.Error(rw, "no id_token in token response", http.StatusInternalServerError) +func toJson(rw http.ResponseWriter, ui UserInfo, logger logr.Logger) { + b, err := json.Marshal(ui) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to marshal to JSON: %v", err), http.StatusInternalServerError) return } - _, err := c.verifier().Verify(r.Context(), rawIDToken) + _, err = rw.Write(b) if err != nil { - http.Error(rw, fmt.Sprintf("failed to verify ID token: %v", err), http.StatusInternalServerError) + logger.Error(err, "Failing to write response") + } +} + +func (c *AuthServer) startAuthFlow(rw http.ResponseWriter, r *http.Request) { + nonce, err := generateNonce() + if err != nil { + http.Error(rw, fmt.Sprintf("failed to generate nonce: %v", err), http.StatusInternalServerError) return } - // Issue ID token cookie - http.SetCookie(rw, c.createCookie(IDTokenCookieName, rawIDToken)) + returnUrl := r.URL.Query().Get("return_url") + + if returnUrl == "" { + returnUrl = r.URL.String() + } - // Some OIDC providers may not include a refresh token - if token.RefreshToken != "" { - // Issue refresh token cookie - http.SetCookie(rw, c.createCookie(RefreshTokenCookieName, token.RefreshToken)) + b, err := json.Marshal(SessionState{ + Nonce: nonce, + ReturnURL: returnUrl, + }) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to marshal state to JSON: %v", err), http.StatusInternalServerError) + return } - // Clear state cookie - http.SetCookie(rw, c.clearCookie(StateCookieName)) + state := base64.StdEncoding.EncodeToString(b) + + var scopes []string + // "openid", "offline_access", "email" and "groups" scopes added by default + scopes = append(scopes, scopeProfile) + authCodeUrl := c.oauth2Config(scopes).AuthCodeURL(state) - http.Redirect(rw, r, state.ReturnURL, http.StatusSeeOther) + // Issue state cookie + http.SetCookie(rw, c.createCookie(StateCookieName, state)) + + http.Redirect(rw, r, authCodeUrl, http.StatusSeeOther) } func (s *AuthServer) Logout() http.HandlerFunc { @@ -213,12 +389,9 @@ func (c *AuthServer) createCookie(name, value string) *http.Cookie { Name: name, Value: value, Path: "/", - Expires: time.Now().UTC().Add(c.config.CookieDuration), + Expires: time.Now().UTC().Add(c.config.TokenDuration), HttpOnly: true, - } - - if c.config.IssueSecureCookies { - cookie.Secure = true + Secure: true, } return cookie diff --git a/pkg/server/auth/server_test.go b/pkg/server/auth/server_test.go index 80cd6c5abc9..f5f34828994 100644 --- a/pkg/server/auth/server_test.go +++ b/pkg/server/auth/server_test.go @@ -1,45 +1,366 @@ package auth_test import ( + "bytes" "context" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" "net/http" "net/http/httptest" + "net/url" "testing" + "time" "github.com/go-logr/logr" "github.com/oauth2-proxy/mockoidc" "github.com/stretchr/testify/assert" "github.com/weaveworks/weave-gitops/pkg/server/auth" + "golang.org/x/crypto/bcrypt" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + ctrlclientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func TestLogoutSuccess(t *testing.T) { - m, err := mockoidc.Run() +// A custom client that doesn't automatically follow redirects +var httpClient = &http.Client{ + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, +} + +func TestCallbackAllowsGet(t *testing.T) { + methods := []string{ + http.MethodPost, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodHead, + http.MethodOptions, + } + + s, _ := makeAuthServer(t, nil, nil) + + for _, m := range methods { + req := httptest.NewRequest(m, "https://example.com/callback", nil) + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Errorf("expected status to be 405 but got %v instead", resp.StatusCode) + } + + if resp.Header.Get("Allow") != "GET" { + t.Errorf("expected `Allow` header to be set to `GET` but was not") + } + } +} + +func TestCallbackErrorFromOIDC(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + req := httptest.NewRequest(http.MethodGet, "https://example.com/callback?error=invalid_request&error_description=Unsupported%20response_type%20value", nil) + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestCallbackCodeIsEmpty(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + req := httptest.NewRequest(http.MethodGet, "https://example.com/callback", nil) + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestCallbackStateCookieNotSet(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + req := httptest.NewRequest(http.MethodGet, "https://example.com/callback?code=123", nil) + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestCallbackStateCookieNotValid(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + req := httptest.NewRequest(http.MethodGet, "https://example.com/callback?code=123&state=some_state", nil) + req.AddCookie(&http.Cookie{ + Name: auth.StateCookieName, + Value: "some_different_state", + }) + + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestCallbackStateCookieNotBase64Encoded(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + req := httptest.NewRequest(http.MethodGet, "https://example.com/callback?code=123&state=some_state", nil) + req.AddCookie(&http.Cookie{ + Name: auth.StateCookieName, + Value: "some_state", + }) + + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestCallbackStateCookieNotJSONPayload(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + encState := base64.StdEncoding.EncodeToString([]byte("some_state")) + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://example.com/callback?code=123&state=%s", encState), nil) + req.AddCookie(&http.Cookie{ + Name: auth.StateCookieName, + Value: encState, + }) + + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestCallbackCodeExchangeError(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + state, _ := json.Marshal(auth.SessionState{ + Nonce: "abcde", + ReturnURL: "https://example.com", + }) + encState := base64.StdEncoding.EncodeToString(state) + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://example.com/callback?code=123&state=%s", encState), nil) + req.AddCookie(&http.Cookie{ + Name: auth.StateCookieName, + Value: encState, + }) + + w := httptest.NewRecorder() + s.Callback().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusInternalServerError { + t.Errorf("expected status to be 500 but got %v instead", resp.StatusCode) + } +} + +func TestSignInAllowsPOST(t *testing.T) { + methods := []string{ + http.MethodGet, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodHead, + http.MethodOptions, + } + + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) if err != nil { - t.Errorf("failed to create mock OIDC server: %v", err) + t.Errorf("failed to create HMAC signer: %v", err) } - s, err := auth.NewAuthServer(context.Background(), logr.Discard(), http.DefaultClient, auth.AuthConfig{ - OIDCConfig: auth.OIDCConfig{ - IssuerURL: m.Config().Issuer, + s, _ := makeAuthServer(t, ctrlclientfake.NewClientBuilder().Build(), tokenSignerVerifier) + + for _, m := range methods { + req := httptest.NewRequest(m, "https://example.com/signin", nil) + w := httptest.NewRecorder() + s.SignIn().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Errorf("expected status to be 405 but got %v instead", resp.StatusCode) + } + + if resp.Header.Get("Allow") != "POST" { + t.Errorf("expected `Allow` header to be set to `POST` but was not") + } + } +} + +func TestSignInNoPayloadReturnsBadRequest(t *testing.T) { + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } + + s, _ := makeAuthServer(t, ctrlclientfake.NewClientBuilder().Build(), tokenSignerVerifier) + + req := httptest.NewRequest(http.MethodPost, "https://example.com/signin", nil) + w := httptest.NewRecorder() + s.SignIn().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("expected to read response body successfully but got error instead: %v", err) + } + + respBody := string(b) + if respBody != "Failed to read request body.\n" { + t.Errorf("expected different response body but got instead: %q", respBody) + } +} + +func TestSignInNoSecret(t *testing.T) { + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } + + s, _ := makeAuthServer(t, ctrlclientfake.NewClientBuilder().Build(), tokenSignerVerifier) + + j, err := json.Marshal(auth.LoginRequest{}) + if err != nil { + t.Errorf("failed to marshal to JSON: %v", err) + } + + reader := bytes.NewReader(j) + + req := httptest.NewRequest(http.MethodPost, "https://example.com/signin", reader) + w := httptest.NewRecorder() + s.SignIn().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestSignInWrongPasswordReturnsUnauthorized(t *testing.T) { + password := "my-secret-password" + + hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + + if err != nil { + t.Errorf("failed to generate a hash from password: %v", err) + } + + hashedSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-password-hash", + Namespace: "wego-system", }, - CookieConfig: auth.CookieConfig{ - CookieDuration: 5, + Data: map[string][]byte{ + "password": hashed, }, - }) + } - assert.Nil(t, err) + fakeKubernetesClient := ctrlclientfake.NewClientBuilder().WithObjects(hashedSecret).Build() + + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } + + s, _ := makeAuthServer(t, fakeKubernetesClient, tokenSignerVerifier) + + login := auth.LoginRequest{ + Password: "wrong", + } + j, err := json.Marshal(login) + if err != nil { + t.Errorf("failed to marshal to JSON: %v", err) + } + + reader := bytes.NewReader(j) + + req := httptest.NewRequest(http.MethodPost, "https://example.com/signin", reader) w := httptest.NewRecorder() + s.SignIn().ServeHTTP(w, req) - req := httptest.NewRequest(http.MethodPost, "https://example.com/logout", nil) - s.Logout().ServeHTTP(w, req) + resp := w.Result() + if resp.StatusCode != http.StatusUnauthorized { + t.Errorf("expected status to be 401 but got %v instead", resp.StatusCode) + } +} + +func TestSingInCorrectPassword(t *testing.T) { + password := "my-secret-password" + + hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + t.Errorf("failed to generate a hash from password: %v", err) + } + + hashedSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-password-hash", + Namespace: "wego-system", + }, + Data: map[string][]byte{ + "password": hashed, + }, + } + + fakeKubernetesClient := ctrlclientfake.NewClientBuilder().WithObjects(hashedSecret).Build() + + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } + + s, _ := makeAuthServer(t, fakeKubernetesClient, tokenSignerVerifier) + + login := auth.LoginRequest{ + Password: password, + } + + j, err := json.Marshal(login) + if err != nil { + t.Errorf("failed to marshal to JSON: %v", err) + } + + reader := bytes.NewReader(j) + + req := httptest.NewRequest(http.MethodPost, "https://example.com/signin", reader) + w := httptest.NewRecorder() + s.SignIn().ServeHTTP(w, req) resp := w.Result() if resp.StatusCode != http.StatusOK { t.Errorf("expected status to be 200 but got %v instead", resp.StatusCode) } - cookie := &http.Cookie{} + var cookie *http.Cookie for _, c := range resp.Cookies() { if c.Name == auth.IDTokenCookieName { @@ -48,25 +369,247 @@ func TestLogoutSuccess(t *testing.T) { } } - assert.Equal(t, cookie.Value, "") + if cookie == nil { + t.Errorf("expected to find cookie %q but did not", auth.IDTokenCookieName) + // Make linter happy about possible nil deref below + return + } + + if _, err := tokenSignerVerifier.Verify(cookie.Value); err != nil { + t.Errorf("expected to verify the issued token but got an error instead: %v", err) + } } -func TestLogoutWithWrongMethod(t *testing.T) { +func TestUserInfoAllowsGET(t *testing.T) { + methods := []string{ + http.MethodPost, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodHead, + http.MethodOptions, + } + + s, _ := makeAuthServer(t, nil, nil) + + for _, m := range methods { + req := httptest.NewRequest(m, "https://example.com/userinfo", nil) + w := httptest.NewRecorder() + s.UserInfo().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Errorf("expected status to be 405 but got %v instead", resp.StatusCode) + } + + if resp.Header.Get("Allow") != "GET" { + t.Errorf("expected `Allow` header to be set to `GET` but was not") + } + } +} + +func TestUserInfoIDTokenCookieNotSet(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + req := httptest.NewRequest(http.MethodGet, "https://example.com/userinfo", nil) + w := httptest.NewRecorder() + s.UserInfo().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("expected status to be 400 but got %v instead", resp.StatusCode) + } +} + +func TestUserInfoAdminFlow(t *testing.T) { + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } + + s, _ := makeAuthServer(t, nil, tokenSignerVerifier) + + signed, err := tokenSignerVerifier.Sign() + if err != nil { + t.Errorf("failed to sign token: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "https://example.com/userinfo", nil) + req.AddCookie(&http.Cookie{ + Name: auth.IDTokenCookieName, + Value: signed, + }) + + w := httptest.NewRecorder() + s.UserInfo().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status to be 200 but got %v instead", resp.StatusCode) + } + + var info auth.UserInfo + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { + t.Errorf("expected to decode response body to UserInfo object but got an error: %v", err) + } + + if info.Email != "admin" { + t.Errorf("expected admin flow to return `admin` as the email but got %q instead", info.Email) + } +} + +func TestUserInfoOIDCFlow(t *testing.T) { + const ( + state = "abcdef" + nonce = "ghijkl" + code = "mnopqr" + ) + + tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute) + if err != nil { + t.Errorf("failed to create HMAC signer: %v", err) + } + + s, m := makeAuthServer(t, nil, tokenSignerVerifier) + + authorizeQuery := url.Values{} + authorizeQuery.Set("client_id", m.Config().ClientID) + authorizeQuery.Set("scope", "openid email profile groups") + authorizeQuery.Set("response_type", "code") + authorizeQuery.Set("redirect_uri", "https://example.com/oauth2/callback") + authorizeQuery.Set("state", state) + authorizeQuery.Set("nonce", nonce) + + authorizeURL, err := url.Parse(m.AuthorizationEndpoint()) + if err != nil { + t.Errorf("failed to parse authorization endpoint: %v", err) + } + + authorizeURL.RawQuery = authorizeQuery.Encode() + + authorizeReq, err := http.NewRequest(http.MethodGet, authorizeURL.String(), nil) + if err != nil { + t.Errorf("failed to call the authorization endpoint: %v", err) + } + + m.QueueCode(code) + + authorizeResp, err := httpClient.Do(authorizeReq) + assert.NoError(t, err) + assert.Equal(t, http.StatusFound, authorizeResp.StatusCode) + + appRedirect, err := url.Parse(authorizeResp.Header.Get("Location")) + assert.NoError(t, err) + assert.Equal(t, code, appRedirect.Query().Get("code")) + assert.Equal(t, state, appRedirect.Query().Get("state")) + + tokenForm := url.Values{} + tokenForm.Set("client_id", m.Config().ClientID) + tokenForm.Set("client_secret", m.Config().ClientSecret) + tokenForm.Set("grant_type", "authorization_code") + tokenForm.Set("code", code) + + tokenReq, err := http.NewRequest( + http.MethodPost, m.TokenEndpoint(), bytes.NewBufferString(tokenForm.Encode())) + assert.NoError(t, err) + tokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + tokenResp, err := httpClient.Do(tokenReq) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, tokenResp.StatusCode) + + defer tokenResp.Body.Close() + body, err := ioutil.ReadAll(tokenResp.Body) + assert.NoError(t, err) + + tokens := make(map[string]interface{}) + err = json.Unmarshal(body, &tokens) + assert.NoError(t, err) + + _, err = m.Keypair.VerifyJWT(tokens["access_token"].(string)) + assert.NoError(t, err) + _, err = m.Keypair.VerifyJWT(tokens["refresh_token"].(string)) + assert.NoError(t, err) + idToken, err := m.Keypair.VerifyJWT(tokens["id_token"].(string)) + assert.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "https://example.com/userinfo", nil) + req.AddCookie(&http.Cookie{ + Name: auth.IDTokenCookieName, + Value: idToken.Raw, + }) + + w := httptest.NewRecorder() + s.UserInfo().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status to be 200 but got %v instead", resp.StatusCode) + } + + var info auth.UserInfo + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { + t.Errorf("expected to decode response body to UserInfo object but got an error: %v", err) + } + + if info.Email != "jane.doe@example.com" { + t.Errorf("expected admin flow to return `jane.doe@example.com` as the email but got %q instead", info.Email) + } +} + +func makeAuthServer(t *testing.T, client ctrlclient.Client, tokenSignerVerifier auth.TokenSignerVerifier) (*auth.AuthServer, *mockoidc.MockOIDC) { + t.Helper() + m, err := mockoidc.Run() if err != nil { t.Errorf("failed to create mock OIDC server: %v", err) } + t.Cleanup(func() { + _ = m.Shutdown() + }) + s, err := auth.NewAuthServer(context.Background(), logr.Discard(), http.DefaultClient, auth.AuthConfig{ OIDCConfig: auth.OIDCConfig{ - IssuerURL: m.Config().Issuer, + ClientID: m.Config().ClientID, + ClientSecret: m.Config().ClientSecret, + IssuerURL: m.Config().Issuer, }, - CookieConfig: auth.CookieConfig{ - CookieDuration: 5, - }, - }) + }, client, tokenSignerVerifier) + if err != nil { + t.Errorf("failed to create a new AuthServer instance: %v", err) + } + + return s, m +} + +func TestLogoutSuccess(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) + + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, "https://example.com/logout", nil) + s.Logout().ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status to be 200 but got %v instead", resp.StatusCode) + } - assert.Nil(t, err) + cookie := &http.Cookie{} + + for _, c := range resp.Cookies() { + if c.Name == auth.IDTokenCookieName { + cookie = c + break + } + } + + assert.Equal(t, cookie.Value, "") +} + +func TestLogoutWithWrongMethod(t *testing.T) { + s, _ := makeAuthServer(t, nil, nil) w := httptest.NewRecorder() diff --git a/pkg/server/auth/token_signer_verifier.go b/pkg/server/auth/token_signer_verifier.go new file mode 100644 index 00000000000..7eab39246b1 --- /dev/null +++ b/pkg/server/auth/token_signer_verifier.go @@ -0,0 +1,81 @@ +package auth + +import ( + "crypto/rand" + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +type AdminClaims struct { + jwt.StandardClaims +} + +type TokenSigner interface { + Sign() (string, error) +} + +type TokenVerifier interface { + Verify(token string) (*AdminClaims, error) +} + +type TokenSignerVerifier interface { + TokenSigner + TokenVerifier +} + +type HMACTokenSignerVerifier struct { + expireAfter time.Duration + hmacSecret []byte +} + +func NewHMACTokenSignerVerifier(expireAfter time.Duration) (TokenSignerVerifier, error) { + hmacSecret := make([]byte, 64) + + _, err := rand.Read(hmacSecret) + if err != nil { + return nil, fmt.Errorf("could not generate random HMAC secret: %w", err) + } + + return &HMACTokenSignerVerifier{ + expireAfter: expireAfter, + hmacSecret: hmacSecret, + }, nil +} + +func (sv *HMACTokenSignerVerifier) Sign() (string, error) { + claims := AdminClaims{ + StandardClaims: jwt.StandardClaims{ + IssuedAt: time.Now().UTC().Unix(), + ExpiresAt: time.Now().Add(sv.expireAfter).UTC().Unix(), + NotBefore: time.Now().UTC().Unix(), + Subject: "admin", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString(sv.hmacSecret) +} + +func (sv *HMACTokenSignerVerifier) Verify(tokenString string) (*AdminClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &AdminClaims{}, + func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return sv.hmacSecret, nil + }) + if err != nil { + return nil, fmt.Errorf("failed to verify token: %w", err) + } + + if claims, ok := token.Claims.(*AdminClaims); ok && token.Valid { + return claims, nil + } else { + return nil, errors.New("invalid token") + } +} diff --git a/pkg/server/handler.go b/pkg/server/handler.go index 7c12af89404..221d4903f99 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -35,7 +35,7 @@ func NewHandlers(ctx context.Context, cfg *Config) (http.Handler, error) { httpHandler = middleware.WithProviderToken(cfg.AppConfig.JwtClient, httpHandler, cfg.AppConfig.Logger) if AuthEnabled() { - httpHandler = auth.WithAPIAuth(httpHandler, cfg.AuthServer) + httpHandler = auth.WithAPIAuth(httpHandler, cfg.AuthServer, []string{"/v1/featureflags"}) } appsSrv := NewApplicationsServer(cfg.AppConfig, cfg.AppOptions...) diff --git a/pkg/server/server.go b/pkg/server/server.go index 3d5a07917ca..ca22a695acd 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -718,6 +718,14 @@ func (s *applicationServer) ValidateProviderToken(ctx context.Context, msg *pb.V }, nil } +func (s *applicationServer) GetFeatureFlags(ctx context.Context, msg *pb.GetFeatureFlagsRequest) (*pb.GetFeatureFlagsResponse, error) { + return &pb.GetFeatureFlagsResponse{ + Flags: map[string]string{ + "WEAVE_GITOPS_AUTH_ENABLED": os.Getenv("WEAVE_GITOPS_AUTH_ENABLED"), + }, + }, nil +} + func mapHelmReleaseSpecToResponse(helm *helmv2.HelmRelease) *pb.HelmRelease { if helm == nil { return nil diff --git a/ui/App.tsx b/ui/App.tsx index a2cd905e7ce..8913f60e87f 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -13,6 +13,7 @@ import { ThemeProvider } from "styled-components"; import ErrorBoundary from "./components/ErrorBoundary"; import Layout from "./components/Layout"; import AppContextProvider from "./contexts/AppContext"; +import AuthContextProvider, { AuthCheck } from "./contexts/AuthContext"; import { Applications as appsClient, GitProvider, @@ -26,71 +27,104 @@ import ApplicationRemove from "./pages/ApplicationRemove"; import Applications from "./pages/Applications"; import Error from "./pages/Error"; import OAuthCallback from "./pages/OAuthCallback"; +import SignIn from "./pages/SignIn"; export default function App() { + const [authFlag, setAuthFlag] = React.useState(null); + + const App = ( + + + + + + { + const params = qs.parse(location.search); + + return ; + }} + /> + + { + const params = qs.parse(location.search); + + return ( + + ); + }} + /> + { + const params = qs.parse(location.search); + + return ; + }} + /> + + + + + + + + ); + + const getAuthFlag = React.useCallback(() => { + fetch("./config") + .then((response) => { + return response.json(); + }) + .then((data) => setAuthFlag(data.flag)) + .catch((err) => console.log(err)); + }, []); + + React.useEffect(() => { + getAuthFlag(); + }, [getAuthFlag]); + return ( - - - - - - { - const params = qs.parse(location.search); - - return ; - }} - /> - - { - const params = qs.parse(location.search); - - return ( - - ); - }} - /> - { - const params = qs.parse(location.search); - - return ; - }} - /> - - - - - - - + {!authFlag ? ( + + + {/* does not use the base page so pull it up here */} + + + {/* Check we've got a logged in user otherwise redirect back to signin */} + {App} + + + + ) : ( + App + )} diff --git a/ui/contexts/AuthContext.tsx b/ui/contexts/AuthContext.tsx new file mode 100644 index 00000000000..f19f37403e5 --- /dev/null +++ b/ui/contexts/AuthContext.tsx @@ -0,0 +1,100 @@ +import * as React from "react"; +import { useHistory, Redirect } from "react-router-dom"; +import Layout from "../components/Layout"; +import LoadingPage from "../components/LoadingPage"; + +const USER_INFO = "/oauth2/userinfo"; +const SIGN_IN = "/oauth2/sign_in"; +const AUTH_PATH_SIGNIN = "/sign_in"; + +export const AuthCheck = ({ children }) => { + const { loading, userInfo } = React.useContext(Auth); + + // Wait until userInfo is loaded before showing signin or app content + if (loading) { + return ( + + + + ); + } + + // Signed in! Show app + if (userInfo?.email) { + return children; + } + + // User appears not be logged in, off to signin + return ; +}; + +export type AuthContext = { + signIn: (data: any) => void; + userInfo: { + email: string; + groups: string[]; + }; + error: { status: number; statusText: string }; + loading: boolean; +}; + +export const Auth = React.createContext(null); + +export default function AuthContextProvider({ children }) { + const [userInfo, setUserInfo] = React.useState<{ + email: string; + groups: string[]; + }>(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + const history = useHistory(); + + const signIn = React.useCallback((data) => { + setLoading(true); + fetch(SIGN_IN, { + method: "POST", + body: JSON.stringify(data), + }) + .then((response) => { + if (response.status !== 200) { + setError(response); + return; + } + getUserInfo().then(() => history.push("/")); + }) + .finally(() => setLoading(false)); + }, []); + + const getUserInfo = React.useCallback(() => { + setLoading(true); + return fetch(USER_INFO) + .then((response) => { + if (response.status === 400 || response.status === 401) { + setUserInfo(null); + return; + } + return response.json(); + }) + .then((data) => setUserInfo({ email: data?.email, groups: [] })) + .catch((err) => console.log(err)) + .finally(() => setLoading(false)); + }, []); + + React.useEffect(() => { + getUserInfo(); + return history.listen(getUserInfo); + }, [getUserInfo, history]); + + return ( + + {children} + + ); +} diff --git a/ui/images/SignInBackground.svg b/ui/images/SignInBackground.svg new file mode 100644 index 00000000000..d59ce155334 --- /dev/null +++ b/ui/images/SignInBackground.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/images/SignInWheel.svg b/ui/images/SignInWheel.svg new file mode 100644 index 00000000000..d067b306ae2 --- /dev/null +++ b/ui/images/SignInWheel.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/images/WeaveLogo.svg b/ui/images/WeaveLogo.svg new file mode 100644 index 00000000000..87d15f8c307 --- /dev/null +++ b/ui/images/WeaveLogo.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/lib/api/applications/applications.pb.ts b/ui/lib/api/applications/applications.pb.ts index 793b8dbac75..4a649f74db0 100644 --- a/ui/lib/api/applications/applications.pb.ts +++ b/ui/lib/api/applications/applications.pb.ts @@ -264,6 +264,13 @@ export type ValidateProviderTokenResponse = { valid?: boolean } +export type GetFeatureFlagsRequest = { +} + +export type GetFeatureFlagsResponse = { + flags?: {[key: string]: string} +} + export class Applications { static Authenticate(req: AuthenticateRequest, initReq?: fm.InitReq): Promise { return fm.fetchReq(`/v1/authenticate/${req["providerName"]}`, {...initReq, method: "POST", body: JSON.stringify(req)}) @@ -310,4 +317,7 @@ export class Applications { static ValidateProviderToken(req: ValidateProviderTokenRequest, initReq?: fm.InitReq): Promise { return fm.fetchReq(`/v1/applications/validate_token`, {...initReq, method: "POST", body: JSON.stringify(req)}) } + static GetFeatureFlags(req: GetFeatureFlagsRequest, initReq?: fm.InitReq): Promise { + return fm.fetchReq(`/v1/featureflags?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"}) + } } \ No newline at end of file diff --git a/ui/pages/SignIn.tsx b/ui/pages/SignIn.tsx new file mode 100644 index 00000000000..8a5269ea227 --- /dev/null +++ b/ui/pages/SignIn.tsx @@ -0,0 +1,154 @@ +import * as React from "react"; +import styled from "styled-components"; +import { Divider, Input, InputAdornment, IconButton } from "@material-ui/core"; +import { Visibility, VisibilityOff } from "@material-ui/icons"; +import Alert from "../components/Alert"; +import Button from "../components/Button"; +import Flex from "../components/Flex"; +import LoadingPage from "../components/LoadingPage"; +import { Auth } from "../contexts/AuthContext"; +import { theme } from "../lib/theme"; +// @ts-ignore +import SignInWheel from "./../images/SignInWheel.svg"; +// @ts-ignore +import SignInBackground from "./../images/SignInBackground.svg"; +// @ts-ignore +import WeaveLogo from "./../images/WeaveLogo.svg"; + +export const SignInPageWrapper = styled(Flex)` + background: url(${SignInBackground}); + height: 100%; + width: 100%; +`; + +export const FormWrapper = styled(Flex)` + background-color: ${(props) => props.theme.colors.white}; + width: 500px; + padding-top: ${(props) => props.theme.spacing.medium}; + align-content: space-between; + .MuiButton-label { + width: 250px; + } + .MuiInputBase-root { + width: 275px; + } +`; + +const Logo = styled(Flex)` + margin-bottom: ${(props) => props.theme.spacing.medium}; +`; + +const Action = styled(Flex)` + flex-wrap: wrap; +`; + +const Footer = styled(Flex)` + & img { + width: 500px; + } +`; + +const AlertWrapper = styled(Alert)` + .MuiAlert-root { + width: 470px; + margin-bottom: ${(props) => props.theme.spacing.small}; + } +`; + +function SignIn() { + const formRef = React.useRef(); + const { signIn, error, loading } = React.useContext(Auth); + const [password, setPassword] = React.useState(""); + const [showPassword, setShowPassword] = React.useState(false); + + const handleOIDCSubmit = () => { + const CURRENT_URL = window.origin; + return (window.location.href = `/oauth2?return_url=${encodeURIComponent( + CURRENT_URL + )}`); + }; + + const handleUserPassSubmit = () => signIn({ password }); + + return ( + + {error && ( + + )} + +
+ + + + + + + +
{ + e.preventDefault(); + handleUserPassSubmit(); + }} + > + + setPassword(e.currentTarget.value)} + required + id="password" + placeholder="Password" + type={showPassword ? "text" : "password"} + value={password} + endAdornment={ + + setShowPassword(!showPassword)} + > + {showPassword ? : } + + + } + /> + + + {!loading ? ( + + ) : ( +
+ +
+ )} +
+
+
+ +
+
+ ); +} + +export default styled(SignIn)``;