diff --git a/.proxyrc b/.proxyrc index 751cb293176..add302a43ad 100644 --- a/.proxyrc +++ b/.proxyrc @@ -1,5 +1,8 @@ { "/v1": { "target": "http://localhost:9000/", - } + }, + "/oauth2": { + "target": "http://localhost:9001/" + }, } diff --git a/api/applications/applications.proto b/api/applications/applications.proto index 69b3ea05037..fd405ccecf8 100644 --- a/api/applications/applications.proto +++ b/api/applications/applications.proto @@ -154,6 +154,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. @@ -385,3 +394,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 ac01e26c7f6..8c38635dc21 100644 --- a/api/applications/applications.swagger.json +++ b/api/applications/applications.swagger.json @@ -494,6 +494,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": { @@ -679,6 +702,17 @@ } } }, + "v1GetFeatureFlagsResponse": { + "type": "object", + "properties": { + "flags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "v1GetGithubAuthStatusRequest": { "type": "object", "properties": { diff --git a/cmd/gitops-server/cmd/cmd.go b/cmd/gitops-server/cmd/cmd.go index 46907b2ec94..2fdbfa14fb2 100644 --- a/cmd/gitops-server/cmd/cmd.go +++ b/cmd/gitops-server/cmd/cmd.go @@ -45,11 +45,11 @@ type Options struct { // OIDCAuthenticationOptions contains the OIDC authentication options for the // gitops-server 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 @@ -79,27 +79,32 @@ 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 } func preRunCmd(cmd *cobra.Command, args []string) error { - if server.AuthEnabled() { - if options.OIDC.IssuerURL == "" { + issuerURL := options.OIDC.IssuerURL + clientID := options.OIDC.ClientID + clientSecret := options.OIDC.ClientSecret + redirectURL := options.OIDC.RedirectURL + + if issuerURL != "" || clientID != "" || clientSecret != "" || redirectURL != "" { + if issuerURL == "" { return cmderrors.ErrNoIssuerURL } - if options.OIDC.ClientID == "" { + if clientID == "" { return cmderrors.ErrNoClientID } - if options.OIDC.ClientSecret == "" { + if clientSecret == "" { return cmderrors.ErrNoClientSecret } - if options.OIDC.RedirectURL == "" { + if redirectURL == "" { return cmderrors.ErrNoRedirectURL } } @@ -180,29 +185,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) @@ -228,11 +230,7 @@ func runCmd(cmd *cobra.Command, args []string) error { // This will return a 404 on normal page requests, ie /some-page. // Redirect all non-file requests to index.html, where the JS routing will take over. if extension == "" { - if server.AuthEnabled() { - auth.WithWebAuth(redirector, authServer).ServeHTTP(w, req) - } else { - redirector(w, req) - } + redirector(w, req) return } assetHandler.ServeHTTP(w, req) diff --git a/cmd/gitops-server/cmd/cmd_test.go b/cmd/gitops-server/cmd/cmd_test.go index 34e482b02af..aea950885c3 100644 --- a/cmd/gitops-server/cmd/cmd_test.go +++ b/cmd/gitops-server/cmd/cmd_test.go @@ -8,19 +8,6 @@ import ( "github.com/weaveworks/weave-gitops/cmd/gitops/cmderrors" ) -func TestNoIssuerURL(t *testing.T) { - os.Setenv("WEAVE_GITOPS_AUTH_ENABLED", "true") - defer os.Unsetenv("WEAVE_GITOPS_AUTH_ENABLED") - - cmd := NewCommand() - cmd.SetArgs([]string{ - "ui", "run", - }) - - err := cmd.Execute() - assert.ErrorIs(t, err, cmderrors.ErrNoIssuerURL) -} - func TestNoClientID(t *testing.T) { os.Setenv("WEAVE_GITOPS_AUTH_ENABLED", "true") defer os.Unsetenv("WEAVE_GITOPS_AUTH_ENABLED") diff --git a/go.mod b/go.mod index e1cae575413..c8a0f4767f8 100644 --- a/go.mod +++ b/go.mod @@ -67,6 +67,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 ) @@ -224,7 +225,6 @@ require ( go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/mod v0.5.1 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect diff --git a/package.json b/package.json index 366be85cb03..01680ef6c94 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 702b1d077c5..96d6f93f489 100644 --- a/pkg/api/applications/applications.pb.go +++ b/pkg/api/applications/applications.pb.go @@ -2328,6 +2328,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[35] + 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[35] + 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{35} +} + +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[36] + 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[36] + 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{36} +} + +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{ @@ -2615,147 +2700,167 @@ var file_api_applications_applications_proto_rawDesc = []byte{ 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, 0x8b, 0x0f, 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, + 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, 0x89, 0x10, 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, 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, + 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, - 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, 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, + 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, 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, 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, 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, + 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, 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, 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 ( @@ -2771,7 +2876,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, 35) +var file_api_applications_applications_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_api_applications_applications_proto_goTypes = []interface{}{ (AutomationKind)(0), // 0: wego_server.v1.AutomationKind (GitProvider)(0), // 1: wego_server.v1.GitProvider @@ -2811,6 +2916,9 @@ var file_api_applications_applications_proto_goTypes = []interface{}{ (*AuthorizeGitlabResponse)(nil), // 35: wego_server.v1.AuthorizeGitlabResponse (*ValidateProviderTokenRequest)(nil), // 36: wego_server.v1.ValidateProviderTokenRequest (*ValidateProviderTokenResponse)(nil), // 37: wego_server.v1.ValidateProviderTokenResponse + (*GetFeatureFlagsRequest)(nil), // 38: wego_server.v1.GetFeatureFlagsRequest + (*GetFeatureFlagsResponse)(nil), // 39: wego_server.v1.GetFeatureFlagsResponse + nil, // 40: 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 @@ -2836,37 +2944,40 @@ var file_api_applications_applications_proto_depIdxs = []int32{ 21, // 20: wego_server.v1.GetChildObjectsRes.objects:type_name -> wego_server.v1.UnstructuredObject 1, // 21: wego_server.v1.ParseRepoURLResponse.provider:type_name -> wego_server.v1.GitProvider 1, // 22: wego_server.v1.ValidateProviderTokenRequest.provider:type_name -> wego_server.v1.GitProvider - 9, // 23: wego_server.v1.Applications.Authenticate:input_type -> wego_server.v1.AuthenticateRequest - 11, // 24: wego_server.v1.Applications.ListApplications:input_type -> wego_server.v1.ListApplicationsRequest - 13, // 25: wego_server.v1.Applications.GetApplication:input_type -> wego_server.v1.GetApplicationRequest - 18, // 26: wego_server.v1.Applications.ListCommits:input_type -> wego_server.v1.ListCommitsRequest - 22, // 27: wego_server.v1.Applications.GetReconciledObjects:input_type -> wego_server.v1.GetReconciledObjectsReq - 24, // 28: wego_server.v1.Applications.GetChildObjects:input_type -> wego_server.v1.GetChildObjectsReq - 26, // 29: wego_server.v1.Applications.GetGithubDeviceCode:input_type -> wego_server.v1.GetGithubDeviceCodeRequest - 28, // 30: wego_server.v1.Applications.GetGithubAuthStatus:input_type -> wego_server.v1.GetGithubAuthStatusRequest - 32, // 31: wego_server.v1.Applications.GetGitlabAuthURL:input_type -> wego_server.v1.GetGitlabAuthURLRequest - 34, // 32: wego_server.v1.Applications.AuthorizeGitlab:input_type -> wego_server.v1.AuthorizeGitlabRequest - 15, // 33: wego_server.v1.Applications.SyncApplication:input_type -> wego_server.v1.SyncApplicationRequest - 30, // 34: wego_server.v1.Applications.ParseRepoURL:input_type -> wego_server.v1.ParseRepoURLRequest - 36, // 35: wego_server.v1.Applications.ValidateProviderToken:input_type -> wego_server.v1.ValidateProviderTokenRequest - 10, // 36: wego_server.v1.Applications.Authenticate:output_type -> wego_server.v1.AuthenticateResponse - 12, // 37: wego_server.v1.Applications.ListApplications:output_type -> wego_server.v1.ListApplicationsResponse - 14, // 38: wego_server.v1.Applications.GetApplication:output_type -> wego_server.v1.GetApplicationResponse - 19, // 39: wego_server.v1.Applications.ListCommits:output_type -> wego_server.v1.ListCommitsResponse - 23, // 40: wego_server.v1.Applications.GetReconciledObjects:output_type -> wego_server.v1.GetReconciledObjectsRes - 25, // 41: wego_server.v1.Applications.GetChildObjects:output_type -> wego_server.v1.GetChildObjectsRes - 27, // 42: wego_server.v1.Applications.GetGithubDeviceCode:output_type -> wego_server.v1.GetGithubDeviceCodeResponse - 29, // 43: wego_server.v1.Applications.GetGithubAuthStatus:output_type -> wego_server.v1.GetGithubAuthStatusResponse - 33, // 44: wego_server.v1.Applications.GetGitlabAuthURL:output_type -> wego_server.v1.GetGitlabAuthURLResponse - 35, // 45: wego_server.v1.Applications.AuthorizeGitlab:output_type -> wego_server.v1.AuthorizeGitlabResponse - 16, // 46: wego_server.v1.Applications.SyncApplication:output_type -> wego_server.v1.SyncApplicationResponse - 31, // 47: wego_server.v1.Applications.ParseRepoURL:output_type -> wego_server.v1.ParseRepoURLResponse - 37, // 48: wego_server.v1.Applications.ValidateProviderToken:output_type -> wego_server.v1.ValidateProviderTokenResponse - 36, // [36:49] is the sub-list for method output_type - 23, // [23:36] is the sub-list for method input_type - 23, // [23:23] is the sub-list for extension type_name - 23, // [23:23] is the sub-list for extension extendee - 0, // [0:23] is the sub-list for field type_name + 40, // 23: wego_server.v1.GetFeatureFlagsResponse.flags:type_name -> wego_server.v1.GetFeatureFlagsResponse.FlagsEntry + 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 + 18, // 27: wego_server.v1.Applications.ListCommits:input_type -> wego_server.v1.ListCommitsRequest + 22, // 28: wego_server.v1.Applications.GetReconciledObjects:input_type -> wego_server.v1.GetReconciledObjectsReq + 24, // 29: wego_server.v1.Applications.GetChildObjects:input_type -> wego_server.v1.GetChildObjectsReq + 26, // 30: wego_server.v1.Applications.GetGithubDeviceCode:input_type -> wego_server.v1.GetGithubDeviceCodeRequest + 28, // 31: wego_server.v1.Applications.GetGithubAuthStatus:input_type -> wego_server.v1.GetGithubAuthStatusRequest + 32, // 32: wego_server.v1.Applications.GetGitlabAuthURL:input_type -> wego_server.v1.GetGitlabAuthURLRequest + 34, // 33: wego_server.v1.Applications.AuthorizeGitlab:input_type -> wego_server.v1.AuthorizeGitlabRequest + 15, // 34: wego_server.v1.Applications.SyncApplication:input_type -> wego_server.v1.SyncApplicationRequest + 30, // 35: wego_server.v1.Applications.ParseRepoURL:input_type -> wego_server.v1.ParseRepoURLRequest + 36, // 36: wego_server.v1.Applications.ValidateProviderToken:input_type -> wego_server.v1.ValidateProviderTokenRequest + 38, // 37: wego_server.v1.Applications.GetFeatureFlags:input_type -> wego_server.v1.GetFeatureFlagsRequest + 10, // 38: wego_server.v1.Applications.Authenticate:output_type -> wego_server.v1.AuthenticateResponse + 12, // 39: wego_server.v1.Applications.ListApplications:output_type -> wego_server.v1.ListApplicationsResponse + 14, // 40: wego_server.v1.Applications.GetApplication:output_type -> wego_server.v1.GetApplicationResponse + 19, // 41: wego_server.v1.Applications.ListCommits:output_type -> wego_server.v1.ListCommitsResponse + 23, // 42: wego_server.v1.Applications.GetReconciledObjects:output_type -> wego_server.v1.GetReconciledObjectsRes + 25, // 43: wego_server.v1.Applications.GetChildObjects:output_type -> wego_server.v1.GetChildObjectsRes + 27, // 44: wego_server.v1.Applications.GetGithubDeviceCode:output_type -> wego_server.v1.GetGithubDeviceCodeResponse + 29, // 45: wego_server.v1.Applications.GetGithubAuthStatus:output_type -> wego_server.v1.GetGithubAuthStatusResponse + 33, // 46: wego_server.v1.Applications.GetGitlabAuthURL:output_type -> wego_server.v1.GetGitlabAuthURLResponse + 35, // 47: wego_server.v1.Applications.AuthorizeGitlab:output_type -> wego_server.v1.AuthorizeGitlabResponse + 16, // 48: wego_server.v1.Applications.SyncApplication:output_type -> wego_server.v1.SyncApplicationResponse + 31, // 49: wego_server.v1.Applications.ParseRepoURL:output_type -> wego_server.v1.ParseRepoURLResponse + 37, // 50: wego_server.v1.Applications.ValidateProviderToken:output_type -> wego_server.v1.ValidateProviderTokenResponse + 39, // 51: wego_server.v1.Applications.GetFeatureFlags:output_type -> wego_server.v1.GetFeatureFlagsResponse + 38, // [38:52] is the sub-list for method output_type + 24, // [24:38] 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 } func init() { file_api_applications_applications_proto_init() } @@ -3295,6 +3406,30 @@ func file_api_applications_applications_proto_init() { return nil } } + file_api_applications_applications_proto_msgTypes[35].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[36].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[15].OneofWrappers = []interface{}{} type x struct{} @@ -3303,7 +3438,7 @@ func file_api_applications_applications_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_applications_applications_proto_rawDesc, NumEnums: 3, - NumMessages: 35, + NumMessages: 38, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/api/applications/applications.pb.gw.go b/pkg/api/applications/applications.pb.gw.go index 195002d45e5..6c434c55ea7 100644 --- a/pkg/api/applications/applications.pb.gw.go +++ b/pkg/api/applications/applications.pb.gw.go @@ -637,6 +637,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. @@ -942,6 +960,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 } @@ -1243,6 +1284,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 } @@ -1272,6 +1333,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 ( @@ -1300,4 +1363,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 971bb4f7c14..3a189de3db9 100644 --- a/pkg/api/applications/applications_grpc.pb.go +++ b/pkg/api/applications/applications_grpc.pb.go @@ -70,6 +70,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 { @@ -197,6 +200,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 @@ -253,6 +265,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() } @@ -299,6 +314,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. @@ -546,6 +564,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) @@ -605,6 +641,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 6a7bf44f91f..15d47970cf4 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,7 +29,11 @@ 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, srv.OAuth2Flow()) + 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()) } type principalCtxKey struct{} @@ -59,46 +62,29 @@ 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 { - cookieAuth := NewJWTCookiePrincipalGetter(srv.logger, - srv.verifier(), IDTokenCookieName) - headerAuth := NewJWTAuthorizationHeaderPrincipalGetter(srv.logger, srv.verifier()) - multi := MultiAuthPrincipal{cookieAuth, headerAuth} +func WithAPIAuth(next http.Handler, srv *AuthServer, publicRoutes []string) http.Handler { + adminAuth := NewJWTAdminCookiePrincipalGetter(srv.logger, srv.tokenSignerVerifier, IDTokenCookieName) + multi := MultiAuthPrincipal{adminAuth} + + if srv.oidcEnabled() { + headerAuth := NewJWTAuthorizationHeaderPrincipalGetter(srv.logger, srv.verifier()) + cookieAuth := NewJWTCookiePrincipalGetter(srv.logger, srv.verifier(), IDTokenCookieName) + multi = append(multi, headerAuth, cookieAuth) + } return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - principal, err := multi.Principal(r) - if err != nil { - srv.logger.Error(err, "failed to get principal") - } - - if principal == nil || err != nil { - http.Error(rw, "Authentication required", http.StatusUnauthorized) + if IsPublicRoute(r.URL, publicRoutes) { + next.ServeHTTP(rw, r) return } - next.ServeHTTP(rw, r.Clone(WithPrincipal(r.Context(), principal))) - }) -} - -// WithWebAuth middleware adds auth validation to HTML handlers. -// -// Unauthorized requests will be redirected to the OIDC Provider. -// It is meant to be used with routes that serve HTML content, -// not API routes. -func WithWebAuth(next http.Handler, srv *AuthServer) http.Handler { - cookieAuth := NewJWTCookiePrincipalGetter(srv.logger, - srv.verifier(), IDTokenCookieName) - headerAuth := NewJWTAuthorizationHeaderPrincipalGetter(srv.logger, srv.verifier()) - multi := MultiAuthPrincipal{cookieAuth, headerAuth} - - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { principal, err := multi.Principal(r) if err != nil { srv.logger.Error(err, "failed to get principal") } if principal == nil || err != nil { - startAuthFlow(rw, r, srv) + http.Error(rw, "Authentication required", http.StatusUnauthorized) return } @@ -106,35 +92,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) @@ -145,3 +102,13 @@ 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..37c2709b2cf 100644 --- a/pkg/server/auth/auth_test.go +++ b/pkg/server/auth/auth_test.go @@ -13,7 +13,9 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/go-logr/logr" "github.com/oauth2-proxy/mockoidc" + "github.com/stretchr/testify/assert" "github.com/weaveworks/weave-gitops/pkg/server/auth" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestWithAPIAuthReturns401ForUnauthenticatedRequests(t *testing.T) { @@ -30,20 +32,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: "", - }, - 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") } @@ -58,14 +63,23 @@ 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) { +func TestOauth2FlowRedirectsToOIDCIssuerForUnauthenticatedRequests(t *testing.T) { ctx := context.Background() m, err := mockoidc.Run() @@ -79,20 +93,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: "", + 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") } @@ -108,7 +125,7 @@ func TestWithWebAuthRedirectsToOIDCIssuerForUnauthenticatedRequests(t *testing.T res := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, s.URL, nil) - auth.WithWebAuth(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}), srv).ServeHTTP(res, req) + srv.OAuth2Flow().ServeHTTP(res, req) if res.Result().StatusCode != http.StatusSeeOther { t.Errorf("expected status of %d but got %d", http.StatusSeeOther, res.Result().StatusCode) @@ -119,3 +136,9 @@ func TestWithWebAuthRedirectsToOIDCIssuerForUnauthenticatedRequests(t *testing.T t.Errorf("expected Location header URL to include scopes %s but does not: %s", authCodeURL, res.Result().Header.Get("Location")) } } + +func TestIsPublicRoute(t *testing.T) { + assert.True(t, auth.IsPublicRoute(&url.URL{Path: "/foo"}, []string{"/foo"})) + assert.False(t, auth.IsPublicRoute(&url.URL{Path: "foo"}, []string{"/foo"})) + assert.False(t, auth.IsPublicRoute(&url.URL{Path: "/foob"}, []string{"/foo"})) +} diff --git a/pkg/server/auth/jwt.go b/pkg/server/auth/jwt.go index 1d4a01b5b07..257ce571497 100644 --- a/pkg/server/auth/jwt.go +++ b/pkg/server/auth/jwt.go @@ -102,6 +102,43 @@ 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 { + // FIXME: do some better handling here + // return nil, fmt.Errorf("failed to verify JWT token: %w", err) + // ANYWAY:, its probably not our token? e.g. an OIDC one + return nil, nil + } + + 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 ea47ad03afc..6beb13e813f 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,98 @@ 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) { - provider, err := oidc.NewProvider(ctx, config.IssuerURL) +func NewAuthServer(ctx context.Context, logger logr.Logger, client *http.Client, config AuthConfig, kubernetesClient ctrlclient.Client, tokenSignerVerifier TokenSignerVerifier) (*AuthServer, error) { + var provider *oidc.Provider + + if config.IssuerURL != "" { + var err 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 create provider: %w", err) + 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 (s *AuthServer) oidcEnabled() bool { + return s.config.IssuerURL != "" } -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 +124,280 @@ 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) OAuth2Flow() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + if !s.oidcEnabled() { + http.Error(rw, "oidc provider not configured", http.StatusBadRequest) + return + } + + s.startAuthFlow(rw, r) + } +} + +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 + } - rawIDToken, ok := token.Extra("id_token").(string) - if !ok { - http.Error(rw, "no id_token in token response", http.StatusInternalServerError) + 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 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") - // Some OIDC providers may not include a refresh token - if token.RefreshToken != "" { - // Issue refresh token cookie - http.SetCookie(rw, c.createCookie(RefreshTokenCookieName, token.RefreshToken)) + if returnUrl == "" { + returnUrl = r.URL.String() } - // Clear state cookie - http.SetCookie(rw, c.clearCookie(StateCookieName)) + 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 + } + + 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) + + // Issue state cookie + http.SetCookie(rw, c.createCookie(StateCookieName, state)) - http.Redirect(rw, r, state.ReturnURL, http.StatusSeeOther) + http.Redirect(rw, r, authCodeUrl, http.StatusSeeOther) +} + +func (s *AuthServer) Logout() http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + s.logger.Info("Only POST requests allowed") + rw.WriteHeader(http.StatusMethodNotAllowed) + + return + } + + http.SetCookie(rw, s.clearCookie(IDTokenCookieName)) + rw.WriteHeader(http.StatusOK) + } } func (c *AuthServer) createCookie(name, value string) *http.Cookie { @@ -199,12 +405,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: false, } return cookie 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 5b966ff07ef..f286e3f020a 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -19,6 +19,12 @@ const ( AuthEnabledFeatureFlag = "WEAVE_GITOPS_AUTH_ENABLED" ) +var ( + PublicRoutes = []string{ + "/v1/featureflags", + } +) + func AuthEnabled() bool { return os.Getenv(AuthEnabledFeatureFlag) == "true" } @@ -36,7 +42,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, PublicRoutes) } appsSrv := NewApplicationsServer(cfg.AppConfig, cfg.AppOptions...) diff --git a/pkg/server/server.go b/pkg/server/server.go index 95394bba17c..19f3b2592a8 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -605,6 +605,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 e6dda48fd75..809a4743deb 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -14,11 +14,14 @@ 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 FeatureFlagsContextProvider from "./contexts/FeatureFlags"; import { Core } from "./lib/api/core/core.pb"; import Fonts from "./lib/fonts"; import theme, { GlobalStyle, muiTheme } from "./lib/theme"; import { V2Routes } from "./lib/types"; import Error from "./pages/Error"; +import SignIn from "./pages/SignIn"; import Automations from "./pages/v2/Automations"; import FluxRuntime from "./pages/v2/FluxRuntime"; import GitRepositoryDetail from "./pages/v2/GitRepositoryDetail"; @@ -35,53 +38,62 @@ function withName(Cmp) { }; } -export default function App() { +const App = () => ( + + + + + + + + + + + + + + + + +); + +export default function AppContainer() { return ( - - + + - - - - - - - - - - - - - - - - + + + + {/* does not use the base page so pull it up here */} + + + {/* Check we've got a logged in user otherwise redirect back to signin */} + + + + + + + - - + + ); } diff --git a/ui/components/Icon.tsx b/ui/components/Icon.tsx index 8e404ab503c..4783475fc78 100644 --- a/ui/components/Icon.tsx +++ b/ui/components/Icon.tsx @@ -5,6 +5,7 @@ import CheckCircleIcon from "@material-ui/icons/CheckCircle"; import ClearIcon from "@material-ui/icons/Clear"; import DeleteIcon from "@material-ui/icons/Delete"; import ErrorIcon from "@material-ui/icons/Error"; +import LogoutIcon from "@material-ui/icons/ExitToApp"; import FilterIcon from "@material-ui/icons/FilterList"; import HourglassFullIcon from "@material-ui/icons/HourglassFull"; import LaunchIcon from "@material-ui/icons/Launch"; @@ -42,6 +43,7 @@ export enum IconType { ClearIcon, Circle, SearchIcon, + LogoutIcon, } type Props = { @@ -115,6 +117,9 @@ function getIcon(i: IconType) { case IconType.SearchIcon: return SearchIcon; + case IconType.LogoutIcon: + return LogoutIcon; + default: break; } diff --git a/ui/components/Layout.tsx b/ui/components/Layout.tsx index 3db370f6153..5d124a14e7f 100644 --- a/ui/components/Layout.tsx +++ b/ui/components/Layout.tsx @@ -2,12 +2,14 @@ import { Tab, Tabs } from "@material-ui/core"; import _ from "lodash"; import React, { forwardRef } from "react"; import styled from "styled-components"; +import { FeatureFlags } from "../contexts/FeatureFlags"; import useNavigation from "../hooks/navigation"; import { formatURL, getParentNavValue } from "../lib/nav"; import { V2Routes } from "../lib/types"; import Flex from "./Flex"; import Link from "./Link"; import Logo from "./Logo"; +import UserSettings from "./UserSettings"; type Props = { className?: string; @@ -93,7 +95,7 @@ const NavContent = styled.div` } ${Link} { justify-content: flex-start; - &.sub-item: { + &.sub-item { font-weight: 400; } } @@ -126,19 +128,15 @@ const TopToolBar = styled(Flex)` } `; -//style for account icon - disabled while no account functionality exists -// const UserAvatar = styled(Icon)` -// padding-right: ${(props) => props.theme.spacing.medium}; -// `; - function Layout({ className, children }: Props) { + const { authFlag } = React.useContext(FeatureFlags); const { currentPage } = useNavigation(); return (
- {/* code for account icon - disabled while no account functionality exists */} + {authFlag ? : null}
diff --git a/ui/components/UserSettings.tsx b/ui/components/UserSettings.tsx new file mode 100644 index 00000000000..13f1f6bcd1a --- /dev/null +++ b/ui/components/UserSettings.tsx @@ -0,0 +1,68 @@ +import { + IconButton, + ListItemIcon, + Menu, + MenuItem, + Tooltip, +} from "@material-ui/core"; +import * as React from "react"; +import styled from "styled-components"; +import { Auth } from "../contexts/AuthContext"; +import Icon, { IconType } from "./Icon"; + +const UserAvatar = styled(Icon)` + padding-right: ${(props) => props.theme.spacing.medium}; +`; + +const SettingsMenu = styled(Menu)` + .MuiListItemIcon-root { + min-width: 25px; + color: ${(props) => props.theme.colors.black}; + } +`; + +function UserSettings() { + const [anchorEl, setAnchorEl] = React.useState(null); + const { userInfo, logOut } = React.useContext(Auth); + + const open = Boolean(anchorEl); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + + + + + Hello, {userInfo?.email} + logOut()}> + + + + Logout + + + + ); +} + +export default styled(UserSettings)``; diff --git a/ui/contexts/AuthContext.tsx b/ui/contexts/AuthContext.tsx new file mode 100644 index 00000000000..e06619df1cb --- /dev/null +++ b/ui/contexts/AuthContext.tsx @@ -0,0 +1,137 @@ +import * as React from "react"; +import { useHistory, Redirect } from "react-router-dom"; +import Layout from "../components/Layout"; +import LoadingPage from "../components/LoadingPage"; +import { FeatureFlags } from "../contexts/FeatureFlags"; + +const USER_INFO = "/oauth2/userinfo"; +const SIGN_IN = "/oauth2/sign_in"; +const LOG_OUT = "/oauth2/logout"; +const AUTH_PATH_SIGNIN = "/sign_in"; + +export const AuthCheck = ({ children }) => { + // If the auth flag is null go straight to rendering the children + const { authFlag } = React.useContext(FeatureFlags); + + if (!authFlag) { + return 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; + logOut: () => void; +}; + +export const Auth = React.createContext(null); + +export default function AuthContextProvider({ children }) { + const { authFlag } = React.useContext(FeatureFlags); + 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)); + }, []); + + const logOut = React.useCallback(() => { + setLoading(true); + fetch(LOG_OUT, { + method: "POST", + }) + .then((response) => { + if (response.status !== 200) { + setError(response); + return; + } + history.push("/sign_in"); + }) + .finally(() => setLoading(false)); + }, []); + + React.useEffect(() => { + if (!authFlag) { + return null; + } + getUserInfo(); + return history.listen(getUserInfo); + }, [getUserInfo, history]); + + return ( + <> + {authFlag ? ( + + {children} + + ) : ( + children + )} + + ); +} diff --git a/ui/contexts/FeatureFlags.tsx b/ui/contexts/FeatureFlags.tsx new file mode 100644 index 00000000000..1e33a1b568e --- /dev/null +++ b/ui/contexts/FeatureFlags.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; + +export type FeatureFlagsContext = { + authFlag: boolean | null; +}; + +export const FeatureFlags = + React.createContext(null); + +export default function FeatureFlagsContextProvider({ children }) { + const [authFlag, setAuthFlag] = React.useState(null); + + const getAuthFlag = React.useCallback(() => { + fetch("/v1/featureflags") + .then((response) => response.json()) + .then((data) => + setAuthFlag(data.flags.WEAVE_GITOPS_AUTH_ENABLED === "true") + ) + .catch((err) => console.log(err)); + }, []); + + React.useEffect(() => { + getAuthFlag(); + }, [getAuthFlag]); + + 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/index.ts b/ui/index.ts index dd6dde8733e..abaa775a3ad 100644 --- a/ui/index.ts +++ b/ui/index.ts @@ -3,8 +3,13 @@ import Footer from "./components/Footer"; import GithubDeviceAuthModal from "./components/GithubDeviceAuthModal"; import LoadingPage from "./components/LoadingPage"; import RepoInputWithAuth from "./components/RepoInputWithAuth"; +import UserSettings from "./components/UserSettings"; import Icon, { IconType } from "./components/Icon"; import AppContextProvider from "./contexts/AppContext"; +import AuthContextProvider from "./contexts/AuthContext"; +import FeatureFlagsContextProvider, { + FeatureFlags, +} from "./contexts/FeatureFlags"; import CallbackStateContextProvider from "./contexts/CallbackStateContext"; import useApplications from "./hooks/applications"; import { Applications as applicationsClient } from "./lib/api/applications/applications.pb"; @@ -19,6 +24,9 @@ import Applications from "./pages/Applications"; import OAuthCallback from "./pages/OAuthCallback"; export { + FeatureFlagsContextProvider, + FeatureFlags, + AuthContextProvider, AppContextProvider, ApplicationDetail, Applications, @@ -38,4 +46,5 @@ export { Button, Icon, IconType, + UserSettings, }; diff --git a/ui/lib/api/applications/applications.pb.ts b/ui/lib/api/applications/applications.pb.ts index b3cdb42e0b0..c69a3e10121 100644 --- a/ui/lib/api/applications/applications.pb.ts +++ b/ui/lib/api/applications/applications.pb.ts @@ -238,6 +238,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)}) @@ -278,4 +285,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)``;