From 1e37a178dd152d21fcb952acda6be6b5d82f1bc8 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 21 Jul 2023 12:23:34 -0500 Subject: [PATCH 01/56] update db --- apikey/queries.sql | 26 +++++ gadb/models.go | 13 +++ gadb/queries.sql.go | 94 ++++++++++++++++ .../20230721104236-graphql-api-key.sql | 17 +++ migrate/schema.sql | 103 ++++++++++++------ sqlc.yaml | 1 + 6 files changed, 220 insertions(+), 34 deletions(-) create mode 100644 apikey/queries.sql create mode 100644 migrate/migrations/20230721104236-graphql-api-key.sql diff --git a/apikey/queries.sql b/apikey/queries.sql new file mode 100644 index 0000000000..3b084b6c96 --- /dev/null +++ b/apikey/queries.sql @@ -0,0 +1,26 @@ +-- name: APIKeyInsert :exec +INSERT INTO api_keys(id, version, user_id, service_id, name, data, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7); + +-- name: APIKeyDelete :exec +DELETE FROM api_keys +WHERE id = $1; + +-- name: APIKeyGet :one +SELECT + * +FROM + api_keys +WHERE + id = $1; + +-- name: APIKeyAuth :one +UPDATE + api_keys +SET + last_used_at = now() +WHERE + id = $1 +RETURNING + *; + diff --git a/gadb/models.go b/gadb/models.go index c5c9f78db1..8903c12600 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -798,6 +798,19 @@ type AlertStatusSubscription struct { LastAlertStatus EnumAlertStatus } +type ApiKey struct { + ID uuid.UUID + Name string + UserID uuid.NullUUID + ServiceID uuid.NullUUID + Version int32 + Data json.RawMessage + CreatedAt time.Time + UpdatedAt time.Time + ExpiresAt time.Time + LastUsedAt sql.NullTime +} + type AuthBasicUser struct { UserID uuid.UUID Username string diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index 484022b624..ab957bc07d 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -16,6 +16,100 @@ import ( "github.com/sqlc-dev/pqtype" ) +const aPIKeyAuth = `-- name: APIKeyAuth :one +UPDATE + api_keys +SET + last_used_at = now() +WHERE + id = $1 +RETURNING + id, name, user_id, service_id, version, data, created_at, updated_at, expires_at, last_used_at +` + +func (q *Queries) APIKeyAuth(ctx context.Context, id uuid.UUID) (ApiKey, error) { + row := q.db.QueryRowContext(ctx, aPIKeyAuth, id) + var i ApiKey + err := row.Scan( + &i.ID, + &i.Name, + &i.UserID, + &i.ServiceID, + &i.Version, + &i.Data, + &i.CreatedAt, + &i.UpdatedAt, + &i.ExpiresAt, + &i.LastUsedAt, + ) + return i, err +} + +const aPIKeyDelete = `-- name: APIKeyDelete :exec +DELETE FROM api_keys +WHERE id = $1 +` + +func (q *Queries) APIKeyDelete(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, aPIKeyDelete, id) + return err +} + +const aPIKeyGet = `-- name: APIKeyGet :one +SELECT + id, name, user_id, service_id, version, data, created_at, updated_at, expires_at, last_used_at +FROM + api_keys +WHERE + id = $1 +` + +func (q *Queries) APIKeyGet(ctx context.Context, id uuid.UUID) (ApiKey, error) { + row := q.db.QueryRowContext(ctx, aPIKeyGet, id) + var i ApiKey + err := row.Scan( + &i.ID, + &i.Name, + &i.UserID, + &i.ServiceID, + &i.Version, + &i.Data, + &i.CreatedAt, + &i.UpdatedAt, + &i.ExpiresAt, + &i.LastUsedAt, + ) + return i, err +} + +const aPIKeyInsert = `-- name: APIKeyInsert :exec +INSERT INTO api_keys(id, version, user_id, service_id, name, data, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) +` + +type APIKeyInsertParams struct { + ID uuid.UUID + Version int32 + UserID uuid.NullUUID + ServiceID uuid.NullUUID + Name string + Data json.RawMessage + ExpiresAt time.Time +} + +func (q *Queries) APIKeyInsert(ctx context.Context, arg APIKeyInsertParams) error { + _, err := q.db.ExecContext(ctx, aPIKeyInsert, + arg.ID, + arg.Version, + arg.UserID, + arg.ServiceID, + arg.Name, + arg.Data, + arg.ExpiresAt, + ) + return err +} + const alertFeedback = `-- name: AlertFeedback :many SELECT alert_id, diff --git a/migrate/migrations/20230721104236-graphql-api-key.sql b/migrate/migrations/20230721104236-graphql-api-key.sql new file mode 100644 index 0000000000..cc506f614a --- /dev/null +++ b/migrate/migrations/20230721104236-graphql-api-key.sql @@ -0,0 +1,17 @@ +-- +migrate Up +CREATE TABLE api_keys( + id uuid PRIMARY KEY, + name text NOT NULL UNIQUE, + user_id uuid REFERENCES users(id) ON DELETE CASCADE, + service_id uuid REFERENCES services(id) ON DELETE CASCADE, + version integer NOT NULL DEFAULT 1, + data jsonb NOT NULL, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now(), + expires_at timestamp with time zone NOT NULL, + last_used_at timestamp with time zone +); + +-- +migrate Down +DROP TABLE api_keys; + diff --git a/migrate/schema.sql b/migrate/schema.sql index d255c0f645..cd3f070473 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,13 +1,13 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=834c3a25163544ad7cf2fe079cc7da7119b95359609b20c584f0561411ff5200 - --- DISK=334f8f372a31ab442509645785948cbcd3fde9edeb632064547bd7d75e8d7754 - --- PSQL=334f8f372a31ab442509645785948cbcd3fde9edeb632064547bd7d75e8d7754 - +-- DATA=30508a117ca8a653673c01ee354def8744b46f97fe4156b0283ec099cf547c20 - +-- DISK=ba641d09f148a96a980e9c03f7606e89f6acbb4b842fd2b381d9df877e0ea29c - +-- PSQL=ba641d09f148a96a980e9c03f7606e89f6acbb4b842fd2b381d9df877e0ea29c - -- -- PostgreSQL database dump -- --- Dumped from database version 12.13 --- Dumped by pg_dump version 15.1 +-- Dumped from database version 13.5 +-- Dumped by pg_dump version 13.6 (Ubuntu 13.6-0ubuntu0.21.10.1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -20,13 +20,6 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; --- --- Name: public; Type: SCHEMA; Schema: -; Owner: - --- - --- *not* creating schema, since initdb creates it - - -- -- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - -- @@ -1673,6 +1666,24 @@ CREATE SEQUENCE public.alerts_id_seq ALTER SEQUENCE public.alerts_id_seq OWNED BY public.alerts.id; +-- +-- Name: api_keys; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.api_keys ( + id uuid NOT NULL, + name text NOT NULL, + user_id uuid, + service_id uuid, + version integer DEFAULT 1 NOT NULL, + data jsonb NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + expires_at timestamp with time zone NOT NULL, + last_used_at timestamp with time zone +); + + -- -- Name: auth_basic_users; Type: TABLE; Schema: public; Owner: - -- @@ -1864,7 +1875,7 @@ ALTER SEQUENCE public.ep_step_on_call_users_id_seq OWNED BY public.ep_step_on_ca -- CREATE TABLE public.escalation_policies ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, repeat integer DEFAULT 0 NOT NULL, @@ -1877,7 +1888,7 @@ CREATE TABLE public.escalation_policies ( -- CREATE TABLE public.escalation_policy_actions ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, escalation_policy_step_id uuid NOT NULL, user_id uuid, schedule_id uuid, @@ -1946,7 +1957,7 @@ ALTER SEQUENCE public.escalation_policy_state_id_seq OWNED BY public.escalation_ -- CREATE TABLE public.escalation_policy_steps ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, delay integer DEFAULT 1 NOT NULL, step_number integer DEFAULT '-1'::integer NOT NULL, escalation_policy_id uuid NOT NULL @@ -1994,7 +2005,7 @@ CREATE SEQUENCE public.incident_number_seq -- CREATE TABLE public.integration_keys ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, type public.enum_integration_keys_type NOT NULL, service_id uuid NOT NULL @@ -2065,7 +2076,7 @@ CREATE TABLE public.notification_channels ( -- CREATE TABLE public.notification_policy_cycles ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, user_id uuid NOT NULL, alert_id integer NOT NULL, repeat_count integer DEFAULT 0 NOT NULL, @@ -2081,7 +2092,7 @@ WITH (fillfactor='65'); -- CREATE TABLE public.outgoing_messages ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, message_type public.enum_outgoing_messages_type NOT NULL, contact_method_id uuid, created_at timestamp with time zone DEFAULT now() NOT NULL, @@ -2155,7 +2166,7 @@ ALTER SEQUENCE public.region_ids_id_seq OWNED BY public.region_ids.id; -- CREATE TABLE public.rotation_participants ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, rotation_id uuid NOT NULL, "position" integer NOT NULL, user_id uuid NOT NULL @@ -2200,7 +2211,7 @@ ALTER SEQUENCE public.rotation_state_id_seq OWNED BY public.rotation_state.id; -- CREATE TABLE public.rotations ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, type public.enum_rotation_type NOT NULL, @@ -2282,7 +2293,7 @@ ALTER SEQUENCE public.schedule_on_call_users_id_seq OWNED BY public.schedule_on_ -- CREATE TABLE public.schedule_rules ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, schedule_id uuid NOT NULL, sunday boolean DEFAULT true NOT NULL, monday boolean DEFAULT true NOT NULL, @@ -2306,7 +2317,7 @@ CREATE TABLE public.schedule_rules ( -- CREATE TABLE public.schedules ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, time_zone text NOT NULL, @@ -2319,7 +2330,7 @@ CREATE TABLE public.schedules ( -- CREATE TABLE public.services ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, escalation_policy_id uuid NOT NULL, @@ -2345,7 +2356,7 @@ CREATE TABLE public.switchover_log ( CREATE TABLE public.switchover_state ( ok boolean NOT NULL, current_state public.enum_switchover_state NOT NULL, - db_id uuid DEFAULT public.gen_random_uuid() NOT NULL, + db_id uuid DEFAULT gen_random_uuid() NOT NULL, CONSTRAINT switchover_state_ok_check CHECK (ok) ); @@ -2470,7 +2481,7 @@ CREATE TABLE public.user_calendar_subscriptions ( -- CREATE TABLE public.user_contact_methods ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, type public.enum_user_contact_method_type NOT NULL, value text NOT NULL, @@ -2522,7 +2533,7 @@ ALTER SEQUENCE public.user_favorites_id_seq OWNED BY public.user_favorites.id; -- CREATE TABLE public.user_notification_rules ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, delay_minutes integer DEFAULT 0 NOT NULL, contact_method_id uuid NOT NULL, user_id uuid NOT NULL, @@ -2767,6 +2778,22 @@ ALTER TABLE ONLY public.alerts ADD CONSTRAINT alerts_pkey PRIMARY KEY (id); +-- +-- Name: api_keys api_keys_name_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_keys + ADD CONSTRAINT api_keys_name_key UNIQUE (name); + + +-- +-- Name: api_keys api_keys_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_keys + ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id); + + -- -- Name: auth_basic_users auth_basic_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4140,6 +4167,22 @@ ALTER TABLE ONLY public.alerts ADD CONSTRAINT alerts_services_id_fkey FOREIGN KEY (service_id) REFERENCES public.services(id) ON DELETE CASCADE; +-- +-- Name: api_keys api_keys_service_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_keys + ADD CONSTRAINT api_keys_service_id_fkey FOREIGN KEY (service_id) REFERENCES public.services(id) ON DELETE CASCADE; + + +-- +-- Name: api_keys api_keys_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_keys + ADD CONSTRAINT api_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + -- -- Name: auth_basic_users auth_basic_users_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4636,14 +4679,6 @@ ALTER TABLE ONLY public.users ADD CONSTRAINT users_alert_status_log_contact_method_id_fkey FOREIGN KEY (alert_status_log_contact_method_id) REFERENCES public.user_contact_methods(id) ON DELETE SET NULL DEFERRABLE; --- --- Name: SCHEMA public; Type: ACL; Schema: -; Owner: - --- - -REVOKE USAGE ON SCHEMA public FROM PUBLIC; -GRANT ALL ON SCHEMA public TO PUBLIC; - - -- -- PostgreSQL database dump complete -- diff --git a/sqlc.yaml b/sqlc.yaml index 9e920e9674..2429365695 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -24,6 +24,7 @@ sql: - engine/statusmgr/queries.sql - auth/authlink/queries.sql - alert/alertlog/queries.sql + - apikey/queries.sql engine: postgresql gen: go: From ef5f5de67e3f89122572a8de91f2474335463c84 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 21 Jul 2023 12:23:54 -0500 Subject: [PATCH 02/56] add gqlauth --- graphql2/gqlauth/extension.go | 25 +++++++++++++++ graphql2/gqlauth/query_test.go | 57 ++++++++++++++++++++++++++++++++++ graphql2/schema.go | 12 +++++++ graphql2/schema.graphql | 15 +++++++++ 4 files changed, 109 insertions(+) create mode 100644 graphql2/gqlauth/extension.go create mode 100644 graphql2/gqlauth/query_test.go create mode 100644 graphql2/schema.go diff --git a/graphql2/gqlauth/extension.go b/graphql2/gqlauth/extension.go new file mode 100644 index 0000000000..1b3f2e470e --- /dev/null +++ b/graphql2/gqlauth/extension.go @@ -0,0 +1,25 @@ +package gqlauth + +import ( + "context" + + "github.com/99designs/gqlgen/graphql" + "github.com/target/goalert/permission" +) + +func (q *Query) ExtensionName() string { + return "gqlauth" +} + +func (q *Query) Validate(schema graphql.ExecutableSchema) error { + return nil +} +func (q *Query) InterceptField(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { + f := graphql.GetFieldContext(ctx) + err = q.ValidateField(f.Field.Field) + if err != nil { + return nil, permission.NewAccessDenied(err.Error()) + } + + return next(ctx) +} diff --git a/graphql2/gqlauth/query_test.go b/graphql2/gqlauth/query_test.go new file mode 100644 index 0000000000..32bdf9f1fe --- /dev/null +++ b/graphql2/gqlauth/query_test.go @@ -0,0 +1,57 @@ +package gqlauth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQuery(t *testing.T) { + q, err := NewQuery(`query ($id: ID!) { user(id: $id) { id, name } }`) + require.NoError(t, err) + + isSub, err := q.IsSubset(`{ user { id } }`) + require.NoError(t, err) + assert.True(t, isSub) + + isSub, err = q.IsSubset(`query { user(id: "afs") { id } }`) + require.NoError(t, err) + assert.True(t, isSub) + + isSub, err = q.IsSubset(`query { user { id, name, email } }`) + require.NoError(t, err) + assert.False(t, isSub) +} +func TestQueryObj(t *testing.T) { + + q, err := NewQuery(`query ($in: DebugMessagesInput) { debugMessages(input: $in) { id } }`) + require.NoError(t, err) + + isSub, err := q.IsSubset(`{ debugMessages { id } }`) + require.NoError(t, err) + assert.True(t, isSub) + + isSub, err = q.IsSubset(`{ debugMessages(input:{first: 3}) { id } }`) + require.NoError(t, err) + assert.True(t, isSub) + +} +func TestQueryObjLim(t *testing.T) { + + q, err := NewQuery(`query ($first: Int) { debugMessages(input: {first: $first}) { id } }`) + require.NoError(t, err) + + isSub, err := q.IsSubset(`{ debugMessages { id } }`) + require.NoError(t, err) + assert.True(t, isSub) + + isSub, err = q.IsSubset(`{ debugMessages(input:{first: 3}) { id } }`) + require.NoError(t, err) + assert.True(t, isSub) + + isSub, err = q.IsSubset(`{ debugMessages(input:{createdBefore: "asdf"}) { id } }`) + require.NoError(t, err) + assert.False(t, isSub) + +} diff --git a/graphql2/schema.go b/graphql2/schema.go new file mode 100644 index 0000000000..6d949a69d6 --- /dev/null +++ b/graphql2/schema.go @@ -0,0 +1,12 @@ +package graphql2 + +import ( + _ "embed" +) + +//go:embed schema.graphql +var schema string + +func Schema() string { + return schema +} diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index fd198f7a4b..e7b17a448c 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -579,10 +579,25 @@ type Mutation { setConfig(input: [ConfigValueInput!]): Boolean! setSystemLimits(input: [SystemLimitInput!]!): Boolean! + createGQLAPIKey(input: CreateGQLAPIKeyInput!): GQLAPIKey! + createBasicAuth(input: CreateBasicAuthInput!): Boolean! updateBasicAuth(input: UpdateBasicAuthInput!): Boolean! } +input CreateGQLAPIKeyInput { + name: String! + query: String! + expiresAt: ISOTimestamp! +} + +type GQLAPIKey { + id: ID! + name: String! + token: String + expiresAt: ISOTimestamp! +} + input CreateBasicAuthInput { username: String! password: String! From 313794c51d3ceba16430a9b8f382e0de35473a85 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 21 Jul 2023 12:24:10 -0500 Subject: [PATCH 03/56] add api method --- graphql2/generated.go | 427 ++++++++++++++++++++++++++++++++ graphql2/gqlauth/query.go | 136 ++++++++++ graphql2/graphqlapp/app.go | 23 ++ graphql2/graphqlapp/mutation.go | 20 ++ graphql2/models_gen.go | 13 + web/src/schema.d.ts | 14 ++ 6 files changed, 633 insertions(+) create mode 100644 graphql2/gqlauth/query.go diff --git a/graphql2/generated.go b/graphql2/generated.go index 8c3260353e..1da7eaa365 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -231,6 +231,13 @@ type ComplexityRoot struct { Targets func(childComplexity int) int } + GQLAPIKey struct { + ExpiresAt func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + Token func(childComplexity int) int + } + HeartbeatMonitor struct { Href func(childComplexity int) int ID func(childComplexity int) int @@ -294,6 +301,7 @@ type ComplexityRoot struct { CreateBasicAuth func(childComplexity int, input CreateBasicAuthInput) int CreateEscalationPolicy func(childComplexity int, input CreateEscalationPolicyInput) int CreateEscalationPolicyStep func(childComplexity int, input CreateEscalationPolicyStepInput) int + CreateGQLAPIKey func(childComplexity int, input CreateGQLAPIKeyInput) int CreateHeartbeatMonitor func(childComplexity int, input CreateHeartbeatMonitorInput) int CreateIntegrationKey func(childComplexity int, input CreateIntegrationKeyInput) int CreateRotation func(childComplexity int, input CreateRotationInput) int @@ -756,6 +764,7 @@ type MutationResolver interface { UpdateAlertsByService(ctx context.Context, input UpdateAlertsByServiceInput) (bool, error) SetConfig(ctx context.Context, input []ConfigValueInput) (bool, error) SetSystemLimits(ctx context.Context, input []SystemLimitInput) (bool, error) + CreateGQLAPIKey(ctx context.Context, input CreateGQLAPIKeyInput) (*GQLAPIKey, error) CreateBasicAuth(ctx context.Context, input CreateBasicAuthInput) (bool, error) UpdateBasicAuth(ctx context.Context, input UpdateBasicAuthInput) (bool, error) } @@ -1480,6 +1489,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.EscalationPolicyStep.Targets(childComplexity), true + case "GQLAPIKey.expiresAt": + if e.complexity.GQLAPIKey.ExpiresAt == nil { + break + } + + return e.complexity.GQLAPIKey.ExpiresAt(childComplexity), true + + case "GQLAPIKey.id": + if e.complexity.GQLAPIKey.ID == nil { + break + } + + return e.complexity.GQLAPIKey.ID(childComplexity), true + + case "GQLAPIKey.name": + if e.complexity.GQLAPIKey.Name == nil { + break + } + + return e.complexity.GQLAPIKey.Name(childComplexity), true + + case "GQLAPIKey.token": + if e.complexity.GQLAPIKey.Token == nil { + break + } + + return e.complexity.GQLAPIKey.Token(childComplexity), true + case "HeartbeatMonitor.href": if e.complexity.HeartbeatMonitor.Href == nil { break @@ -1760,6 +1797,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateEscalationPolicyStep(childComplexity, args["input"].(CreateEscalationPolicyStepInput)), true + case "Mutation.createGQLAPIKey": + if e.complexity.Mutation.CreateGQLAPIKey == nil { + break + } + + args, err := ec.field_Mutation_createGQLAPIKey_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateGQLAPIKey(childComplexity, args["input"].(CreateGQLAPIKeyInput)), true + case "Mutation.createHeartbeatMonitor": if e.complexity.Mutation.CreateHeartbeatMonitor == nil { break @@ -3959,6 +4008,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputCreateBasicAuthInput, ec.unmarshalInputCreateEscalationPolicyInput, ec.unmarshalInputCreateEscalationPolicyStepInput, + ec.unmarshalInputCreateGQLAPIKeyInput, ec.unmarshalInputCreateHeartbeatMonitorInput, ec.unmarshalInputCreateIntegrationKeyInput, ec.unmarshalInputCreateRotationInput, @@ -4250,6 +4300,21 @@ func (ec *executionContext) field_Mutation_createEscalationPolicy_args(ctx conte return args, nil } +func (ec *executionContext) field_Mutation_createGQLAPIKey_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 CreateGQLAPIKeyInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNCreateGQLAPIKeyInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreateGQLAPIKeyInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_createHeartbeatMonitor_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -9331,6 +9396,179 @@ func (ec *executionContext) fieldContext_EscalationPolicyStep_escalationPolicy(c return fc, nil } +func (ec *executionContext) _GQLAPIKey_id(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKey_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKey", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GQLAPIKey_name(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKey_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKey", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GQLAPIKey_token(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_token(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Token, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKey_token(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKey", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GQLAPIKey_expiresAt(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ExpiresAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNISOTimestamp2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKey_expiresAt(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKey", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ISOTimestamp does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _HeartbeatMonitor_id(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { fc, err := ec.fieldContext_HeartbeatMonitor_id(ctx, field) if err != nil { @@ -13495,6 +13733,71 @@ func (ec *executionContext) fieldContext_Mutation_setSystemLimits(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_createGQLAPIKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createGQLAPIKey(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateGQLAPIKey(rctx, fc.Args["input"].(CreateGQLAPIKeyInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*GQLAPIKey) + fc.Result = res + return ec.marshalNGQLAPIKey2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKey(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createGQLAPIKey(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_GQLAPIKey_id(ctx, field) + case "name": + return ec.fieldContext_GQLAPIKey_name(ctx, field) + case "token": + return ec.fieldContext_GQLAPIKey_token(ctx, field) + case "expiresAt": + return ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type GQLAPIKey", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createGQLAPIKey_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createBasicAuth(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createBasicAuth(ctx, field) if err != nil { @@ -26983,6 +27286,53 @@ func (ec *executionContext) unmarshalInputCreateEscalationPolicyStepInput(ctx co return it, nil } +func (ec *executionContext) unmarshalInputCreateGQLAPIKeyInput(ctx context.Context, obj interface{}) (CreateGQLAPIKeyInput, error) { + var it CreateGQLAPIKeyInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"name", "query", "expiresAt"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "name": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "query": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("query")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Query = data + case "expiresAt": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("expiresAt")) + data, err := ec.unmarshalNISOTimestamp2timeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.ExpiresAt = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputCreateHeartbeatMonitorInput(ctx context.Context, obj interface{}) (CreateHeartbeatMonitorInput, error) { var it CreateHeartbeatMonitorInput asMap := map[string]interface{}{} @@ -31940,6 +32290,57 @@ func (ec *executionContext) _EscalationPolicyStep(ctx context.Context, sel ast.S return out } +var gQLAPIKeyImplementors = []string{"GQLAPIKey"} + +func (ec *executionContext) _GQLAPIKey(ctx context.Context, sel ast.SelectionSet, obj *GQLAPIKey) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, gQLAPIKeyImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("GQLAPIKey") + case "id": + out.Values[i] = ec._GQLAPIKey_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "name": + out.Values[i] = ec._GQLAPIKey_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "token": + out.Values[i] = ec._GQLAPIKey_token(ctx, field, obj) + case "expiresAt": + out.Values[i] = ec._GQLAPIKey_expiresAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var heartbeatMonitorImplementors = []string{"HeartbeatMonitor"} func (ec *executionContext) _HeartbeatMonitor(ctx context.Context, sel ast.SelectionSet, obj *heartbeat.Monitor) graphql.Marshaler { @@ -32833,6 +33234,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createGQLAPIKey": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createGQLAPIKey(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createBasicAuth": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createBasicAuth(ctx, field) @@ -38049,6 +38457,11 @@ func (ec *executionContext) unmarshalNCreateEscalationPolicyStepInput2githubᚗc return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNCreateGQLAPIKeyInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreateGQLAPIKeyInput(ctx context.Context, v interface{}) (CreateGQLAPIKeyInput, error) { + res, err := ec.unmarshalInputCreateGQLAPIKeyInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNCreateHeartbeatMonitorInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreateHeartbeatMonitorInput(ctx context.Context, v interface{}) (CreateHeartbeatMonitorInput, error) { res, err := ec.unmarshalInputCreateHeartbeatMonitorInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -38300,6 +38713,20 @@ func (ec *executionContext) marshalNEscalationPolicyStep2ᚕgithubᚗcomᚋtarge return ret } +func (ec *executionContext) marshalNGQLAPIKey2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKey(ctx context.Context, sel ast.SelectionSet, v GQLAPIKey) graphql.Marshaler { + return ec._GQLAPIKey(ctx, sel, &v) +} + +func (ec *executionContext) marshalNGQLAPIKey2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKey(ctx context.Context, sel ast.SelectionSet, v *GQLAPIKey) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._GQLAPIKey(ctx, sel, v) +} + func (ec *executionContext) marshalNHeartbeatMonitor2githubᚗcomᚋtargetᚋgoalertᚋheartbeatᚐMonitor(ctx context.Context, sel ast.SelectionSet, v heartbeat.Monitor) graphql.Marshaler { return ec._HeartbeatMonitor(ctx, sel, &v) } diff --git a/graphql2/gqlauth/query.go b/graphql2/gqlauth/query.go new file mode 100644 index 0000000000..7fe4dfa878 --- /dev/null +++ b/graphql2/gqlauth/query.go @@ -0,0 +1,136 @@ +package gqlauth + +import ( + "errors" + "fmt" + "strings" + + "github.com/target/goalert/graphql2" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" + "github.com/vektah/gqlparser/v2/validator" +) + +var s = gqlparser.MustLoadSchema(&ast.Source{Input: graphql2.Schema()}) + +type Query struct { + doc *ast.QueryDocument + + allowedFields map[Field]struct{} + allowedArgs map[FieldArg][]AllowedArg +} +type Field struct { + Name string + Object string + Kind ast.DefinitionKind +} + +type FieldArg struct { + Field + Arg string +} + +type AllowedArg struct { + Raw string + Variable bool +} + +func NewQuery(query string) (*Query, error) { + doc, err := gqlparser.LoadQuery(s, query) + if err != nil { + return nil, err + } + + q := &Query{ + doc: doc, + allowedFields: make(map[Field]struct{}), + allowedArgs: make(map[FieldArg][]AllowedArg), + } + + var errs []error + var e validator.Events + e.OnField(func(w *validator.Walker, d *ast.Field) { + f := Field{ + Name: d.Definition.Name, + Object: d.ObjectDefinition.Name, + Kind: d.ObjectDefinition.Kind, + } + q.allowedFields[f] = struct{}{} + + for _, arg := range d.Arguments { + a := FieldArg{ + Field: f, + Arg: arg.Name, + } + + fmt.Println(arg.Name, arg.Value.Kind) + q.allowedArgs[a] = append(q.allowedArgs[a], AllowedArg{ + Raw: arg.Value.Raw, + Variable: arg.Value.Kind == ast.Variable, + }) + } + }) + validator.Walk(s, doc, &e) + + return q, errors.Join(errs...) +} + +func (q *Query) ValidateField(af *ast.Field) error { + f := Field{ + Name: af.Definition.Name, + Object: af.ObjectDefinition.Name, + Kind: af.ObjectDefinition.Kind, + } + if strings.HasPrefix(f.Name, "__") { + return nil + } + if strings.HasPrefix(f.Object, "__") { + return nil + } + + if _, ok := q.allowedFields[f]; !ok { + return fmt.Errorf("field %s.%s not allowed", f.Object, f.Name) + } +argCheck: + for _, arg := range af.Arguments { + fmt.Println(arg.Name, q.allowedArgs) + a := FieldArg{ + Field: f, + Arg: arg.Name, + } + if aaList, ok := q.allowedArgs[a]; ok { + for _, aa := range aaList { + + if aa.Variable { + continue argCheck + } + if aa.Raw == arg.Value.Raw { + continue argCheck + } + } + } + + return fmt.Errorf("field %s.%s arg %s not allowed or has a forbidden value", f.Object, f.Name, arg.Name) + } + + return nil +} + +// IsSubset checks if a is a subset of b. +func (q *Query) IsSubset(query string) (bool, error) { + doc, err := gqlparser.LoadQuery(s, query) + if err != nil { + return false, err + } + + var invalid bool + var e validator.Events + e.OnField(func(w *validator.Walker, d *ast.Field) { + if q.ValidateField(d) != nil { + invalid = true + } + }) + validator.Walk(s, doc, &e) + + return !invalid, nil +} diff --git a/graphql2/graphqlapp/app.go b/graphql2/graphqlapp/app.go index dc9f5243a7..b8125a9961 100644 --- a/graphql2/graphqlapp/app.go +++ b/graphql2/graphqlapp/app.go @@ -16,6 +16,7 @@ import ( "github.com/target/goalert/alert" "github.com/target/goalert/alert/alertlog" "github.com/target/goalert/alert/alertmetrics" + "github.com/target/goalert/apikey" "github.com/target/goalert/auth" "github.com/target/goalert/auth/authlink" "github.com/target/goalert/auth/basic" @@ -23,6 +24,7 @@ import ( "github.com/target/goalert/config" "github.com/target/goalert/escalation" "github.com/target/goalert/graphql2" + "github.com/target/goalert/graphql2/gqlauth" "github.com/target/goalert/heartbeat" "github.com/target/goalert/integrationkey" "github.com/target/goalert/label" @@ -77,6 +79,7 @@ type App struct { SlackStore *slack.ChannelSender HeartbeatStore *heartbeat.Store NoticeStore *notice.Store + APIKeyStore *apikey.Store AuthLinkStore *authlink.Store @@ -153,6 +156,25 @@ func (a *App) Handler() http.Handler { return ok && enabled }}) + h.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { + src := permission.Source(ctx) + if src.Type != permission.SourceTypeGQLAPIKey { + return next(ctx) + } + + qs, err := a.APIKeyStore.ContextQuery(ctx) + if err != nil { + return nil, permission.NewAccessDenied("invalid API key") + } + + q, err := gqlauth.NewQuery(qs) + if err != nil { + return nil, permission.NewAccessDenied("invalid API key") + } + + return q.InterceptField(ctx, next) + }) + h.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { defer func() { err := recover() @@ -239,6 +261,7 @@ func (a *App) Handler() http.Handler { // ensure some sort of auth before continuing err := permission.LimitCheckAny(ctx) if errutil.HTTPError(ctx, w, err) { + log.Logf(ctx, "GraphQL: %s", err) return } diff --git a/graphql2/graphqlapp/mutation.go b/graphql2/graphqlapp/mutation.go index f1ba1cccdb..303ef5ef84 100644 --- a/graphql2/graphqlapp/mutation.go +++ b/graphql2/graphqlapp/mutation.go @@ -11,6 +11,7 @@ import ( "github.com/target/goalert/assignment" "github.com/target/goalert/config" "github.com/target/goalert/graphql2" + "github.com/target/goalert/graphql2/gqlauth" "github.com/target/goalert/notification/webhook" "github.com/target/goalert/notificationchannel" "github.com/target/goalert/permission" @@ -28,6 +29,25 @@ type Mutation App func (a *App) Mutation() graphql2.MutationResolver { return (*Mutation)(a) } +func (a *Mutation) CreateGQLAPIKey(ctx context.Context, input graphql2.CreateGQLAPIKeyInput) (*graphql2.GQLAPIKey, error) { + _, err := gqlauth.NewQuery(input.Query) + if err != nil { + return nil, validation.NewFieldError("Query", err.Error()) + } + + key, err := a.APIKeyStore.CreateAdminGraphQLKey(ctx, input.Name, input.Query, input.ExpiresAt) + if err != nil { + return nil, err + } + + return &graphql2.GQLAPIKey{ + ID: key.ID.String(), + Name: key.Name, + ExpiresAt: key.ExpiresAt, + Token: &key.Token, + }, nil +} + func (a *Mutation) SetFavorite(ctx context.Context, input graphql2.SetFavoriteInput) (bool, error) { var err error if input.Favorite { diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 7af777028d..01326bc2b8 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -139,6 +139,12 @@ type CreateEscalationPolicyStepInput struct { NewSchedule *CreateScheduleInput `json:"newSchedule,omitempty"` } +type CreateGQLAPIKeyInput struct { + Name string `json:"name"` + Query string `json:"query"` + ExpiresAt time.Time `json:"expiresAt"` +} + type CreateHeartbeatMonitorInput struct { ServiceID *string `json:"serviceID,omitempty"` Name string `json:"name"` @@ -282,6 +288,13 @@ type EscalationPolicySearchOptions struct { FavoritesFirst *bool `json:"favoritesFirst,omitempty"` } +type GQLAPIKey struct { + ID string `json:"id"` + Name string `json:"name"` + Token *string `json:"token,omitempty"` + ExpiresAt time.Time `json:"expiresAt"` +} + type IntegrationKeyConnection struct { Nodes []integrationkey.IntegrationKey `json:"nodes"` PageInfo *PageInfo `json:"pageInfo"` diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index aa212234cb..449fe79ca0 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -422,10 +422,24 @@ export interface Mutation { updateAlertsByService: boolean setConfig: boolean setSystemLimits: boolean + createGQLAPIKey: GQLAPIKey createBasicAuth: boolean updateBasicAuth: boolean } +export interface CreateGQLAPIKeyInput { + name: string + query: string + expiresAt: ISOTimestamp +} + +export interface GQLAPIKey { + id: string + name: string + token?: null | string + expiresAt: ISOTimestamp +} + export interface CreateBasicAuthInput { username: string password: string From 9ac62914574cddedc56f0f51d7aeace334d45c19 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 21 Jul 2023 12:24:20 -0500 Subject: [PATCH 04/56] add apikey package --- apikey/graphqlv1.go | 46 ++++++++++++ apikey/store.go | 168 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 apikey/graphqlv1.go create mode 100644 apikey/store.go diff --git a/apikey/graphqlv1.go b/apikey/graphqlv1.go new file mode 100644 index 0000000000..eeca11e996 --- /dev/null +++ b/apikey/graphqlv1.go @@ -0,0 +1,46 @@ +package apikey + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +type Type string + +const ( + TypeGraphQLV1 Type = "graphql-v1" +) + +type V1 struct { + Type Type + + GraphQLV1 *GraphQLV1 `json:",omitempty"` +} + +type GraphQLV1 struct { + Query string + SHA256 [32]byte +} + +type GraphQLClaims struct { + jwt.RegisteredClaims + AuthHash [32]byte `json:"q"` +} + +func NewGraphQLClaims(id uuid.UUID, queryHash [32]byte, expires time.Time) jwt.Claims { + n := time.Now() + return &GraphQLClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + ID: uuid.NewString(), + Subject: id.String(), + ExpiresAt: jwt.NewNumericDate(expires), + IssuedAt: jwt.NewNumericDate(n), + NotBefore: jwt.NewNumericDate(n.Add(-time.Minute)), + Issuer: "goalert", + Audience: []string{"apikey-v1/graphql-v1"}, + }, + AuthHash: queryHash, + } +} diff --git a/apikey/store.go b/apikey/store.go new file mode 100644 index 0000000000..c6dd5366dc --- /dev/null +++ b/apikey/store.go @@ -0,0 +1,168 @@ +package apikey + +import ( + "context" + "crypto/sha256" + "database/sql" + "encoding/json" + "errors" + "sync" + "time" + + "github.com/google/uuid" + "github.com/target/goalert/gadb" + "github.com/target/goalert/keyring" + "github.com/target/goalert/permission" + "github.com/target/goalert/util/log" + "github.com/target/goalert/validation/validate" +) + +type Store struct { + db *sql.DB + key keyring.Keyring + + mx sync.Mutex + queries map[string]string +} + +func NewStore(ctx context.Context, db *sql.DB, key keyring.Keyring) (*Store, error) { + s := &Store{db: db, key: key, queries: make(map[string]string)} + + return s, nil +} + +func (s *Store) ContextQuery(ctx context.Context) (string, error) { + src := permission.Source(ctx) + if src == nil { + return "", errors.New("no permission source") + } + if src.Type != permission.SourceTypeGQLAPIKey { + return "", errors.New("permission source is not a GQLAPIKey") + } + s.mx.Lock() + q := s.queries[src.ID] + s.mx.Unlock() + if q == "" { + return "", errors.New("no query found for key") + } + return q, nil +} + +func (s *Store) AuthorizeGraphQL(ctx context.Context, tok string) (context.Context, error) { + var claims GraphQLClaims + _, err := s.key.VerifyJWT(tok, &claims, "goalert", "apikey-v1/graphql-v1") + if err != nil { + log.Logf(ctx, "apikey: verify failed: %v", err) + return ctx, permission.Unauthorized() + } + id, err := uuid.Parse(claims.Subject) + if err != nil { + log.Logf(ctx, "apikey: invalid subject: %v", err) + return ctx, permission.Unauthorized() + } + + key, err := gadb.New(s.db).APIKeyAuth(ctx, id) + if err != nil { + log.Logf(ctx, "apikey: lookup failed: %v", err) + return ctx, permission.Unauthorized() + } + if key.Version != 1 { + log.Logf(ctx, "apikey: invalid version: %v", key.Version) + return ctx, permission.Unauthorized() + } + + var v1 V1 + err = json.Unmarshal(key.Data, &v1) + if err != nil { + log.Logf(ctx, "apikey: invalid data: %v", err) + return ctx, permission.Unauthorized() + } + + if v1.Type != TypeGraphQLV1 || v1.GraphQLV1 == nil { + log.Logf(ctx, "apikey: invalid type: %v", v1.Type) + return ctx, permission.Unauthorized() + } + + if v1.GraphQLV1.SHA256 != claims.AuthHash { + log.Log(log.WithField(ctx, "key_id", id.String()), errors.New("apikey: query hash mismatch (claims)")) + return ctx, permission.Unauthorized() + } + + hash := sha256.Sum256([]byte(v1.GraphQLV1.Query)) + if hash != claims.AuthHash { + log.Log(log.WithField(ctx, "key_id", id.String()), errors.New("apikey: query hash mismatch (key)")) + return ctx, permission.Unauthorized() + } + + s.mx.Lock() + s.queries[id.String()] = v1.GraphQLV1.Query + s.mx.Unlock() + + ctx = permission.SourceContext(ctx, &permission.SourceInfo{ + ID: id.String(), + Type: permission.SourceTypeGQLAPIKey, + }) + ctx = permission.UserContext(ctx, "", permission.RoleUnknown) + return ctx, nil +} + +type Key struct { + ID uuid.UUID + Name string + Type Type + ExpiresAt time.Time + Token string +} + +func (s *Store) CreateAdminGraphQLKey(ctx context.Context, name, query string, exp time.Time) (*Key, error) { + err := permission.LimitCheckAny(ctx, permission.Admin) + if err != nil { + return nil, err + } + err = validate.IDName("Name", name) + if err != nil { + return nil, err + } + err = validate.RequiredText("Query", query, 1, 8192) + if err != nil { + return nil, err + } + + hash := sha256.Sum256([]byte(query)) + + id := uuid.New() + + data, err := json.Marshal(V1{ + Type: TypeGraphQLV1, + GraphQLV1: &GraphQLV1{ + Query: query, + SHA256: hash, + }, + }) + if err != nil { + return nil, err + } + tok, err := s.key.SignJWT(NewGraphQLClaims(id, hash, exp)) + if err != nil { + return nil, err + } + + err = gadb.New(s.db).APIKeyInsert(ctx, gadb.APIKeyInsertParams{ + ID: id, + Name: name, + Version: 1, + ExpiresAt: exp, + Data: data, + }) + if err != nil { + return nil, err + } + + return &Key{ + ID: id, + Name: name, + Type: TypeGraphQLV1, + ExpiresAt: exp, + Token: tok, + }, nil +} From fce2145231b4a5f06b43414ba462e3c5fa5e1b4d Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 21 Jul 2023 12:24:56 -0500 Subject: [PATCH 05/56] add auth for api keys --- app/app.go | 2 ++ app/initauth.go | 1 + app/initgraphql.go | 1 + app/initstores.go | 8 ++++++++ auth/handler.go | 13 +++++++++++-- auth/handlerconfig.go | 2 ++ permission/source.go | 3 +++ 7 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/app.go b/app/app.go index edd78578ca..32e2132c46 100644 --- a/app/app.go +++ b/app/app.go @@ -12,6 +12,7 @@ import ( "github.com/target/goalert/alert" "github.com/target/goalert/alert/alertlog" "github.com/target/goalert/alert/alertmetrics" + "github.com/target/goalert/apikey" "github.com/target/goalert/app/lifecycle" "github.com/target/goalert/auth" "github.com/target/goalert/auth/authlink" @@ -117,6 +118,7 @@ type App struct { TimeZoneStore *timezone.Store NoticeStore *notice.Store AuthLinkStore *authlink.Store + APIKeyStore *apikey.Store } // NewApp constructs a new App and binds the listening socket. diff --git a/app/initauth.go b/app/initauth.go index cfe9d63062..1f98473fad 100644 --- a/app/initauth.go +++ b/app/initauth.go @@ -19,6 +19,7 @@ func (app *App) initAuth(ctx context.Context) error { IntKeyStore: app.IntegrationKeyStore, CalSubStore: app.CalSubStore, APIKeyring: app.APIKeyring, + APIKeyStore: app.APIKeyStore, }) if err != nil { return errors.Wrap(err, "init auth handler") diff --git a/app/initgraphql.go b/app/initgraphql.go index 3fdf01740f..861eccd89d 100644 --- a/app/initgraphql.go +++ b/app/initgraphql.go @@ -41,6 +41,7 @@ func (app *App) initGraphQL(ctx context.Context) error { NotificationManager: app.notificationManager, AuthLinkStore: app.AuthLinkStore, SWO: app.cfg.SWO, + APIKeyStore: app.APIKeyStore, } return nil diff --git a/app/initstores.go b/app/initstores.go index f30d4f8692..3ef0ab46f7 100644 --- a/app/initstores.go +++ b/app/initstores.go @@ -7,6 +7,7 @@ import ( "github.com/target/goalert/alert" "github.com/target/goalert/alert/alertlog" "github.com/target/goalert/alert/alertmetrics" + "github.com/target/goalert/apikey" "github.com/target/goalert/auth/authlink" "github.com/target/goalert/auth/basic" "github.com/target/goalert/auth/nonce" @@ -290,5 +291,12 @@ func (app *App) initStores(ctx context.Context) error { return errors.Wrap(err, "init notice store") } + if app.APIKeyStore == nil { + app.APIKeyStore, err = apikey.NewStore(ctx, app.db, app.APIKeyring) + } + if err != nil { + return errors.Wrap(err, "init API key store") + } + return nil } diff --git a/auth/handler.go b/auth/handler.go index ac863eb0f3..4b03ee3149 100644 --- a/auth/handler.go +++ b/auth/handler.go @@ -554,6 +554,17 @@ func (h *Handler) authWithToken(w http.ResponseWriter, req *http.Request, next h return false } + ctx := req.Context() + if req.URL.Path == "/api/graphql" && strings.HasPrefix(tokStr, "ey") { + ctx, err = h.cfg.APIKeyStore.AuthorizeGraphQL(ctx, tokStr) + if errutil.HTTPError(req.Context(), w, err) { + return true + } + + next.ServeHTTP(w, req.WithContext(ctx)) + return true + } + tok, _, err := authtoken.Parse(tokStr, func(t authtoken.Type, p, sig []byte) (bool, bool) { if t == authtoken.TypeSession { return h.cfg.SessionKeyring.Verify(p, sig) @@ -565,8 +576,6 @@ func (h *Handler) authWithToken(w http.ResponseWriter, req *http.Request, next h return true } - // TODO: update once scopes are implemented - ctx := req.Context() switch req.URL.Path { case "/v1/api/alerts", "/api/v2/generic/incoming": ctx, err = h.cfg.IntKeyStore.Authorize(ctx, *tok, integrationkey.TypeGeneric) diff --git a/auth/handlerconfig.go b/auth/handlerconfig.go index 7526e113ad..ceb502df12 100644 --- a/auth/handlerconfig.go +++ b/auth/handlerconfig.go @@ -1,6 +1,7 @@ package auth import ( + "github.com/target/goalert/apikey" "github.com/target/goalert/calsub" "github.com/target/goalert/integrationkey" "github.com/target/goalert/keyring" @@ -14,4 +15,5 @@ type HandlerConfig struct { APIKeyring keyring.Keyring IntKeyStore *integrationkey.Store CalSubStore *calsub.Store + APIKeyStore *apikey.Store } diff --git a/permission/source.go b/permission/source.go index ef01fa2839..221c1b20f7 100644 --- a/permission/source.go +++ b/permission/source.go @@ -26,6 +26,9 @@ const ( // SourceTypeCalendarSubscription is set when a context is authorized for use of a calendar subscription. SourceTypeCalendarSubscription + + // SourceTypeGQLAPIKey is set when a context is authorized for use of the GraphQL API. + SourceTypeGQLAPIKey ) // SourceInfo provides information about the source of a context's authorization. From 01a4bb548529c4eabf7fbf2b7d59e9da89065790 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 28 Jul 2023 12:38:47 -0500 Subject: [PATCH 06/56] simplify policy --- apikey/context.go | 20 +++ apikey/graphqlv1.go | 33 +++-- apikey/queries.sql | 4 +- apikey/store.go | 80 +++++------ gadb/models.go | 3 +- gadb/queries.sql.go | 20 ++- graphql2/gqlauth/extension.go | 25 ---- graphql2/gqlauth/query.go | 136 ------------------ graphql2/gqlauth/query_test.go | 57 -------- graphql2/graphqlapp/app.go | 18 ++- .../20230721104236-graphql-api-key.sql | 3 +- migrate/schema.sql | 5 +- 12 files changed, 101 insertions(+), 303 deletions(-) create mode 100644 apikey/context.go delete mode 100644 graphql2/gqlauth/extension.go delete mode 100644 graphql2/gqlauth/query.go delete mode 100644 graphql2/gqlauth/query_test.go diff --git a/apikey/context.go b/apikey/context.go new file mode 100644 index 0000000000..bd93afb54a --- /dev/null +++ b/apikey/context.go @@ -0,0 +1,20 @@ +package apikey + +import "context" + +type contextKey int + +const ( + contextKeyPolicy contextKey = iota +) + +// PolicyFromContext returns the Policy associated with the given context. +func PolicyFromContext(ctx context.Context) *Policy { + p, _ := ctx.Value(contextKeyPolicy).(*Policy) + return p +} + +// ContextWithPolicy returns a new context with the given Policy attached. +func ContextWithPolicy(ctx context.Context, p *Policy) context.Context { + return context.WithValue(ctx, contextKeyPolicy, p) +} diff --git a/apikey/graphqlv1.go b/apikey/graphqlv1.go index eeca11e996..d59abe53db 100644 --- a/apikey/graphqlv1.go +++ b/apikey/graphqlv1.go @@ -7,6 +7,18 @@ import ( "github.com/google/uuid" ) +type PolicyType string + +const ( + PolicyTypeGraphQLV1 PolicyType = "graphql-v1" +) + +type Policy struct { + Type PolicyType + + GraphQLV1 *GraphQLV1 `json:",omitempty"` +} + type Type string const ( @@ -20,27 +32,30 @@ type V1 struct { } type GraphQLV1 struct { - Query string - SHA256 [32]byte + AllowedFields []GraphQLField `json:"f"` +} +type GraphQLField struct { + ObjectName string `json:"o"` + Name string `json:"n"` } -type GraphQLClaims struct { +type Claims struct { jwt.RegisteredClaims - AuthHash [32]byte `json:"q"` + PolicyHash []byte `json:"ph"` } -func NewGraphQLClaims(id uuid.UUID, queryHash [32]byte, expires time.Time) jwt.Claims { +func NewGraphQLClaims(id uuid.UUID, policyHash []byte, expires time.Time) jwt.Claims { n := time.Now() - return &GraphQLClaims{ + return &Claims{ RegisteredClaims: jwt.RegisteredClaims{ ID: uuid.NewString(), Subject: id.String(), ExpiresAt: jwt.NewNumericDate(expires), IssuedAt: jwt.NewNumericDate(n), NotBefore: jwt.NewNumericDate(n.Add(-time.Minute)), - Issuer: "goalert", - Audience: []string{"apikey-v1/graphql-v1"}, + Issuer: Issuer, + Audience: []string{Audience}, }, - AuthHash: queryHash, + PolicyHash: policyHash, } } diff --git a/apikey/queries.sql b/apikey/queries.sql index 3b084b6c96..20fc0a3b7c 100644 --- a/apikey/queries.sql +++ b/apikey/queries.sql @@ -1,6 +1,6 @@ -- name: APIKeyInsert :exec -INSERT INTO api_keys(id, version, user_id, service_id, name, data, expires_at) - VALUES ($1, $2, $3, $4, $5, $6, $7); +INSERT INTO api_keys(id, user_id, service_id, name, POLICY, expires_at) + VALUES ($1, $2, $3, $4, $5, $6); -- name: APIKeyDelete :exec DELETE FROM api_keys diff --git a/apikey/store.go b/apikey/store.go index c6dd5366dc..d6b1b59f8b 100644 --- a/apikey/store.go +++ b/apikey/store.go @@ -1,11 +1,11 @@ package apikey import ( + "bytes" "context" "crypto/sha256" "database/sql" "encoding/json" - "errors" "sync" "time" @@ -21,36 +21,27 @@ type Store struct { db *sql.DB key keyring.Keyring - mx sync.Mutex - queries map[string]string + mx sync.Mutex + policies map[uuid.UUID]*policyInfo +} + +type policyInfo struct { + Hash []byte + Policy Policy } func NewStore(ctx context.Context, db *sql.DB, key keyring.Keyring) (*Store, error) { - s := &Store{db: db, key: key, queries: make(map[string]string)} + s := &Store{db: db, key: key, policies: make(map[uuid.UUID]*policyInfo)} return s, nil } -func (s *Store) ContextQuery(ctx context.Context) (string, error) { - src := permission.Source(ctx) - if src == nil { - return "", errors.New("no permission source") - } - if src.Type != permission.SourceTypeGQLAPIKey { - return "", errors.New("permission source is not a GQLAPIKey") - } - s.mx.Lock() - q := s.queries[src.ID] - s.mx.Unlock() - if q == "" { - return "", errors.New("no query found for key") - } - return q, nil -} +const Issuer = "goalert" +const Audience = "apikey-v1/graphql-v1" func (s *Store) AuthorizeGraphQL(ctx context.Context, tok string) (context.Context, error) { - var claims GraphQLClaims - _, err := s.key.VerifyJWT(tok, &claims, "goalert", "apikey-v1/graphql-v1") + var claims Claims + _, err := s.key.VerifyJWT(tok, &claims, Issuer, Audience) if err != nil { log.Logf(ctx, "apikey: verify failed: %v", err) return ctx, permission.Unauthorized() @@ -66,36 +57,30 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok string) (context.Conte log.Logf(ctx, "apikey: lookup failed: %v", err) return ctx, permission.Unauthorized() } - if key.Version != 1 { - log.Logf(ctx, "apikey: invalid version: %v", key.Version) - return ctx, permission.Unauthorized() - } - var v1 V1 - err = json.Unmarshal(key.Data, &v1) - if err != nil { - log.Logf(ctx, "apikey: invalid data: %v", err) - return ctx, permission.Unauthorized() + // TODO: cache policy hash by key ID when loading + policyHash := sha256.Sum256(key.Policy) + if !bytes.Equal(policyHash[:], claims.PolicyHash) { + log.Logf(ctx, "apikey: policy hash mismatch") } - if v1.Type != TypeGraphQLV1 || v1.GraphQLV1 == nil { - log.Logf(ctx, "apikey: invalid type: %v", v1.Type) - return ctx, permission.Unauthorized() - } - - if v1.GraphQLV1.SHA256 != claims.AuthHash { - log.Log(log.WithField(ctx, "key_id", id.String()), errors.New("apikey: query hash mismatch (claims)")) + var p Policy + err = json.Unmarshal(key.Policy, &p) + if err != nil { + log.Logf(ctx, "apikey: invalid policy: %v", err) return ctx, permission.Unauthorized() } - hash := sha256.Sum256([]byte(v1.GraphQLV1.Query)) - if hash != claims.AuthHash { - log.Log(log.WithField(ctx, "key_id", id.String()), errors.New("apikey: query hash mismatch (key)")) + if p.Type != PolicyTypeGraphQLV1 || p.GraphQLV1 == nil { + log.Logf(ctx, "apikey: invalid policy type: %v", p.Type) return ctx, permission.Unauthorized() } s.mx.Lock() - s.queries[id.String()] = v1.GraphQLV1.Query + s.policies[id] = &policyInfo{ + Hash: policyHash[:], + Policy: p, + } s.mx.Unlock() ctx = permission.SourceContext(ctx, &permission.SourceInfo{ @@ -133,16 +118,16 @@ func (s *Store) CreateAdminGraphQLKey(ctx context.Context, name, query string, e id := uuid.New() data, err := json.Marshal(V1{ - Type: TypeGraphQLV1, + Type: TypeGraphQLV1, GraphQLV1: &GraphQLV1{ - Query: query, - SHA256: hash, + // Query: query, + // SHA256: hash, }, }) if err != nil { return nil, err } - tok, err := s.key.SignJWT(NewGraphQLClaims(id, hash, exp)) + tok, err := s.key.SignJWT(NewGraphQLClaims(id, hash[:], exp)) if err != nil { return nil, err } @@ -150,9 +135,8 @@ func (s *Store) CreateAdminGraphQLKey(ctx context.Context, name, query string, e err = gadb.New(s.db).APIKeyInsert(ctx, gadb.APIKeyInsertParams{ ID: id, Name: name, - Version: 1, ExpiresAt: exp, - Data: data, + Policy: data, }) if err != nil { return nil, err diff --git a/gadb/models.go b/gadb/models.go index 8903c12600..27a315d5ab 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -803,8 +803,7 @@ type ApiKey struct { Name string UserID uuid.NullUUID ServiceID uuid.NullUUID - Version int32 - Data json.RawMessage + Policy json.RawMessage CreatedAt time.Time UpdatedAt time.Time ExpiresAt time.Time diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index ab957bc07d..b405c4e8e4 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -24,7 +24,7 @@ SET WHERE id = $1 RETURNING - id, name, user_id, service_id, version, data, created_at, updated_at, expires_at, last_used_at + id, name, user_id, service_id, policy, created_at, updated_at, expires_at, last_used_at ` func (q *Queries) APIKeyAuth(ctx context.Context, id uuid.UUID) (ApiKey, error) { @@ -35,8 +35,7 @@ func (q *Queries) APIKeyAuth(ctx context.Context, id uuid.UUID) (ApiKey, error) &i.Name, &i.UserID, &i.ServiceID, - &i.Version, - &i.Data, + &i.Policy, &i.CreatedAt, &i.UpdatedAt, &i.ExpiresAt, @@ -57,7 +56,7 @@ func (q *Queries) APIKeyDelete(ctx context.Context, id uuid.UUID) error { const aPIKeyGet = `-- name: APIKeyGet :one SELECT - id, name, user_id, service_id, version, data, created_at, updated_at, expires_at, last_used_at + id, name, user_id, service_id, policy, created_at, updated_at, expires_at, last_used_at FROM api_keys WHERE @@ -72,8 +71,7 @@ func (q *Queries) APIKeyGet(ctx context.Context, id uuid.UUID) (ApiKey, error) { &i.Name, &i.UserID, &i.ServiceID, - &i.Version, - &i.Data, + &i.Policy, &i.CreatedAt, &i.UpdatedAt, &i.ExpiresAt, @@ -83,28 +81,26 @@ func (q *Queries) APIKeyGet(ctx context.Context, id uuid.UUID) (ApiKey, error) { } const aPIKeyInsert = `-- name: APIKeyInsert :exec -INSERT INTO api_keys(id, version, user_id, service_id, name, data, expires_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) +INSERT INTO api_keys(id, user_id, service_id, name, POLICY, expires_at) + VALUES ($1, $2, $3, $4, $5, $6) ` type APIKeyInsertParams struct { ID uuid.UUID - Version int32 UserID uuid.NullUUID ServiceID uuid.NullUUID Name string - Data json.RawMessage + Policy json.RawMessage ExpiresAt time.Time } func (q *Queries) APIKeyInsert(ctx context.Context, arg APIKeyInsertParams) error { _, err := q.db.ExecContext(ctx, aPIKeyInsert, arg.ID, - arg.Version, arg.UserID, arg.ServiceID, arg.Name, - arg.Data, + arg.Policy, arg.ExpiresAt, ) return err diff --git a/graphql2/gqlauth/extension.go b/graphql2/gqlauth/extension.go deleted file mode 100644 index 1b3f2e470e..0000000000 --- a/graphql2/gqlauth/extension.go +++ /dev/null @@ -1,25 +0,0 @@ -package gqlauth - -import ( - "context" - - "github.com/99designs/gqlgen/graphql" - "github.com/target/goalert/permission" -) - -func (q *Query) ExtensionName() string { - return "gqlauth" -} - -func (q *Query) Validate(schema graphql.ExecutableSchema) error { - return nil -} -func (q *Query) InterceptField(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { - f := graphql.GetFieldContext(ctx) - err = q.ValidateField(f.Field.Field) - if err != nil { - return nil, permission.NewAccessDenied(err.Error()) - } - - return next(ctx) -} diff --git a/graphql2/gqlauth/query.go b/graphql2/gqlauth/query.go deleted file mode 100644 index 7fe4dfa878..0000000000 --- a/graphql2/gqlauth/query.go +++ /dev/null @@ -1,136 +0,0 @@ -package gqlauth - -import ( - "errors" - "fmt" - "strings" - - "github.com/target/goalert/graphql2" - "github.com/vektah/gqlparser/v2" - "github.com/vektah/gqlparser/v2/ast" - "github.com/vektah/gqlparser/v2/validator" -) - -var s = gqlparser.MustLoadSchema(&ast.Source{Input: graphql2.Schema()}) - -type Query struct { - doc *ast.QueryDocument - - allowedFields map[Field]struct{} - allowedArgs map[FieldArg][]AllowedArg -} -type Field struct { - Name string - Object string - Kind ast.DefinitionKind -} - -type FieldArg struct { - Field - Arg string -} - -type AllowedArg struct { - Raw string - Variable bool -} - -func NewQuery(query string) (*Query, error) { - doc, err := gqlparser.LoadQuery(s, query) - if err != nil { - return nil, err - } - - q := &Query{ - doc: doc, - allowedFields: make(map[Field]struct{}), - allowedArgs: make(map[FieldArg][]AllowedArg), - } - - var errs []error - var e validator.Events - e.OnField(func(w *validator.Walker, d *ast.Field) { - f := Field{ - Name: d.Definition.Name, - Object: d.ObjectDefinition.Name, - Kind: d.ObjectDefinition.Kind, - } - q.allowedFields[f] = struct{}{} - - for _, arg := range d.Arguments { - a := FieldArg{ - Field: f, - Arg: arg.Name, - } - - fmt.Println(arg.Name, arg.Value.Kind) - q.allowedArgs[a] = append(q.allowedArgs[a], AllowedArg{ - Raw: arg.Value.Raw, - Variable: arg.Value.Kind == ast.Variable, - }) - } - }) - validator.Walk(s, doc, &e) - - return q, errors.Join(errs...) -} - -func (q *Query) ValidateField(af *ast.Field) error { - f := Field{ - Name: af.Definition.Name, - Object: af.ObjectDefinition.Name, - Kind: af.ObjectDefinition.Kind, - } - if strings.HasPrefix(f.Name, "__") { - return nil - } - if strings.HasPrefix(f.Object, "__") { - return nil - } - - if _, ok := q.allowedFields[f]; !ok { - return fmt.Errorf("field %s.%s not allowed", f.Object, f.Name) - } -argCheck: - for _, arg := range af.Arguments { - fmt.Println(arg.Name, q.allowedArgs) - a := FieldArg{ - Field: f, - Arg: arg.Name, - } - if aaList, ok := q.allowedArgs[a]; ok { - for _, aa := range aaList { - - if aa.Variable { - continue argCheck - } - if aa.Raw == arg.Value.Raw { - continue argCheck - } - } - } - - return fmt.Errorf("field %s.%s arg %s not allowed or has a forbidden value", f.Object, f.Name, arg.Name) - } - - return nil -} - -// IsSubset checks if a is a subset of b. -func (q *Query) IsSubset(query string) (bool, error) { - doc, err := gqlparser.LoadQuery(s, query) - if err != nil { - return false, err - } - - var invalid bool - var e validator.Events - e.OnField(func(w *validator.Walker, d *ast.Field) { - if q.ValidateField(d) != nil { - invalid = true - } - }) - validator.Walk(s, doc, &e) - - return !invalid, nil -} diff --git a/graphql2/gqlauth/query_test.go b/graphql2/gqlauth/query_test.go deleted file mode 100644 index 32bdf9f1fe..0000000000 --- a/graphql2/gqlauth/query_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package gqlauth - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestQuery(t *testing.T) { - q, err := NewQuery(`query ($id: ID!) { user(id: $id) { id, name } }`) - require.NoError(t, err) - - isSub, err := q.IsSubset(`{ user { id } }`) - require.NoError(t, err) - assert.True(t, isSub) - - isSub, err = q.IsSubset(`query { user(id: "afs") { id } }`) - require.NoError(t, err) - assert.True(t, isSub) - - isSub, err = q.IsSubset(`query { user { id, name, email } }`) - require.NoError(t, err) - assert.False(t, isSub) -} -func TestQueryObj(t *testing.T) { - - q, err := NewQuery(`query ($in: DebugMessagesInput) { debugMessages(input: $in) { id } }`) - require.NoError(t, err) - - isSub, err := q.IsSubset(`{ debugMessages { id } }`) - require.NoError(t, err) - assert.True(t, isSub) - - isSub, err = q.IsSubset(`{ debugMessages(input:{first: 3}) { id } }`) - require.NoError(t, err) - assert.True(t, isSub) - -} -func TestQueryObjLim(t *testing.T) { - - q, err := NewQuery(`query ($first: Int) { debugMessages(input: {first: $first}) { id } }`) - require.NoError(t, err) - - isSub, err := q.IsSubset(`{ debugMessages { id } }`) - require.NoError(t, err) - assert.True(t, isSub) - - isSub, err = q.IsSubset(`{ debugMessages(input:{first: 3}) { id } }`) - require.NoError(t, err) - assert.True(t, isSub) - - isSub, err = q.IsSubset(`{ debugMessages(input:{createdBefore: "asdf"}) { id } }`) - require.NoError(t, err) - assert.False(t, isSub) - -} diff --git a/graphql2/graphqlapp/app.go b/graphql2/graphqlapp/app.go index b8125a9961..e6923d4f2a 100644 --- a/graphql2/graphqlapp/app.go +++ b/graphql2/graphqlapp/app.go @@ -24,7 +24,6 @@ import ( "github.com/target/goalert/config" "github.com/target/goalert/escalation" "github.com/target/goalert/graphql2" - "github.com/target/goalert/graphql2/gqlauth" "github.com/target/goalert/heartbeat" "github.com/target/goalert/integrationkey" "github.com/target/goalert/label" @@ -162,17 +161,22 @@ func (a *App) Handler() http.Handler { return next(ctx) } - qs, err := a.APIKeyStore.ContextQuery(ctx) - if err != nil { + p := apikey.PolicyFromContext(ctx) + if p == nil || p.GraphQLV1 == nil { return nil, permission.NewAccessDenied("invalid API key") } - q, err := gqlauth.NewQuery(qs) - if err != nil { - return nil, permission.NewAccessDenied("invalid API key") + f := graphql.GetFieldContext(ctx) + objName := f.Field.Field.ObjectDefinition.Name + fieldName := f.Field.Field.Definition.Name + + for _, allowed := range p.GraphQLV1.AllowedFields { + if allowed.ObjectName == objName && allowed.Name == fieldName { + return next(ctx) + } } - return q.InterceptField(ctx, next) + return nil, permission.NewAccessDenied("field not allowed by API key") }) h.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { diff --git a/migrate/migrations/20230721104236-graphql-api-key.sql b/migrate/migrations/20230721104236-graphql-api-key.sql index cc506f614a..94bcacd48d 100644 --- a/migrate/migrations/20230721104236-graphql-api-key.sql +++ b/migrate/migrations/20230721104236-graphql-api-key.sql @@ -4,8 +4,7 @@ CREATE TABLE api_keys( name text NOT NULL UNIQUE, user_id uuid REFERENCES users(id) ON DELETE CASCADE, service_id uuid REFERENCES services(id) ON DELETE CASCADE, - version integer NOT NULL DEFAULT 1, - data jsonb NOT NULL, + policy jsonb NOT NULL, created_at timestamp with time zone NOT NULL DEFAULT now(), updated_at timestamp with time zone NOT NULL DEFAULT now(), expires_at timestamp with time zone NOT NULL, diff --git a/migrate/schema.sql b/migrate/schema.sql index cd3f070473..1827d434ec 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,5 +1,5 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=30508a117ca8a653673c01ee354def8744b46f97fe4156b0283ec099cf547c20 - +-- DATA=c0065c713a31ae7431b49112f0d4063a9645a449e2a5753eadb6248723c44505 - -- DISK=ba641d09f148a96a980e9c03f7606e89f6acbb4b842fd2b381d9df877e0ea29c - -- PSQL=ba641d09f148a96a980e9c03f7606e89f6acbb4b842fd2b381d9df877e0ea29c - -- @@ -1675,8 +1675,7 @@ CREATE TABLE public.api_keys ( name text NOT NULL, user_id uuid, service_id uuid, - version integer DEFAULT 1 NOT NULL, - data jsonb NOT NULL, + policy jsonb NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, expires_at timestamp with time zone NOT NULL, From 48d0610f8cae5eea1ad0ef1391d74b097d5dadd3 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 11:06:18 -0500 Subject: [PATCH 07/56] update schema --- migrate/schema.sql | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/migrate/schema.sql b/migrate/schema.sql index d5602ff0e2..e355c7d5d2 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,7 +1,7 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=35c108711cef0a38d78d5872c062fe576844c7f16e309378a051faa813578de0 - --- DISK=b18bab67b9291c444025e882a3360c022c034d85477cad010781b2ec1b49fe3b - --- PSQL=b18bab67b9291c444025e882a3360c022c034d85477cad010781b2ec1b49fe3b - +-- DATA=b22dc5f530071e8dc981a1411df399a55ceae290e8d52f92d6db6fd7b92ff912 - +-- DISK=0bc7602e896fa75956e969791faa4e469ef984af481a6c83bf73d81c54a7ab01 - +-- PSQL=0bc7602e896fa75956e969791faa4e469ef984af481a6c83bf73d81c54a7ab01 - -- -- pgdump-lite database dump -- @@ -1361,6 +1361,26 @@ CREATE CONSTRAINT TRIGGER trg_enforce_alert_limit AFTER INSERT ON public.alerts CREATE TRIGGER trg_prevent_reopen BEFORE UPDATE OF status ON public.alerts FOR EACH ROW EXECUTE FUNCTION fn_prevent_reopen(); +CREATE TABLE api_keys ( + created_at timestamp with time zone DEFAULT now() NOT NULL, + expires_at timestamp with time zone NOT NULL, + id uuid NOT NULL, + last_used_at timestamp with time zone, + name text NOT NULL, + policy jsonb NOT NULL, + service_id uuid, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + user_id uuid, + CONSTRAINT api_keys_name_key UNIQUE (name), + CONSTRAINT api_keys_pkey PRIMARY KEY (id), + CONSTRAINT api_keys_service_id_fkey FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE, + CONSTRAINT api_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX api_keys_name_key ON public.api_keys USING btree (name); +CREATE UNIQUE INDEX api_keys_pkey ON public.api_keys USING btree (id); + + CREATE TABLE auth_basic_users ( id bigint DEFAULT nextval('auth_basic_users_id_seq'::regclass) NOT NULL, password_hash text NOT NULL, From 89eb6c0297d1dc542ce5326b0fcaf82f2f41fc03 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 11:07:30 -0500 Subject: [PATCH 08/56] fix circular dep with db-schema cmd --- Makefile | 10 +++++----- migrate/cmd/goalert-migrate/main.go | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 migrate/cmd/goalert-migrate/main.go diff --git a/Makefile b/Makefile index 7d2fe38fad..5f401352f5 100644 --- a/Makefile +++ b/Makefile @@ -260,15 +260,15 @@ smoketest: test-migrations: bin/goalert (cd test/smoke && go test -run TestMigrations) -db-schema: bin/goalert bin/psql-lite bin/pgdump-lite - ./bin/psql-lite -d "$(DB_URL)" -c 'DROP DATABASE IF EXISTS mk_dump_schema; CREATE DATABASE mk_dump_schema;' - ./bin/goalert migrate --db-url "$(dir $(DB_URL))mk_dump_schema" +db-schema: + go run ./devtools/psql-lite -d "$(DB_URL)" -c 'DROP DATABASE IF EXISTS mk_dump_schema; CREATE DATABASE mk_dump_schema;' + go run ./migrate/cmd/goalert-migrate --db-url "$(dir $(DB_URL))mk_dump_schema" up echo '-- This file is auto-generated by "make db-schema"; DO NOT EDIT' > migrate/schema.sql echo "-- DATA=$(shell $(SHA_CMD) migrate/migrations/* | sort | $(SHA_CMD))" >> migrate/schema.sql echo "-- DISK=$(shell ls migrate/migrations | sort | $(SHA_CMD))" >> migrate/schema.sql echo "-- PSQL=$$(psql -d '$(dir $(DB_URL))mk_dump_schema' -XqAtc 'select id from gorp_migrations order by id' | sort | $(SHA_CMD))" >> migrate/schema.sql - ./bin/pgdump-lite -d "$(dir $(DB_URL))mk_dump_schema" -s >> migrate/schema.sql - ./bin/psql-lite -d "$(DB_URL)" -c 'DROP DATABASE IF EXISTS mk_dump_schema;' + go run ./devtools/pgdump-lite/cmd/pgdump-lite -d "$(dir $(DB_URL))mk_dump_schema" -s >> migrate/schema.sql + go run ./devtools/psql-lite -d "$(DB_URL)" -c 'DROP DATABASE IF EXISTS mk_dump_schema;' tools: go get -u golang.org/x/tools/cmd/gorename diff --git a/migrate/cmd/goalert-migrate/main.go b/migrate/cmd/goalert-migrate/main.go new file mode 100644 index 0000000000..f6bf3467d4 --- /dev/null +++ b/migrate/cmd/goalert-migrate/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "flag" + "os" + + "github.com/target/goalert/migrate" +) + +func main() { + db := flag.String("db-url", os.Getenv("GOALERT_DB_URL"), "Database URL") + flag.Parse() + + _, err := migrate.ApplyAll(context.Background(), *db) + if err != nil { + panic(err) + } +} From 61e33057574960e05eb7b5a6d37edc81a5c75258 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 11:21:15 -0500 Subject: [PATCH 09/56] update schema --- gadb/models.go | 10 +- gadb/queries.sql.go | 24 +- graphql2/generated.go | 1421 +++++++++++++++++++++++++++++++-------- graphql2/models_gen.go | 27 +- graphql2/schema.graphql | 23 +- web/src/schema.d.ts | 21 +- 6 files changed, 1220 insertions(+), 306 deletions(-) diff --git a/gadb/models.go b/gadb/models.go index 9660d3791e..622328ffb9 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -800,15 +800,15 @@ type AlertStatusSubscription struct { } type ApiKey struct { + CreatedAt time.Time + ExpiresAt time.Time ID uuid.UUID + LastUsedAt sql.NullTime Name string - UserID uuid.NullUUID - ServiceID uuid.NullUUID Policy json.RawMessage - CreatedAt time.Time + ServiceID uuid.NullUUID UpdatedAt time.Time - ExpiresAt time.Time - LastUsedAt sql.NullTime + UserID uuid.NullUUID } type AuthBasicUser struct { diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index 4f1b260da9..0576f3c23c 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -24,22 +24,22 @@ SET WHERE id = $1 RETURNING - id, name, user_id, service_id, policy, created_at, updated_at, expires_at, last_used_at + created_at, expires_at, id, last_used_at, name, policy, service_id, updated_at, user_id ` func (q *Queries) APIKeyAuth(ctx context.Context, id uuid.UUID) (ApiKey, error) { row := q.db.QueryRowContext(ctx, aPIKeyAuth, id) var i ApiKey err := row.Scan( + &i.CreatedAt, + &i.ExpiresAt, &i.ID, + &i.LastUsedAt, &i.Name, - &i.UserID, - &i.ServiceID, &i.Policy, - &i.CreatedAt, + &i.ServiceID, &i.UpdatedAt, - &i.ExpiresAt, - &i.LastUsedAt, + &i.UserID, ) return i, err } @@ -56,7 +56,7 @@ func (q *Queries) APIKeyDelete(ctx context.Context, id uuid.UUID) error { const aPIKeyGet = `-- name: APIKeyGet :one SELECT - id, name, user_id, service_id, policy, created_at, updated_at, expires_at, last_used_at + created_at, expires_at, id, last_used_at, name, policy, service_id, updated_at, user_id FROM api_keys WHERE @@ -67,15 +67,15 @@ func (q *Queries) APIKeyGet(ctx context.Context, id uuid.UUID) (ApiKey, error) { row := q.db.QueryRowContext(ctx, aPIKeyGet, id) var i ApiKey err := row.Scan( + &i.CreatedAt, + &i.ExpiresAt, &i.ID, + &i.LastUsedAt, &i.Name, - &i.UserID, - &i.ServiceID, &i.Policy, - &i.CreatedAt, + &i.ServiceID, &i.UpdatedAt, - &i.ExpiresAt, - &i.LastUsedAt, + &i.UserID, ) return i, err } diff --git a/graphql2/generated.go b/graphql2/generated.go index 1da7eaa365..e93ce4369d 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -232,10 +232,16 @@ type ComplexityRoot struct { } GQLAPIKey struct { - ExpiresAt func(childComplexity int) int - ID func(childComplexity int) int - Name func(childComplexity int) int - Token func(childComplexity int) int + AllowedFields func(childComplexity int) int + CreatedAt func(childComplexity int) int + CreatedBy func(childComplexity int) int + Description func(childComplexity int) int + ExpiresAt func(childComplexity int) int + ID func(childComplexity int) int + LastUsed func(childComplexity int) int + LastUsedUa func(childComplexity int) int + Name func(childComplexity int) int + Token func(childComplexity int) int } HeartbeatMonitor struct { @@ -316,6 +322,7 @@ type ComplexityRoot struct { DebugSendSms func(childComplexity int, input DebugSendSMSInput) int DeleteAll func(childComplexity int, input []assignment.RawTarget) int DeleteAuthSubject func(childComplexity int, input user.AuthSubject) int + DeleteGQLAPIKey func(childComplexity int, id string) int EndAllAuthSessionsByCurrentUser func(childComplexity int) int EscalateAlerts func(childComplexity int, input []int) int LinkAccount func(childComplexity int, token string) int @@ -334,6 +341,7 @@ type ComplexityRoot struct { UpdateBasicAuth func(childComplexity int, input UpdateBasicAuthInput) int UpdateEscalationPolicy func(childComplexity int, input UpdateEscalationPolicyInput) int UpdateEscalationPolicyStep func(childComplexity int, input UpdateEscalationPolicyStepInput) int + UpdateGQLAPIKey func(childComplexity int, input UpdateGQLAPIKeyInput) int UpdateHeartbeatMonitor func(childComplexity int, input UpdateHeartbeatMonitorInput) int UpdateRotation func(childComplexity int, input UpdateRotationInput) int UpdateSchedule func(childComplexity int, input UpdateScheduleInput) int @@ -400,6 +408,7 @@ type ComplexityRoot struct { EscalationPolicy func(childComplexity int, id string) int ExperimentalFlags func(childComplexity int) int GenerateSlackAppManifest func(childComplexity int) int + GqlAPIKeys func(childComplexity int) int HeartbeatMonitor func(childComplexity int, id string) int IntegrationKey func(childComplexity int, id string) int IntegrationKeyTypes func(childComplexity int) int @@ -408,6 +417,7 @@ type ComplexityRoot struct { LabelValues func(childComplexity int, input *LabelValueSearchOptions) int Labels func(childComplexity int, input *LabelSearchOptions) int LinkAccountInfo func(childComplexity int, token string) int + ListGQLFields func(childComplexity int, query *string) int MessageLogs func(childComplexity int, input *MessageLogSearchOptions) int PhoneNumberInfo func(childComplexity int, number string) int Rotation func(childComplexity int, id string) int @@ -765,6 +775,8 @@ type MutationResolver interface { SetConfig(ctx context.Context, input []ConfigValueInput) (bool, error) SetSystemLimits(ctx context.Context, input []SystemLimitInput) (bool, error) CreateGQLAPIKey(ctx context.Context, input CreateGQLAPIKeyInput) (*GQLAPIKey, error) + UpdateGQLAPIKey(ctx context.Context, input UpdateGQLAPIKeyInput) (bool, error) + DeleteGQLAPIKey(ctx context.Context, id string) (bool, error) CreateBasicAuth(ctx context.Context, input CreateBasicAuthInput) (bool, error) UpdateBasicAuth(ctx context.Context, input UpdateBasicAuthInput) (bool, error) } @@ -816,6 +828,8 @@ type QueryResolver interface { GenerateSlackAppManifest(ctx context.Context) (string, error) LinkAccountInfo(ctx context.Context, token string) (*LinkAccountInfo, error) SwoStatus(ctx context.Context) (*SWOStatus, error) + GqlAPIKeys(ctx context.Context) ([]GQLAPIKey, error) + ListGQLFields(ctx context.Context, query *string) ([]string, error) } type RotationResolver interface { IsFavorite(ctx context.Context, obj *rotation.Rotation) (bool, error) @@ -1489,6 +1503,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.EscalationPolicyStep.Targets(childComplexity), true + case "GQLAPIKey.allowedFields": + if e.complexity.GQLAPIKey.AllowedFields == nil { + break + } + + return e.complexity.GQLAPIKey.AllowedFields(childComplexity), true + + case "GQLAPIKey.createdAt": + if e.complexity.GQLAPIKey.CreatedAt == nil { + break + } + + return e.complexity.GQLAPIKey.CreatedAt(childComplexity), true + + case "GQLAPIKey.createdBy": + if e.complexity.GQLAPIKey.CreatedBy == nil { + break + } + + return e.complexity.GQLAPIKey.CreatedBy(childComplexity), true + + case "GQLAPIKey.description": + if e.complexity.GQLAPIKey.Description == nil { + break + } + + return e.complexity.GQLAPIKey.Description(childComplexity), true + case "GQLAPIKey.expiresAt": if e.complexity.GQLAPIKey.ExpiresAt == nil { break @@ -1503,6 +1545,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.GQLAPIKey.ID(childComplexity), true + case "GQLAPIKey.lastUsed": + if e.complexity.GQLAPIKey.LastUsed == nil { + break + } + + return e.complexity.GQLAPIKey.LastUsed(childComplexity), true + + case "GQLAPIKey.lastUsedUA": + if e.complexity.GQLAPIKey.LastUsedUa == nil { + break + } + + return e.complexity.GQLAPIKey.LastUsedUa(childComplexity), true + case "GQLAPIKey.name": if e.complexity.GQLAPIKey.Name == nil { break @@ -1977,6 +2033,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteAuthSubject(childComplexity, args["input"].(user.AuthSubject)), true + case "Mutation.deleteGQLAPIKey": + if e.complexity.Mutation.DeleteGQLAPIKey == nil { + break + } + + args, err := ec.field_Mutation_deleteGQLAPIKey_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteGQLAPIKey(childComplexity, args["id"].(string)), true + case "Mutation.endAllAuthSessionsByCurrentUser": if e.complexity.Mutation.EndAllAuthSessionsByCurrentUser == nil { break @@ -2188,6 +2256,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateEscalationPolicyStep(childComplexity, args["input"].(UpdateEscalationPolicyStepInput)), true + case "Mutation.updateGQLAPIKey": + if e.complexity.Mutation.UpdateGQLAPIKey == nil { + break + } + + args, err := ec.field_Mutation_updateGQLAPIKey_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateGQLAPIKey(childComplexity, args["input"].(UpdateGQLAPIKeyInput)), true + case "Mutation.updateHeartbeatMonitor": if e.complexity.Mutation.UpdateHeartbeatMonitor == nil { break @@ -2598,6 +2678,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.GenerateSlackAppManifest(childComplexity), true + case "Query.gqlAPIKeys": + if e.complexity.Query.GqlAPIKeys == nil { + break + } + + return e.complexity.Query.GqlAPIKeys(childComplexity), true + case "Query.heartbeatMonitor": if e.complexity.Query.HeartbeatMonitor == nil { break @@ -2689,6 +2776,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.LinkAccountInfo(childComplexity, args["token"].(string)), true + case "Query.listGQLFields": + if e.complexity.Query.ListGQLFields == nil { + break + } + + args, err := ec.field_Query_listGQLFields_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.ListGQLFields(childComplexity, args["query"].(*string)), true + case "Query.messageLogs": if e.complexity.Query.MessageLogs == nil { break @@ -4053,6 +4152,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputUpdateBasicAuthInput, ec.unmarshalInputUpdateEscalationPolicyInput, ec.unmarshalInputUpdateEscalationPolicyStepInput, + ec.unmarshalInputUpdateGQLAPIKeyInput, ec.unmarshalInputUpdateHeartbeatMonitorInput, ec.unmarshalInputUpdateRotationInput, ec.unmarshalInputUpdateScheduleInput, @@ -4525,6 +4625,21 @@ func (ec *executionContext) field_Mutation_deleteAuthSubject_args(ctx context.Co return args, nil } +func (ec *executionContext) field_Mutation_deleteGQLAPIKey_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_escalateAlerts_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4780,6 +4895,21 @@ func (ec *executionContext) field_Mutation_updateEscalationPolicy_args(ctx conte return args, nil } +func (ec *executionContext) field_Mutation_updateGQLAPIKey_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 UpdateGQLAPIKeyInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNUpdateGQLAPIKeyInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateGQLAPIKeyInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_updateHeartbeatMonitor_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -5203,6 +5333,21 @@ func (ec *executionContext) field_Query_linkAccountInfo_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Query_listGQLFields_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 *string + if tmp, ok := rawArgs["query"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("query")) + arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["query"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_messageLogs_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -9484,8 +9629,8 @@ func (ec *executionContext) fieldContext_GQLAPIKey_name(ctx context.Context, fie return fc, nil } -func (ec *executionContext) _GQLAPIKey_token(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_GQLAPIKey_token(ctx, field) +func (ec *executionContext) _GQLAPIKey_description(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_description(ctx, field) if err != nil { return graphql.Null } @@ -9498,21 +9643,24 @@ func (ec *executionContext) _GQLAPIKey_token(ctx context.Context, field graphql. }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Token, nil + return obj.Description, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*string) + res := resTmp.(string) fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_GQLAPIKey_token(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "GQLAPIKey", Field: field, @@ -9525,8 +9673,8 @@ func (ec *executionContext) fieldContext_GQLAPIKey_token(ctx context.Context, fi return fc, nil } -func (ec *executionContext) _GQLAPIKey_expiresAt(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) +func (ec *executionContext) _GQLAPIKey_createdAt(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_createdAt(ctx, field) if err != nil { return graphql.Null } @@ -9539,7 +9687,7 @@ func (ec *executionContext) _GQLAPIKey_expiresAt(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.ExpiresAt, nil + return obj.CreatedAt, nil }) if err != nil { ec.Error(ctx, err) @@ -9556,7 +9704,7 @@ func (ec *executionContext) _GQLAPIKey_expiresAt(ctx context.Context, field grap return ec.marshalNISOTimestamp2timeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_GQLAPIKey_expiresAt(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_createdAt(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "GQLAPIKey", Field: field, @@ -9569,8 +9717,8 @@ func (ec *executionContext) fieldContext_GQLAPIKey_expiresAt(ctx context.Context return fc, nil } -func (ec *executionContext) _HeartbeatMonitor_id(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_HeartbeatMonitor_id(ctx, field) +func (ec *executionContext) _GQLAPIKey_createdBy(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_createdBy(ctx, field) if err != nil { return graphql.Null } @@ -9583,38 +9731,61 @@ func (ec *executionContext) _HeartbeatMonitor_id(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.ID, nil + return obj.CreatedBy, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.(string) + res := resTmp.(*user.User) fc.Result = res - return ec.marshalNID2string(ctx, field.Selections, res) + return ec.marshalOUser2ᚖgithubᚗcomᚋtargetᚋgoalertᚋuserᚐUser(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_HeartbeatMonitor_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_createdBy(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "HeartbeatMonitor", + Object: "GQLAPIKey", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_User_id(ctx, field) + case "role": + return ec.fieldContext_User_role(ctx, field) + case "name": + return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "contactMethods": + return ec.fieldContext_User_contactMethods(ctx, field) + case "notificationRules": + return ec.fieldContext_User_notificationRules(ctx, field) + case "calendarSubscriptions": + return ec.fieldContext_User_calendarSubscriptions(ctx, field) + case "statusUpdateContactMethodID": + return ec.fieldContext_User_statusUpdateContactMethodID(ctx, field) + case "authSubjects": + return ec.fieldContext_User_authSubjects(ctx, field) + case "sessions": + return ec.fieldContext_User_sessions(ctx, field) + case "onCallSteps": + return ec.fieldContext_User_onCallSteps(ctx, field) + case "isFavorite": + return ec.fieldContext_User_isFavorite(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, } return fc, nil } -func (ec *executionContext) _HeartbeatMonitor_serviceID(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_HeartbeatMonitor_serviceID(ctx, field) +func (ec *executionContext) _GQLAPIKey_lastUsed(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_lastUsed(ctx, field) if err != nil { return graphql.Null } @@ -9627,7 +9798,7 @@ func (ec *executionContext) _HeartbeatMonitor_serviceID(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.ServiceID, nil + return obj.LastUsed, nil }) if err != nil { ec.Error(ctx, err) @@ -9639,26 +9810,26 @@ func (ec *executionContext) _HeartbeatMonitor_serviceID(ctx context.Context, fie } return graphql.Null } - res := resTmp.(string) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNID2string(ctx, field.Selections, res) + return ec.marshalNISOTimestamp2timeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_HeartbeatMonitor_serviceID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_lastUsed(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "HeartbeatMonitor", + Object: "GQLAPIKey", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") + return nil, errors.New("field of type ISOTimestamp does not have child fields") }, } return fc, nil } -func (ec *executionContext) _HeartbeatMonitor_name(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_HeartbeatMonitor_name(ctx, field) +func (ec *executionContext) _GQLAPIKey_lastUsedUA(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_lastUsedUA(ctx, field) if err != nil { return graphql.Null } @@ -9671,7 +9842,7 @@ func (ec *executionContext) _HeartbeatMonitor_name(ctx context.Context, field gr }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Name, nil + return obj.LastUsedUa, nil }) if err != nil { ec.Error(ctx, err) @@ -9688,9 +9859,9 @@ func (ec *executionContext) _HeartbeatMonitor_name(ctx context.Context, field gr return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_HeartbeatMonitor_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_lastUsedUA(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "HeartbeatMonitor", + Object: "GQLAPIKey", Field: field, IsMethod: false, IsResolver: false, @@ -9701,8 +9872,8 @@ func (ec *executionContext) fieldContext_HeartbeatMonitor_name(ctx context.Conte return fc, nil } -func (ec *executionContext) _HeartbeatMonitor_timeoutMinutes(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_HeartbeatMonitor_timeoutMinutes(ctx, field) +func (ec *executionContext) _GQLAPIKey_expiresAt(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) if err != nil { return graphql.Null } @@ -9715,7 +9886,7 @@ func (ec *executionContext) _HeartbeatMonitor_timeoutMinutes(ctx context.Context }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.HeartbeatMonitor().TimeoutMinutes(rctx, obj) + return obj.ExpiresAt, nil }) if err != nil { ec.Error(ctx, err) @@ -9727,26 +9898,26 @@ func (ec *executionContext) _HeartbeatMonitor_timeoutMinutes(ctx context.Context } return graphql.Null } - res := resTmp.(int) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) + return ec.marshalNISOTimestamp2timeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_HeartbeatMonitor_timeoutMinutes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_expiresAt(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "HeartbeatMonitor", + Object: "GQLAPIKey", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return nil, errors.New("field of type ISOTimestamp does not have child fields") }, } return fc, nil } -func (ec *executionContext) _HeartbeatMonitor_lastState(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_HeartbeatMonitor_lastState(ctx, field) +func (ec *executionContext) _GQLAPIKey_allowedFields(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_allowedFields(ctx, field) if err != nil { return graphql.Null } @@ -9759,7 +9930,7 @@ func (ec *executionContext) _HeartbeatMonitor_lastState(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.LastState(), nil + return obj.AllowedFields, nil }) if err != nil { ec.Error(ctx, err) @@ -9771,26 +9942,26 @@ func (ec *executionContext) _HeartbeatMonitor_lastState(ctx context.Context, fie } return graphql.Null } - res := resTmp.(heartbeat.State) + res := resTmp.([]string) fc.Result = res - return ec.marshalNHeartbeatMonitorState2githubᚗcomᚋtargetᚋgoalertᚋheartbeatᚐState(ctx, field.Selections, res) + return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_HeartbeatMonitor_lastState(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_allowedFields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "HeartbeatMonitor", + Object: "GQLAPIKey", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type HeartbeatMonitorState does not have child fields") + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } -func (ec *executionContext) _HeartbeatMonitor_lastHeartbeat(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_HeartbeatMonitor_lastHeartbeat(ctx, field) +func (ec *executionContext) _GQLAPIKey_token(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_token(ctx, field) if err != nil { return graphql.Null } @@ -9803,7 +9974,7 @@ func (ec *executionContext) _HeartbeatMonitor_lastHeartbeat(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.LastHeartbeat(), nil + return obj.Token, nil }) if err != nil { ec.Error(ctx, err) @@ -9812,61 +9983,17 @@ func (ec *executionContext) _HeartbeatMonitor_lastHeartbeat(ctx context.Context, if resTmp == nil { return graphql.Null } - res := resTmp.(time.Time) + res := resTmp.(*string) fc.Result = res - return ec.marshalOISOTimestamp2timeᚐTime(ctx, field.Selections, res) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_HeartbeatMonitor_lastHeartbeat(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_token(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "HeartbeatMonitor", + Object: "GQLAPIKey", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ISOTimestamp does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _HeartbeatMonitor_href(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_HeartbeatMonitor_href(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.HeartbeatMonitor().Href(rctx, obj) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_HeartbeatMonitor_href(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "HeartbeatMonitor", - Field: field, - IsMethod: true, - IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, @@ -9874,8 +10001,8 @@ func (ec *executionContext) fieldContext_HeartbeatMonitor_href(ctx context.Conte return fc, nil } -func (ec *executionContext) _IntegrationKey_id(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKey_id(ctx, field) +func (ec *executionContext) _HeartbeatMonitor_id(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_id(ctx, field) if err != nil { return graphql.Null } @@ -9905,9 +10032,9 @@ func (ec *executionContext) _IntegrationKey_id(ctx context.Context, field graphq return ec.marshalNID2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKey_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_HeartbeatMonitor_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKey", + Object: "HeartbeatMonitor", Field: field, IsMethod: false, IsResolver: false, @@ -9918,8 +10045,8 @@ func (ec *executionContext) fieldContext_IntegrationKey_id(ctx context.Context, return fc, nil } -func (ec *executionContext) _IntegrationKey_serviceID(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKey_serviceID(ctx, field) +func (ec *executionContext) _HeartbeatMonitor_serviceID(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_serviceID(ctx, field) if err != nil { return graphql.Null } @@ -9949,9 +10076,9 @@ func (ec *executionContext) _IntegrationKey_serviceID(ctx context.Context, field return ec.marshalNID2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKey_serviceID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_HeartbeatMonitor_serviceID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKey", + Object: "HeartbeatMonitor", Field: field, IsMethod: false, IsResolver: false, @@ -9962,52 +10089,8 @@ func (ec *executionContext) fieldContext_IntegrationKey_serviceID(ctx context.Co return fc, nil } -func (ec *executionContext) _IntegrationKey_type(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKey_type(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.IntegrationKey().Type(rctx, obj) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(IntegrationKeyType) - fc.Result = res - return ec.marshalNIntegrationKeyType2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐIntegrationKeyType(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_IntegrationKey_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "IntegrationKey", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type IntegrationKeyType does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _IntegrationKey_name(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKey_name(ctx, field) +func (ec *executionContext) _HeartbeatMonitor_name(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_name(ctx, field) if err != nil { return graphql.Null } @@ -10037,9 +10120,9 @@ func (ec *executionContext) _IntegrationKey_name(ctx context.Context, field grap return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKey_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_HeartbeatMonitor_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKey", + Object: "HeartbeatMonitor", Field: field, IsMethod: false, IsResolver: false, @@ -10050,8 +10133,8 @@ func (ec *executionContext) fieldContext_IntegrationKey_name(ctx context.Context return fc, nil } -func (ec *executionContext) _IntegrationKey_href(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKey_href(ctx, field) +func (ec *executionContext) _HeartbeatMonitor_timeoutMinutes(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_timeoutMinutes(ctx, field) if err != nil { return graphql.Null } @@ -10064,7 +10147,7 @@ func (ec *executionContext) _IntegrationKey_href(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.IntegrationKey().Href(rctx, obj) + return ec.resolvers.HeartbeatMonitor().TimeoutMinutes(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -10076,26 +10159,26 @@ func (ec *executionContext) _IntegrationKey_href(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(string) + res := resTmp.(int) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNInt2int(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKey_href(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_HeartbeatMonitor_timeoutMinutes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKey", + Object: "HeartbeatMonitor", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, errors.New("field of type Int does not have child fields") }, } return fc, nil } -func (ec *executionContext) _IntegrationKeyConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKeyConnection_nodes(ctx, field) +func (ec *executionContext) _HeartbeatMonitor_lastState(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_lastState(ctx, field) if err != nil { return graphql.Null } @@ -10108,7 +10191,7 @@ func (ec *executionContext) _IntegrationKeyConnection_nodes(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Nodes, nil + return obj.LastState(), nil }) if err != nil { ec.Error(ctx, err) @@ -10120,38 +10203,26 @@ func (ec *executionContext) _IntegrationKeyConnection_nodes(ctx context.Context, } return graphql.Null } - res := resTmp.([]integrationkey.IntegrationKey) + res := resTmp.(heartbeat.State) fc.Result = res - return ec.marshalNIntegrationKey2ᚕgithubᚗcomᚋtargetᚋgoalertᚋintegrationkeyᚐIntegrationKeyᚄ(ctx, field.Selections, res) + return ec.marshalNHeartbeatMonitorState2githubᚗcomᚋtargetᚋgoalertᚋheartbeatᚐState(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKeyConnection_nodes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_HeartbeatMonitor_lastState(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKeyConnection", + Object: "HeartbeatMonitor", Field: field, - IsMethod: false, + IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_IntegrationKey_id(ctx, field) - case "serviceID": - return ec.fieldContext_IntegrationKey_serviceID(ctx, field) - case "type": - return ec.fieldContext_IntegrationKey_type(ctx, field) - case "name": - return ec.fieldContext_IntegrationKey_name(ctx, field) - case "href": - return ec.fieldContext_IntegrationKey_href(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type IntegrationKey", field.Name) + return nil, errors.New("field of type HeartbeatMonitorState does not have child fields") }, } return fc, nil } -func (ec *executionContext) _IntegrationKeyConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKeyConnection_pageInfo(ctx, field) +func (ec *executionContext) _HeartbeatMonitor_lastHeartbeat(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_lastHeartbeat(ctx, field) if err != nil { return graphql.Null } @@ -10164,44 +10235,35 @@ func (ec *executionContext) _IntegrationKeyConnection_pageInfo(ctx context.Conte }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.PageInfo, nil + return obj.LastHeartbeat(), nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.(*PageInfo) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNPageInfo2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐPageInfo(ctx, field.Selections, res) + return ec.marshalOISOTimestamp2timeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKeyConnection_pageInfo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_HeartbeatMonitor_lastHeartbeat(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKeyConnection", + Object: "HeartbeatMonitor", Field: field, - IsMethod: false, + IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "endCursor": - return ec.fieldContext_PageInfo_endCursor(ctx, field) - case "hasNextPage": - return ec.fieldContext_PageInfo_hasNextPage(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + return nil, errors.New("field of type ISOTimestamp does not have child fields") }, } return fc, nil } -func (ec *executionContext) _IntegrationKeyTypeInfo_id(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKeyTypeInfo_id(ctx, field) +func (ec *executionContext) _HeartbeatMonitor_href(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_href(ctx, field) if err != nil { return graphql.Null } @@ -10214,7 +10276,7 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_id(ctx context.Context, fiel }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.ID, nil + return ec.resolvers.HeartbeatMonitor().Href(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -10228,24 +10290,24 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_id(ctx context.Context, fiel } res := resTmp.(string) fc.Result = res - return ec.marshalNID2string(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_HeartbeatMonitor_href(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKeyTypeInfo", + Object: "HeartbeatMonitor", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } -func (ec *executionContext) _IntegrationKeyTypeInfo_name(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKeyTypeInfo_name(ctx, field) +func (ec *executionContext) _IntegrationKey_id(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKey_id(ctx, field) if err != nil { return graphql.Null } @@ -10258,7 +10320,7 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_name(ctx context.Context, fi }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Name, nil + return obj.ID, nil }) if err != nil { ec.Error(ctx, err) @@ -10272,24 +10334,24 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_name(ctx context.Context, fi } res := resTmp.(string) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNID2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_IntegrationKey_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKeyTypeInfo", + Object: "IntegrationKey", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, errors.New("field of type ID does not have child fields") }, } return fc, nil } -func (ec *executionContext) _IntegrationKeyTypeInfo_label(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKeyTypeInfo_label(ctx, field) +func (ec *executionContext) _IntegrationKey_serviceID(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKey_serviceID(ctx, field) if err != nil { return graphql.Null } @@ -10302,7 +10364,7 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_label(ctx context.Context, f }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Label, nil + return obj.ServiceID, nil }) if err != nil { ec.Error(ctx, err) @@ -10316,24 +10378,24 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_label(ctx context.Context, f } res := resTmp.(string) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNID2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_label(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_IntegrationKey_serviceID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKeyTypeInfo", + Object: "IntegrationKey", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, errors.New("field of type ID does not have child fields") }, } return fc, nil } -func (ec *executionContext) _IntegrationKeyTypeInfo_enabled(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_IntegrationKeyTypeInfo_enabled(ctx, field) +func (ec *executionContext) _IntegrationKey_type(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKey_type(ctx, field) if err != nil { return graphql.Null } @@ -10346,7 +10408,7 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_enabled(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Enabled, nil + return ec.resolvers.IntegrationKey().Type(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -10358,26 +10420,26 @@ func (ec *executionContext) _IntegrationKeyTypeInfo_enabled(ctx context.Context, } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(IntegrationKeyType) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNIntegrationKeyType2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐIntegrationKeyType(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_enabled(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_IntegrationKey_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "IntegrationKeyTypeInfo", + Object: "IntegrationKey", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return nil, errors.New("field of type IntegrationKeyType does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Label_key(ctx context.Context, field graphql.CollectedField, obj *label.Label) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Label_key(ctx, field) +func (ec *executionContext) _IntegrationKey_name(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKey_name(ctx, field) if err != nil { return graphql.Null } @@ -10390,7 +10452,7 @@ func (ec *executionContext) _Label_key(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Key, nil + return obj.Name, nil }) if err != nil { ec.Error(ctx, err) @@ -10407,9 +10469,9 @@ func (ec *executionContext) _Label_key(ctx context.Context, field graphql.Collec return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Label_key(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_IntegrationKey_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Label", + Object: "IntegrationKey", Field: field, IsMethod: false, IsResolver: false, @@ -10420,8 +10482,8 @@ func (ec *executionContext) fieldContext_Label_key(ctx context.Context, field gr return fc, nil } -func (ec *executionContext) _Label_value(ctx context.Context, field graphql.CollectedField, obj *label.Label) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Label_value(ctx, field) +func (ec *executionContext) _IntegrationKey_href(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKey_href(ctx, field) if err != nil { return graphql.Null } @@ -10434,7 +10496,7 @@ func (ec *executionContext) _Label_value(ctx context.Context, field graphql.Coll }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Value, nil + return ec.resolvers.IntegrationKey().Href(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -10451,12 +10513,12 @@ func (ec *executionContext) _Label_value(ctx context.Context, field graphql.Coll return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Label_value(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_IntegrationKey_href(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Label", + Object: "IntegrationKey", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, @@ -10464,8 +10526,8 @@ func (ec *executionContext) fieldContext_Label_value(ctx context.Context, field return fc, nil } -func (ec *executionContext) _LabelConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *LabelConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_LabelConnection_nodes(ctx, field) +func (ec *executionContext) _IntegrationKeyConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKeyConnection_nodes(ctx, field) if err != nil { return graphql.Null } @@ -10490,32 +10552,402 @@ func (ec *executionContext) _LabelConnection_nodes(ctx context.Context, field gr } return graphql.Null } - res := resTmp.([]label.Label) + res := resTmp.([]integrationkey.IntegrationKey) fc.Result = res - return ec.marshalNLabel2ᚕgithubᚗcomᚋtargetᚋgoalertᚋlabelᚐLabelᚄ(ctx, field.Selections, res) + return ec.marshalNIntegrationKey2ᚕgithubᚗcomᚋtargetᚋgoalertᚋintegrationkeyᚐIntegrationKeyᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_LabelConnection_nodes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_IntegrationKeyConnection_nodes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "LabelConnection", + Object: "IntegrationKeyConnection", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "key": - return ec.fieldContext_Label_key(ctx, field) - case "value": - return ec.fieldContext_Label_value(ctx, field) + case "id": + return ec.fieldContext_IntegrationKey_id(ctx, field) + case "serviceID": + return ec.fieldContext_IntegrationKey_serviceID(ctx, field) + case "type": + return ec.fieldContext_IntegrationKey_type(ctx, field) + case "name": + return ec.fieldContext_IntegrationKey_name(ctx, field) + case "href": + return ec.fieldContext_IntegrationKey_href(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type Label", field.Name) + return nil, fmt.Errorf("no field named %q was found under type IntegrationKey", field.Name) }, } return fc, nil } -func (ec *executionContext) _LabelConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *LabelConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_LabelConnection_pageInfo(ctx, field) +func (ec *executionContext) _IntegrationKeyConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKeyConnection_pageInfo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PageInfo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*PageInfo) + fc.Result = res + return ec.marshalNPageInfo2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐPageInfo(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_IntegrationKeyConnection_pageInfo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IntegrationKeyConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "endCursor": + return ec.fieldContext_PageInfo_endCursor(ctx, field) + case "hasNextPage": + return ec.fieldContext_PageInfo_hasNextPage(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _IntegrationKeyTypeInfo_id(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKeyTypeInfo_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IntegrationKeyTypeInfo", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _IntegrationKeyTypeInfo_name(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKeyTypeInfo_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IntegrationKeyTypeInfo", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _IntegrationKeyTypeInfo_label(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKeyTypeInfo_label(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Label, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_label(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IntegrationKeyTypeInfo", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _IntegrationKeyTypeInfo_enabled(ctx context.Context, field graphql.CollectedField, obj *IntegrationKeyTypeInfo) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_IntegrationKeyTypeInfo_enabled(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Enabled, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_IntegrationKeyTypeInfo_enabled(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IntegrationKeyTypeInfo", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Label_key(ctx context.Context, field graphql.CollectedField, obj *label.Label) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Label_key(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Key, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Label_key(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Label", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Label_value(ctx context.Context, field graphql.CollectedField, obj *label.Label) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Label_value(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Value, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Label_value(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Label", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _LabelConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *LabelConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_LabelConnection_nodes(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Nodes, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]label.Label) + fc.Result = res + return ec.marshalNLabel2ᚕgithubᚗcomᚋtargetᚋgoalertᚋlabelᚐLabelᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_LabelConnection_nodes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "LabelConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "key": + return ec.fieldContext_Label_key(ctx, field) + case "value": + return ec.fieldContext_Label_value(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Label", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _LabelConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *LabelConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_LabelConnection_pageInfo(ctx, field) if err != nil { return graphql.Null } @@ -13776,10 +14208,22 @@ func (ec *executionContext) fieldContext_Mutation_createGQLAPIKey(ctx context.Co return ec.fieldContext_GQLAPIKey_id(ctx, field) case "name": return ec.fieldContext_GQLAPIKey_name(ctx, field) - case "token": - return ec.fieldContext_GQLAPIKey_token(ctx, field) + case "description": + return ec.fieldContext_GQLAPIKey_description(ctx, field) + case "createdAt": + return ec.fieldContext_GQLAPIKey_createdAt(ctx, field) + case "createdBy": + return ec.fieldContext_GQLAPIKey_createdBy(ctx, field) + case "lastUsed": + return ec.fieldContext_GQLAPIKey_lastUsed(ctx, field) + case "lastUsedUA": + return ec.fieldContext_GQLAPIKey_lastUsedUA(ctx, field) case "expiresAt": return ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) + case "allowedFields": + return ec.fieldContext_GQLAPIKey_allowedFields(ctx, field) + case "token": + return ec.fieldContext_GQLAPIKey_token(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type GQLAPIKey", field.Name) }, @@ -13798,6 +14242,116 @@ func (ec *executionContext) fieldContext_Mutation_createGQLAPIKey(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_updateGQLAPIKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateGQLAPIKey(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateGQLAPIKey(rctx, fc.Args["input"].(UpdateGQLAPIKeyInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updateGQLAPIKey(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateGQLAPIKey_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_deleteGQLAPIKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteGQLAPIKey(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteGQLAPIKey(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteGQLAPIKey(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteGQLAPIKey_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createBasicAuth(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createBasicAuth(ctx, field) if err != nil { @@ -17543,6 +18097,127 @@ func (ec *executionContext) fieldContext_Query_swoStatus(ctx context.Context, fi return fc, nil } +func (ec *executionContext) _Query_gqlAPIKeys(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_gqlAPIKeys(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().GqlAPIKeys(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]GQLAPIKey) + fc.Result = res + return ec.marshalNGQLAPIKey2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKeyᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_gqlAPIKeys(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_GQLAPIKey_id(ctx, field) + case "name": + return ec.fieldContext_GQLAPIKey_name(ctx, field) + case "description": + return ec.fieldContext_GQLAPIKey_description(ctx, field) + case "createdAt": + return ec.fieldContext_GQLAPIKey_createdAt(ctx, field) + case "createdBy": + return ec.fieldContext_GQLAPIKey_createdBy(ctx, field) + case "lastUsed": + return ec.fieldContext_GQLAPIKey_lastUsed(ctx, field) + case "lastUsedUA": + return ec.fieldContext_GQLAPIKey_lastUsedUA(ctx, field) + case "expiresAt": + return ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) + case "allowedFields": + return ec.fieldContext_GQLAPIKey_allowedFields(ctx, field) + case "token": + return ec.fieldContext_GQLAPIKey_token(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type GQLAPIKey", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_listGQLFields(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_listGQLFields(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().ListGQLFields(rctx, fc.Args["query"].(*string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]string) + fc.Result = res + return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_listGQLFields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_listGQLFields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -27293,7 +27968,7 @@ func (ec *executionContext) unmarshalInputCreateGQLAPIKeyInput(ctx context.Conte asMap[k] = v } - fieldsInOrder := [...]string{"name", "query", "expiresAt"} + fieldsInOrder := [...]string{"name", "description", "allowedFields", "expiresAt"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -27309,15 +27984,24 @@ func (ec *executionContext) unmarshalInputCreateGQLAPIKeyInput(ctx context.Conte return it, err } it.Name = data - case "query": + case "description": var err error - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("query")) + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Query = data + it.Description = data + case "allowedFields": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("allowedFields")) + data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.AllowedFields = data case "expiresAt": var err error @@ -29911,6 +30595,53 @@ func (ec *executionContext) unmarshalInputUpdateEscalationPolicyStepInput(ctx co return it, nil } +func (ec *executionContext) unmarshalInputUpdateGQLAPIKeyInput(ctx context.Context, obj interface{}) (UpdateGQLAPIKeyInput, error) { + var it UpdateGQLAPIKeyInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"id", "name", "description"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "id": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + data, err := ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + it.ID = data + case "name": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Name = data + case "description": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Description = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateHeartbeatMonitorInput(ctx context.Context, obj interface{}) (UpdateHeartbeatMonitorInput, error) { var it UpdateHeartbeatMonitorInput asMap := map[string]interface{}{} @@ -32311,13 +33042,40 @@ func (ec *executionContext) _GQLAPIKey(ctx context.Context, sel ast.SelectionSet if out.Values[i] == graphql.Null { out.Invalids++ } - case "token": - out.Values[i] = ec._GQLAPIKey_token(ctx, field, obj) + case "description": + out.Values[i] = ec._GQLAPIKey_description(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdAt": + out.Values[i] = ec._GQLAPIKey_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdBy": + out.Values[i] = ec._GQLAPIKey_createdBy(ctx, field, obj) + case "lastUsed": + out.Values[i] = ec._GQLAPIKey_lastUsed(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "lastUsedUA": + out.Values[i] = ec._GQLAPIKey_lastUsedUA(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "expiresAt": out.Values[i] = ec._GQLAPIKey_expiresAt(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } + case "allowedFields": + out.Values[i] = ec._GQLAPIKey_allowedFields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "token": + out.Values[i] = ec._GQLAPIKey_token(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -33241,6 +33999,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "updateGQLAPIKey": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateGQLAPIKey(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteGQLAPIKey": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteGQLAPIKey(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createBasicAuth": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createBasicAuth(ctx, field) @@ -34519,6 +35291,50 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "gqlAPIKeys": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_gqlAPIKeys(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "listGQLFields": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_listGQLFields(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -38717,6 +39533,50 @@ func (ec *executionContext) marshalNGQLAPIKey2githubᚗcomᚋtargetᚋgoalertᚋ return ec._GQLAPIKey(ctx, sel, &v) } +func (ec *executionContext) marshalNGQLAPIKey2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKeyᚄ(ctx context.Context, sel ast.SelectionSet, v []GQLAPIKey) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNGQLAPIKey2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKey(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalNGQLAPIKey2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKey(ctx context.Context, sel ast.SelectionSet, v *GQLAPIKey) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -40432,6 +41292,11 @@ func (ec *executionContext) unmarshalNUpdateEscalationPolicyStepInput2githubᚗc return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNUpdateGQLAPIKeyInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateGQLAPIKeyInput(ctx context.Context, v interface{}) (UpdateGQLAPIKeyInput, error) { + res, err := ec.unmarshalInputUpdateGQLAPIKeyInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNUpdateHeartbeatMonitorInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateHeartbeatMonitorInput(ctx context.Context, v interface{}) (UpdateHeartbeatMonitorInput, error) { res, err := ec.unmarshalInputUpdateHeartbeatMonitorInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 01326bc2b8..50b04928b1 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -140,9 +140,10 @@ type CreateEscalationPolicyStepInput struct { } type CreateGQLAPIKeyInput struct { - Name string `json:"name"` - Query string `json:"query"` - ExpiresAt time.Time `json:"expiresAt"` + Name string `json:"name"` + Description string `json:"description"` + AllowedFields []string `json:"allowedFields"` + ExpiresAt time.Time `json:"expiresAt"` } type CreateHeartbeatMonitorInput struct { @@ -289,10 +290,16 @@ type EscalationPolicySearchOptions struct { } type GQLAPIKey struct { - ID string `json:"id"` - Name string `json:"name"` - Token *string `json:"token,omitempty"` - ExpiresAt time.Time `json:"expiresAt"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + CreatedBy *user.User `json:"createdBy,omitempty"` + LastUsed time.Time `json:"lastUsed"` + LastUsedUa string `json:"lastUsedUA"` + ExpiresAt time.Time `json:"expiresAt"` + AllowedFields []string `json:"allowedFields"` + Token *string `json:"token,omitempty"` } type IntegrationKeyConnection struct { @@ -603,6 +610,12 @@ type UpdateEscalationPolicyStepInput struct { Targets []assignment.RawTarget `json:"targets,omitempty"` } +type UpdateGQLAPIKeyInput struct { + ID string `json:"id"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` +} + type UpdateHeartbeatMonitorInput struct { ID string `json:"id"` Name *string `json:"name,omitempty"` diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index e7b17a448c..d629f3bddb 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -128,6 +128,10 @@ type Query { linkAccountInfo(token: ID!): LinkAccountInfo swoStatus: SWOStatus! + + gqlAPIKeys: [GQLAPIKey!]! + + listGQLFields(query: String): [String!]! } type IntegrationKeyTypeInfo { @@ -580,6 +584,8 @@ type Mutation { setSystemLimits(input: [SystemLimitInput!]!): Boolean! createGQLAPIKey(input: CreateGQLAPIKeyInput!): GQLAPIKey! + updateGQLAPIKey(input: UpdateGQLAPIKeyInput!): Boolean! + deleteGQLAPIKey(id: ID!): Boolean! createBasicAuth(input: CreateBasicAuthInput!): Boolean! updateBasicAuth(input: UpdateBasicAuthInput!): Boolean! @@ -587,15 +593,28 @@ type Mutation { input CreateGQLAPIKeyInput { name: String! - query: String! + description: String! + allowedFields: [String!]! expiresAt: ISOTimestamp! } +input UpdateGQLAPIKeyInput { + id: ID! + name: String + description: String +} + type GQLAPIKey { id: ID! name: String! - token: String + description: String! + createdAt: ISOTimestamp! + createdBy: User + lastUsed: ISOTimestamp! + lastUsedUA: String! expiresAt: ISOTimestamp! + allowedFields: [String!]! + token: String } input CreateBasicAuthInput { diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index 449fe79ca0..a190d2bc8b 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -42,6 +42,8 @@ export interface Query { generateSlackAppManifest: string linkAccountInfo?: null | LinkAccountInfo swoStatus: SWOStatus + gqlAPIKeys: GQLAPIKey[] + listGQLFields: string[] } export interface IntegrationKeyTypeInfo { @@ -423,21 +425,36 @@ export interface Mutation { setConfig: boolean setSystemLimits: boolean createGQLAPIKey: GQLAPIKey + updateGQLAPIKey: boolean + deleteGQLAPIKey: boolean createBasicAuth: boolean updateBasicAuth: boolean } export interface CreateGQLAPIKeyInput { name: string - query: string + description: string + allowedFields: string[] expiresAt: ISOTimestamp } +export interface UpdateGQLAPIKeyInput { + id: string + name?: null | string + description?: null | string +} + export interface GQLAPIKey { id: string name: string - token?: null | string + description: string + createdAt: ISOTimestamp + createdBy?: null | User + lastUsed: ISOTimestamp + lastUsedUA: string expiresAt: ISOTimestamp + allowedFields: string[] + token?: null | string } export interface CreateBasicAuthInput { From fc338b726302d02884be266b46091c5a62d078de Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 11:21:50 -0500 Subject: [PATCH 10/56] add stub methods & list fields --- graphql2/graphqlapp/gqlapikeys.go | 85 +++++++++++++++++++++++++++++++ graphql2/graphqlapp/mutation.go | 20 -------- 2 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 graphql2/graphqlapp/gqlapikeys.go diff --git a/graphql2/graphqlapp/gqlapikeys.go b/graphql2/graphqlapp/gqlapikeys.go new file mode 100644 index 0000000000..abdd504f89 --- /dev/null +++ b/graphql2/graphqlapp/gqlapikeys.go @@ -0,0 +1,85 @@ +package graphqlapp + +import ( + "context" + "fmt" + "sort" + + "github.com/target/goalert/graphql2" + "github.com/target/goalert/validation" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" + "github.com/vektah/gqlparser/v2/parser" + "github.com/vektah/gqlparser/v2/validator" +) + +func (q *Query) GqlAPIKeys(ctx context.Context) ([]graphql2.GQLAPIKey, error) { + return nil, nil +} + +func (q *Query) ListGQLFields(ctx context.Context, query *string) ([]string, error) { + if query == nil || *query == "" { + sch, err := parser.ParseSchema(&ast.Source{Input: graphql2.Schema()}) + if err != nil { + return nil, fmt.Errorf("parse schema: %w", err) + } + + var fields []string + for _, typ := range sch.Definitions { + if typ.Kind != ast.Object { + continue + } + for _, f := range typ.Fields { + fields = append(fields, typ.Name+"."+f.Name) + } + } + sort.Strings(fields) + return fields, nil + } + + sch, err := gqlparser.LoadSchema(&ast.Source{Input: graphql2.Schema()}) + if err != nil { + return nil, fmt.Errorf("parse schema: %w", err) + } + + qDoc, qErr := gqlparser.LoadQuery(sch, *query) + if len(qErr) > 0 { + return nil, validation.NewFieldError("Query", qErr.Error()) + } + + var fields []string + var e validator.Events + e.OnField(func(w *validator.Walker, field *ast.Field) { + fields = append(fields, field.ObjectDefinition.Name+"."+field.Name) + }) + validator.Walk(sch, qDoc, &e) + + sort.Strings(fields) + return fields, nil +} + +func (a *Mutation) UpdateGQLAPIKey(ctx context.Context, input graphql2.UpdateGQLAPIKeyInput) (bool, error) { + return false, nil +} +func (a *Mutation) DeleteGQLAPIKey(ctx context.Context, input string) (bool, error) { + return false, nil +} +func (a *Mutation) CreateGQLAPIKey(ctx context.Context, input graphql2.CreateGQLAPIKeyInput) (*graphql2.GQLAPIKey, error) { + return nil, nil + // _, err := gqlauth.NewQuery(input.Query) + // if err != nil { + // return nil, validation.NewFieldError("Query", err.Error()) + // } + + // key, err := a.APIKeyStore.CreateAdminGraphQLKey(ctx, input.Name, input.Query, input.ExpiresAt) + // if err != nil { + // return nil, err + // } + + // return &graphql2.GQLAPIKey{ + // ID: key.ID.String(), + // Name: key.Name, + // ExpiresAt: key.ExpiresAt, + // Token: &key.Token, + // }, nil +} diff --git a/graphql2/graphqlapp/mutation.go b/graphql2/graphqlapp/mutation.go index 303ef5ef84..f1ba1cccdb 100644 --- a/graphql2/graphqlapp/mutation.go +++ b/graphql2/graphqlapp/mutation.go @@ -11,7 +11,6 @@ import ( "github.com/target/goalert/assignment" "github.com/target/goalert/config" "github.com/target/goalert/graphql2" - "github.com/target/goalert/graphql2/gqlauth" "github.com/target/goalert/notification/webhook" "github.com/target/goalert/notificationchannel" "github.com/target/goalert/permission" @@ -29,25 +28,6 @@ type Mutation App func (a *App) Mutation() graphql2.MutationResolver { return (*Mutation)(a) } -func (a *Mutation) CreateGQLAPIKey(ctx context.Context, input graphql2.CreateGQLAPIKeyInput) (*graphql2.GQLAPIKey, error) { - _, err := gqlauth.NewQuery(input.Query) - if err != nil { - return nil, validation.NewFieldError("Query", err.Error()) - } - - key, err := a.APIKeyStore.CreateAdminGraphQLKey(ctx, input.Name, input.Query, input.ExpiresAt) - if err != nil { - return nil, err - } - - return &graphql2.GQLAPIKey{ - ID: key.ID.String(), - Name: key.Name, - ExpiresAt: key.ExpiresAt, - Token: &key.Token, - }, nil -} - func (a *Mutation) SetFavorite(ctx context.Context, input graphql2.SetFavoriteInput) (bool, error) { var err error if input.Favorite { From 9d84cd61ed86bc9d3505f7b6db2a6cc9f5ff1e56 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 11:44:00 -0500 Subject: [PATCH 11/56] update tables --- .../20230721104236-graphql-api-key.sql | 16 ----- .../20230907112347-graphql-api-key.sql | 28 +++++++++ migrate/schema.sql | 60 ++++++++++++------- 3 files changed, 65 insertions(+), 39 deletions(-) delete mode 100644 migrate/migrations/20230721104236-graphql-api-key.sql create mode 100644 migrate/migrations/20230907112347-graphql-api-key.sql diff --git a/migrate/migrations/20230721104236-graphql-api-key.sql b/migrate/migrations/20230721104236-graphql-api-key.sql deleted file mode 100644 index 94bcacd48d..0000000000 --- a/migrate/migrations/20230721104236-graphql-api-key.sql +++ /dev/null @@ -1,16 +0,0 @@ --- +migrate Up -CREATE TABLE api_keys( - id uuid PRIMARY KEY, - name text NOT NULL UNIQUE, - user_id uuid REFERENCES users(id) ON DELETE CASCADE, - service_id uuid REFERENCES services(id) ON DELETE CASCADE, - policy jsonb NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - expires_at timestamp with time zone NOT NULL, - last_used_at timestamp with time zone -); - --- +migrate Down -DROP TABLE api_keys; - diff --git a/migrate/migrations/20230907112347-graphql-api-key.sql b/migrate/migrations/20230907112347-graphql-api-key.sql new file mode 100644 index 0000000000..48d41d3537 --- /dev/null +++ b/migrate/migrations/20230907112347-graphql-api-key.sql @@ -0,0 +1,28 @@ +-- +migrate Up +CREATE TABLE gql_api_keys( + id uuid PRIMARY KEY, + name text NOT NULL UNIQUE, + description text NOT NULL, + created_at timestamp with time zone NOT NULL DEFAULT now(), + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + updated_at timestamp with time zone NOT NULL DEFAULT now(), + updated_by uuid REFERENCES users(id) ON DELETE SET NULL, + policy jsonb NOT NULL, + expires_at timestamp with time zone NOT NULL +); + +CREATE TABLE gql_api_key_usage( + id bigserial PRIMARY KEY, + api_key_id uuid REFERENCES gql_api_keys(id) ON DELETE CASCADE, + used_at timestamp with time zone NOT NULL DEFAULT now(), + user_agent text, + ip_address inet +); + +CREATE INDEX idx_gql_most_recent_use ON gql_api_key_usage(api_key_id, used_at DESC); + +-- +migrate Down +DROP TABLE gql_api_key_usage; + +DROP TABLE gql_api_keys; + diff --git a/migrate/schema.sql b/migrate/schema.sql index e355c7d5d2..9bafabdb24 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,7 +1,7 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=b22dc5f530071e8dc981a1411df399a55ceae290e8d52f92d6db6fd7b92ff912 - --- DISK=0bc7602e896fa75956e969791faa4e469ef984af481a6c83bf73d81c54a7ab01 - --- PSQL=0bc7602e896fa75956e969791faa4e469ef984af481a6c83bf73d81c54a7ab01 - +-- DATA=a4b057ec82c637cc0833ebf63e60d421b137f75c7dd9355f1f57e33bf4697f55 - +-- DISK=05f86ed0fdc6cf25e0162624d600d9a8efd491d4dd4eb5a1411ba1f9704f22d8 - +-- PSQL=05f86ed0fdc6cf25e0162624d600d9a8efd491d4dd4eb5a1411ba1f9704f22d8 - -- -- pgdump-lite database dump -- @@ -1361,26 +1361,6 @@ CREATE CONSTRAINT TRIGGER trg_enforce_alert_limit AFTER INSERT ON public.alerts CREATE TRIGGER trg_prevent_reopen BEFORE UPDATE OF status ON public.alerts FOR EACH ROW EXECUTE FUNCTION fn_prevent_reopen(); -CREATE TABLE api_keys ( - created_at timestamp with time zone DEFAULT now() NOT NULL, - expires_at timestamp with time zone NOT NULL, - id uuid NOT NULL, - last_used_at timestamp with time zone, - name text NOT NULL, - policy jsonb NOT NULL, - service_id uuid, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - user_id uuid, - CONSTRAINT api_keys_name_key UNIQUE (name), - CONSTRAINT api_keys_pkey PRIMARY KEY (id), - CONSTRAINT api_keys_service_id_fkey FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE, - CONSTRAINT api_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE UNIQUE INDEX api_keys_name_key ON public.api_keys USING btree (name); -CREATE UNIQUE INDEX api_keys_pkey ON public.api_keys USING btree (id); - - CREATE TABLE auth_basic_users ( id bigint DEFAULT nextval('auth_basic_users_id_seq'::regclass) NOT NULL, password_hash text NOT NULL, @@ -1622,6 +1602,40 @@ CREATE TABLE gorp_migrations ( CREATE UNIQUE INDEX gorp_migrations_pkey ON public.gorp_migrations USING btree (id); +CREATE TABLE gql_api_key_usage ( + api_key_id uuid, + id bigint DEFAULT nextval('gql_api_key_usage_id_seq'::regclass) NOT NULL, + ip_address inet, + used_at timestamp with time zone DEFAULT now() NOT NULL, + user_agent text, + CONSTRAINT gql_api_key_usage_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES gql_api_keys(id) ON DELETE CASCADE, + CONSTRAINT gql_api_key_usage_pkey PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX gql_api_key_usage_pkey ON public.gql_api_key_usage USING btree (id); +CREATE INDEX idx_gql_most_recent_use ON public.gql_api_key_usage USING btree (api_key_id, used_at DESC); + + +CREATE TABLE gql_api_keys ( + created_at timestamp with time zone DEFAULT now() NOT NULL, + created_by uuid, + description text NOT NULL, + expires_at timestamp with time zone NOT NULL, + id uuid NOT NULL, + name text NOT NULL, + policy jsonb NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + updated_by uuid, + CONSTRAINT gql_api_keys_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT gql_api_keys_name_key UNIQUE (name), + CONSTRAINT gql_api_keys_pkey PRIMARY KEY (id), + CONSTRAINT gql_api_keys_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE UNIQUE INDEX gql_api_keys_name_key ON public.gql_api_keys USING btree (name); +CREATE UNIQUE INDEX gql_api_keys_pkey ON public.gql_api_keys USING btree (id); + + CREATE TABLE heartbeat_monitors ( heartbeat_interval interval NOT NULL, id uuid NOT NULL, From 659f7531319f487cd04318de9b82285d436769d8 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 15:23:16 -0500 Subject: [PATCH 12/56] api poc working --- apikey/context.go | 6 +- apikey/graphqlv1.go | 2 +- apikey/queries.sql | 65 +- apikey/store.go | 277 ++++-- auth/handler.go | 2 +- gadb/models.go | 32 +- gadb/queries.sql.go | 206 +++-- graphql2/generated.go | 809 ++++++++++++++---- graphql2/gqlgen.yml | 2 - graphql2/graphqlapp/app.go | 11 +- graphql2/graphqlapp/gqlapikeys.go | 157 ++-- graphql2/graphqlapp/user.go | 34 +- graphql2/models_gen.go | 40 +- graphql2/schema.go | 55 ++ graphql2/schema.graphql | 26 +- .../20230907112347-graphql-api-key.sql | 4 +- validation/validate/oneof.go | 5 +- web/src/schema.d.ts | 18 +- 18 files changed, 1352 insertions(+), 399 deletions(-) diff --git a/apikey/context.go b/apikey/context.go index bd93afb54a..840b283093 100644 --- a/apikey/context.go +++ b/apikey/context.go @@ -9,12 +9,12 @@ const ( ) // PolicyFromContext returns the Policy associated with the given context. -func PolicyFromContext(ctx context.Context) *Policy { - p, _ := ctx.Value(contextKeyPolicy).(*Policy) +func PolicyFromContext(ctx context.Context) *GQLPolicy { + p, _ := ctx.Value(contextKeyPolicy).(*GQLPolicy) return p } // ContextWithPolicy returns a new context with the given Policy attached. -func ContextWithPolicy(ctx context.Context, p *Policy) context.Context { +func ContextWithPolicy(ctx context.Context, p *GQLPolicy) context.Context { return context.WithValue(ctx, contextKeyPolicy, p) } diff --git a/apikey/graphqlv1.go b/apikey/graphqlv1.go index d59abe53db..7220a0c60b 100644 --- a/apikey/graphqlv1.go +++ b/apikey/graphqlv1.go @@ -41,7 +41,7 @@ type GraphQLField struct { type Claims struct { jwt.RegisteredClaims - PolicyHash []byte `json:"ph"` + PolicyHash []byte `json:"pol"` } func NewGraphQLClaims(id uuid.UUID, policyHash []byte, expires time.Time) jwt.Claims { diff --git a/apikey/queries.sql b/apikey/queries.sql index 20fc0a3b7c..e31b0125b5 100644 --- a/apikey/queries.sql +++ b/apikey/queries.sql @@ -1,26 +1,61 @@ -- name: APIKeyInsert :exec -INSERT INTO api_keys(id, user_id, service_id, name, POLICY, expires_at) - VALUES ($1, $2, $3, $4, $5, $6); +INSERT INTO gql_api_keys(id, name, description, POLICY, created_by, updated_by, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7); + +-- name: APIKeyUpdate :exec +UPDATE + gql_api_keys +SET + name = $2, + description = $3, + updated_by = $4 +WHERE + id = $1; + +-- name: APIKeyForUpdate :one +SELECT + name, + description +FROM + gql_api_keys +WHERE + id = $1 + AND deleted_at IS NULL +FOR UPDATE; -- name: APIKeyDelete :exec -DELETE FROM api_keys +DELETE FROM gql_api_keys WHERE id = $1; --- name: APIKeyGet :one +-- name: APIKeyRecordUsage :exec +-- APIKeyRecordUsage records the usage of an API key. +INSERT INTO gql_api_key_usage(api_key_id, user_agent, ip_address) + VALUES (@key_id::uuid, @user_agent::text, @ip_address::inet); + +-- name: APIKeyAuthPolicy :one +-- APIKeyAuth returns the API key policy with the given id, if it exists and is not expired. SELECT - * + gql_api_keys.policy FROM - api_keys + gql_api_keys WHERE - id = $1; + gql_api_keys.id = $1 + AND gql_api_keys.deleted_at IS NULL + AND gql_api_keys.expires_at > now(); --- name: APIKeyAuth :one -UPDATE - api_keys -SET - last_used_at = now() +-- name: APIKeyList :many +-- APIKeyList returns all API keys, along with the last time they were used. +SELECT DISTINCT ON (gql_api_keys.id) + gql_api_keys.*, + gql_api_key_usage.used_at AS last_used_at, + gql_api_key_usage.user_agent AS last_user_agent, + gql_api_key_usage.ip_address AS last_ip_address +FROM + gql_api_keys + LEFT JOIN gql_api_key_usage ON gql_api_keys.id = gql_api_key_usage.api_key_id WHERE - id = $1 -RETURNING - *; + gql_api_keys.deleted_at IS NULL +ORDER BY + gql_api_keys.id, + gql_api_key_usage.used_at DESC; diff --git a/apikey/store.go b/apikey/store.go index d6b1b59f8b..6850676d98 100644 --- a/apikey/store.go +++ b/apikey/store.go @@ -6,14 +6,20 @@ import ( "crypto/sha256" "database/sql" "encoding/json" + "fmt" + "net" + "sort" "sync" "time" "github.com/google/uuid" "github.com/target/goalert/gadb" + "github.com/target/goalert/graphql2" "github.com/target/goalert/keyring" "github.com/target/goalert/permission" "github.com/target/goalert/util/log" + "github.com/target/goalert/util/sqlutil" + "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" ) @@ -27,7 +33,7 @@ type Store struct { type policyInfo struct { Hash []byte - Policy Policy + Policy GQLPolicy } func NewStore(ctx context.Context, db *sql.DB, key keyring.Keyring) (*Store, error) { @@ -39,7 +45,148 @@ func NewStore(ctx context.Context, db *sql.DB, key keyring.Keyring) (*Store, err const Issuer = "goalert" const Audience = "apikey-v1/graphql-v1" -func (s *Store) AuthorizeGraphQL(ctx context.Context, tok string) (context.Context, error) { +type APIKeyInfo struct { + ID uuid.UUID + Name string + Description string + ExpiresAt time.Time + LastUsed *APIKeyUsage + CreatedAt time.Time + UpdatedAt time.Time + CreatedBy *uuid.UUID + UpdatedBy *uuid.UUID + AllowedFields []string +} + +func (s *Store) FindAllAdminGraphQLKeys(ctx context.Context) ([]APIKeyInfo, error) { + err := permission.LimitCheckAny(ctx, permission.Admin) + if err != nil { + return nil, err + } + + keys, err := gadb.New(s.db).APIKeyList(ctx) + if err != nil { + return nil, err + } + + res := make([]APIKeyInfo, 0, len(keys)) + for _, k := range keys { + k := k + + var p GQLPolicy + err = json.Unmarshal(k.Policy, &p) + if err != nil { + log.Log(ctx, fmt.Errorf("invalid policy for key %s: %w", k.ID, err)) + continue + } + if p.Version != 1 { + log.Log(ctx, fmt.Errorf("unknown policy version for key %s: %d", k.ID, p.Version)) + continue + } + + var lastUsed *APIKeyUsage + if k.LastUsedAt.Valid { + var ip string + if k.LastIpAddress.Valid { + ip = k.LastIpAddress.IPNet.IP.String() + } + lastUsed = &APIKeyUsage{ + UserAgent: k.LastUserAgent.String, + IP: ip, + Time: k.LastUsedAt.Time, + } + } + + res = append(res, APIKeyInfo{ + ID: k.ID, + Name: k.Name, + Description: k.Description, + ExpiresAt: k.ExpiresAt, + LastUsed: lastUsed, + CreatedAt: k.CreatedAt, + UpdatedAt: k.UpdatedAt, + CreatedBy: &k.CreatedBy.UUID, + UpdatedBy: &k.UpdatedBy.UUID, + AllowedFields: p.AllowedFields, + }) + } + + return res, nil +} + +type APIKeyUsage struct { + UserAgent string + IP string + Time time.Time +} + +type UpdateKey struct { + ID uuid.UUID + Name string + Description string +} + +func (s *Store) UpdateAdminGraphQLKey(ctx context.Context, id uuid.UUID, name, desc *string) error { + err := permission.LimitCheckAny(ctx, permission.Admin) + if err != nil { + return err + } + + if name != nil { + err = validate.IDName("Name", *name) + } + if desc != nil { + err = validate.Many(err, validate.Text("Description", *desc, 0, 255)) + } + if err != nil { + return err + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer sqlutil.Rollback(ctx, "UpdateAdminGraphQLKey", tx) + + key, err := gadb.New(tx).APIKeyForUpdate(ctx, id) + if err != nil { + return err + } + if name != nil { + key.Name = *name + } + if desc != nil { + key.Description = *desc + } + + var user uuid.NullUUID + if u, err := uuid.Parse(permission.UserID(ctx)); err == nil { + user = uuid.NullUUID{UUID: u, Valid: true} + } + + err = gadb.New(tx).APIKeyUpdate(ctx, gadb.APIKeyUpdateParams{ + ID: id, + Name: key.Name, + Description: key.Description, + UpdatedBy: user, + }) + if err != nil { + return err + } + + return tx.Commit() +} + +func (s *Store) DeleteAdminGraphQLKey(ctx context.Context, id uuid.UUID) error { + err := permission.LimitCheckAny(ctx, permission.Admin) + if err != nil { + return err + } + + return gadb.New(s.db).APIKeyDelete(ctx, id) +} + +func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (context.Context, error) { var claims Claims _, err := s.key.VerifyJWT(tok, &claims, Issuer, Audience) if err != nil { @@ -52,28 +199,47 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok string) (context.Conte return ctx, permission.Unauthorized() } - key, err := gadb.New(s.db).APIKeyAuth(ctx, id) + polData, err := gadb.New(s.db).APIKeyAuthPolicy(ctx, id) if err != nil { log.Logf(ctx, "apikey: lookup failed: %v", err) return ctx, permission.Unauthorized() } + var buf bytes.Buffer + err = json.Compact(&buf, polData) + if err != nil { + log.Logf(ctx, "apikey: invalid policy: %v", err) + return ctx, permission.Unauthorized() + } // TODO: cache policy hash by key ID when loading - policyHash := sha256.Sum256(key.Policy) + policyHash := sha256.Sum256(buf.Bytes()) if !bytes.Equal(policyHash[:], claims.PolicyHash) { log.Logf(ctx, "apikey: policy hash mismatch") + return ctx, permission.Unauthorized() } - var p Policy - err = json.Unmarshal(key.Policy, &p) - if err != nil { + var p GQLPolicy + err = json.Unmarshal(polData, &p) + if err != nil || p.Version != 1 { log.Logf(ctx, "apikey: invalid policy: %v", err) return ctx, permission.Unauthorized() } - if p.Type != PolicyTypeGraphQLV1 || p.GraphQLV1 == nil { - log.Logf(ctx, "apikey: invalid policy type: %v", p.Type) - return ctx, permission.Unauthorized() + ua = validate.SanitizeText(ua, 1024) + ip, _, _ = net.SplitHostPort(ip) + ip = validate.SanitizeText(ip, 255) + params := gadb.APIKeyRecordUsageParams{ + KeyID: id, + UserAgent: ua, + } + params.IpAddress.IPNet.IP = net.ParseIP(ip) + params.IpAddress.IPNet.Mask = net.CIDRMask(32, 32) + if params.IpAddress.IPNet.IP != nil { + params.IpAddress.Valid = true + } + err = gadb.New(s.db).APIKeyRecordUsage(ctx, params) + if err != nil { + log.Logf(ctx, "apikey: failed to record usage: %v", err) } s.mx.Lock() @@ -88,65 +254,78 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok string) (context.Conte Type: permission.SourceTypeGQLAPIKey, }) ctx = permission.UserContext(ctx, "", permission.RoleUnknown) + + ctx = ContextWithPolicy(ctx, &p) return ctx, nil } -type Key struct { - ID uuid.UUID - Name string - Type Type - ExpiresAt time.Time - Token string +type NewAdminGQLKeyOpts struct { + Name string + Desc string + Fields []string + Expires time.Time +} + +type GQLPolicy struct { + Version int + AllowedFields []string } -func (s *Store) CreateAdminGraphQLKey(ctx context.Context, name, query string, exp time.Time) (*Key, error) { +func (s *Store) CreateAdminGraphQLKey(ctx context.Context, opt NewAdminGQLKeyOpts) (uuid.UUID, string, error) { err := permission.LimitCheckAny(ctx, permission.Admin) if err != nil { - return nil, err + return uuid.Nil, "", err } - err = validate.IDName("Name", name) - if err != nil { - return nil, err + + err = validate.Many( + validate.IDName("Name", opt.Name), + validate.Text("Description", opt.Desc, 0, 255), + validate.Range("Fields", len(opt.Fields), 1, len(graphql2.SchemaFields())), + ) + for i, f := range opt.Fields { + err = validate.Many(err, validate.OneOf(fmt.Sprintf("Fields[%d]", i), f, graphql2.SchemaFields()...)) + } + if time.Until(opt.Expires) <= 0 { + err = validate.Many(err, validation.NewFieldError("Expires", "must be in the future")) } - err = validate.RequiredText("Query", query, 1, 8192) if err != nil { - return nil, err + return uuid.Nil, "", err } - hash := sha256.Sum256([]byte(query)) - - id := uuid.New() - - data, err := json.Marshal(V1{ - Type: TypeGraphQLV1, - GraphQLV1: &GraphQLV1{ - // Query: query, - // SHA256: hash, - }, + sort.Strings(opt.Fields) + policyData, err := json.Marshal(GQLPolicy{ + Version: 1, + AllowedFields: opt.Fields, }) if err != nil { - return nil, err + return uuid.Nil, "", err } - tok, err := s.key.SignJWT(NewGraphQLClaims(id, hash[:], exp)) - if err != nil { - return nil, err + + var user uuid.NullUUID + userID, err := uuid.Parse(permission.UserID(ctx)) + if err == nil { + user = uuid.NullUUID{UUID: userID, Valid: true} } + id := uuid.New() err = gadb.New(s.db).APIKeyInsert(ctx, gadb.APIKeyInsertParams{ - ID: id, - Name: name, - ExpiresAt: exp, - Policy: data, + ID: id, + Name: opt.Name, + Description: opt.Desc, + ExpiresAt: opt.Expires, + Policy: policyData, + CreatedBy: user, + UpdatedBy: user, }) if err != nil { - return nil, err + return uuid.Nil, "", err + } + + hash := sha256.Sum256([]byte(policyData)) + tok, err := s.key.SignJWT(NewGraphQLClaims(id, hash[:], opt.Expires)) + if err != nil { + return uuid.Nil, "", err } - return &Key{ - ID: id, - Name: name, - Type: TypeGraphQLV1, - ExpiresAt: exp, - Token: tok, - }, nil + return id, tok, nil } diff --git a/auth/handler.go b/auth/handler.go index 4b03ee3149..7cf08c1a31 100644 --- a/auth/handler.go +++ b/auth/handler.go @@ -556,7 +556,7 @@ func (h *Handler) authWithToken(w http.ResponseWriter, req *http.Request, next h ctx := req.Context() if req.URL.Path == "/api/graphql" && strings.HasPrefix(tokStr, "ey") { - ctx, err = h.cfg.APIKeyStore.AuthorizeGraphQL(ctx, tokStr) + ctx, err = h.cfg.APIKeyStore.AuthorizeGraphQL(ctx, tokStr, req.UserAgent(), req.RemoteAddr) if errutil.HTTPError(req.Context(), w, err) { return true } diff --git a/gadb/models.go b/gadb/models.go index 622328ffb9..5fe45c1440 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -799,18 +799,6 @@ type AlertStatusSubscription struct { LastAlertStatus EnumAlertStatus } -type ApiKey struct { - CreatedAt time.Time - ExpiresAt time.Time - ID uuid.UUID - LastUsedAt sql.NullTime - Name string - Policy json.RawMessage - ServiceID uuid.NullUUID - UpdatedAt time.Time - UserID uuid.NullUUID -} - type AuthBasicUser struct { ID int64 PasswordHash string @@ -916,6 +904,26 @@ type GorpMigration struct { ID string } +type GqlApiKey struct { + CreatedAt time.Time + CreatedBy uuid.NullUUID + Description string + ExpiresAt time.Time + ID uuid.UUID + Name string + Policy json.RawMessage + UpdatedAt time.Time + UpdatedBy uuid.NullUUID +} + +type GqlApiKeyUsage struct { + ApiKeyID uuid.NullUUID + ID int64 + IpAddress pqtype.Inet + UsedAt time.Time + UserAgent sql.NullString +} + type HeartbeatMonitor struct { HeartbeatInterval int64 ID uuid.UUID diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index 0576f3c23c..deb00dde32 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -16,36 +16,27 @@ import ( "github.com/sqlc-dev/pqtype" ) -const aPIKeyAuth = `-- name: APIKeyAuth :one -UPDATE - api_keys -SET - last_used_at = now() +const aPIKeyAuthPolicy = `-- name: APIKeyAuthPolicy :one +SELECT + gql_api_keys.policy +FROM + gql_api_keys WHERE - id = $1 -RETURNING - created_at, expires_at, id, last_used_at, name, policy, service_id, updated_at, user_id + gql_api_keys.id = $1 + AND gql_api_keys.deleted_at IS NULL + AND gql_api_keys.expires_at > now() ` -func (q *Queries) APIKeyAuth(ctx context.Context, id uuid.UUID) (ApiKey, error) { - row := q.db.QueryRowContext(ctx, aPIKeyAuth, id) - var i ApiKey - err := row.Scan( - &i.CreatedAt, - &i.ExpiresAt, - &i.ID, - &i.LastUsedAt, - &i.Name, - &i.Policy, - &i.ServiceID, - &i.UpdatedAt, - &i.UserID, - ) - return i, err +// APIKeyAuth returns the API key policy with the given id, if it exists and is not expired. +func (q *Queries) APIKeyAuthPolicy(ctx context.Context, id uuid.UUID) (json.RawMessage, error) { + row := q.db.QueryRowContext(ctx, aPIKeyAuthPolicy, id) + var policy json.RawMessage + err := row.Scan(&policy) + return policy, err } const aPIKeyDelete = `-- name: APIKeyDelete :exec -DELETE FROM api_keys +DELETE FROM gql_api_keys WHERE id = $1 ` @@ -54,58 +45,171 @@ func (q *Queries) APIKeyDelete(ctx context.Context, id uuid.UUID) error { return err } -const aPIKeyGet = `-- name: APIKeyGet :one +const aPIKeyForUpdate = `-- name: APIKeyForUpdate :one SELECT - created_at, expires_at, id, last_used_at, name, policy, service_id, updated_at, user_id + name, + description FROM - api_keys + gql_api_keys WHERE id = $1 + AND deleted_at IS NULL +FOR UPDATE ` -func (q *Queries) APIKeyGet(ctx context.Context, id uuid.UUID) (ApiKey, error) { - row := q.db.QueryRowContext(ctx, aPIKeyGet, id) - var i ApiKey - err := row.Scan( - &i.CreatedAt, - &i.ExpiresAt, - &i.ID, - &i.LastUsedAt, - &i.Name, - &i.Policy, - &i.ServiceID, - &i.UpdatedAt, - &i.UserID, - ) +type APIKeyForUpdateRow struct { + Name string + Description string +} + +func (q *Queries) APIKeyForUpdate(ctx context.Context, id uuid.UUID) (APIKeyForUpdateRow, error) { + row := q.db.QueryRowContext(ctx, aPIKeyForUpdate, id) + var i APIKeyForUpdateRow + err := row.Scan(&i.Name, &i.Description) return i, err } const aPIKeyInsert = `-- name: APIKeyInsert :exec -INSERT INTO api_keys(id, user_id, service_id, name, POLICY, expires_at) - VALUES ($1, $2, $3, $4, $5, $6) +INSERT INTO gql_api_keys(id, name, description, POLICY, created_by, updated_by, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) ` type APIKeyInsertParams struct { - ID uuid.UUID - UserID uuid.NullUUID - ServiceID uuid.NullUUID - Name string - Policy json.RawMessage - ExpiresAt time.Time + ID uuid.UUID + Name string + Description string + Policy json.RawMessage + CreatedBy uuid.NullUUID + UpdatedBy uuid.NullUUID + ExpiresAt time.Time } func (q *Queries) APIKeyInsert(ctx context.Context, arg APIKeyInsertParams) error { _, err := q.db.ExecContext(ctx, aPIKeyInsert, arg.ID, - arg.UserID, - arg.ServiceID, arg.Name, + arg.Description, arg.Policy, + arg.CreatedBy, + arg.UpdatedBy, arg.ExpiresAt, ) return err } +const aPIKeyList = `-- name: APIKeyList :many +SELECT DISTINCT ON (gql_api_keys.id) + gql_api_keys.created_at, gql_api_keys.created_by, gql_api_keys.description, gql_api_keys.expires_at, gql_api_keys.id, gql_api_keys.name, gql_api_keys.policy, gql_api_keys.updated_at, gql_api_keys.updated_by, + gql_api_key_usage.used_at AS last_used_at, + gql_api_key_usage.user_agent AS last_user_agent, + gql_api_key_usage.ip_address AS last_ip_address +FROM + gql_api_keys + LEFT JOIN gql_api_key_usage ON gql_api_keys.id = gql_api_key_usage.api_key_id +WHERE + gql_api_keys.deleted_at IS NULL +ORDER BY + gql_api_keys.id, + gql_api_key_usage.used_at DESC +` + +type APIKeyListRow struct { + CreatedAt time.Time + CreatedBy uuid.NullUUID + Description string + ExpiresAt time.Time + ID uuid.UUID + Name string + Policy json.RawMessage + UpdatedAt time.Time + UpdatedBy uuid.NullUUID + LastUsedAt sql.NullTime + LastUserAgent sql.NullString + LastIpAddress pqtype.Inet +} + +// APIKeyList returns all API keys, along with the last time they were used. +func (q *Queries) APIKeyList(ctx context.Context) ([]APIKeyListRow, error) { + rows, err := q.db.QueryContext(ctx, aPIKeyList) + if err != nil { + return nil, err + } + defer rows.Close() + var items []APIKeyListRow + for rows.Next() { + var i APIKeyListRow + if err := rows.Scan( + &i.CreatedAt, + &i.CreatedBy, + &i.Description, + &i.ExpiresAt, + &i.ID, + &i.Name, + &i.Policy, + &i.UpdatedAt, + &i.UpdatedBy, + &i.LastUsedAt, + &i.LastUserAgent, + &i.LastIpAddress, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const aPIKeyRecordUsage = `-- name: APIKeyRecordUsage :exec +INSERT INTO gql_api_key_usage(api_key_id, user_agent, ip_address) + VALUES ($1::uuid, $2::text, $3::inet) +` + +type APIKeyRecordUsageParams struct { + KeyID uuid.UUID + UserAgent string + IpAddress pqtype.Inet +} + +// APIKeyRecordUsage records the usage of an API key. +func (q *Queries) APIKeyRecordUsage(ctx context.Context, arg APIKeyRecordUsageParams) error { + _, err := q.db.ExecContext(ctx, aPIKeyRecordUsage, arg.KeyID, arg.UserAgent, arg.IpAddress) + return err +} + +const aPIKeyUpdate = `-- name: APIKeyUpdate :exec +UPDATE + gql_api_keys +SET + name = $2, + description = $3, + updated_by = $4 +WHERE + id = $1 +` + +type APIKeyUpdateParams struct { + ID uuid.UUID + Name string + Description string + UpdatedBy uuid.NullUUID +} + +func (q *Queries) APIKeyUpdate(ctx context.Context, arg APIKeyUpdateParams) error { + _, err := q.db.ExecContext(ctx, aPIKeyUpdate, + arg.ID, + arg.Name, + arg.Description, + arg.UpdatedBy, + ) + return err +} + const alertFeedback = `-- name: AlertFeedback :many SELECT alert_id, diff --git a/graphql2/generated.go b/graphql2/generated.go index e93ce4369d..966a661664 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -19,7 +19,6 @@ import ( "github.com/target/goalert/alert/alertlog" "github.com/target/goalert/alert/alertmetrics" "github.com/target/goalert/assignment" - "github.com/target/goalert/auth" "github.com/target/goalert/calsub" "github.com/target/goalert/escalation" "github.com/target/goalert/heartbeat" @@ -67,6 +66,7 @@ type ResolverRoot interface { AlertMetric() AlertMetricResolver EscalationPolicy() EscalationPolicyResolver EscalationPolicyStep() EscalationPolicyStepResolver + GQLAPIKey() GQLAPIKeyResolver HeartbeatMonitor() HeartbeatMonitorResolver IntegrationKey() IntegrationKeyResolver MessageLogConnectionStats() MessageLogConnectionStatsResolver @@ -85,7 +85,6 @@ type ResolverRoot interface { UserContactMethod() UserContactMethodResolver UserNotificationRule() UserNotificationRuleResolver UserOverride() UserOverrideResolver - UserSession() UserSessionResolver } type DirectiveRoot struct { @@ -172,6 +171,11 @@ type ComplexityRoot struct { Value func(childComplexity int) int } + CreatedGQLAPIKey struct { + ID func(childComplexity int) int + Token func(childComplexity int) int + } + DebugCarrierInfo struct { MobileCountryCode func(childComplexity int) int MobileNetworkCode func(childComplexity int) int @@ -239,9 +243,16 @@ type ComplexityRoot struct { ExpiresAt func(childComplexity int) int ID func(childComplexity int) int LastUsed func(childComplexity int) int - LastUsedUa func(childComplexity int) int Name func(childComplexity int) int Token func(childComplexity int) int + UpdatedAt func(childComplexity int) int + UpdatedBy func(childComplexity int) int + } + + GQLAPIKeyUsage struct { + IP func(childComplexity int) int + Time func(childComplexity int) int + Ua func(childComplexity int) int } HeartbeatMonitor struct { @@ -714,6 +725,11 @@ type EscalationPolicyStepResolver interface { Targets(ctx context.Context, obj *escalation.Step) ([]assignment.RawTarget, error) EscalationPolicy(ctx context.Context, obj *escalation.Step) (*escalation.Policy, error) } +type GQLAPIKeyResolver interface { + CreatedBy(ctx context.Context, obj *GQLAPIKey) (*user.User, error) + + UpdatedBy(ctx context.Context, obj *GQLAPIKey) (*user.User, error) +} type HeartbeatMonitorResolver interface { TimeoutMinutes(ctx context.Context, obj *heartbeat.Monitor) (int, error) @@ -774,7 +790,7 @@ type MutationResolver interface { UpdateAlertsByService(ctx context.Context, input UpdateAlertsByServiceInput) (bool, error) SetConfig(ctx context.Context, input []ConfigValueInput) (bool, error) SetSystemLimits(ctx context.Context, input []SystemLimitInput) (bool, error) - CreateGQLAPIKey(ctx context.Context, input CreateGQLAPIKeyInput) (*GQLAPIKey, error) + CreateGQLAPIKey(ctx context.Context, input CreateGQLAPIKeyInput) (*CreatedGQLAPIKey, error) UpdateGQLAPIKey(ctx context.Context, input UpdateGQLAPIKeyInput) (bool, error) DeleteGQLAPIKey(ctx context.Context, id string) (bool, error) CreateBasicAuth(ctx context.Context, input CreateBasicAuthInput) (bool, error) @@ -878,7 +894,7 @@ type UserResolver interface { CalendarSubscriptions(ctx context.Context, obj *user.User) ([]calsub.Subscription, error) AuthSubjects(ctx context.Context, obj *user.User) ([]user.AuthSubject, error) - Sessions(ctx context.Context, obj *user.User) ([]auth.UserSession, error) + Sessions(ctx context.Context, obj *user.User) ([]UserSession, error) OnCallSteps(ctx context.Context, obj *user.User) ([]escalation.Step, error) IsFavorite(ctx context.Context, obj *user.User) (bool, error) } @@ -905,9 +921,6 @@ type UserOverrideResolver interface { RemoveUser(ctx context.Context, obj *override.UserOverride) (*user.User, error) Target(ctx context.Context, obj *override.UserOverride) (*assignment.RawTarget, error) } -type UserSessionResolver interface { - Current(ctx context.Context, obj *auth.UserSession) (bool, error) -} type executableSchema struct { resolvers ResolverRoot @@ -1237,6 +1250,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ConfigValue.Value(childComplexity), true + case "CreatedGQLAPIKey.id": + if e.complexity.CreatedGQLAPIKey.ID == nil { + break + } + + return e.complexity.CreatedGQLAPIKey.ID(childComplexity), true + + case "CreatedGQLAPIKey.token": + if e.complexity.CreatedGQLAPIKey.Token == nil { + break + } + + return e.complexity.CreatedGQLAPIKey.Token(childComplexity), true + case "DebugCarrierInfo.mobileCountryCode": if e.complexity.DebugCarrierInfo.MobileCountryCode == nil { break @@ -1552,13 +1579,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.GQLAPIKey.LastUsed(childComplexity), true - case "GQLAPIKey.lastUsedUA": - if e.complexity.GQLAPIKey.LastUsedUa == nil { - break - } - - return e.complexity.GQLAPIKey.LastUsedUa(childComplexity), true - case "GQLAPIKey.name": if e.complexity.GQLAPIKey.Name == nil { break @@ -1573,6 +1593,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.GQLAPIKey.Token(childComplexity), true + case "GQLAPIKey.updatedAt": + if e.complexity.GQLAPIKey.UpdatedAt == nil { + break + } + + return e.complexity.GQLAPIKey.UpdatedAt(childComplexity), true + + case "GQLAPIKey.updatedBy": + if e.complexity.GQLAPIKey.UpdatedBy == nil { + break + } + + return e.complexity.GQLAPIKey.UpdatedBy(childComplexity), true + + case "GQLAPIKeyUsage.ip": + if e.complexity.GQLAPIKeyUsage.IP == nil { + break + } + + return e.complexity.GQLAPIKeyUsage.IP(childComplexity), true + + case "GQLAPIKeyUsage.time": + if e.complexity.GQLAPIKeyUsage.Time == nil { + break + } + + return e.complexity.GQLAPIKeyUsage.Time(childComplexity), true + + case "GQLAPIKeyUsage.ua": + if e.complexity.GQLAPIKeyUsage.Ua == nil { + break + } + + return e.complexity.GQLAPIKeyUsage.Ua(childComplexity), true + case "HeartbeatMonitor.href": if e.complexity.HeartbeatMonitor.Href == nil { break @@ -7810,6 +7865,94 @@ func (ec *executionContext) fieldContext_ConfigValue_deprecated(ctx context.Cont return fc, nil } +func (ec *executionContext) _CreatedGQLAPIKey_id(ctx context.Context, field graphql.CollectedField, obj *CreatedGQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CreatedGQLAPIKey_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CreatedGQLAPIKey_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreatedGQLAPIKey", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _CreatedGQLAPIKey_token(ctx context.Context, field graphql.CollectedField, obj *CreatedGQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CreatedGQLAPIKey_token(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Token, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CreatedGQLAPIKey_token(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreatedGQLAPIKey", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _DebugCarrierInfo_name(ctx context.Context, field graphql.CollectedField, obj *twilio.CarrierInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DebugCarrierInfo_name(ctx, field) if err != nil { @@ -9731,7 +9874,7 @@ func (ec *executionContext) _GQLAPIKey_createdBy(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.CreatedBy, nil + return ec.resolvers.GQLAPIKey().CreatedBy(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -9749,8 +9892,8 @@ func (ec *executionContext) fieldContext_GQLAPIKey_createdBy(ctx context.Context fc = &graphql.FieldContext{ Object: "GQLAPIKey", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": @@ -9784,8 +9927,8 @@ func (ec *executionContext) fieldContext_GQLAPIKey_createdBy(ctx context.Context return fc, nil } -func (ec *executionContext) _GQLAPIKey_lastUsed(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_GQLAPIKey_lastUsed(ctx, field) +func (ec *executionContext) _GQLAPIKey_updatedAt(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_updatedAt(ctx, field) if err != nil { return graphql.Null } @@ -9798,7 +9941,7 @@ func (ec *executionContext) _GQLAPIKey_lastUsed(ctx context.Context, field graph }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.LastUsed, nil + return obj.UpdatedAt, nil }) if err != nil { ec.Error(ctx, err) @@ -9815,7 +9958,7 @@ func (ec *executionContext) _GQLAPIKey_lastUsed(ctx context.Context, field graph return ec.marshalNISOTimestamp2timeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_GQLAPIKey_lastUsed(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_updatedAt(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "GQLAPIKey", Field: field, @@ -9828,8 +9971,8 @@ func (ec *executionContext) fieldContext_GQLAPIKey_lastUsed(ctx context.Context, return fc, nil } -func (ec *executionContext) _GQLAPIKey_lastUsedUA(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_GQLAPIKey_lastUsedUA(ctx, field) +func (ec *executionContext) _GQLAPIKey_updatedBy(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_updatedBy(ctx, field) if err != nil { return graphql.Null } @@ -9842,31 +9985,103 @@ func (ec *executionContext) _GQLAPIKey_lastUsedUA(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.LastUsedUa, nil + return ec.resolvers.GQLAPIKey().UpdatedBy(rctx, obj) }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") + return graphql.Null + } + res := resTmp.(*user.User) + fc.Result = res + return ec.marshalOUser2ᚖgithubᚗcomᚋtargetᚋgoalertᚋuserᚐUser(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKey_updatedBy(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKey", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_User_id(ctx, field) + case "role": + return ec.fieldContext_User_role(ctx, field) + case "name": + return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "contactMethods": + return ec.fieldContext_User_contactMethods(ctx, field) + case "notificationRules": + return ec.fieldContext_User_notificationRules(ctx, field) + case "calendarSubscriptions": + return ec.fieldContext_User_calendarSubscriptions(ctx, field) + case "statusUpdateContactMethodID": + return ec.fieldContext_User_statusUpdateContactMethodID(ctx, field) + case "authSubjects": + return ec.fieldContext_User_authSubjects(ctx, field) + case "sessions": + return ec.fieldContext_User_sessions(ctx, field) + case "onCallSteps": + return ec.fieldContext_User_onCallSteps(ctx, field) + case "isFavorite": + return ec.fieldContext_User_isFavorite(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type User", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _GQLAPIKey_lastUsed(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKey) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKey_lastUsed(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.LastUsed, nil + }) + if err != nil { + ec.Error(ctx, err) return graphql.Null } - res := resTmp.(string) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*GQLAPIKeyUsage) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOGQLAPIKeyUsage2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKeyUsage(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_GQLAPIKey_lastUsedUA(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_GQLAPIKey_lastUsed(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "GQLAPIKey", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "time": + return ec.fieldContext_GQLAPIKeyUsage_time(ctx, field) + case "ua": + return ec.fieldContext_GQLAPIKeyUsage_ua(ctx, field) + case "ip": + return ec.fieldContext_GQLAPIKeyUsage_ip(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type GQLAPIKeyUsage", field.Name) }, } return fc, nil @@ -10001,6 +10216,138 @@ func (ec *executionContext) fieldContext_GQLAPIKey_token(ctx context.Context, fi return fc, nil } +func (ec *executionContext) _GQLAPIKeyUsage_time(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKeyUsage) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKeyUsage_time(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Time, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNISOTimestamp2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKeyUsage_time(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKeyUsage", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ISOTimestamp does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GQLAPIKeyUsage_ua(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKeyUsage) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKeyUsage_ua(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Ua, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKeyUsage_ua(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKeyUsage", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GQLAPIKeyUsage_ip(ctx context.Context, field graphql.CollectedField, obj *GQLAPIKeyUsage) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GQLAPIKeyUsage_ip(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IP, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GQLAPIKeyUsage_ip(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GQLAPIKeyUsage", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _HeartbeatMonitor_id(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { fc, err := ec.fieldContext_HeartbeatMonitor_id(ctx, field) if err != nil { @@ -14191,9 +14538,9 @@ func (ec *executionContext) _Mutation_createGQLAPIKey(ctx context.Context, field } return graphql.Null } - res := resTmp.(*GQLAPIKey) + res := resTmp.(*CreatedGQLAPIKey) fc.Result = res - return ec.marshalNGQLAPIKey2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKey(ctx, field.Selections, res) + return ec.marshalNCreatedGQLAPIKey2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreatedGQLAPIKey(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Mutation_createGQLAPIKey(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -14205,27 +14552,11 @@ func (ec *executionContext) fieldContext_Mutation_createGQLAPIKey(ctx context.Co Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_GQLAPIKey_id(ctx, field) - case "name": - return ec.fieldContext_GQLAPIKey_name(ctx, field) - case "description": - return ec.fieldContext_GQLAPIKey_description(ctx, field) - case "createdAt": - return ec.fieldContext_GQLAPIKey_createdAt(ctx, field) - case "createdBy": - return ec.fieldContext_GQLAPIKey_createdBy(ctx, field) - case "lastUsed": - return ec.fieldContext_GQLAPIKey_lastUsed(ctx, field) - case "lastUsedUA": - return ec.fieldContext_GQLAPIKey_lastUsedUA(ctx, field) - case "expiresAt": - return ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) - case "allowedFields": - return ec.fieldContext_GQLAPIKey_allowedFields(ctx, field) + return ec.fieldContext_CreatedGQLAPIKey_id(ctx, field) case "token": - return ec.fieldContext_GQLAPIKey_token(ctx, field) + return ec.fieldContext_CreatedGQLAPIKey_token(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type GQLAPIKey", field.Name) + return nil, fmt.Errorf("no field named %q was found under type CreatedGQLAPIKey", field.Name) }, } defer func() { @@ -18146,10 +18477,12 @@ func (ec *executionContext) fieldContext_Query_gqlAPIKeys(ctx context.Context, f return ec.fieldContext_GQLAPIKey_createdAt(ctx, field) case "createdBy": return ec.fieldContext_GQLAPIKey_createdBy(ctx, field) + case "updatedAt": + return ec.fieldContext_GQLAPIKey_updatedAt(ctx, field) + case "updatedBy": + return ec.fieldContext_GQLAPIKey_updatedBy(ctx, field) case "lastUsed": return ec.fieldContext_GQLAPIKey_lastUsed(ctx, field) - case "lastUsedUA": - return ec.fieldContext_GQLAPIKey_lastUsedUA(ctx, field) case "expiresAt": return ec.fieldContext_GQLAPIKey_expiresAt(ctx, field) case "allowedFields": @@ -23472,9 +23805,9 @@ func (ec *executionContext) _User_sessions(ctx context.Context, field graphql.Co } return graphql.Null } - res := resTmp.([]auth.UserSession) + res := resTmp.([]UserSession) fc.Result = res - return ec.marshalNUserSession2ᚕgithubᚗcomᚋtargetᚋgoalertᚋauthᚐUserSessionᚄ(ctx, field.Selections, res) + return ec.marshalNUserSession2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUserSessionᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_User_sessions(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -25295,7 +25628,7 @@ func (ec *executionContext) fieldContext_UserOverrideConnection_pageInfo(ctx con return fc, nil } -func (ec *executionContext) _UserSession_id(ctx context.Context, field graphql.CollectedField, obj *auth.UserSession) (ret graphql.Marshaler) { +func (ec *executionContext) _UserSession_id(ctx context.Context, field graphql.CollectedField, obj *UserSession) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserSession_id(ctx, field) if err != nil { return graphql.Null @@ -25339,7 +25672,7 @@ func (ec *executionContext) fieldContext_UserSession_id(ctx context.Context, fie return fc, nil } -func (ec *executionContext) _UserSession_current(ctx context.Context, field graphql.CollectedField, obj *auth.UserSession) (ret graphql.Marshaler) { +func (ec *executionContext) _UserSession_current(ctx context.Context, field graphql.CollectedField, obj *UserSession) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserSession_current(ctx, field) if err != nil { return graphql.Null @@ -25353,7 +25686,7 @@ func (ec *executionContext) _UserSession_current(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.UserSession().Current(rctx, obj) + return obj.Current, nil }) if err != nil { ec.Error(ctx, err) @@ -25374,8 +25707,8 @@ func (ec *executionContext) fieldContext_UserSession_current(ctx context.Context fc = &graphql.FieldContext{ Object: "UserSession", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") }, @@ -25383,7 +25716,7 @@ func (ec *executionContext) fieldContext_UserSession_current(ctx context.Context return fc, nil } -func (ec *executionContext) _UserSession_userAgent(ctx context.Context, field graphql.CollectedField, obj *auth.UserSession) (ret graphql.Marshaler) { +func (ec *executionContext) _UserSession_userAgent(ctx context.Context, field graphql.CollectedField, obj *UserSession) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserSession_userAgent(ctx, field) if err != nil { return graphql.Null @@ -25427,7 +25760,7 @@ func (ec *executionContext) fieldContext_UserSession_userAgent(ctx context.Conte return fc, nil } -func (ec *executionContext) _UserSession_createdAt(ctx context.Context, field graphql.CollectedField, obj *auth.UserSession) (ret graphql.Marshaler) { +func (ec *executionContext) _UserSession_createdAt(ctx context.Context, field graphql.CollectedField, obj *UserSession) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserSession_createdAt(ctx, field) if err != nil { return graphql.Null @@ -25471,7 +25804,7 @@ func (ec *executionContext) fieldContext_UserSession_createdAt(ctx context.Conte return fc, nil } -func (ec *executionContext) _UserSession_lastAccessAt(ctx context.Context, field graphql.CollectedField, obj *auth.UserSession) (ret graphql.Marshaler) { +func (ec *executionContext) _UserSession_lastAccessAt(ctx context.Context, field graphql.CollectedField, obj *UserSession) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserSession_lastAccessAt(ctx, field) if err != nil { return graphql.Null @@ -32282,24 +32615,68 @@ func (ec *executionContext) _AuthSubject(ctx context.Context, sel ast.SelectionS return out } -var authSubjectConnectionImplementors = []string{"AuthSubjectConnection"} +var authSubjectConnectionImplementors = []string{"AuthSubjectConnection"} + +func (ec *executionContext) _AuthSubjectConnection(ctx context.Context, sel ast.SelectionSet, obj *AuthSubjectConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, authSubjectConnectionImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AuthSubjectConnection") + case "nodes": + out.Values[i] = ec._AuthSubjectConnection_nodes(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pageInfo": + out.Values[i] = ec._AuthSubjectConnection_pageInfo(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var configHintImplementors = []string{"ConfigHint"} -func (ec *executionContext) _AuthSubjectConnection(ctx context.Context, sel ast.SelectionSet, obj *AuthSubjectConnection) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, authSubjectConnectionImplementors) +func (ec *executionContext) _ConfigHint(ctx context.Context, sel ast.SelectionSet, obj *ConfigHint) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, configHintImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("AuthSubjectConnection") - case "nodes": - out.Values[i] = ec._AuthSubjectConnection_nodes(ctx, field, obj) + out.Values[i] = graphql.MarshalString("ConfigHint") + case "id": + out.Values[i] = ec._ConfigHint_id(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "pageInfo": - out.Values[i] = ec._AuthSubjectConnection_pageInfo(ctx, field, obj) + case "value": + out.Values[i] = ec._ConfigHint_value(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -32326,24 +32703,44 @@ func (ec *executionContext) _AuthSubjectConnection(ctx context.Context, sel ast. return out } -var configHintImplementors = []string{"ConfigHint"} +var configValueImplementors = []string{"ConfigValue"} -func (ec *executionContext) _ConfigHint(ctx context.Context, sel ast.SelectionSet, obj *ConfigHint) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, configHintImplementors) +func (ec *executionContext) _ConfigValue(ctx context.Context, sel ast.SelectionSet, obj *ConfigValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, configValueImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("ConfigHint") + out.Values[i] = graphql.MarshalString("ConfigValue") case "id": - out.Values[i] = ec._ConfigHint_id(ctx, field, obj) + out.Values[i] = ec._ConfigValue_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec._ConfigValue_description(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } case "value": - out.Values[i] = ec._ConfigHint_value(ctx, field, obj) + out.Values[i] = ec._ConfigValue_value(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "type": + out.Values[i] = ec._ConfigValue_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "password": + out.Values[i] = ec._ConfigValue_password(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecated": + out.Values[i] = ec._ConfigValue_deprecated(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -32370,44 +32767,24 @@ func (ec *executionContext) _ConfigHint(ctx context.Context, sel ast.SelectionSe return out } -var configValueImplementors = []string{"ConfigValue"} +var createdGQLAPIKeyImplementors = []string{"CreatedGQLAPIKey"} -func (ec *executionContext) _ConfigValue(ctx context.Context, sel ast.SelectionSet, obj *ConfigValue) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, configValueImplementors) +func (ec *executionContext) _CreatedGQLAPIKey(ctx context.Context, sel ast.SelectionSet, obj *CreatedGQLAPIKey) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, createdGQLAPIKeyImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("ConfigValue") + out.Values[i] = graphql.MarshalString("CreatedGQLAPIKey") case "id": - out.Values[i] = ec._ConfigValue_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "description": - out.Values[i] = ec._ConfigValue_description(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "value": - out.Values[i] = ec._ConfigValue_value(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "type": - out.Values[i] = ec._ConfigValue_type(ctx, field, obj) + out.Values[i] = ec._CreatedGQLAPIKey_id(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "password": - out.Values[i] = ec._ConfigValue_password(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "deprecated": - out.Values[i] = ec._ConfigValue_deprecated(ctx, field, obj) + case "token": + out.Values[i] = ec._CreatedGQLAPIKey_token(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -33035,44 +33412,105 @@ func (ec *executionContext) _GQLAPIKey(ctx context.Context, sel ast.SelectionSet case "id": out.Values[i] = ec._GQLAPIKey_id(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "name": out.Values[i] = ec._GQLAPIKey_name(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "description": out.Values[i] = ec._GQLAPIKey_description(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "createdAt": out.Values[i] = ec._GQLAPIKey_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "createdBy": - out.Values[i] = ec._GQLAPIKey_createdBy(ctx, field, obj) - case "lastUsed": - out.Values[i] = ec._GQLAPIKey_lastUsed(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._GQLAPIKey_createdBy(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue } - case "lastUsedUA": - out.Values[i] = ec._GQLAPIKey_lastUsedUA(ctx, field, obj) + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "updatedAt": + out.Values[i] = ec._GQLAPIKey_updatedAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) + } + case "updatedBy": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._GQLAPIKey_updatedBy(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "lastUsed": + out.Values[i] = ec._GQLAPIKey_lastUsed(ctx, field, obj) case "expiresAt": out.Values[i] = ec._GQLAPIKey_expiresAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "allowedFields": out.Values[i] = ec._GQLAPIKey_allowedFields(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "token": out.Values[i] = ec._GQLAPIKey_token(ctx, field, obj) @@ -33099,6 +33537,55 @@ func (ec *executionContext) _GQLAPIKey(ctx context.Context, sel ast.SelectionSet return out } +var gQLAPIKeyUsageImplementors = []string{"GQLAPIKeyUsage"} + +func (ec *executionContext) _GQLAPIKeyUsage(ctx context.Context, sel ast.SelectionSet, obj *GQLAPIKeyUsage) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, gQLAPIKeyUsageImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("GQLAPIKeyUsage") + case "time": + out.Values[i] = ec._GQLAPIKeyUsage_time(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "ua": + out.Values[i] = ec._GQLAPIKeyUsage_ua(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "ip": + out.Values[i] = ec._GQLAPIKeyUsage_ip(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var heartbeatMonitorImplementors = []string{"HeartbeatMonitor"} func (ec *executionContext) _HeartbeatMonitor(ctx context.Context, sel ast.SelectionSet, obj *heartbeat.Monitor) graphql.Marshaler { @@ -38434,7 +38921,7 @@ func (ec *executionContext) _UserOverrideConnection(ctx context.Context, sel ast var userSessionImplementors = []string{"UserSession"} -func (ec *executionContext) _UserSession(ctx context.Context, sel ast.SelectionSet, obj *auth.UserSession) graphql.Marshaler { +func (ec *executionContext) _UserSession(ctx context.Context, sel ast.SelectionSet, obj *UserSession) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, userSessionImplementors) out := graphql.NewFieldSet(fields) @@ -38446,58 +38933,27 @@ func (ec *executionContext) _UserSession(ctx context.Context, sel ast.SelectionS case "id": out.Values[i] = ec._UserSession_id(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "current": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._UserSession_current(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + out.Values[i] = ec._UserSession_current(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "userAgent": out.Values[i] = ec._UserSession_userAgent(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "createdAt": out.Values[i] = ec._UserSession_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "lastAccessAt": out.Values[i] = ec._UserSession_lastAccessAt(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } default: panic("unknown field " + strconv.Quote(field.Name)) @@ -39328,6 +39784,20 @@ func (ec *executionContext) unmarshalNCreateUserOverrideInput2githubᚗcomᚋtar return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalNCreatedGQLAPIKey2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreatedGQLAPIKey(ctx context.Context, sel ast.SelectionSet, v CreatedGQLAPIKey) graphql.Marshaler { + return ec._CreatedGQLAPIKey(ctx, sel, &v) +} + +func (ec *executionContext) marshalNCreatedGQLAPIKey2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreatedGQLAPIKey(ctx context.Context, sel ast.SelectionSet, v *CreatedGQLAPIKey) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._CreatedGQLAPIKey(ctx, sel, v) +} + func (ec *executionContext) marshalNDebugCarrierInfo2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋtwilioᚐCarrierInfo(ctx context.Context, sel ast.SelectionSet, v twilio.CarrierInfo) graphql.Marshaler { return ec._DebugCarrierInfo(ctx, sel, &v) } @@ -39577,16 +40047,6 @@ func (ec *executionContext) marshalNGQLAPIKey2ᚕgithubᚗcomᚋtargetᚋgoalert return ret } -func (ec *executionContext) marshalNGQLAPIKey2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKey(ctx context.Context, sel ast.SelectionSet, v *GQLAPIKey) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._GQLAPIKey(ctx, sel, v) -} - func (ec *executionContext) marshalNHeartbeatMonitor2githubᚗcomᚋtargetᚋgoalertᚋheartbeatᚐMonitor(ctx context.Context, sel ast.SelectionSet, v heartbeat.Monitor) graphql.Marshaler { return ec._HeartbeatMonitor(ctx, sel, &v) } @@ -41625,11 +42085,11 @@ func (ec *executionContext) marshalNUserRole2githubᚗcomᚋtargetᚋgoalertᚋg return v } -func (ec *executionContext) marshalNUserSession2githubᚗcomᚋtargetᚋgoalertᚋauthᚐUserSession(ctx context.Context, sel ast.SelectionSet, v auth.UserSession) graphql.Marshaler { +func (ec *executionContext) marshalNUserSession2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUserSession(ctx context.Context, sel ast.SelectionSet, v UserSession) graphql.Marshaler { return ec._UserSession(ctx, sel, &v) } -func (ec *executionContext) marshalNUserSession2ᚕgithubᚗcomᚋtargetᚋgoalertᚋauthᚐUserSessionᚄ(ctx context.Context, sel ast.SelectionSet, v []auth.UserSession) graphql.Marshaler { +func (ec *executionContext) marshalNUserSession2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUserSessionᚄ(ctx context.Context, sel ast.SelectionSet, v []UserSession) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -41653,7 +42113,7 @@ func (ec *executionContext) marshalNUserSession2ᚕgithubᚗcomᚋtargetᚋgoale if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNUserSession2githubᚗcomᚋtargetᚋgoalertᚋauthᚐUserSession(ctx, sel, v[i]) + ret[i] = ec.marshalNUserSession2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUserSession(ctx, sel, v[i]) } if isLen1 { f(i) @@ -42369,6 +42829,13 @@ func (ec *executionContext) marshalOEscalationPolicyStep2ᚖgithubᚗcomᚋtarge return ec._EscalationPolicyStep(ctx, sel, v) } +func (ec *executionContext) marshalOGQLAPIKeyUsage2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐGQLAPIKeyUsage(ctx context.Context, sel ast.SelectionSet, v *GQLAPIKeyUsage) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._GQLAPIKeyUsage(ctx, sel, v) +} + func (ec *executionContext) marshalOHeartbeatMonitor2ᚖgithubᚗcomᚋtargetᚋgoalertᚋheartbeatᚐMonitor(ctx context.Context, sel ast.SelectionSet, v *heartbeat.Monitor) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graphql2/gqlgen.yml b/graphql2/gqlgen.yml index d3794aabcb..727c747b21 100644 --- a/graphql2/gqlgen.yml +++ b/graphql2/gqlgen.yml @@ -84,8 +84,6 @@ models: model: github.com/target/goalert/limit.ID DebugCarrierInfo: model: github.com/target/goalert/notification/twilio.CarrierInfo - UserSession: - model: github.com/target/goalert/auth.UserSession Notice: model: github.com/target/goalert/notice.Notice NoticeType: diff --git a/graphql2/graphqlapp/app.go b/graphql2/graphqlapp/app.go index e6923d4f2a..20b210a83b 100644 --- a/graphql2/graphqlapp/app.go +++ b/graphql2/graphqlapp/app.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "net/http" + "slices" "strconv" "time" @@ -162,7 +163,7 @@ func (a *App) Handler() http.Handler { } p := apikey.PolicyFromContext(ctx) - if p == nil || p.GraphQLV1 == nil { + if p == nil || p.Version != 1 { return nil, permission.NewAccessDenied("invalid API key") } @@ -170,10 +171,10 @@ func (a *App) Handler() http.Handler { objName := f.Field.Field.ObjectDefinition.Name fieldName := f.Field.Field.Definition.Name - for _, allowed := range p.GraphQLV1.AllowedFields { - if allowed.ObjectName == objName && allowed.Name == fieldName { - return next(ctx) - } + field := objName + "." + fieldName + + if slices.Contains(p.AllowedFields, field) { + return next(ctx) } return nil, permission.NewAccessDenied("field not allowed by API key") diff --git a/graphql2/graphqlapp/gqlapikeys.go b/graphql2/graphqlapp/gqlapikeys.go index abdd504f89..27d0d680be 100644 --- a/graphql2/graphqlapp/gqlapikeys.go +++ b/graphql2/graphqlapp/gqlapikeys.go @@ -2,84 +2,125 @@ package graphqlapp import ( "context" - "fmt" - "sort" + "database/sql" + "time" + "github.com/target/goalert/apikey" "github.com/target/goalert/graphql2" - "github.com/target/goalert/validation" - "github.com/vektah/gqlparser/v2" - "github.com/vektah/gqlparser/v2/ast" - "github.com/vektah/gqlparser/v2/parser" - "github.com/vektah/gqlparser/v2/validator" + "github.com/target/goalert/permission" + "github.com/target/goalert/user" ) -func (q *Query) GqlAPIKeys(ctx context.Context) ([]graphql2.GQLAPIKey, error) { - return nil, nil +type GQLAPIKey App + +func (a *App) GQLAPIKey() graphql2.GQLAPIKeyResolver { return (*GQLAPIKey)(a) } + +func (a *GQLAPIKey) CreatedBy(ctx context.Context, obj *graphql2.GQLAPIKey) (*user.User, error) { + if obj.CreatedBy == nil { + return nil, nil + } + + return (*App)(a).FindOneUser(ctx, obj.CreatedBy.ID) } -func (q *Query) ListGQLFields(ctx context.Context, query *string) ([]string, error) { - if query == nil || *query == "" { - sch, err := parser.ParseSchema(&ast.Source{Input: graphql2.Schema()}) - if err != nil { - return nil, fmt.Errorf("parse schema: %w", err) +func (a *GQLAPIKey) UpdatedBy(ctx context.Context, obj *graphql2.GQLAPIKey) (*user.User, error) { + if obj.UpdatedBy == nil { + return nil, nil + } + + return (*App)(a).FindOneUser(ctx, obj.UpdatedBy.ID) +} + +func (q *Query) GqlAPIKeys(ctx context.Context) ([]graphql2.GQLAPIKey, error) { + err := permission.LimitCheckAny(ctx, permission.Admin) + if err != nil { + return nil, err + } + + keys, err := q.APIKeyStore.FindAllAdminGraphQLKeys(ctx) + if err != nil { + return nil, err + } + + res := make([]graphql2.GQLAPIKey, len(keys)) + for i, k := range keys { + res[i] = graphql2.GQLAPIKey{ + ID: k.ID.String(), + Name: k.Name, + Description: k.Description, + CreatedAt: k.CreatedAt, + UpdatedAt: k.UpdatedAt, + ExpiresAt: k.ExpiresAt, + AllowedFields: k.AllowedFields, } - var fields []string - for _, typ := range sch.Definitions { - if typ.Kind != ast.Object { - continue - } - for _, f := range typ.Fields { - fields = append(fields, typ.Name+"."+f.Name) + if k.CreatedBy != nil { + res[i].CreatedBy = &user.User{ID: k.CreatedBy.String()} + } + if k.UpdatedBy != nil { + res[i].UpdatedBy = &user.User{ID: k.UpdatedBy.String()} + } + + if k.LastUsed != nil { + res[i].LastUsed = &graphql2.GQLAPIKeyUsage{ + Time: k.LastUsed.Time, + Ua: k.LastUsed.UserAgent, + IP: k.LastUsed.IP, } } - sort.Strings(fields) - return fields, nil } - sch, err := gqlparser.LoadSchema(&ast.Source{Input: graphql2.Schema()}) - if err != nil { - return nil, fmt.Errorf("parse schema: %w", err) - } + return res, nil +} - qDoc, qErr := gqlparser.LoadQuery(sch, *query) - if len(qErr) > 0 { - return nil, validation.NewFieldError("Query", qErr.Error()) +func nullTimeToPointer(nt sql.NullTime) *time.Time { + if !nt.Valid { + return nil } + return &nt.Time +} - var fields []string - var e validator.Events - e.OnField(func(w *validator.Walker, field *ast.Field) { - fields = append(fields, field.ObjectDefinition.Name+"."+field.Name) - }) - validator.Walk(sch, qDoc, &e) +func (q *Query) ListGQLFields(ctx context.Context, query *string) ([]string, error) { + if query == nil || *query == "" { + return graphql2.SchemaFields(), nil + } - sort.Strings(fields) - return fields, nil + return graphql2.QueryFields(*query) } func (a *Mutation) UpdateGQLAPIKey(ctx context.Context, input graphql2.UpdateGQLAPIKeyInput) (bool, error) { - return false, nil + id, err := parseUUID("ID", input.ID) + if err != nil { + return false, err + } + + err = a.APIKeyStore.UpdateAdminGraphQLKey(ctx, id, input.Name, input.Description) + return err == nil, err } + func (a *Mutation) DeleteGQLAPIKey(ctx context.Context, input string) (bool, error) { - return false, nil + id, err := parseUUID("ID", input) + if err != nil { + return false, err + } + + err = a.APIKeyStore.DeleteAdminGraphQLKey(ctx, id) + return err == nil, err } -func (a *Mutation) CreateGQLAPIKey(ctx context.Context, input graphql2.CreateGQLAPIKeyInput) (*graphql2.GQLAPIKey, error) { - return nil, nil - // _, err := gqlauth.NewQuery(input.Query) - // if err != nil { - // return nil, validation.NewFieldError("Query", err.Error()) - // } - - // key, err := a.APIKeyStore.CreateAdminGraphQLKey(ctx, input.Name, input.Query, input.ExpiresAt) - // if err != nil { - // return nil, err - // } - - // return &graphql2.GQLAPIKey{ - // ID: key.ID.String(), - // Name: key.Name, - // ExpiresAt: key.ExpiresAt, - // Token: &key.Token, - // }, nil + +func (a *Mutation) CreateGQLAPIKey(ctx context.Context, input graphql2.CreateGQLAPIKeyInput) (*graphql2.CreatedGQLAPIKey, error) { + id, tok, err := a.APIKeyStore.CreateAdminGraphQLKey(ctx, apikey.NewAdminGQLKeyOpts{ + Name: input.Name, + Desc: input.Description, + Expires: input.ExpiresAt, + Fields: input.AllowedFields, + }) + if err != nil { + return nil, err + } + + return &graphql2.CreatedGQLAPIKey{ + ID: id.String(), + Token: tok, + }, nil } diff --git a/graphql2/graphqlapp/user.go b/graphql2/graphqlapp/user.go index 350a02794d..0ef315fff8 100644 --- a/graphql2/graphqlapp/user.go +++ b/graphql2/graphqlapp/user.go @@ -4,7 +4,6 @@ import ( context "context" "database/sql" - "github.com/target/goalert/auth" "github.com/target/goalert/auth/basic" "github.com/target/goalert/calsub" "github.com/target/goalert/validation" @@ -22,28 +21,41 @@ import ( ) type ( - User App - UserSession App + User App ) func (a *App) User() graphql2.UserResolver { return (*User)(a) } -func (a *App) UserSession() graphql2.UserSessionResolver { return (*UserSession)(a) } +func (a *User) Sessions(ctx context.Context, obj *user.User) ([]graphql2.UserSession, error) { + sess, err := a.AuthHandler.FindAllUserSessions(ctx, obj.ID) + if err != nil { + return nil, err + } -func (a *User) Sessions(ctx context.Context, obj *user.User) ([]auth.UserSession, error) { - return a.AuthHandler.FindAllUserSessions(ctx, obj.ID) -} + out := make([]graphql2.UserSession, len(sess)) + for i, s := range sess { -func (a *UserSession) Current(ctx context.Context, obj *auth.UserSession) (bool, error) { + out[i] = graphql2.UserSession{ + ID: s.ID, + UserAgent: s.UserAgent, + CreatedAt: s.CreatedAt, + LastAccessAt: s.LastAccessAt, + Current: isCurrentSession(ctx, s.ID), + } + } + + return out, nil +} +func isCurrentSession(ctx context.Context, sessID string) bool { src := permission.Source(ctx) if src == nil { - return false, nil + return false } if src.Type != permission.SourceTypeAuthProvider { - return false, nil + return false } - return obj.ID == src.ID, nil + return src.ID == sessID } func (a *User) AuthSubjects(ctx context.Context, obj *user.User) ([]user.AuthSubject, error) { diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 50b04928b1..fd7341f648 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -227,6 +227,11 @@ type CreateUserOverrideInput struct { RemoveUserID *string `json:"removeUserID,omitempty"` } +type CreatedGQLAPIKey struct { + ID string `json:"id"` + Token string `json:"token"` +} + type DebugCarrierInfoInput struct { Number string `json:"number"` } @@ -290,16 +295,23 @@ type EscalationPolicySearchOptions struct { } type GQLAPIKey struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - CreatedAt time.Time `json:"createdAt"` - CreatedBy *user.User `json:"createdBy,omitempty"` - LastUsed time.Time `json:"lastUsed"` - LastUsedUa string `json:"lastUsedUA"` - ExpiresAt time.Time `json:"expiresAt"` - AllowedFields []string `json:"allowedFields"` - Token *string `json:"token,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + CreatedBy *user.User `json:"createdBy,omitempty"` + UpdatedAt time.Time `json:"updatedAt"` + UpdatedBy *user.User `json:"updatedBy,omitempty"` + LastUsed *GQLAPIKeyUsage `json:"lastUsed,omitempty"` + ExpiresAt time.Time `json:"expiresAt"` + AllowedFields []string `json:"allowedFields"` + Token *string `json:"token,omitempty"` +} + +type GQLAPIKeyUsage struct { + Time time.Time `json:"time"` + Ua string `json:"ua"` + IP string `json:"ip"` } type IntegrationKeyConnection struct { @@ -712,6 +724,14 @@ type UserSearchOptions struct { FavoritesFirst *bool `json:"favoritesFirst,omitempty"` } +type UserSession struct { + ID string `json:"id"` + Current bool `json:"current"` + UserAgent string `json:"userAgent"` + CreatedAt time.Time `json:"createdAt"` + LastAccessAt time.Time `json:"lastAccessAt"` +} + type VerifyContactMethodInput struct { ContactMethodID string `json:"contactMethodID"` Code int `json:"code"` diff --git a/graphql2/schema.go b/graphql2/schema.go index 6d949a69d6..823e7ddef4 100644 --- a/graphql2/schema.go +++ b/graphql2/schema.go @@ -2,11 +2,66 @@ package graphql2 import ( _ "embed" + "sort" + + "github.com/target/goalert/validation" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" + "github.com/vektah/gqlparser/v2/parser" + "github.com/vektah/gqlparser/v2/validator" ) //go:embed schema.graphql var schema string +var schemaFields []string +var astSchema *ast.Schema + +// Schema will return the GraphQL schema. func Schema() string { return schema } + +func init() { + schDoc, err := parser.ParseSchema(&ast.Source{Input: schema}) + if err != nil { + panic(err) + } + + sch, err := gqlparser.LoadSchema(&ast.Source{Input: schema}) + if err != nil { + panic(err) + } + astSchema = sch + + for _, typ := range schDoc.Definitions { + if typ.Kind != ast.Object { + continue + } + for _, f := range typ.Fields { + schemaFields = append(schemaFields, typ.Name+"."+f.Name) + } + } + sort.Strings(schemaFields) +} + +// SchemaFields will return a list of all fields in the schema. +func SchemaFields() []string { return schemaFields } + +// QueryFields will return a list of all fields that the given query references. +func QueryFields(query string) ([]string, error) { + qDoc, qErr := gqlparser.LoadQuery(astSchema, query) + if len(qErr) > 0 { + return nil, validation.NewFieldError("Query", qErr.Error()) + } + + var fields []string + var e validator.Events + e.OnField(func(w *validator.Walker, field *ast.Field) { + fields = append(fields, field.ObjectDefinition.Name+"."+field.Name) + }) + validator.Walk(astSchema, qDoc, &e) + + sort.Strings(fields) + return fields, nil +} diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index d629f3bddb..829de72335 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -1,3 +1,9 @@ +directive @goField( + forceResolver: Boolean + name: String + omittable: Boolean +) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION + type Query { phoneNumberInfo(number: String!): PhoneNumberInfo @@ -583,7 +589,7 @@ type Mutation { setConfig(input: [ConfigValueInput!]): Boolean! setSystemLimits(input: [SystemLimitInput!]!): Boolean! - createGQLAPIKey(input: CreateGQLAPIKeyInput!): GQLAPIKey! + createGQLAPIKey(input: CreateGQLAPIKeyInput!): CreatedGQLAPIKey! updateGQLAPIKey(input: UpdateGQLAPIKeyInput!): Boolean! deleteGQLAPIKey(id: ID!): Boolean! @@ -591,6 +597,11 @@ type Mutation { updateBasicAuth(input: UpdateBasicAuthInput!): Boolean! } +type CreatedGQLAPIKey { + id: ID! + token: String! +} + input CreateGQLAPIKeyInput { name: String! description: String! @@ -609,14 +620,21 @@ type GQLAPIKey { name: String! description: String! createdAt: ISOTimestamp! - createdBy: User - lastUsed: ISOTimestamp! - lastUsedUA: String! + createdBy: User @goField(forceResolver: true) + updatedAt: ISOTimestamp! + updatedBy: User @goField(forceResolver: true) + lastUsed: GQLAPIKeyUsage expiresAt: ISOTimestamp! allowedFields: [String!]! token: String } +type GQLAPIKeyUsage { + time: ISOTimestamp! + ua: String! + ip: String! +} + input CreateBasicAuthInput { username: String! password: String! diff --git a/migrate/migrations/20230907112347-graphql-api-key.sql b/migrate/migrations/20230907112347-graphql-api-key.sql index 48d41d3537..f138dff278 100644 --- a/migrate/migrations/20230907112347-graphql-api-key.sql +++ b/migrate/migrations/20230907112347-graphql-api-key.sql @@ -8,7 +8,9 @@ CREATE TABLE gql_api_keys( updated_at timestamp with time zone NOT NULL DEFAULT now(), updated_by uuid REFERENCES users(id) ON DELETE SET NULL, policy jsonb NOT NULL, - expires_at timestamp with time zone NOT NULL + expires_at timestamp with time zone NOT NULL, + deleted_at timestamp with time zone, + deleted_by uuid REFERENCES users(id) ON DELETE SET NULL ); CREATE TABLE gql_api_key_usage( diff --git a/validation/validate/oneof.go b/validation/validate/oneof.go index 6e7a6163f8..9af9f0f787 100644 --- a/validation/validate/oneof.go +++ b/validation/validate/oneof.go @@ -2,12 +2,13 @@ package validate import ( "fmt" - "github.com/target/goalert/validation" "strings" + + "github.com/target/goalert/validation" ) // OneOf will check that value is one of the provided options. -func OneOf(fname string, value interface{}, options ...interface{}) error { +func OneOf[T comparable](fname string, value T, options ...T) error { for _, o := range options { if o == value { return nil diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index a190d2bc8b..684009f5e1 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -424,13 +424,18 @@ export interface Mutation { updateAlertsByService: boolean setConfig: boolean setSystemLimits: boolean - createGQLAPIKey: GQLAPIKey + createGQLAPIKey: CreatedGQLAPIKey updateGQLAPIKey: boolean deleteGQLAPIKey: boolean createBasicAuth: boolean updateBasicAuth: boolean } +export interface CreatedGQLAPIKey { + id: string + token: string +} + export interface CreateGQLAPIKeyInput { name: string description: string @@ -450,13 +455,20 @@ export interface GQLAPIKey { description: string createdAt: ISOTimestamp createdBy?: null | User - lastUsed: ISOTimestamp - lastUsedUA: string + updatedAt: ISOTimestamp + updatedBy?: null | User + lastUsed?: null | GQLAPIKeyUsage expiresAt: ISOTimestamp allowedFields: string[] token?: null | string } +export interface GQLAPIKeyUsage { + time: ISOTimestamp + ua: string + ip: string +} + export interface CreateBasicAuthInput { username: string password: string From 0298031e18a09431e60af651e47c405857b0fbd9 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 15:29:20 -0500 Subject: [PATCH 13/56] tweak logging --- apikey/store.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apikey/store.go b/apikey/store.go index 6850676d98..146c69042c 100644 --- a/apikey/store.go +++ b/apikey/store.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "database/sql" "encoding/json" + "errors" "fmt" "net" "sort" @@ -190,7 +191,6 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte var claims Claims _, err := s.key.VerifyJWT(tok, &claims, Issuer, Audience) if err != nil { - log.Logf(ctx, "apikey: verify failed: %v", err) return ctx, permission.Unauthorized() } id, err := uuid.Parse(claims.Subject) @@ -201,7 +201,9 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte polData, err := gadb.New(s.db).APIKeyAuthPolicy(ctx, id) if err != nil { - log.Logf(ctx, "apikey: lookup failed: %v", err) + if !errors.Is(err, sql.ErrNoRows) { + log.Log(ctx, err) + } return ctx, permission.Unauthorized() } var buf bytes.Buffer @@ -239,7 +241,8 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte } err = gadb.New(s.db).APIKeyRecordUsage(ctx, params) if err != nil { - log.Logf(ctx, "apikey: failed to record usage: %v", err) + log.Log(ctx, err) + // don't fail authorization if we can't record usage } s.mx.Lock() From e8070a25347376a9a611ae7987b3b6bc5e1a7af9 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 7 Sep 2023 15:47:36 -0500 Subject: [PATCH 14/56] single row per last-used-key --- apikey/queries.sql | 12 ++++++------ gadb/queries.sql.go | 8 ++++---- .../migrations/20230907112347-graphql-api-key.sql | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apikey/queries.sql b/apikey/queries.sql index e31b0125b5..2e332369fe 100644 --- a/apikey/queries.sql +++ b/apikey/queries.sql @@ -30,7 +30,10 @@ WHERE id = $1; -- name: APIKeyRecordUsage :exec -- APIKeyRecordUsage records the usage of an API key. INSERT INTO gql_api_key_usage(api_key_id, user_agent, ip_address) - VALUES (@key_id::uuid, @user_agent::text, @ip_address::inet); + VALUES (@key_id::uuid, @user_agent::text, @ip_address::inet) +ON CONFLICT (api_key_id) + DO UPDATE SET + used_at = now(), user_agent = @user_agent::text, ip_address = @ip_address::inet; -- name: APIKeyAuthPolicy :one -- APIKeyAuth returns the API key policy with the given id, if it exists and is not expired. @@ -45,7 +48,7 @@ WHERE -- name: APIKeyList :many -- APIKeyList returns all API keys, along with the last time they were used. -SELECT DISTINCT ON (gql_api_keys.id) +SELECT gql_api_keys.*, gql_api_key_usage.used_at AS last_used_at, gql_api_key_usage.user_agent AS last_user_agent, @@ -54,8 +57,5 @@ FROM gql_api_keys LEFT JOIN gql_api_key_usage ON gql_api_keys.id = gql_api_key_usage.api_key_id WHERE - gql_api_keys.deleted_at IS NULL -ORDER BY - gql_api_keys.id, - gql_api_key_usage.used_at DESC; + gql_api_keys.deleted_at IS NULL; diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index deb00dde32..4cccc42e9b 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -98,7 +98,7 @@ func (q *Queries) APIKeyInsert(ctx context.Context, arg APIKeyInsertParams) erro } const aPIKeyList = `-- name: APIKeyList :many -SELECT DISTINCT ON (gql_api_keys.id) +SELECT gql_api_keys.created_at, gql_api_keys.created_by, gql_api_keys.description, gql_api_keys.expires_at, gql_api_keys.id, gql_api_keys.name, gql_api_keys.policy, gql_api_keys.updated_at, gql_api_keys.updated_by, gql_api_key_usage.used_at AS last_used_at, gql_api_key_usage.user_agent AS last_user_agent, @@ -108,9 +108,6 @@ FROM LEFT JOIN gql_api_key_usage ON gql_api_keys.id = gql_api_key_usage.api_key_id WHERE gql_api_keys.deleted_at IS NULL -ORDER BY - gql_api_keys.id, - gql_api_key_usage.used_at DESC ` type APIKeyListRow struct { @@ -168,6 +165,9 @@ func (q *Queries) APIKeyList(ctx context.Context) ([]APIKeyListRow, error) { const aPIKeyRecordUsage = `-- name: APIKeyRecordUsage :exec INSERT INTO gql_api_key_usage(api_key_id, user_agent, ip_address) VALUES ($1::uuid, $2::text, $3::inet) +ON CONFLICT (api_key_id) + DO UPDATE SET + used_at = now(), user_agent = $2::text, ip_address = $3::inet ` type APIKeyRecordUsageParams struct { diff --git a/migrate/migrations/20230907112347-graphql-api-key.sql b/migrate/migrations/20230907112347-graphql-api-key.sql index f138dff278..60edd7b301 100644 --- a/migrate/migrations/20230907112347-graphql-api-key.sql +++ b/migrate/migrations/20230907112347-graphql-api-key.sql @@ -15,7 +15,7 @@ CREATE TABLE gql_api_keys( CREATE TABLE gql_api_key_usage( id bigserial PRIMARY KEY, - api_key_id uuid REFERENCES gql_api_keys(id) ON DELETE CASCADE, + api_key_id uuid REFERENCES gql_api_keys(id) ON DELETE CASCADE UNIQUE, used_at timestamp with time zone NOT NULL DEFAULT now(), user_agent text, ip_address inet From 8478ce50522365d3bc285672cc66f41b0444e144 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 8 Sep 2023 09:11:43 -0500 Subject: [PATCH 15/56] add cache for last used --- apikey/store.go | 52 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/apikey/store.go b/apikey/store.go index 146c69042c..7f168302ca 100644 --- a/apikey/store.go +++ b/apikey/store.go @@ -30,6 +30,8 @@ type Store struct { mx sync.Mutex policies map[uuid.UUID]*policyInfo + + lastUsed map[uuid.UUID]time.Time } type policyInfo struct { @@ -38,7 +40,12 @@ type policyInfo struct { } func NewStore(ctx context.Context, db *sql.DB, key keyring.Keyring) (*Store, error) { - s := &Store{db: db, key: key, policies: make(map[uuid.UUID]*policyInfo)} + s := &Store{ + db: db, + key: key, + policies: make(map[uuid.UUID]*policyInfo), + lastUsed: make(map[uuid.UUID]time.Time), + } return s, nil } @@ -199,6 +206,12 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte return ctx, permission.Unauthorized() } + // TODO: cache policy hash by key ID when loading and just do an existence check here + // if the policy hash for this key is already known. + // + // map key = hash, value = policy + // + // cleanup on negative db lookup or on timer? polData, err := gadb.New(s.db).APIKeyAuthPolicy(ctx, id) if err != nil { if !errors.Is(err, sql.ErrNoRows) { @@ -227,22 +240,27 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte return ctx, permission.Unauthorized() } - ua = validate.SanitizeText(ua, 1024) - ip, _, _ = net.SplitHostPort(ip) - ip = validate.SanitizeText(ip, 255) - params := gadb.APIKeyRecordUsageParams{ - KeyID: id, - UserAgent: ua, - } - params.IpAddress.IPNet.IP = net.ParseIP(ip) - params.IpAddress.IPNet.Mask = net.CIDRMask(32, 32) - if params.IpAddress.IPNet.IP != nil { - params.IpAddress.Valid = true - } - err = gadb.New(s.db).APIKeyRecordUsage(ctx, params) - if err != nil { - log.Log(ctx, err) - // don't fail authorization if we can't record usage + if time.Since(s.lastUsed[id]) > time.Minute { + // TODO: cleanup lastUsed map on timer + // set time as a constant and use for both + s.lastUsed[id] = time.Now() + ua = validate.SanitizeText(ua, 1024) + ip, _, _ = net.SplitHostPort(ip) + ip = validate.SanitizeText(ip, 255) + params := gadb.APIKeyRecordUsageParams{ + KeyID: id, + UserAgent: ua, + } + params.IpAddress.IPNet.IP = net.ParseIP(ip) + params.IpAddress.IPNet.Mask = net.CIDRMask(32, 32) + if params.IpAddress.IPNet.IP != nil { + params.IpAddress.Valid = true + } + err = gadb.New(s.db).APIKeyRecordUsage(ctx, params) + if err != nil { + log.Log(ctx, err) + // don't fail authorization if we can't record usage + } } s.mx.Lock() From aca7ea47b73ed7df68ddebbd6bfd4a31e6d0cfba Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 8 Sep 2023 12:44:21 -0500 Subject: [PATCH 16/56] use json to ensure consistant hashes --- apikey/store.go | 8 +------- migrate/migrations/20230907112347-graphql-api-key.sql | 6 +++++- migrate/schema.sql | 9 +++++++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apikey/store.go b/apikey/store.go index 7f168302ca..2fe2c4416c 100644 --- a/apikey/store.go +++ b/apikey/store.go @@ -219,15 +219,9 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte } return ctx, permission.Unauthorized() } - var buf bytes.Buffer - err = json.Compact(&buf, polData) - if err != nil { - log.Logf(ctx, "apikey: invalid policy: %v", err) - return ctx, permission.Unauthorized() - } // TODO: cache policy hash by key ID when loading - policyHash := sha256.Sum256(buf.Bytes()) + policyHash := sha256.Sum256(polData) if !bytes.Equal(policyHash[:], claims.PolicyHash) { log.Logf(ctx, "apikey: policy hash mismatch") return ctx, permission.Unauthorized() diff --git a/migrate/migrations/20230907112347-graphql-api-key.sql b/migrate/migrations/20230907112347-graphql-api-key.sql index 60edd7b301..dc32604430 100644 --- a/migrate/migrations/20230907112347-graphql-api-key.sql +++ b/migrate/migrations/20230907112347-graphql-api-key.sql @@ -7,7 +7,11 @@ CREATE TABLE gql_api_keys( created_by uuid REFERENCES users(id) ON DELETE SET NULL, updated_at timestamp with time zone NOT NULL DEFAULT now(), updated_by uuid REFERENCES users(id) ON DELETE SET NULL, - policy jsonb NOT NULL, + -- We must use json instead of jsonb because we need to be able to compute a reproducable hash of the policy + -- jsonb will not work because it does not guarantee a stable order of keys or whitespace consistency. + -- + -- We also don't need to be able to query the policy, so json is fine. + policy json NOT NULL, expires_at timestamp with time zone NOT NULL, deleted_at timestamp with time zone, deleted_by uuid REFERENCES users(id) ON DELETE SET NULL diff --git a/migrate/schema.sql b/migrate/schema.sql index 9bafabdb24..0176381713 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,5 +1,5 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=a4b057ec82c637cc0833ebf63e60d421b137f75c7dd9355f1f57e33bf4697f55 - +-- DATA=46c09bdf7ac325e53fe2ec9d991b58b46633e7199930793692dd44c215a8dabf - -- DISK=05f86ed0fdc6cf25e0162624d600d9a8efd491d4dd4eb5a1411ba1f9704f22d8 - -- PSQL=05f86ed0fdc6cf25e0162624d600d9a8efd491d4dd4eb5a1411ba1f9704f22d8 - -- @@ -1609,9 +1609,11 @@ CREATE TABLE gql_api_key_usage ( used_at timestamp with time zone DEFAULT now() NOT NULL, user_agent text, CONSTRAINT gql_api_key_usage_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES gql_api_keys(id) ON DELETE CASCADE, + CONSTRAINT gql_api_key_usage_api_key_id_key UNIQUE (api_key_id), CONSTRAINT gql_api_key_usage_pkey PRIMARY KEY (id) ); +CREATE UNIQUE INDEX gql_api_key_usage_api_key_id_key ON public.gql_api_key_usage USING btree (api_key_id); CREATE UNIQUE INDEX gql_api_key_usage_pkey ON public.gql_api_key_usage USING btree (id); CREATE INDEX idx_gql_most_recent_use ON public.gql_api_key_usage USING btree (api_key_id, used_at DESC); @@ -1619,14 +1621,17 @@ CREATE INDEX idx_gql_most_recent_use ON public.gql_api_key_usage USING btree (ap CREATE TABLE gql_api_keys ( created_at timestamp with time zone DEFAULT now() NOT NULL, created_by uuid, + deleted_at timestamp with time zone, + deleted_by uuid, description text NOT NULL, expires_at timestamp with time zone NOT NULL, id uuid NOT NULL, name text NOT NULL, - policy jsonb NOT NULL, + policy json NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, updated_by uuid, CONSTRAINT gql_api_keys_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT gql_api_keys_deleted_by_fkey FOREIGN KEY (deleted_by) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT gql_api_keys_name_key UNIQUE (name), CONSTRAINT gql_api_keys_pkey PRIMARY KEY (id), CONSTRAINT gql_api_keys_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL From 6c4570e755b9e86d8753086d39c8d165bd660e21 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 11 Sep 2023 15:24:24 -0500 Subject: [PATCH 17/56] cache policy data and throttle usage updates --- apikey/{graphqlv1.go => claims.go} | 32 ------- apikey/lastusedcache.go | 53 ++++++++++++ apikey/polcache.go | 131 +++++++++++++++++++++++++++++ apikey/queries.sql | 10 +++ apikey/store.go | 86 ++++++------------- gadb/models.go | 2 + gadb/queries.sql.go | 24 +++++- 7 files changed, 245 insertions(+), 93 deletions(-) rename apikey/{graphqlv1.go => claims.go} (57%) create mode 100644 apikey/lastusedcache.go create mode 100644 apikey/polcache.go diff --git a/apikey/graphqlv1.go b/apikey/claims.go similarity index 57% rename from apikey/graphqlv1.go rename to apikey/claims.go index 7220a0c60b..b95604585e 100644 --- a/apikey/graphqlv1.go +++ b/apikey/claims.go @@ -7,38 +7,6 @@ import ( "github.com/google/uuid" ) -type PolicyType string - -const ( - PolicyTypeGraphQLV1 PolicyType = "graphql-v1" -) - -type Policy struct { - Type PolicyType - - GraphQLV1 *GraphQLV1 `json:",omitempty"` -} - -type Type string - -const ( - TypeGraphQLV1 Type = "graphql-v1" -) - -type V1 struct { - Type Type - - GraphQLV1 *GraphQLV1 `json:",omitempty"` -} - -type GraphQLV1 struct { - AllowedFields []GraphQLField `json:"f"` -} -type GraphQLField struct { - ObjectName string `json:"o"` - Name string `json:"n"` -} - type Claims struct { jwt.RegisteredClaims PolicyHash []byte `json:"pol"` diff --git a/apikey/lastusedcache.go b/apikey/lastusedcache.go new file mode 100644 index 0000000000..951e5881ab --- /dev/null +++ b/apikey/lastusedcache.go @@ -0,0 +1,53 @@ +package apikey + +import ( + "context" + "net" + "sync" + "time" + + "github.com/golang/groupcache/lru" + "github.com/google/uuid" + "github.com/target/goalert/gadb" + "github.com/target/goalert/validation/validate" +) + +type lastUsedCache struct { + lru *lru.Cache + + mx sync.Mutex + updateFunc func(ctx context.Context, id uuid.UUID, ua, ip string) error +} + +func newLastUsedCache(max int, updateFunc func(ctx context.Context, id uuid.UUID, ua, ip string) error) *lastUsedCache { + return &lastUsedCache{ + lru: lru.New(max), + updateFunc: updateFunc, + } +} +func (c *lastUsedCache) RecordUsage(ctx context.Context, id uuid.UUID, ua, ip string) error { + c.mx.Lock() + defer c.mx.Unlock() + if t, ok := c.lru.Get(id); ok && time.Since(t.(time.Time)) < time.Minute { + return nil + } + + c.lru.Add(id, time.Now()) + return c.updateFunc(ctx, id, ua, ip) +} + +func (s *Store) _updateLastUsed(ctx context.Context, id uuid.UUID, ua, ip string) error { + ua = validate.SanitizeText(ua, 1024) + ip, _, _ = net.SplitHostPort(ip) + ip = validate.SanitizeText(ip, 255) + params := gadb.APIKeyRecordUsageParams{ + KeyID: id, + UserAgent: ua, + } + params.IpAddress.IPNet.IP = net.ParseIP(ip) + params.IpAddress.IPNet.Mask = net.CIDRMask(32, 32) + if params.IpAddress.IPNet.IP != nil { + params.IpAddress.Valid = true + } + return gadb.New(s.db).APIKeyRecordUsage(ctx, params) +} diff --git a/apikey/polcache.go b/apikey/polcache.go new file mode 100644 index 0000000000..9e652f7877 --- /dev/null +++ b/apikey/polcache.go @@ -0,0 +1,131 @@ +package apikey + +import ( + "context" + "crypto/sha256" + "database/sql" + "encoding/json" + "errors" + "sync" + + "github.com/golang/groupcache/lru" + "github.com/google/uuid" + "github.com/target/goalert/gadb" +) + +type polCache struct { + lru *lru.Cache + neg *lru.Cache + mx sync.Mutex + + cfg polCacheConfig +} + +type polCacheConfig struct { + FillFunc func(context.Context, uuid.UUID) (*policyInfo, bool, error) + Verify func(context.Context, uuid.UUID) (bool, error) + MaxSize int +} + +func newPolCache(cfg polCacheConfig) *polCache { + return &polCache{ + lru: lru.New(cfg.MaxSize), + neg: lru.New(cfg.MaxSize), + cfg: cfg, + } +} + +func (c *polCache) Revoke(ctx context.Context, key uuid.UUID) error { + c.mx.Lock() + defer c.mx.Unlock() + + c.neg.Add(key, nil) + c.lru.Remove(key) + + return nil +} + +// Get will return the policyInfo for the given key. +// +// If the key is in the cache, it will be verified before returning. +// +// If it is not in the cache, it will be fetched and added to the cache. +// +// If either the key is invalid or the policy is invalid, the key will be +// added to the negative cache. +func (c *polCache) Get(ctx context.Context, key uuid.UUID) (value *policyInfo, ok bool, err error) { + c.mx.Lock() + defer c.mx.Unlock() + + if _, ok := c.neg.Get(key); ok { + return value, false, nil + } + + if v, ok := c.lru.Get(key); ok { + // Check if the key is still valid before returning it, + // if it is not valid, we can remove it from the cache. + isValid, err := c.cfg.Verify(ctx, key) + if err != nil { + return value, false, err + } + + // Since each key has a unique ID and is signed, we can + // safely assume that an invalid key will always be invalid + // and can be cached. + if !isValid { + c.neg.Add(key, nil) + c.lru.Remove(key) + return value, false, nil + } + + return v.(*policyInfo), true, nil + } + + // If the key is not in the cache, we need to fetch it, + // and add it to the cache. We can safely assume that + // the key is valid, when returned from the FillFunc. + value, isValid, err := c.cfg.FillFunc(ctx, key) + if err != nil { + return value, false, err + } + if !isValid { + c.neg.Add(key, nil) + return value, false, nil + } + + c.lru.Add(key, value) + return value, true, nil +} + +func (s *Store) _verifyPolicyID(ctx context.Context, id uuid.UUID) (bool, error) { + valid, err := gadb.New(s.db).APIKeyAuthCheck(ctx, id) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + if err != nil { + return false, err + } + + return valid, nil +} + +func (s *Store) _fetchPolicyInfo(ctx context.Context, id uuid.UUID) (*policyInfo, bool, error) { + polData, err := gadb.New(s.db).APIKeyAuthPolicy(ctx, id) + if errors.Is(err, sql.ErrNoRows) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + + var info policyInfo + err = json.Unmarshal(polData, &info.Policy) + if err != nil { + return nil, false, err + } + + h := sha256.Sum256(polData) + info.Hash = h[:] + + return &info, true, nil +} diff --git a/apikey/queries.sql b/apikey/queries.sql index 2e332369fe..e1b475c623 100644 --- a/apikey/queries.sql +++ b/apikey/queries.sql @@ -46,6 +46,16 @@ WHERE AND gql_api_keys.deleted_at IS NULL AND gql_api_keys.expires_at > now(); +-- name: APIKeyAuthCheck :one +SELECT + TRUE +FROM + gql_api_keys +WHERE + gql_api_keys.id = $1 + AND gql_api_keys.deleted_at IS NULL + AND gql_api_keys.expires_at > now(); + -- name: APIKeyList :many -- APIKeyList returns all API keys, along with the last time they were used. SELECT diff --git a/apikey/store.go b/apikey/store.go index 2fe2c4416c..78679c0131 100644 --- a/apikey/store.go +++ b/apikey/store.go @@ -6,11 +6,8 @@ import ( "crypto/sha256" "database/sql" "encoding/json" - "errors" "fmt" - "net" "sort" - "sync" "time" "github.com/google/uuid" @@ -28,10 +25,8 @@ type Store struct { db *sql.DB key keyring.Keyring - mx sync.Mutex - policies map[uuid.UUID]*policyInfo - - lastUsed map[uuid.UUID]time.Time + polCache *polCache + lastUsedCache *lastUsedCache } type policyInfo struct { @@ -41,12 +36,18 @@ type policyInfo struct { func NewStore(ctx context.Context, db *sql.DB, key keyring.Keyring) (*Store, error) { s := &Store{ - db: db, - key: key, - policies: make(map[uuid.UUID]*policyInfo), - lastUsed: make(map[uuid.UUID]time.Time), + db: db, + key: key, } + s.polCache = newPolCache(polCacheConfig{ + FillFunc: s._fetchPolicyInfo, + Verify: s._verifyPolicyID, + MaxSize: 1000, + }) + + s.lastUsedCache = newLastUsedCache(1000, s._updateLastUsed) + return s, nil } @@ -206,63 +207,28 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte return ctx, permission.Unauthorized() } - // TODO: cache policy hash by key ID when loading and just do an existence check here - // if the policy hash for this key is already known. - // - // map key = hash, value = policy - // - // cleanup on negative db lookup or on timer? - polData, err := gadb.New(s.db).APIKeyAuthPolicy(ctx, id) + info, valid, err := s.polCache.Get(ctx, id) if err != nil { - if !errors.Is(err, sql.ErrNoRows) { - log.Log(ctx, err) - } - return ctx, permission.Unauthorized() + return nil, err } - - // TODO: cache policy hash by key ID when loading - policyHash := sha256.Sum256(polData) - if !bytes.Equal(policyHash[:], claims.PolicyHash) { - log.Logf(ctx, "apikey: policy hash mismatch") + if !valid { + // Successful negative cache lookup, we return Unauthorized because althought the token was validated, the key was revoked/removed. return ctx, permission.Unauthorized() } + if !bytes.Equal(info.Hash, claims.PolicyHash) { + // Successful cache lookup, but the policy has changed since the token was issued and so the token is no longer valid. + s.polCache.Revoke(ctx, id) - var p GQLPolicy - err = json.Unmarshal(polData, &p) - if err != nil || p.Version != 1 { - log.Logf(ctx, "apikey: invalid policy: %v", err) + // We want to log this as a warning, because it is a potential security issue. + log.Log(ctx, fmt.Errorf("apikey: policy hash mismatch for key %s", id)) return ctx, permission.Unauthorized() } - if time.Since(s.lastUsed[id]) > time.Minute { - // TODO: cleanup lastUsed map on timer - // set time as a constant and use for both - s.lastUsed[id] = time.Now() - ua = validate.SanitizeText(ua, 1024) - ip, _, _ = net.SplitHostPort(ip) - ip = validate.SanitizeText(ip, 255) - params := gadb.APIKeyRecordUsageParams{ - KeyID: id, - UserAgent: ua, - } - params.IpAddress.IPNet.IP = net.ParseIP(ip) - params.IpAddress.IPNet.Mask = net.CIDRMask(32, 32) - if params.IpAddress.IPNet.IP != nil { - params.IpAddress.Valid = true - } - err = gadb.New(s.db).APIKeyRecordUsage(ctx, params) - if err != nil { - log.Log(ctx, err) - // don't fail authorization if we can't record usage - } - } - - s.mx.Lock() - s.policies[id] = &policyInfo{ - Hash: policyHash[:], - Policy: p, + err = s.lastUsedCache.RecordUsage(ctx, id, ua, ip) + if err != nil { + // Recording usage is not critical, so we log the error and continue. + log.Log(ctx, err) } - s.mx.Unlock() ctx = permission.SourceContext(ctx, &permission.SourceInfo{ ID: id.String(), @@ -270,7 +236,7 @@ func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (conte }) ctx = permission.UserContext(ctx, "", permission.RoleUnknown) - ctx = ContextWithPolicy(ctx, &p) + ctx = ContextWithPolicy(ctx, &info.Policy) return ctx, nil } diff --git a/gadb/models.go b/gadb/models.go index 5fe45c1440..5723cf8d36 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -907,6 +907,8 @@ type GorpMigration struct { type GqlApiKey struct { CreatedAt time.Time CreatedBy uuid.NullUUID + DeletedAt sql.NullTime + DeletedBy uuid.NullUUID Description string ExpiresAt time.Time ID uuid.UUID diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index 4cccc42e9b..c311aed26d 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -16,6 +16,24 @@ import ( "github.com/sqlc-dev/pqtype" ) +const aPIKeyAuthCheck = `-- name: APIKeyAuthCheck :one +SELECT + TRUE +FROM + gql_api_keys +WHERE + gql_api_keys.id = $1 + AND gql_api_keys.deleted_at IS NULL + AND gql_api_keys.expires_at > now() +` + +func (q *Queries) APIKeyAuthCheck(ctx context.Context, id uuid.UUID) (bool, error) { + row := q.db.QueryRowContext(ctx, aPIKeyAuthCheck, id) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + const aPIKeyAuthPolicy = `-- name: APIKeyAuthPolicy :one SELECT gql_api_keys.policy @@ -99,7 +117,7 @@ func (q *Queries) APIKeyInsert(ctx context.Context, arg APIKeyInsertParams) erro const aPIKeyList = `-- name: APIKeyList :many SELECT - gql_api_keys.created_at, gql_api_keys.created_by, gql_api_keys.description, gql_api_keys.expires_at, gql_api_keys.id, gql_api_keys.name, gql_api_keys.policy, gql_api_keys.updated_at, gql_api_keys.updated_by, + gql_api_keys.created_at, gql_api_keys.created_by, gql_api_keys.deleted_at, gql_api_keys.deleted_by, gql_api_keys.description, gql_api_keys.expires_at, gql_api_keys.id, gql_api_keys.name, gql_api_keys.policy, gql_api_keys.updated_at, gql_api_keys.updated_by, gql_api_key_usage.used_at AS last_used_at, gql_api_key_usage.user_agent AS last_user_agent, gql_api_key_usage.ip_address AS last_ip_address @@ -113,6 +131,8 @@ WHERE type APIKeyListRow struct { CreatedAt time.Time CreatedBy uuid.NullUUID + DeletedAt sql.NullTime + DeletedBy uuid.NullUUID Description string ExpiresAt time.Time ID uuid.UUID @@ -138,6 +158,8 @@ func (q *Queries) APIKeyList(ctx context.Context) ([]APIKeyListRow, error) { if err := rows.Scan( &i.CreatedAt, &i.CreatedBy, + &i.DeletedAt, + &i.DeletedBy, &i.Description, &i.ExpiresAt, &i.ID, From a686da2e917010e3e08f24ab96c59a563abc69ea Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 11 Sep 2023 15:30:24 -0500 Subject: [PATCH 18/56] add missing comments --- apikey/polcache.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apikey/polcache.go b/apikey/polcache.go index 9e652f7877..2d18b7596f 100644 --- a/apikey/polcache.go +++ b/apikey/polcache.go @@ -13,6 +13,8 @@ import ( "github.com/target/goalert/gadb" ) +// polCache handles caching of policyInfo objects, as well as negative caching +// of invalid keys. type polCache struct { lru *lru.Cache neg *lru.Cache @@ -35,6 +37,7 @@ func newPolCache(cfg polCacheConfig) *polCache { } } +// Revoke will add the key to the negative cache. func (c *polCache) Revoke(ctx context.Context, key uuid.UUID) error { c.mx.Lock() defer c.mx.Unlock() From 1f4f320bffe3dbd9cb1cee3566bb3959cc1d543e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Tue, 19 Sep 2023 20:49:43 +0800 Subject: [PATCH 19/56] Initial commit for 3007 item --- web/src/app/admin/AdminAPIKeys.tsx | 188 ++++++++++++++++++ .../admin-api-keys/AdminAPIKeyCreateForm.tsx | 125 ++++++++++++ .../AdminAPIKeyExpirationField.tsx | 103 ++++++++++ .../AdminAPIKeysCreateDialog.tsx | 79 ++++++++ .../AdminAPIKeysDeleteDialog.tsx | 52 +++++ .../admin-api-keys/AdminAPIKeysDrawer.tsx | 129 ++++++++++++ .../AdminAPIKeysTokenDialog.tsx | 72 +++++++ web/src/app/main/AppRoutes.tsx | 2 + web/src/app/main/NavBar.tsx | 1 + 9 files changed, 751 insertions(+) create mode 100644 web/src/app/admin/AdminAPIKeys.tsx create mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx create mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx create mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx create mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx create mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx create mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx new file mode 100644 index 0000000000..e55c872e9a --- /dev/null +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -0,0 +1,188 @@ +import React, { useState } from 'react' +import makeStyles from '@mui/styles/makeStyles' +import { + Button, + ButtonGroup, + Grid, + Typography, + Card, + ButtonBase, +} from '@mui/material' +import AdminAPIKeysDrawer from './admin-api-keys/AdminAPIKeysDrawer' +import { GQLAPIKey, CreatedGQLAPIKey } from '../../schema' +import { Time } from '../util/Time' +import { gql, useQuery } from '@apollo/client' +import FlatList, { FlatListListItem } from '../lists/FlatList' +import Spinner from '../loading/components/Spinner' +import { GenericError } from '../error-pages' +import { Theme } from '@mui/material/styles' +import AdminAPIKeysCreateDialog from './admin-api-keys/AdminAPIKeysCreateDialog' +import AdminAPIKeysTokenDialog from './admin-api-keys/AdminAPIKeysTokenDialog' + +const getAPIKeysQuery = gql` + query gqlAPIKeysQuery { + gqlAPIKeys { + id + name + description + createdAt + createdBy { + id + role + name + email + } + updatedAt + updatedBy { + id + role + name + email + } + lastUsed { + time + ua + ip + } + expiresAt + allowedFields + token + } + } +` +const useStyles = makeStyles((theme: Theme) => ({ + root: { + '& .MuiListItem-root': { + 'border-bottom': '1px solid #333333', + }, + '& .MuiListItem-root:not(.Mui-selected):hover ': { + 'background-color': '#474747', + }, + }, + buttons: { + 'padding-bottom': '15px', + }, + containerDefault: { + [theme.breakpoints.up('md')]: { + maxWidth: '100%', + transition: `max-width ${theme.transitions.duration.leavingScreen}ms ease`, + }, + '& .MuiListItem-root': { + padding: '0px', + }, + }, + containerSelected: { + [theme.breakpoints.up('md')]: { + maxWidth: '70%', + transition: `max-width ${theme.transitions.duration.enteringScreen}ms ease`, + }, + '& .MuiListItem-root': { + padding: '0px', + }, + }, +})) + +export default function AdminAPIKeys(): JSX.Element { + const classes = useStyles() + const [selectedAPIKey, setSelectedAPIKey] = useState(null) + const [reloadFlag, setReloadFlag] = useState(0) + const [tokenDialogClose, onTokenDialogClose] = useState(false) + const [openCreateAPIKeyDialog, setOpenCreateAPIKeyDialog] = useState(false) + const [token, setToken] = useState({ + id: '', + token: '', + }) + const handleOpenCreateDialog = (): void => { + setOpenCreateAPIKeyDialog(!openCreateAPIKeyDialog) + } + const { data, loading, error } = useQuery(getAPIKeysQuery, { + variables: { + reloadData: reloadFlag, + }, + }) + + if (error) { + return + } + + if (loading && !data) { + return + } + + const items = data.gqlAPIKeys.map( + (key: GQLAPIKey): FlatListListItem => ({ + selected: (key as GQLAPIKey).id === selectedAPIKey?.id, + highlight: (key as GQLAPIKey).id === selectedAPIKey?.id, + subText: ( + { + setSelectedAPIKey(key) + }} + style={{ width: '100%', textAlign: 'left', padding: '5px 15px' }} + > + + + + {key.name} + + + + + {key.allowedFields.length + ' allowed fields (read-only)'} + + + + + + + + + ), + }), + ) + + return ( + +
+ setSelectedAPIKey(null)} + apiKey={selectedAPIKey} + /> + {openCreateAPIKeyDialog ? ( + + ) : null} + {tokenDialogClose ? ( + + ) : null} + + + + + + + + +
+
+ ) +} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx new file mode 100644 index 0000000000..6f5013a1e4 --- /dev/null +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx @@ -0,0 +1,125 @@ +import React, { useState, useEffect, SyntheticEvent } from 'react' +import Grid from '@mui/material/Grid' +import { FormContainer, FormField } from '../../forms' +import { FieldError } from '../../util/errutil' +import { CreateGQLAPIKeyInput } from '../../../schema' +import AdminAPIKeyExpirationField from './AdminAPIKeyExpirationField' +import dayjs from 'dayjs' +import { gql, useQuery } from '@apollo/client' +import { GenericError } from '../../error-pages' +import Spinner from '../../loading/components/Spinner' +import { TextField, Autocomplete, MenuItem } from '@mui/material' +import CheckIcon from '@mui/icons-material/Check' + +const listGQLFieldsQuery = gql` + query ListGQLFieldsQuery { + listGQLFields + } +` + +const MaxDetailsLength = 6 * 1024 // 6KiB + +interface AdminAPIKeyCreateFormProps { + value: CreateGQLAPIKeyInput + errors: FieldError[] + onChange: (key: CreateGQLAPIKeyInput) => void + disabled?: boolean +} + +export default function AdminAPIKeyCreateForm( + props: AdminAPIKeyCreateFormProps, +): JSX.Element { + const { ...containerProps } = props + const [expiresAt, setExpiresAt] = useState( + dayjs().add(7, 'day').toString(), + ) + const [allowedFields, setAllowedFields] = useState([]) + const handleAutocompleteChange = ( + event: SyntheticEvent, + value: string[], + ): void => { + setAllowedFields(value) + } + + useEffect(() => { + const valTemp = props.value + valTemp.expiresAt = new Date(expiresAt).toISOString() + valTemp.allowedFields = allowedFields + + props.onChange(valTemp) + }) + + const { data, loading, error } = useQuery(listGQLFieldsQuery) + + if (error) { + return + } + + if (loading && !data) { + return + } + + return ( + + + + + + + + + + + + + option} + onChange={handleAutocompleteChange} + disableCloseOnSelect + renderInput={(params) => ( + + )} + renderOption={(props, option, { selected }) => ( + + {option} + {selected ? : null} + + )} + /> + + + + ) +} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx new file mode 100644 index 0000000000..2e52996a03 --- /dev/null +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect } from 'react' +import { FormContainer } from '../../forms' +import InputLabel from '@mui/material/InputLabel' +import MenuItem from '@mui/material/MenuItem' +import FormControl from '@mui/material/FormControl' +import Select, { SelectChangeEvent } from '@mui/material/Select' +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' +import dayjs, { Dayjs } from 'dayjs' +import Grid from '@mui/material/Grid' +import { Typography } from '@mui/material' +import makeStyles from '@mui/styles/makeStyles' + +const useStyles = makeStyles(() => ({ + expiresCon: { + 'padding-top': '15px', + }, +})) + +interface FieldProps { + setValue: (val: string) => void + value: string +} + +export default function AdminAPIKeyExpirationField( + props: FieldProps, +): JSX.Element { + const classes = useStyles() + const { value, setValue } = props + const [dateVal, setDateVal] = useState(value) + const [options, setOptions] = useState('7') + const [showPicker, setShowPicker] = useState(false) + const today = dayjs() + const handleChange = (event: SelectChangeEvent): void => { + const val = event.target.value as string + setOptions(val) + setShowPicker(val.toString() === '0') + + if (val !== '0') { + setDateVal(today.add(parseInt(val), 'day').toString()) + } + } + + const handleDatePickerChange = (val: Dayjs | null): void => { + // eslint-disable-next-line prettier/prettier + if(val != null) { + setDateVal(val.toString()) + } + } + + useEffect(() => { + setValue(dateVal) + }) + + return ( + + + + + Expires At* + + + {showPicker ? ( + + + + + + ) : null} + + + The token will expires on {dateVal} + + + + + + ) +} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx new file mode 100644 index 0000000000..992ea2c494 --- /dev/null +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react' +import { gql, useMutation } from '@apollo/client' +import { fieldErrors, nonFieldErrors } from '../../util/errutil' +import FormDialog from '../../dialogs/FormDialog' +import AdminAPIKeyCreateForm from './AdminAPIKeyCreateForm' +import { CreateGQLAPIKeyInput, CreatedGQLAPIKey } from '../../../schema' +import Spinner from '../../loading/components/Spinner' +import { GenericError } from '../../error-pages' + +const newGQLAPIKeyQuery = gql` + mutation CreateGQLAPIKey($input: CreateGQLAPIKeyInput!) { + createGQLAPIKey(input: $input) { + id + token + } + } +` + +export default function AdminAPIKeysCreateDialog(props: { + onClose: (param: boolean) => void + setToken: (token: CreatedGQLAPIKey) => void + setReloadFlag: (inc: number) => void + onTokenDialogClose: (prama: boolean) => void +}): JSX.Element { + const [key, setKey] = useState({ + name: '', + description: '', + allowedFields: [], + expiresAt: '', + }) + const [createAPIKey, createAPIKeyStatus] = useMutation(newGQLAPIKeyQuery, { + onCompleted: (data) => { + props.setToken(data.createGQLAPIKey) + props.onClose(false) + props.onTokenDialogClose(true) + props.setReloadFlag(Math.random()) + }, + }) + const { loading, data, error } = createAPIKeyStatus + const fieldErrs = fieldErrors(error) + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const handleOnSubmit = () => { + createAPIKey({ + variables: { + input: key, + }, + }).then((result) => { + if (!result.errors) { + return result + } + }) + } + + if (error) { + return + } + + if (loading && !data) { + return + } + + return ( + + } + /> + ) +} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx new file mode 100644 index 0000000000..a9efab8496 --- /dev/null +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react' +import { GQLAPIKey } from '../../../schema' +import Button from '@mui/material/Button' +import Dialog from '@mui/material/Dialog' +import DialogActions from '@mui/material/DialogActions' +import DialogContent from '@mui/material/DialogContent' +import DialogContentText from '@mui/material/DialogContentText' +import DialogTitle from '@mui/material/DialogTitle' + +/** +const mutation = gql` + mutation delete($input: [TargetInput!]!) { + deleteAll(input: $input) + } +` */ + +export default function AdminAPIKeysDeleteDialog(props: { + apiKey: GQLAPIKey | null + onClose: (param: boolean) => void + close: boolean +}): JSX.Element { + const { apiKey, onClose, close } = props + const [dialogClose, onDialogClose] = useState(close) + const handleNo = (): void => { + onClose(false) + onDialogClose(!dialogClose) + } + + const handleYes = (): void => { + onClose(false) + onDialogClose(!dialogClose) + } + + return ( + + DELETE API KEY + + + Are you sure you want to delete the API KEY {apiKey?.name}? + + + + + + + + ) +} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx new file mode 100644 index 0000000000..71a496b141 --- /dev/null +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react' +import { + ClickAwayListener, + Divider, + Drawer, + Grid, + List, + ListItem, + ListItemText, + Toolbar, + Typography, + Button, + ButtonGroup, +} from '@mui/material' +import makeStyles from '@mui/styles/makeStyles' + +import { GQLAPIKey } from '../../../schema' +import AdminAPIKeysDeleteDialog from './AdminAPIKeysDeleteDialog' + +interface Props { + onClose: () => void + apiKey: GQLAPIKey | null +} + +const useStyles = makeStyles(() => ({ + buttons: { + textAlign: 'right', + width: '30vw', + padding: '15px 10px', + }, +})) + +export default function AdminAPIKeysDrawer(props: Props): JSX.Element { + const { onClose, apiKey } = props + const classes = useStyles() + const isOpen = Boolean(apiKey) + const [deleteDialog, onDeleteDialog] = useState(false) + + const handleDeleteConfirmation = (): void => { + onDeleteDialog(!deleteDialog) + } + + return ( + + {deleteDialog ? ( + + ) : null} + + + + + + API Key Details + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx new file mode 100644 index 0000000000..2a3121f7dc --- /dev/null +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx @@ -0,0 +1,72 @@ +/* eslint-disable prettier/prettier */ +import React, { useState, useEffect } from 'react' +import { CreatedGQLAPIKey } from '../../../schema' +import Dialog from '@mui/material/Dialog' +import DialogActions from '@mui/material/DialogActions' +import DialogContent from '@mui/material/DialogContent' +import DialogContentText from '@mui/material/DialogContentText' +import DialogTitle from '@mui/material/DialogTitle' +import { Typography } from '@mui/material' +import ContentCopyIcon from '@mui/icons-material/ContentCopy' +import Grid from '@mui/material/Grid' +import IconButton from '@mui/material/IconButton' +import CloseIcon from '@mui/icons-material/Close' + +export default function AdminAPIKeysTokenDialog(props: { + input: CreatedGQLAPIKey + onTokenDialogClose: (input: boolean) => void + tokenDialogClose: boolean +}): JSX.Element { + const [close, onClose] = useState(props.tokenDialogClose) + const onClickCopy = (): void => { + navigator.clipboard.writeText(props.input.token) + } + const onCloseDialog = (): void => { + onClose(!close) + } + + useEffect(() => { + props.onTokenDialogClose(close) + }) + + return ( + + + + + API Key Token + + + (Please copy and save the token as this is the only time you'll be able to view it.) + + + + + + + + + + + + + + {props.input.token} + eyJhbGciOiJFUzIyNCIsImtleSI6MCwidHlwIjoiSldUIn0.eyJpc3MiOiJnb2FsZXJ0Iiwic3ViIjoiYzY2MDNiNmQtNzc1ZC00ZTc2LThiMzYtOThiMDkwM2NhZjg2IiwiYXVkIjpbImFwaWtleS12MS9ncmFwaHFsLXYxIl0sImV4cCI6MTY5NTM3ODA2MCwibmJmIjoxNjk1MTE4ODA3LCJpYXQiOjE2OTUxMTg4NjcsImp0aSI6ImEyNWM4N2RiLTUyMGUtNGZlMi04MmY5LWFmZmFlNjBmZjhiMCIsInBvbCI6IkE1RGp2TExERjkzaUI4cUpkRHBwTnFUWkw5OUkrMVJRbCtoT2NQNHU2NTA9In0.dXTqhTmKXPM-VVmBelnETs_o-QUxGoltECRZTdOhOLoJZ508WYZNNnJD8qcobNQMDoIsx25v-Yo + + + + + + + + + + ) +} diff --git a/web/src/app/main/AppRoutes.tsx b/web/src/app/main/AppRoutes.tsx index b56195aa1a..084f05af86 100644 --- a/web/src/app/main/AppRoutes.tsx +++ b/web/src/app/main/AppRoutes.tsx @@ -7,6 +7,7 @@ import AdminConfig from '../admin/AdminConfig' import AdminLimits from '../admin/AdminLimits' import AdminToolbox from '../admin/AdminToolbox' import AdminSwitchover from '../admin/switchover/AdminSwitchover' +import AdminAPIKeys from '../admin/AdminAPIKeys' import AlertsList from '../alerts/AlertsList' import AlertDetailPage from '../alerts/pages/AlertDetailPage' import Documentation from '../documentation/Documentation' @@ -118,6 +119,7 @@ export const routes: Record> = { '/admin/alert-counts': AdminAlertCounts, '/admin/switchover': AdminSwitchover, '/admin/switchover/guide': AdminSwitchoverGuide, + '/admin/api-keys': AdminAPIKeys, '/wizard': WizardRouter, '/docs': Documentation, diff --git a/web/src/app/main/NavBar.tsx b/web/src/app/main/NavBar.tsx index f97d16a4c3..3bddd29d62 100644 --- a/web/src/app/main/NavBar.tsx +++ b/web/src/app/main/NavBar.tsx @@ -82,6 +82,7 @@ export default function NavBar(): JSX.Element { + From 026bb726c906e10c844d63251bcdd2d0fb462890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Tue, 19 Sep 2023 22:21:33 +0800 Subject: [PATCH 20/56] Added confirmation dialog and save edit show hide functions --- .../admin-api-keys/AdminAPIKeysDrawer.tsx | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index 71a496b141..9e51841b92 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -35,10 +35,23 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { const classes = useStyles() const isOpen = Boolean(apiKey) const [deleteDialog, onDeleteDialog] = useState(false) - + const [showEdit, setShowEdit] = useState(true) + const [showSave, setShowSave] = useState(false) const handleDeleteConfirmation = (): void => { onDeleteDialog(!deleteDialog) } + let comma = '' + const allowFieldsStr = apiKey?.allowedFields.map((inp: string): string => { + const inpComma = comma + inp + comma = ', ' + + return inpComma + }) + + const handleSave = (): void => { + setShowSave(!showSave) + setShowEdit(!showEdit) + } return ( @@ -65,17 +78,35 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { + + @@ -105,20 +136,29 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { - - + {showSave ? ( + + ) : null} + {showEdit ? ( + + ) : null} From 9fe249db638b5d4554c63e77957403111947c355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Wed, 20 Sep 2023 15:27:21 +0800 Subject: [PATCH 21/56] Added formfield imports --- web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index 9e51841b92..f05379c34b 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -7,16 +7,19 @@ import { List, ListItem, ListItemText, + TextField, Toolbar, Typography, Button, ButtonGroup, } from '@mui/material' import makeStyles from '@mui/styles/makeStyles' - +import { FormField } from '../../forms' import { GQLAPIKey } from '../../../schema' import AdminAPIKeysDeleteDialog from './AdminAPIKeysDeleteDialog' +const MaxDetailsLength = 6 * 1024 // 6KiB + interface Props { onClose: () => void apiKey: GQLAPIKey | null From 88e4d6097ee2637057e45c5cda31e2f90556d624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Thu, 21 Sep 2023 14:41:48 +0800 Subject: [PATCH 22/56] Implemented delete and update in UI --- web/src/app/admin/AdminAPIKeys.tsx | 14 +- .../AdminAPIKeysDeleteDialog.tsx | 42 +++++- .../admin-api-keys/AdminAPIKeysDrawer.tsx | 141 +++++++++++++----- 3 files changed, 144 insertions(+), 53 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index e55c872e9a..444a17f738 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -146,10 +146,12 @@ export default function AdminAPIKeys(): JSX.Element { return (
- setSelectedAPIKey(null)} - apiKey={selectedAPIKey} - /> + {selectedAPIKey ? ( + setSelectedAPIKey(null)} + apiKey={selectedAPIKey} + /> + ) : null} {openCreateAPIKeyDialog ? ( - +
diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx index a9efab8496..a033f31745 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx @@ -6,13 +6,14 @@ import DialogActions from '@mui/material/DialogActions' import DialogContent from '@mui/material/DialogContent' import DialogContentText from '@mui/material/DialogContentText' import DialogTitle from '@mui/material/DialogTitle' +import { gql, useMutation } from '@apollo/client' +import { GenericError } from '../../error-pages' -/** -const mutation = gql` - mutation delete($input: [TargetInput!]!) { - deleteAll(input: $input) +const deleteGQLAPIKeyQuery = gql` + mutation DeleteGQLAPIKey($input: string!) { + deleteGQLAPIKey(input: $input) } -` */ +` export default function AdminAPIKeysDeleteDialog(props: { apiKey: GQLAPIKey | null @@ -22,13 +23,38 @@ export default function AdminAPIKeysDeleteDialog(props: { const { apiKey, onClose, close } = props const [dialogClose, onDialogClose] = useState(close) const handleNo = (): void => { + console.log('NO...........') onClose(false) onDialogClose(!dialogClose) } - + const [deleteAPIKey, deleteAPIKeyStatus] = useMutation(deleteGQLAPIKeyQuery, { + onCompleted: (data) => { + if (data.deleteGQLAPIKey) { + onClose(false) + onDialogClose(!dialogClose) + } + }, + }) + const { loading, data, error } = deleteAPIKeyStatus const handleYes = (): void => { - onClose(false) - onDialogClose(!dialogClose) + console.log('YES...........') + deleteAPIKey({ + variables: { + input: apiKey?.id, + }, + }).then((result) => { + if (!result.errors) { + return result + } + }) + } + + if (error) { + return + } + + if (loading && !data) { + // return } return ( diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index f05379c34b..95bfe173d0 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -14,11 +14,18 @@ import { ButtonGroup, } from '@mui/material' import makeStyles from '@mui/styles/makeStyles' -import { FormField } from '../../forms' -import { GQLAPIKey } from '../../../schema' +import { GQLAPIKey, UpdateGQLAPIKeyInput } from '../../../schema' import AdminAPIKeysDeleteDialog from './AdminAPIKeysDeleteDialog' +import { gql, useMutation } from '@apollo/client' +import { GenericError } from '../../error-pages' -const MaxDetailsLength = 6 * 1024 // 6KiB +const updateGQLAPIKeyQuery = gql` + mutation UpdateGQLAPIKey($input: UpdateGQLAPIKeyInput!) { + updateGQLAPIKey(input: $input) + } +` + +// const MaxDetailsLength = 6 * 1024 // 6KiB interface Props { onClose: () => void @@ -50,10 +57,39 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { return inpComma }) + const [key, setKey] = useState({ + id: apiKey?.id ?? '', + name: apiKey?.name, + description: apiKey?.description, + }) + const [updateAPIKey, updateAPIKeyStatus] = useMutation(updateGQLAPIKeyQuery, { + onCompleted: (data) => { + if (data.updateGQLAPIKey) { + setShowSave(!showSave) + setShowEdit(!showEdit) + } + }, + }) + const { loading, data, error } = updateAPIKeyStatus + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const handleSave = () => { + updateAPIKey({ + variables: { + input: key, + }, + }).then((result) => { + if (!result.errors) { + return result + } + }) + } + + if (error) { + return + } - const handleSave = (): void => { - setShowSave(!showSave) - setShowEdit(!showEdit) + if (loading && !data) { + // return } return ( @@ -80,31 +116,47 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { - - + {showSave ? ( + { + const keyTemp = key + keyTemp.name = e.target.value + setKey(keyTemp) + }} + variant='standard' + sx={{ width: '100%' }} + /> + ) : ( + + )} - - + {showSave ? ( + { + const keyTemp = key + keyTemp.description = e.target.value + setKey(keyTemp) + }} + variant='standard' + sx={{ width: '100%' }} + /> + ) : ( + + )} - - - {showSave ? ( + {showSave ? ( + + - ) : null} - {showEdit ? ( + + ) : null} + {showEdit ? ( + + - ) : null} - + + ) : null} From 45390713c622d639071d3be9479df4efcfc6a78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Mon, 25 Sep 2023 23:26:18 +0800 Subject: [PATCH 23/56] Implemented delete and update functions. Added role options in creating new API key --- .../20230907112347-graphql-api-key.sql | 34 ------------- web/src/app/admin/AdminAPIKeys.tsx | 1 - .../admin-api-keys/AdminAPIKeyCreateForm.tsx | 39 +++++++++++++-- .../AdminAPIKeyExpirationField.tsx | 49 +++++++++++++------ .../AdminAPIKeysCreateDialog.tsx | 2 + .../AdminAPIKeysDeleteDialog.tsx | 20 +++----- .../admin-api-keys/AdminAPIKeysDrawer.tsx | 45 ++++++++++++----- .../AdminAPIKeysTokenDialog.tsx | 14 ++++-- 8 files changed, 120 insertions(+), 84 deletions(-) delete mode 100644 migrate/migrations/20230907112347-graphql-api-key.sql diff --git a/migrate/migrations/20230907112347-graphql-api-key.sql b/migrate/migrations/20230907112347-graphql-api-key.sql deleted file mode 100644 index dc32604430..0000000000 --- a/migrate/migrations/20230907112347-graphql-api-key.sql +++ /dev/null @@ -1,34 +0,0 @@ --- +migrate Up -CREATE TABLE gql_api_keys( - id uuid PRIMARY KEY, - name text NOT NULL UNIQUE, - description text NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now(), - created_by uuid REFERENCES users(id) ON DELETE SET NULL, - updated_at timestamp with time zone NOT NULL DEFAULT now(), - updated_by uuid REFERENCES users(id) ON DELETE SET NULL, - -- We must use json instead of jsonb because we need to be able to compute a reproducable hash of the policy - -- jsonb will not work because it does not guarantee a stable order of keys or whitespace consistency. - -- - -- We also don't need to be able to query the policy, so json is fine. - policy json NOT NULL, - expires_at timestamp with time zone NOT NULL, - deleted_at timestamp with time zone, - deleted_by uuid REFERENCES users(id) ON DELETE SET NULL -); - -CREATE TABLE gql_api_key_usage( - id bigserial PRIMARY KEY, - api_key_id uuid REFERENCES gql_api_keys(id) ON DELETE CASCADE UNIQUE, - used_at timestamp with time zone NOT NULL DEFAULT now(), - user_agent text, - ip_address inet -); - -CREATE INDEX idx_gql_most_recent_use ON gql_api_key_usage(api_key_id, used_at DESC); - --- +migrate Down -DROP TABLE gql_api_key_usage; - -DROP TABLE gql_api_keys; - diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index 444a17f738..f45acfb2fa 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -46,7 +46,6 @@ const getAPIKeysQuery = gql` } expiresAt allowedFields - token } } ` diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx index 6f5013a1e4..803a14bce0 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx @@ -2,21 +2,21 @@ import React, { useState, useEffect, SyntheticEvent } from 'react' import Grid from '@mui/material/Grid' import { FormContainer, FormField } from '../../forms' import { FieldError } from '../../util/errutil' -import { CreateGQLAPIKeyInput } from '../../../schema' +import { CreateGQLAPIKeyInput, UserRole } from '../../../schema' import AdminAPIKeyExpirationField from './AdminAPIKeyExpirationField' -import dayjs from 'dayjs' import { gql, useQuery } from '@apollo/client' import { GenericError } from '../../error-pages' import Spinner from '../../loading/components/Spinner' import { TextField, Autocomplete, MenuItem } from '@mui/material' import CheckIcon from '@mui/icons-material/Check' +import { DateTime } from 'luxon' +import Select, { SelectChangeEvent } from '@mui/material/Select' const listGQLFieldsQuery = gql` query ListGQLFieldsQuery { listGQLFields } ` - const MaxDetailsLength = 6 * 1024 // 6KiB interface AdminAPIKeyCreateFormProps { @@ -31,9 +31,18 @@ export default function AdminAPIKeyCreateForm( ): JSX.Element { const { ...containerProps } = props const [expiresAt, setExpiresAt] = useState( - dayjs().add(7, 'day').toString(), + DateTime.now().plus({ days: 7 }).toLocaleString({ + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }), ) const [allowedFields, setAllowedFields] = useState([]) + const [role, setRole] = useState('user') const handleAutocompleteChange = ( event: SyntheticEvent, value: string[], @@ -41,10 +50,16 @@ export default function AdminAPIKeyCreateForm( setAllowedFields(value) } + const handleRoleChange = (event: SelectChangeEvent): void => { + const val = event.target.value as UserRole + setRole(val) + } + useEffect(() => { const valTemp = props.value valTemp.expiresAt = new Date(expiresAt).toISOString() valTemp.allowedFields = allowedFields + valTemp.role = role as UserRole props.onChange(valTemp) }) @@ -84,6 +99,20 @@ export default function AdminAPIKeyCreateForm( hint='Markdown Supported' /> + + + option} @@ -117,6 +145,7 @@ export default function AdminAPIKeyCreateForm( {selected ? : null} )} + style={{ width: '100%' }} /> diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx index 2e52996a03..05c182d316 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx @@ -4,13 +4,11 @@ import InputLabel from '@mui/material/InputLabel' import MenuItem from '@mui/material/MenuItem' import FormControl from '@mui/material/FormControl' import Select, { SelectChangeEvent } from '@mui/material/Select' -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' -import { DatePicker } from '@mui/x-date-pickers/DatePicker' -import dayjs, { Dayjs } from 'dayjs' import Grid from '@mui/material/Grid' import { Typography } from '@mui/material' import makeStyles from '@mui/styles/makeStyles' +import { ISODatePicker } from '../../util/ISOPickers' +import { DateTime } from 'luxon' const useStyles = makeStyles(() => ({ expiresCon: { @@ -31,21 +29,43 @@ export default function AdminAPIKeyExpirationField( const [dateVal, setDateVal] = useState(value) const [options, setOptions] = useState('7') const [showPicker, setShowPicker] = useState(false) - const today = dayjs() + // const today = DateTime.local({ zone: 'local' }).toFormat('ZZZZ') const handleChange = (event: SelectChangeEvent): void => { const val = event.target.value as string setOptions(val) setShowPicker(val.toString() === '0') if (val !== '0') { - setDateVal(today.add(parseInt(val), 'day').toString()) + setDateVal( + DateTime.now() + .plus({ days: parseInt(val) }) + .toLocaleString({ + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }), + ) } } - const handleDatePickerChange = (val: Dayjs | null): void => { + const handleDatePickerChange = (val: string): void => { // eslint-disable-next-line prettier/prettier - if(val != null) { - setDateVal(val.toString()) + if(val != null) { + setDateVal( + new Date(val).toLocaleString([], { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }), + ) } } @@ -77,13 +97,10 @@ export default function AdminAPIKeyExpirationField( {showPicker ? ( - - - + ) : null} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx index 992ea2c494..daed25cfd8 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx @@ -27,6 +27,7 @@ export default function AdminAPIKeysCreateDialog(props: { description: '', allowedFields: [], expiresAt: '', + role: 'unknown', }) const [createAPIKey, createAPIKeyStatus] = useMutation(newGQLAPIKeyQuery, { onCompleted: (data) => { @@ -66,6 +67,7 @@ export default function AdminAPIKeysCreateDialog(props: { errors={nonFieldErrors(error)} onClose={props.onClose} onSubmit={handleOnSubmit} + disableBackdropClose form={ { - console.log('NO...........') onClose(false) onDialogClose(!dialogClose) } @@ -37,15 +37,10 @@ export default function AdminAPIKeysDeleteDialog(props: { }) const { loading, data, error } = deleteAPIKeyStatus const handleYes = (): void => { - console.log('YES...........') deleteAPIKey({ variables: { - input: apiKey?.id, + id: apiKey?.id, }, - }).then((result) => { - if (!result.errors) { - return result - } }) } @@ -54,14 +49,15 @@ export default function AdminAPIKeysDeleteDialog(props: { } if (loading && !data) { - // return + return } return ( DELETE API KEY diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index 95bfe173d0..b402b8e5a7 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -47,6 +47,8 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { const [deleteDialog, onDeleteDialog] = useState(false) const [showEdit, setShowEdit] = useState(true) const [showSave, setShowSave] = useState(false) + const [reqNameText, setReqNameText] = useState('') + const [reqDescText, setReqDescText] = useState('') const handleDeleteConfirmation = (): void => { onDeleteDialog(!deleteDialog) } @@ -73,15 +75,28 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { const { loading, data, error } = updateAPIKeyStatus // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const handleSave = () => { - updateAPIKey({ - variables: { - input: key, - }, - }).then((result) => { - if (!result.errors) { - return result + const desc = key.description?.trim() + const name = key.name?.trim() + + if (desc !== '' && name !== '') { + updateAPIKey({ + variables: { + input: key, + }, + }).then((result) => { + if (!result.errors) { + return result + } + }) + } else { + if (desc === '') { + setReqDescText('This field is required.') + } + + if (name === '') { + setReqNameText('This field is required.') } - }) + } } if (error) { @@ -101,7 +116,7 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { close={deleteDialog} /> ) : null} - + { const keyTemp = key keyTemp.name = e.target.value @@ -131,7 +148,7 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { sx={{ width: '100%' }} /> ) : ( - + )} @@ -139,10 +156,12 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { { const keyTemp = key keyTemp.description = e.target.value @@ -154,7 +173,7 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { ) : ( )} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx index 2a3121f7dc..17c135c60c 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx @@ -21,7 +21,15 @@ export default function AdminAPIKeysTokenDialog(props: { const onClickCopy = (): void => { navigator.clipboard.writeText(props.input.token) } - const onCloseDialog = (): void => { + const onCloseDialog = (event: any, reason: any): any => { + if (reason === 'backdropClick' || reason === 'escapeKeyDown') { + return false; + } + + onClose(!close) + } + + const onCloseDialogByButton = (): void => { onClose(!close) } @@ -36,6 +44,7 @@ export default function AdminAPIKeysTokenDialog(props: { aria-labelledby='api-key-token-dialog' aria-describedby='api-key-token-information' maxWidth='xl' + disableEscapeKeyDown > @@ -48,7 +57,7 @@ export default function AdminAPIKeysTokenDialog(props: { - + @@ -58,7 +67,6 @@ export default function AdminAPIKeysTokenDialog(props: { {props.input.token} - eyJhbGciOiJFUzIyNCIsImtleSI6MCwidHlwIjoiSldUIn0.eyJpc3MiOiJnb2FsZXJ0Iiwic3ViIjoiYzY2MDNiNmQtNzc1ZC00ZTc2LThiMzYtOThiMDkwM2NhZjg2IiwiYXVkIjpbImFwaWtleS12MS9ncmFwaHFsLXYxIl0sImV4cCI6MTY5NTM3ODA2MCwibmJmIjoxNjk1MTE4ODA3LCJpYXQiOjE2OTUxMTg4NjcsImp0aSI6ImEyNWM4N2RiLTUyMGUtNGZlMi04MmY5LWFmZmFlNjBmZjhiMCIsInBvbCI6IkE1RGp2TExERjkzaUI4cUpkRHBwTnFUWkw5OUkrMVJRbCtoT2NQNHU2NTA9In0.dXTqhTmKXPM-VVmBelnETs_o-QUxGoltECRZTdOhOLoJZ508WYZNNnJD8qcobNQMDoIsx25v-Yo From c4e7e15d3817d81efca389569b54c3ced7b03199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Wed, 27 Sep 2023 15:42:56 +0800 Subject: [PATCH 24/56] Added allowed fields error message --- .../admin-api-keys/AdminAPIKeyCreateForm.tsx | 6 ++++- .../AdminAPIKeysCreateDialog.tsx | 24 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx index 803a14bce0..3b198221d2 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx @@ -24,6 +24,7 @@ interface AdminAPIKeyCreateFormProps { errors: FieldError[] onChange: (key: CreateGQLAPIKeyInput) => void disabled?: boolean + allowFieldsError: string } export default function AdminAPIKeyCreateForm( @@ -104,7 +105,8 @@ export default function AdminAPIKeyCreateForm( labelId='role-select-label' id='role-select' value={role} - label='Role' + label='User Role' + name='userrole' onChange={handleRoleChange} required style={{ width: '100%' }} @@ -132,6 +134,8 @@ export default function AdminAPIKeyCreateForm( variant='outlined' label='Allowed Fields' placeholder='Allowed Fields' + helperText={props.allowFieldsError} + error={props.allowFieldsError !== ''} /> )} renderOption={(props, option, { selected }) => ( diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx index daed25cfd8..2c14650487 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx @@ -37,19 +37,24 @@ export default function AdminAPIKeysCreateDialog(props: { props.setReloadFlag(Math.random()) }, }) + const [ allowFieldsError, setAllowFieldsError] = useState('') const { loading, data, error } = createAPIKeyStatus const fieldErrs = fieldErrors(error) // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const handleOnSubmit = () => { - createAPIKey({ - variables: { - input: key, - }, - }).then((result) => { - if (!result.errors) { - return result - } - }) + if (key.allowedFields.length > 0) { + createAPIKey({ + variables: { + input: key, + }, + }).then((result) => { + if (!result.errors) { + return result + } + }) + } else { + setAllowFieldsError('Please choose at least one Allowed Fields.') + } } if (error) { @@ -74,6 +79,7 @@ export default function AdminAPIKeysCreateDialog(props: { disabled={loading} value={key} onChange={setKey} + allowFieldsError={allowFieldsError} /> } /> From 7b32b3ae106f1cd2f4abaf13f97fb2f83b07e366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Thu, 28 Sep 2023 00:03:26 +0800 Subject: [PATCH 25/56] Converted Drawer Date Info to Time, Integrated Error Message to Create Dialog and Used URQL instead of APOLLO --- .../AdminAPIKeyExpirationField.tsx | 2 + ...IKeyCreateForm.tsx => AdminAPIKeyForm.tsx} | 27 +++++---- .../AdminAPIKeysCreateDialog.tsx | 59 +++++++++---------- .../admin-api-keys/AdminAPIKeysDrawer.tsx | 5 +- 4 files changed, 48 insertions(+), 45 deletions(-) rename web/src/app/admin/admin-api-keys/{AdminAPIKeyCreateForm.tsx => AdminAPIKeyForm.tsx} (87%) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx index 05c182d316..af82cf976b 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx @@ -19,6 +19,7 @@ const useStyles = makeStyles(() => ({ interface FieldProps { setValue: (val: string) => void value: string + create: boolean } export default function AdminAPIKeyExpirationField( @@ -86,6 +87,7 @@ export default function AdminAPIKeyExpirationField( label='Expires At' onChange={handleChange} required + disabled={!props.create} > 7 days 15 days diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx similarity index 87% rename from web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx rename to web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx index 3b198221d2..c00977e3d4 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx @@ -19,16 +19,18 @@ const listGQLFieldsQuery = gql` ` const MaxDetailsLength = 6 * 1024 // 6KiB -interface AdminAPIKeyCreateFormProps { +interface AdminAPIKeyFormProps { value: CreateGQLAPIKeyInput errors: FieldError[] onChange: (key: CreateGQLAPIKeyInput) => void disabled?: boolean - allowFieldsError: string + allowFieldsError: boolean + setAllowFieldsError: (param: boolean) => void + create: boolean } -export default function AdminAPIKeyCreateForm( - props: AdminAPIKeyCreateFormProps, +export default function AdminAPIKeyForm( + props: AdminAPIKeyFormProps, ): JSX.Element { const { ...containerProps } = props const [expiresAt, setExpiresAt] = useState( @@ -61,7 +63,7 @@ export default function AdminAPIKeyCreateForm( valTemp.expiresAt = new Date(expiresAt).toISOString() valTemp.allowedFields = allowedFields valTemp.role = role as UserRole - + props.setAllowFieldsError(props.value.allowedFields.length <= 0) props.onChange(valTemp) }) @@ -106,6 +108,7 @@ export default function AdminAPIKeyCreateForm( id='role-select' value={role} label='User Role' + disabled={!props.create} name='userrole' onChange={handleRoleChange} required @@ -119,6 +122,7 @@ export default function AdminAPIKeyCreateForm( @@ -129,13 +133,14 @@ export default function AdminAPIKeyCreateForm( onChange={handleAutocompleteChange} disableCloseOnSelect renderInput={(params) => ( - )} renderOption={(props, option, { selected }) => ( diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx index 2c14650487..acdb66bc0e 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx @@ -1,11 +1,10 @@ import React, { useState } from 'react' -import { gql, useMutation } from '@apollo/client' +import { gql, useMutation } from 'urql' import { fieldErrors, nonFieldErrors } from '../../util/errutil' import FormDialog from '../../dialogs/FormDialog' -import AdminAPIKeyCreateForm from './AdminAPIKeyCreateForm' +import AdminAPIKeyForm from './AdminAPIKeyForm' import { CreateGQLAPIKeyInput, CreatedGQLAPIKey } from '../../../schema' import Spinner from '../../loading/components/Spinner' -import { GenericError } from '../../error-pages' const newGQLAPIKeyQuery = gql` mutation CreateGQLAPIKey($input: CreateGQLAPIKeyInput!) { @@ -29,57 +28,53 @@ export default function AdminAPIKeysCreateDialog(props: { expiresAt: '', role: 'unknown', }) - const [createAPIKey, createAPIKeyStatus] = useMutation(newGQLAPIKeyQuery, { - onCompleted: (data) => { - props.setToken(data.createGQLAPIKey) - props.onClose(false) - props.onTokenDialogClose(true) - props.setReloadFlag(Math.random()) - }, - }) - const [ allowFieldsError, setAllowFieldsError] = useState('') + const [createAPIKeyStatus, createAPIKey] = useMutation(newGQLAPIKeyQuery) + const [allowFieldsError, setAllowFieldsError] = useState(true) const { loading, data, error } = createAPIKeyStatus - const fieldErrs = fieldErrors(error) + let fieldErrs = fieldErrors(error) // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const handleOnSubmit = () => { - if (key.allowedFields.length > 0) { - createAPIKey({ - variables: { - input: key, - }, - }).then((result) => { - if (!result.errors) { - return result - } - }) - } else { - setAllowFieldsError('Please choose at least one Allowed Fields.') - } - } - - if (error) { - return + createAPIKey({ + input: key, + }).then((result: any) => { + if (!result.error) { + props.setToken(result.data.createGQLAPIKey) + props.onClose(false) + props.onTokenDialogClose(true) + props.setReloadFlag(Math.random()) + } + }) } if (loading && !data) { return } + if (error) { + fieldErrs = fieldErrs.map((err) => { + return err + }) + } + return ( { + props.onClose(false) + }} onSubmit={handleOnSubmit} disableBackdropClose form={ - } /> diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index b402b8e5a7..94b4dd2768 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -18,6 +18,7 @@ import { GQLAPIKey, UpdateGQLAPIKeyInput } from '../../../schema' import AdminAPIKeysDeleteDialog from './AdminAPIKeysDeleteDialog' import { gql, useMutation } from '@apollo/client' import { GenericError } from '../../error-pages' +import { Time } from '../../util/Time' const updateGQLAPIKeyQuery = gql` mutation UpdateGQLAPIKey($input: UpdateGQLAPIKeyInput!) { @@ -186,7 +187,7 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { } /> @@ -198,7 +199,7 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { } /> From 7ccc711dd9a58b02d04b74867ee3af8287974601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Fri, 29 Sep 2023 02:40:28 +0800 Subject: [PATCH 26/56] Converted create dialog for create and edit, fixed role information --- web/src/app/admin/AdminAPIKeys.tsx | 50 ++++- .../admin/admin-api-keys/AdminAPIKeyForm.tsx | 71 ++++---- ...ialog.tsx => AdminAPIKeysActionDialog.tsx} | 65 +++++-- .../admin-api-keys/AdminAPIKeysDrawer.tsx | 172 +++--------------- 4 files changed, 157 insertions(+), 201 deletions(-) rename web/src/app/admin/admin-api-keys/{AdminAPIKeysCreateDialog.tsx => AdminAPIKeysActionDialog.tsx} (55%) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index f45acfb2fa..9dde27e01f 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import makeStyles from '@mui/styles/makeStyles' import { Button, @@ -16,7 +16,7 @@ import FlatList, { FlatListListItem } from '../lists/FlatList' import Spinner from '../loading/components/Spinner' import { GenericError } from '../error-pages' import { Theme } from '@mui/material/styles' -import AdminAPIKeysCreateDialog from './admin-api-keys/AdminAPIKeysCreateDialog' +import AdminAPIKeysActionDialog from './admin-api-keys/AdminAPIKeysActionDialog' import AdminAPIKeysTokenDialog from './admin-api-keys/AdminAPIKeysTokenDialog' const getAPIKeysQuery = gql` @@ -46,6 +46,7 @@ const getAPIKeysQuery = gql` } expiresAt allowedFields + role } } ` @@ -86,13 +87,28 @@ export default function AdminAPIKeys(): JSX.Element { const [selectedAPIKey, setSelectedAPIKey] = useState(null) const [reloadFlag, setReloadFlag] = useState(0) const [tokenDialogClose, onTokenDialogClose] = useState(false) - const [openCreateAPIKeyDialog, setOpenCreateAPIKeyDialog] = useState(false) + const [openActionAPIKeyDialog, setOpenActionAPIKeyDialog] = useState(false) + const [create, setCreate] = useState(false) + const [apiKey, setAPIKey] = useState({ + id: '', + name: '', + description: '', + createdAt: '', + createdBy: null, + updatedAt: '', + updatedBy: null, + lastUsed: null, + expiresAt: '', + allowedFields: [], + role: 'user', + }) const [token, setToken] = useState({ id: '', token: '', }) const handleOpenCreateDialog = (): void => { - setOpenCreateAPIKeyDialog(!openCreateAPIKeyDialog) + setCreate(true) + setOpenActionAPIKeyDialog(!openActionAPIKeyDialog) } const { data, loading, error } = useQuery(getAPIKeysQuery, { variables: { @@ -147,16 +163,34 @@ export default function AdminAPIKeys(): JSX.Element {
{selectedAPIKey ? ( setSelectedAPIKey(null)} + onClose={() => { + if (!openActionAPIKeyDialog) { + setSelectedAPIKey(null) + } + }} apiKey={selectedAPIKey} + setCreate={setCreate} + setOpenActionAPIKeyDialog={setOpenActionAPIKeyDialog} + setAPIKey={setAPIKey} /> ) : null} - {openCreateAPIKeyDialog ? ( - { + if (!create && selectedAPIKey) { + selectedAPIKey.name = apiKey.name + selectedAPIKey.description = apiKey.description + setSelectedAPIKey(selectedAPIKey) + } + + setOpenActionAPIKeyDialog(false) + }} onTokenDialogClose={onTokenDialogClose} setReloadFlag={setReloadFlag} setToken={setToken} + create={create} + apiKey={apiKey} + setAPIKey={setAPIKey} /> ) : null} {tokenDialogClose ? ( diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx index c00977e3d4..d017c8ee0b 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, SyntheticEvent } from 'react' import Grid from '@mui/material/Grid' import { FormContainer, FormField } from '../../forms' import { FieldError } from '../../util/errutil' -import { CreateGQLAPIKeyInput, UserRole } from '../../../schema' +import { GQLAPIKey } from '../../../schema' import AdminAPIKeyExpirationField from './AdminAPIKeyExpirationField' import { gql, useQuery } from '@apollo/client' import { GenericError } from '../../error-pages' @@ -10,7 +10,6 @@ import Spinner from '../../loading/components/Spinner' import { TextField, Autocomplete, MenuItem } from '@mui/material' import CheckIcon from '@mui/icons-material/Check' import { DateTime } from 'luxon' -import Select, { SelectChangeEvent } from '@mui/material/Select' const listGQLFieldsQuery = gql` query ListGQLFieldsQuery { @@ -20,9 +19,9 @@ const listGQLFieldsQuery = gql` const MaxDetailsLength = 6 * 1024 // 6KiB interface AdminAPIKeyFormProps { - value: CreateGQLAPIKeyInput + value: GQLAPIKey errors: FieldError[] - onChange: (key: CreateGQLAPIKeyInput) => void + onChange: (key: GQLAPIKey) => void disabled?: boolean allowFieldsError: boolean setAllowFieldsError: (param: boolean) => void @@ -45,7 +44,6 @@ export default function AdminAPIKeyForm( }), ) const [allowedFields, setAllowedFields] = useState([]) - const [role, setRole] = useState('user') const handleAutocompleteChange = ( event: SyntheticEvent, value: string[], @@ -53,20 +51,13 @@ export default function AdminAPIKeyForm( setAllowedFields(value) } - const handleRoleChange = (event: SelectChangeEvent): void => { - const val = event.target.value as UserRole - setRole(val) - } - useEffect(() => { const valTemp = props.value valTemp.expiresAt = new Date(expiresAt).toISOString() valTemp.allowedFields = allowedFields - valTemp.role = role as UserRole - props.setAllowFieldsError(props.value.allowedFields.length <= 0) + props.setAllowFieldsError(valTemp.allowedFields.length <= 0) props.onChange(valTemp) }) - const { data, loading, error } = useQuery(listGQLFieldsQuery) if (error) { @@ -87,6 +78,7 @@ export default function AdminAPIKeyForm( name='name' required component={TextField} + value={props.value.name} /> @@ -99,31 +91,46 @@ export default function AdminAPIKeyForm( required component={TextField} charCount={MaxDetailsLength} + value={props.value.description} hint='Markdown Supported' /> - + + User + + + Admin + + - + {props.create ? ( + + ) : ( + + )} option} onChange={handleAutocompleteChange} + disabled={!props.create} disableCloseOnSelect + defaultValue={props.value.allowedFields} renderInput={(params) => ( diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx similarity index 55% rename from web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx rename to web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx index acdb66bc0e..36f79ac1b5 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx @@ -3,7 +3,7 @@ import { gql, useMutation } from 'urql' import { fieldErrors, nonFieldErrors } from '../../util/errutil' import FormDialog from '../../dialogs/FormDialog' import AdminAPIKeyForm from './AdminAPIKeyForm' -import { CreateGQLAPIKeyInput, CreatedGQLAPIKey } from '../../../schema' +import { CreatedGQLAPIKey, GQLAPIKey } from '../../../schema' import Spinner from '../../loading/components/Spinner' const newGQLAPIKeyQuery = gql` @@ -15,33 +15,58 @@ const newGQLAPIKeyQuery = gql` } ` -export default function AdminAPIKeysCreateDialog(props: { +const updateGQLAPIKeyQuery = gql` + mutation UpdateGQLAPIKey($input: UpdateGQLAPIKeyInput!) { + updateGQLAPIKey(input: $input) + } +` + +export default function AdminAPIKeysActionDialog(props: { onClose: (param: boolean) => void setToken: (token: CreatedGQLAPIKey) => void setReloadFlag: (inc: number) => void onTokenDialogClose: (prama: boolean) => void + create: boolean + apiKey: GQLAPIKey + setAPIKey: (param: GQLAPIKey) => void }): JSX.Element { - const [key, setKey] = useState({ - name: '', - description: '', - allowedFields: [], - expiresAt: '', - role: 'unknown', - }) - const [createAPIKeyStatus, createAPIKey] = useMutation(newGQLAPIKeyQuery) + let query = updateGQLAPIKeyQuery + const { create, apiKey, setAPIKey } = props + if (props.create) { + query = newGQLAPIKeyQuery + } + + const [apiKeyActionStatus, apiKeyAction] = useMutation(query) const [allowFieldsError, setAllowFieldsError] = useState(true) - const { loading, data, error } = createAPIKeyStatus + const { loading, data, error } = apiKeyActionStatus let fieldErrs = fieldErrors(error) // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const handleOnSubmit = () => { - createAPIKey({ - input: key, + const updateKey = { + name: apiKey.name, + description: apiKey.description, + id: apiKey.id, + } + + const createKey = { + name: apiKey.name, + description: apiKey.description, + allowedFields: apiKey.allowedFields, + expiresAt: apiKey.expiresAt, + role: apiKey.role, + } + + apiKeyAction({ + input: create ? createKey : updateKey, }).then((result: any) => { if (!result.error) { - props.setToken(result.data.createGQLAPIKey) - props.onClose(false) - props.onTokenDialogClose(true) props.setReloadFlag(Math.random()) + props.onClose(false) + + if (props.create) { + props.setToken(result.data.createGQLAPIKey) + props.onTokenDialogClose(true) + } } }) } @@ -58,7 +83,7 @@ export default function AdminAPIKeysCreateDialog(props: { return ( { @@ -70,11 +95,11 @@ export default function AdminAPIKeysCreateDialog(props: { } /> diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index 94b4dd2768..2709d77c35 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -7,30 +7,24 @@ import { List, ListItem, ListItemText, - TextField, Toolbar, Typography, Button, ButtonGroup, } from '@mui/material' import makeStyles from '@mui/styles/makeStyles' -import { GQLAPIKey, UpdateGQLAPIKeyInput } from '../../../schema' +import { GQLAPIKey } from '../../../schema' import AdminAPIKeysDeleteDialog from './AdminAPIKeysDeleteDialog' -import { gql, useMutation } from '@apollo/client' -import { GenericError } from '../../error-pages' import { Time } from '../../util/Time' -const updateGQLAPIKeyQuery = gql` - mutation UpdateGQLAPIKey($input: UpdateGQLAPIKeyInput!) { - updateGQLAPIKey(input: $input) - } -` - // const MaxDetailsLength = 6 * 1024 // 6KiB interface Props { onClose: () => void - apiKey: GQLAPIKey | null + apiKey: GQLAPIKey + setCreate: (param: boolean) => void + setAPIKey: (param: GQLAPIKey) => void + setOpenActionAPIKeyDialog: (param: boolean) => void } const useStyles = makeStyles(() => ({ @@ -42,14 +36,11 @@ const useStyles = makeStyles(() => ({ })) export default function AdminAPIKeysDrawer(props: Props): JSX.Element { - const { onClose, apiKey } = props + const { onClose, apiKey, setCreate, setOpenActionAPIKeyDialog, setAPIKey } = + props const classes = useStyles() const isOpen = Boolean(apiKey) const [deleteDialog, onDeleteDialog] = useState(false) - const [showEdit, setShowEdit] = useState(true) - const [showSave, setShowSave] = useState(false) - const [reqNameText, setReqNameText] = useState('') - const [reqDescText, setReqDescText] = useState('') const handleDeleteConfirmation = (): void => { onDeleteDialog(!deleteDialog) } @@ -60,53 +51,6 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { return inpComma }) - const [key, setKey] = useState({ - id: apiKey?.id ?? '', - name: apiKey?.name, - description: apiKey?.description, - }) - const [updateAPIKey, updateAPIKeyStatus] = useMutation(updateGQLAPIKeyQuery, { - onCompleted: (data) => { - if (data.updateGQLAPIKey) { - setShowSave(!showSave) - setShowEdit(!showEdit) - } - }, - }) - const { loading, data, error } = updateAPIKeyStatus - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const handleSave = () => { - const desc = key.description?.trim() - const name = key.name?.trim() - - if (desc !== '' && name !== '') { - updateAPIKey({ - variables: { - input: key, - }, - }).then((result) => { - if (!result.errors) { - return result - } - }) - } else { - if (desc === '') { - setReqDescText('This field is required.') - } - - if (name === '') { - setReqNameText('This field is required.') - } - } - } - - if (error) { - return - } - - if (loading && !data) { - // return - } return ( @@ -132,51 +76,13 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { - {showSave ? ( - { - const keyTemp = key - keyTemp.name = e.target.value - setKey(keyTemp) - }} - variant='standard' - sx={{ width: '100%' }} - /> - ) : ( - - )} + - {showSave ? ( - { - const keyTemp = key - keyTemp.description = e.target.value - setKey(keyTemp) - }} - variant='standard' - sx={{ width: '100%' }} - /> - ) : ( - - )} + + + + - {showSave ? ( - - - - - ) : null} - {showEdit ? ( - - - - - ) : null} + + + + From 729360042fd9008d6b76ad36d5ed3acd83e50b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Fri, 29 Sep 2023 23:05:52 +0800 Subject: [PATCH 27/56] Fixed issue on the UI theme, Applied time format on Edit, FIxed issue on role display, code cleanup --- web/src/app/admin/AdminAPIKeys.tsx | 124 +++++++++--------- .../admin/admin-api-keys/AdminAPIKeyForm.tsx | 9 +- .../AdminAPIKeysActionDialog.tsx | 5 +- .../admin-api-keys/AdminAPIKeysDrawer.tsx | 2 +- 4 files changed, 74 insertions(+), 66 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index 9dde27e01f..5af923f68b 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -1,13 +1,14 @@ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import makeStyles from '@mui/styles/makeStyles' import { Button, - ButtonGroup, Grid, Typography, Card, ButtonBase, + CardHeader, } from '@mui/material' +import { Add } from '@mui/icons-material' import AdminAPIKeysDrawer from './admin-api-keys/AdminAPIKeysDrawer' import { GQLAPIKey, CreatedGQLAPIKey } from '../../schema' import { Time } from '../util/Time' @@ -55,12 +56,9 @@ const useStyles = makeStyles((theme: Theme) => ({ '& .MuiListItem-root': { 'border-bottom': '1px solid #333333', }, - '& .MuiListItem-root:not(.Mui-selected):hover ': { - 'background-color': '#474747', - }, }, buttons: { - 'padding-bottom': '15px', + 'margin-bottom': '15px', }, containerDefault: { [theme.breakpoints.up('md')]: { @@ -160,64 +158,70 @@ export default function AdminAPIKeys(): JSX.Element { return ( -
- {selectedAPIKey ? ( - { - if (!openActionAPIKeyDialog) { - setSelectedAPIKey(null) - } - }} - apiKey={selectedAPIKey} - setCreate={setCreate} - setOpenActionAPIKeyDialog={setOpenActionAPIKeyDialog} - setAPIKey={setAPIKey} - /> - ) : null} - {openActionAPIKeyDialog ? ( - { - if (!create && selectedAPIKey) { - selectedAPIKey.name = apiKey.name - selectedAPIKey.description = apiKey.description - setSelectedAPIKey(selectedAPIKey) - } + {selectedAPIKey ? ( + { + if (!openActionAPIKeyDialog) { + setSelectedAPIKey(null) + } + }} + apiKey={selectedAPIKey} + setCreate={setCreate} + setOpenActionAPIKeyDialog={setOpenActionAPIKeyDialog} + setAPIKey={setAPIKey} + /> + ) : null} + {openActionAPIKeyDialog ? ( + { + if (!create && selectedAPIKey) { + selectedAPIKey.name = apiKey.name + selectedAPIKey.description = apiKey.description + setSelectedAPIKey(selectedAPIKey) + } - setOpenActionAPIKeyDialog(false) - }} - onTokenDialogClose={onTokenDialogClose} - setReloadFlag={setReloadFlag} - setToken={setToken} - create={create} - apiKey={apiKey} - setAPIKey={setAPIKey} - /> - ) : null} - {tokenDialogClose ? ( - - ) : null} - - - - - - - - -
+ /> + +
) } diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx index d017c8ee0b..16a9325320 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx @@ -10,6 +10,8 @@ import Spinner from '../../loading/components/Spinner' import { TextField, Autocomplete, MenuItem } from '@mui/material' import CheckIcon from '@mui/icons-material/Check' import { DateTime } from 'luxon' +import { Time } from '../../util/Time' +import { ISODateTimePicker } from '../../util/ISOPickers' const listGQLFieldsQuery = gql` query ListGQLFieldsQuery { @@ -109,7 +111,7 @@ export default function AdminAPIKeyForm( User - + Admin @@ -124,10 +126,9 @@ export default function AdminAPIKeyForm( ) : ( )} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx index 36f79ac1b5..e1abf39146 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx @@ -29,9 +29,10 @@ export default function AdminAPIKeysActionDialog(props: { create: boolean apiKey: GQLAPIKey setAPIKey: (param: GQLAPIKey) => void + setSelectedAPIKey: (param: GQLAPIKey) => void }): JSX.Element { let query = updateGQLAPIKeyQuery - const { create, apiKey, setAPIKey } = props + const { create, apiKey, setAPIKey, setSelectedAPIKey } = props if (props.create) { query = newGQLAPIKeyQuery } @@ -66,6 +67,8 @@ export default function AdminAPIKeysActionDialog(props: { if (props.create) { props.setToken(result.data.createGQLAPIKey) props.onTokenDialogClose(true) + } else { + setSelectedAPIKey(apiKey) } } }) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index 2709d77c35..5f23c4c209 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -119,7 +119,7 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { - + From 25b3ac5c54b1377839f0a98da8ca20912a9267cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Fri, 29 Sep 2023 23:15:19 +0800 Subject: [PATCH 28/56] removed claims.go --- apikey/claims.go | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 apikey/claims.go diff --git a/apikey/claims.go b/apikey/claims.go deleted file mode 100644 index b95604585e..0000000000 --- a/apikey/claims.go +++ /dev/null @@ -1,29 +0,0 @@ -package apikey - -import ( - "time" - - "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" -) - -type Claims struct { - jwt.RegisteredClaims - PolicyHash []byte `json:"pol"` -} - -func NewGraphQLClaims(id uuid.UUID, policyHash []byte, expires time.Time) jwt.Claims { - n := time.Now() - return &Claims{ - RegisteredClaims: jwt.RegisteredClaims{ - ID: uuid.NewString(), - Subject: id.String(), - ExpiresAt: jwt.NewNumericDate(expires), - IssuedAt: jwt.NewNumericDate(n), - NotBefore: jwt.NewNumericDate(n.Add(-time.Minute)), - Issuer: Issuer, - Audience: []string{Audience}, - }, - PolicyHash: policyHash, - } -} From 33112b78851ccfe8014c5c79fdae05f047e4193d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Fri, 29 Sep 2023 23:17:05 +0800 Subject: [PATCH 29/56] discard changes to appgo --- graphql2/graphqlapp/app.go | 1 - 1 file changed, 1 deletion(-) diff --git a/graphql2/graphqlapp/app.go b/graphql2/graphqlapp/app.go index 20b210a83b..c714f2a090 100644 --- a/graphql2/graphqlapp/app.go +++ b/graphql2/graphqlapp/app.go @@ -266,7 +266,6 @@ func (a *App) Handler() http.Handler { // ensure some sort of auth before continuing err := permission.LimitCheckAny(ctx) if errutil.HTTPError(ctx, w, err) { - log.Logf(ctx, "GraphQL: %s", err) return } From 11144aa5c145a9a9325a18aa44283aefc21e7584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Mon, 2 Oct 2023 15:37:30 +0800 Subject: [PATCH 30/56] addressed eslint issues --- .../AdminAPIKeyExpirationField.tsx | 2 +- .../admin/admin-api-keys/AdminAPIKeyForm.tsx | 1 - .../AdminAPIKeysActionDialog.tsx | 10 +++++----- .../AdminAPIKeysTokenDialog.tsx | 19 ++++++++++++++----- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx index af82cf976b..a6f9565860 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx @@ -55,7 +55,7 @@ export default function AdminAPIKeyExpirationField( const handleDatePickerChange = (val: string): void => { // eslint-disable-next-line prettier/prettier - if(val != null) { + if (val != null) { setDateVal( new Date(val).toLocaleString([], { weekday: 'short', diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx index 16a9325320..33b37bdba6 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx @@ -10,7 +10,6 @@ import Spinner from '../../loading/components/Spinner' import { TextField, Autocomplete, MenuItem } from '@mui/material' import CheckIcon from '@mui/icons-material/Check' import { DateTime } from 'luxon' -import { Time } from '../../util/Time' import { ISODateTimePicker } from '../../util/ISOPickers' const listGQLFieldsQuery = gql` diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx index e1abf39146..aae6034230 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx @@ -39,7 +39,7 @@ export default function AdminAPIKeysActionDialog(props: { const [apiKeyActionStatus, apiKeyAction] = useMutation(query) const [allowFieldsError, setAllowFieldsError] = useState(true) - const { loading, data, error } = apiKeyActionStatus + const { fetching, data, error } = apiKeyActionStatus let fieldErrs = fieldErrors(error) // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const handleOnSubmit = () => { @@ -59,7 +59,7 @@ export default function AdminAPIKeysActionDialog(props: { apiKeyAction({ input: create ? createKey : updateKey, - }).then((result: any) => { + }).then((result) => { if (!result.error) { props.setReloadFlag(Math.random()) props.onClose(false) @@ -74,7 +74,7 @@ export default function AdminAPIKeysActionDialog(props: { }) } - if (loading && !data) { + if (fetching && !data) { return } @@ -87,7 +87,7 @@ export default function AdminAPIKeysActionDialog(props: { return ( { props.onClose(false) @@ -97,7 +97,7 @@ export default function AdminAPIKeysActionDialog(props: { form={ { navigator.clipboard.writeText(props.input.token) } - const onCloseDialog = (event: any, reason: any): any => { + const onCloseDialog = ( + event: object, + reason: string, + ): boolean | undefined => { if (reason === 'backdropClick' || reason === 'escapeKeyDown') { - return false; + return false } onClose(!close) @@ -47,12 +50,18 @@ export default function AdminAPIKeysTokenDialog(props: { disableEscapeKeyDown > - + API Key Token - (Please copy and save the token as this is the only time you'll be able to view it.) + (Please copy and save the token as this is the only time you'll + be able to view it.) @@ -65,7 +74,7 @@ export default function AdminAPIKeysTokenDialog(props: { - + {props.input.token} From 0cedff2c5c54db1385735429ac783a06d76d3826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Mon, 2 Oct 2023 17:30:10 +0800 Subject: [PATCH 31/56] Added necessary code comments --- web/src/app/admin/AdminAPIKeys.tsx | 4 +++- .../admin-api-keys/AdminAPIKeyExpirationField.tsx | 8 ++++---- web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx | 4 +++- .../admin/admin-api-keys/AdminAPIKeysActionDialog.tsx | 10 +++++++--- .../admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx | 3 +++ .../app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx | 6 +++--- .../admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx | 5 +++-- 7 files changed, 26 insertions(+), 14 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index 5af923f68b..b79891aa8c 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -19,7 +19,7 @@ import { GenericError } from '../error-pages' import { Theme } from '@mui/material/styles' import AdminAPIKeysActionDialog from './admin-api-keys/AdminAPIKeysActionDialog' import AdminAPIKeysTokenDialog from './admin-api-keys/AdminAPIKeysTokenDialog' - +// query for getting existing API Keys const getAPIKeysQuery = gql` query gqlAPIKeysQuery { gqlAPIKeys { @@ -104,10 +104,12 @@ export default function AdminAPIKeys(): JSX.Element { id: '', token: '', }) + // handles the openning of the create dialog form which is used for creating new API Key const handleOpenCreateDialog = (): void => { setCreate(true) setOpenActionAPIKeyDialog(!openActionAPIKeyDialog) } + // Get API Key triggers/actions const { data, loading, error } = useQuery(getAPIKeysQuery, { variables: { reloadData: reloadFlag, diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx index a6f9565860..da0a3f408f 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx @@ -15,7 +15,7 @@ const useStyles = makeStyles(() => ({ 'padding-top': '15px', }, })) - +// props object for this compoenent interface FieldProps { setValue: (val: string) => void value: string @@ -30,7 +30,7 @@ export default function AdminAPIKeyExpirationField( const [dateVal, setDateVal] = useState(value) const [options, setOptions] = useState('7') const [showPicker, setShowPicker] = useState(false) - // const today = DateTime.local({ zone: 'local' }).toFormat('ZZZZ') + // handles expiration date field options changes: sets and computes expirates at value based on the selected additional days value or sets value to +7 days today if custom option is selected const handleChange = (event: SelectChangeEvent): void => { const val = event.target.value as string setOptions(val) @@ -52,7 +52,7 @@ export default function AdminAPIKeyExpirationField( ) } } - + // handles custon expiration date field option changes: sets and computes expires at value based on the selected date const handleDatePickerChange = (val: string): void => { // eslint-disable-next-line prettier/prettier if (val != null) { @@ -69,7 +69,7 @@ export default function AdminAPIKeyExpirationField( ) } } - + // set expirate at date value to state useEffect(() => { setValue(dateVal) }) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx index 33b37bdba6..66948856ca 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx @@ -19,6 +19,7 @@ const listGQLFieldsQuery = gql` ` const MaxDetailsLength = 6 * 1024 // 6KiB +// property object for this component interface AdminAPIKeyFormProps { value: GQLAPIKey errors: FieldError[] @@ -45,13 +46,14 @@ export default function AdminAPIKeyForm( }), ) const [allowedFields, setAllowedFields] = useState([]) + // handle AllowedFields field option changes: sets selected value to state const handleAutocompleteChange = ( event: SyntheticEvent, value: string[], ): void => { setAllowedFields(value) } - + // sets GQLAPIKey updated value to state useEffect(() => { const valTemp = props.value valTemp.expiresAt = new Date(expiresAt).toISOString() diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx index aae6034230..e6a9f3d519 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx @@ -6,6 +6,8 @@ import AdminAPIKeyForm from './AdminAPIKeyForm' import { CreatedGQLAPIKey, GQLAPIKey } from '../../../schema' import Spinner from '../../loading/components/Spinner' +// query for creating new api key which accepts CreateGQLAPIKeyInput param +// return token created upon successfull transaction const newGQLAPIKeyQuery = gql` mutation CreateGQLAPIKey($input: CreateGQLAPIKeyInput!) { createGQLAPIKey(input: $input) { @@ -14,7 +16,7 @@ const newGQLAPIKeyQuery = gql` } } ` - +// query for updating api key which accepts UpdateGQLAPIKeyInput const updateGQLAPIKeyQuery = gql` mutation UpdateGQLAPIKey($input: UpdateGQLAPIKeyInput!) { updateGQLAPIKey(input: $input) @@ -33,6 +35,7 @@ export default function AdminAPIKeysActionDialog(props: { }): JSX.Element { let query = updateGQLAPIKeyQuery const { create, apiKey, setAPIKey, setSelectedAPIKey } = props + if (props.create) { query = newGQLAPIKeyQuery } @@ -41,8 +44,9 @@ export default function AdminAPIKeysActionDialog(props: { const [allowFieldsError, setAllowFieldsError] = useState(true) const { fetching, data, error } = apiKeyActionStatus let fieldErrs = fieldErrors(error) - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const handleOnSubmit = () => { + // handles form on submit event, based on the action type (edit, create) it will send the necessary type of parameter + // token is also being set here when create action is used + const handleOnSubmit = (): void => { const updateKey = { name: apiKey.name, description: apiKey.description, diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx index 57ae3a6ca9..ef35912962 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx @@ -10,6 +10,7 @@ import { gql, useMutation } from '@apollo/client' import { GenericError } from '../../error-pages' import Spinner from '../../loading/components/Spinner' +// query for deleting API Key which accepts API Key ID const deleteGQLAPIKeyQuery = gql` mutation DeleteGQLAPIKey($id: ID!) { deleteGQLAPIKey(id: $id) @@ -23,6 +24,7 @@ export default function AdminAPIKeysDeleteDialog(props: { }): JSX.Element { const { apiKey, onClose, close } = props const [dialogClose, onDialogClose] = useState(close) + // handles the no confirmation option for delete API Key transactions const handleNo = (): void => { onClose(false) onDialogClose(!dialogClose) @@ -36,6 +38,7 @@ export default function AdminAPIKeysDeleteDialog(props: { }, }) const { loading, data, error } = deleteAPIKeyStatus + // handles the yes confirmation option for delete API Key transactions const handleYes = (): void => { deleteAPIKey({ variables: { diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index 5f23c4c209..08cee59555 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -17,8 +17,7 @@ import { GQLAPIKey } from '../../../schema' import AdminAPIKeysDeleteDialog from './AdminAPIKeysDeleteDialog' import { Time } from '../../util/Time' -// const MaxDetailsLength = 6 * 1024 // 6KiB - +// property for this object interface Props { onClose: () => void apiKey: GQLAPIKey @@ -41,14 +40,15 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { const classes = useStyles() const isOpen = Boolean(apiKey) const [deleteDialog, onDeleteDialog] = useState(false) + // handle for opening/closing delete confirmation dialog of the API Key Delete transaction const handleDeleteConfirmation = (): void => { onDeleteDialog(!deleteDialog) } let comma = '' + // convert allowedfields option array data to comma separated values which will be use for display const allowFieldsStr = apiKey?.allowedFields.map((inp: string): string => { const inpComma = comma + inp comma = ', ' - return inpComma }) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx index 0f75530e21..8d425b2efb 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx @@ -21,6 +21,7 @@ export default function AdminAPIKeysTokenDialog(props: { const onClickCopy = (): void => { navigator.clipboard.writeText(props.input.token) } + // handles onclose dialog for the token dialog, rejects close for backdropclick or escapekeydown actions const onCloseDialog = ( event: object, reason: string, @@ -31,11 +32,11 @@ export default function AdminAPIKeysTokenDialog(props: { onClose(!close) } - + // handles close dialog button action const onCloseDialogByButton = (): void => { onClose(!close) } - + // trigger token dialog close for parent container useEffect(() => { props.onTokenDialogClose(close) }) From c48b85febeafe46b5b1e5bd87abd85bea3df4d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C1ddo=E2=80=9D?= <“idduspace@gmail.com”> Date: Thu, 5 Oct 2023 18:35:12 +0800 Subject: [PATCH 32/56] Switched apollo plugin to urql, switch Dialog to FormDialog for delete, Combined edit and add AllowedFields field with a disabled param --- web/src/app/admin/AdminAPIKeys.tsx | 31 ++--- .../AdminAPIKeyExpirationField.tsx | 109 ++++++++++------- .../admin/admin-api-keys/AdminAPIKeyForm.tsx | 53 ++++----- .../AdminAPIKeysActionDialog.tsx | 26 ++--- .../AdminAPIKeysDeleteDialog.tsx | 110 +++++++++--------- .../admin-api-keys/AdminAPIKeysDrawer.tsx | 11 +- 6 files changed, 169 insertions(+), 171 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index b79891aa8c..7b27027983 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -12,7 +12,7 @@ import { Add } from '@mui/icons-material' import AdminAPIKeysDrawer from './admin-api-keys/AdminAPIKeysDrawer' import { GQLAPIKey, CreatedGQLAPIKey } from '../../schema' import { Time } from '../util/Time' -import { gql, useQuery } from '@apollo/client' +import { gql, useQuery } from 'urql' import FlatList, { FlatListListItem } from '../lists/FlatList' import Spinner from '../loading/components/Spinner' import { GenericError } from '../error-pages' @@ -20,7 +20,7 @@ import { Theme } from '@mui/material/styles' import AdminAPIKeysActionDialog from './admin-api-keys/AdminAPIKeysActionDialog' import AdminAPIKeysTokenDialog from './admin-api-keys/AdminAPIKeysTokenDialog' // query for getting existing API Keys -const getAPIKeysQuery = gql` +const query = gql` query gqlAPIKeysQuery { gqlAPIKeys { id @@ -29,16 +29,12 @@ const getAPIKeysQuery = gql` createdAt createdBy { id - role name - email } updatedAt updatedBy { id - role name - email } lastUsed { time @@ -83,11 +79,10 @@ const useStyles = makeStyles((theme: Theme) => ({ export default function AdminAPIKeys(): JSX.Element { const classes = useStyles() const [selectedAPIKey, setSelectedAPIKey] = useState(null) - const [reloadFlag, setReloadFlag] = useState(0) const [tokenDialogClose, onTokenDialogClose] = useState(false) const [openActionAPIKeyDialog, setOpenActionAPIKeyDialog] = useState(false) const [create, setCreate] = useState(false) - const [apiKey, setAPIKey] = useState({ + const emptyAPIKey = { id: '', name: '', description: '', @@ -99,28 +94,27 @@ export default function AdminAPIKeys(): JSX.Element { expiresAt: '', allowedFields: [], role: 'user', - }) + } + const [apiKey, setAPIKey] = useState(emptyAPIKey as GQLAPIKey) const [token, setToken] = useState({ id: '', token: '', }) // handles the openning of the create dialog form which is used for creating new API Key const handleOpenCreateDialog = (): void => { + setSelectedAPIKey(null) setCreate(true) + setAPIKey(emptyAPIKey as GQLAPIKey) setOpenActionAPIKeyDialog(!openActionAPIKeyDialog) } // Get API Key triggers/actions - const { data, loading, error } = useQuery(getAPIKeysQuery, { - variables: { - reloadData: reloadFlag, - }, - }) + const [{ data, fetching, error }] = useQuery({ query }) if (error) { return } - if (loading && !data) { + if (fetching && !data) { return } @@ -176,16 +170,9 @@ export default function AdminAPIKeys(): JSX.Element { {openActionAPIKeyDialog ? ( { - if (!create && selectedAPIKey) { - selectedAPIKey.name = apiKey.name - selectedAPIKey.description = apiKey.description - setSelectedAPIKey(selectedAPIKey) - } - setOpenActionAPIKeyDialog(false) }} onTokenDialogClose={onTokenDialogClose} - setReloadFlag={setReloadFlag} setToken={setToken} create={create} apiKey={apiKey} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx index da0a3f408f..35a2d97ec6 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyExpirationField.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react' -import { FormContainer } from '../../forms' +import { FormContainer, FormField } from '../../forms' import InputLabel from '@mui/material/InputLabel' import MenuItem from '@mui/material/MenuItem' import FormControl from '@mui/material/FormControl' @@ -7,7 +7,7 @@ import Select, { SelectChangeEvent } from '@mui/material/Select' import Grid from '@mui/material/Grid' import { Typography } from '@mui/material' import makeStyles from '@mui/styles/makeStyles' -import { ISODatePicker } from '../../util/ISOPickers' +import { ISODatePicker, ISODateTimePicker } from '../../util/ISOPickers' import { DateTime } from 'luxon' const useStyles = makeStyles(() => ({ @@ -19,7 +19,7 @@ const useStyles = makeStyles(() => ({ interface FieldProps { setValue: (val: string) => void value: string - create: boolean + disabled: boolean } export default function AdminAPIKeyExpirationField( @@ -37,6 +37,7 @@ export default function AdminAPIKeyExpirationField( setShowPicker(val.toString() === '0') if (val !== '0') { + console.log(val) setDateVal( DateTime.now() .plus({ days: parseInt(val) }) @@ -54,54 +55,70 @@ export default function AdminAPIKeyExpirationField( } // handles custon expiration date field option changes: sets and computes expires at value based on the selected date const handleDatePickerChange = (val: string): void => { - // eslint-disable-next-line prettier/prettier - if (val != null) { - setDateVal( - new Date(val).toLocaleString([], { - weekday: 'short', - month: 'short', - day: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }), - ) - } + if (val === null) return + + setDateVal( + new Date(val).toLocaleString([], { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }), + ) } - // set expirate at date value to state - useEffect(() => { - setValue(dateVal) - }) return ( - - Expires At* - - + {props.disabled ? ( + + + + ) : ( + + Expires At* + + + )} {showPicker ? ( - + ) : null} @@ -112,7 +129,15 @@ export default function AdminAPIKeyExpirationField( component='div' className={classes.expiresCon} > - The token will expires on {dateVal} + The token will expires on {new Date(value).toLocaleString([], { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx index 66948856ca..c783f86b43 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx @@ -4,15 +4,14 @@ import { FormContainer, FormField } from '../../forms' import { FieldError } from '../../util/errutil' import { GQLAPIKey } from '../../../schema' import AdminAPIKeyExpirationField from './AdminAPIKeyExpirationField' -import { gql, useQuery } from '@apollo/client' +import { gql, useQuery } from 'urql' import { GenericError } from '../../error-pages' import Spinner from '../../loading/components/Spinner' import { TextField, Autocomplete, MenuItem } from '@mui/material' import CheckIcon from '@mui/icons-material/Check' import { DateTime } from 'luxon' -import { ISODateTimePicker } from '../../util/ISOPickers' -const listGQLFieldsQuery = gql` +const query = gql` query ListGQLFieldsQuery { listGQLFields } @@ -25,8 +24,8 @@ interface AdminAPIKeyFormProps { errors: FieldError[] onChange: (key: GQLAPIKey) => void disabled?: boolean - allowFieldsError: boolean - setAllowFieldsError: (param: boolean) => void + reqAllowFieldsFlag: boolean + setReqAllowFieldsFlag: (param: boolean) => void create: boolean } @@ -55,19 +54,23 @@ export default function AdminAPIKeyForm( } // sets GQLAPIKey updated value to state useEffect(() => { - const valTemp = props.value - valTemp.expiresAt = new Date(expiresAt).toISOString() - valTemp.allowedFields = allowedFields - props.setAllowFieldsError(valTemp.allowedFields.length <= 0) - props.onChange(valTemp) + if (props.create) { + const valTemp = props.value + valTemp.expiresAt = new Date(expiresAt).toISOString() + valTemp.allowedFields = allowedFields + props.setReqAllowFieldsFlag(valTemp.allowedFields.length <= 0) + props.onChange(valTemp) + } + }) + const [{ data, fetching, error }] = useQuery({ + query, }) - const { data, loading, error } = useQuery(listGQLFieldsQuery) if (error) { return } - if (loading && !data) { + if (fetching && !data) { return } @@ -118,21 +121,11 @@ export default function AdminAPIKeyForm( - {props.create ? ( - - ) : ( - - )} + diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx index e6a9f3d519..a577f0ca1d 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx @@ -26,22 +26,16 @@ const updateGQLAPIKeyQuery = gql` export default function AdminAPIKeysActionDialog(props: { onClose: (param: boolean) => void setToken: (token: CreatedGQLAPIKey) => void - setReloadFlag: (inc: number) => void onTokenDialogClose: (prama: boolean) => void create: boolean apiKey: GQLAPIKey setAPIKey: (param: GQLAPIKey) => void setSelectedAPIKey: (param: GQLAPIKey) => void }): JSX.Element { - let query = updateGQLAPIKeyQuery const { create, apiKey, setAPIKey, setSelectedAPIKey } = props - - if (props.create) { - query = newGQLAPIKeyQuery - } - + const query = props.create ? newGQLAPIKeyQuery : updateGQLAPIKeyQuery const [apiKeyActionStatus, apiKeyAction] = useMutation(query) - const [allowFieldsError, setAllowFieldsError] = useState(true) + const [reqAllowFieldsFlag, setReqAllowFieldsFlag] = useState(true) const { fetching, data, error } = apiKeyActionStatus let fieldErrs = fieldErrors(error) // handles form on submit event, based on the action type (edit, create) it will send the necessary type of parameter @@ -61,11 +55,15 @@ export default function AdminAPIKeysActionDialog(props: { role: apiKey.role, } - apiKeyAction({ - input: create ? createKey : updateKey, - }).then((result) => { + apiKeyAction( + { + input: create ? createKey : updateKey, + }, + { additionalTypenames: ['GQLAPIKey'] }, + ).then((result) => { + setReqAllowFieldsFlag(apiKey.allowedFields.length <= 0) + if (!result.error) { - props.setReloadFlag(Math.random()) props.onClose(false) if (props.create) { @@ -104,8 +102,8 @@ export default function AdminAPIKeysActionDialog(props: { disabled={fetching} value={apiKey} onChange={setAPIKey} - allowFieldsError={allowFieldsError} - setAllowFieldsError={setAllowFieldsError} + reqAllowFieldsFlag={reqAllowFieldsFlag} + setReqAllowFieldsFlag={setReqAllowFieldsFlag} create={props.create} /> } diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx index ef35912962..c8a165e6cd 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDeleteDialog.tsx @@ -1,77 +1,77 @@ -import React, { useState } from 'react' -import { GQLAPIKey } from '../../../schema' -import Button from '@mui/material/Button' -import Dialog from '@mui/material/Dialog' -import DialogActions from '@mui/material/DialogActions' -import DialogContent from '@mui/material/DialogContent' -import DialogContentText from '@mui/material/DialogContentText' -import DialogTitle from '@mui/material/DialogTitle' -import { gql, useMutation } from '@apollo/client' +import React from 'react' +import { nonFieldErrors } from '../../util/errutil' +import FormDialog from '../../dialogs/FormDialog' +import { gql, useMutation, useQuery } from 'urql' import { GenericError } from '../../error-pages' import Spinner from '../../loading/components/Spinner' +import { GQLAPIKey } from '../../../schema' // query for deleting API Key which accepts API Key ID const deleteGQLAPIKeyQuery = gql` - mutation DeleteGQLAPIKey($id: ID!) { + mutation DeleteGQLAPIKey($id: string!) { deleteGQLAPIKey(id: $id) } ` +// query for getting existing API Keys +const query = gql` + query gqlAPIKeysQuery { + gqlAPIKeys { + id + name + } + } +` + export default function AdminAPIKeysDeleteDialog(props: { - apiKey: GQLAPIKey | null - onClose: (param: boolean) => void - close: boolean + apiKeyId: string + onClose: () => void }): JSX.Element { - const { apiKey, onClose, close } = props - const [dialogClose, onDialogClose] = useState(close) - // handles the no confirmation option for delete API Key transactions - const handleNo = (): void => { - onClose(false) - onDialogClose(!dialogClose) - } - const [deleteAPIKey, deleteAPIKeyStatus] = useMutation(deleteGQLAPIKeyQuery, { - onCompleted: (data) => { - if (data.deleteGQLAPIKey) { - onClose(false) - onDialogClose(!dialogClose) - } - }, + const [{ fetching, data, error }] = useQuery({ + query, }) - const { loading, data, error } = deleteAPIKeyStatus - // handles the yes confirmation option for delete API Key transactions - const handleYes = (): void => { - deleteAPIKey({ - variables: { - id: apiKey?.id, + const { apiKeyId, onClose } = props + const [deleteAPIKeyStatus, deleteAPIKey] = useMutation(deleteGQLAPIKeyQuery) + + if (fetching && !data) return + if (error) return + + const apiKeyName = data?.gqlAPIKeys?.find((d: GQLAPIKey) => { + return d.id === apiKeyId + })?.name + + const handleOnSubmit = (): void => { + deleteAPIKey( + { + id: apiKeyId, }, + { additionalTypenames: ['GQLAPIKey'] }, + ).then((result) => { + if (!result.error) onClose() }) } - if (error) { - return - } + const handleOnClose = ( + event: object, + reason: string, + ): boolean | undefined => { + if (reason === 'backdropClick' || reason === 'escapeKeyDown') { + return false + } - if (loading && !data) { - return + props.onClose() } return ( - - DELETE API KEY - - - Are you sure you want to delete the API KEY {apiKey?.name}? - - - - - - - + ) } diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx index 08cee59555..c398f5856e 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeysDrawer.tsx @@ -40,10 +40,6 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { const classes = useStyles() const isOpen = Boolean(apiKey) const [deleteDialog, onDeleteDialog] = useState(false) - // handle for opening/closing delete confirmation dialog of the API Key Delete transaction - const handleDeleteConfirmation = (): void => { - onDeleteDialog(!deleteDialog) - } let comma = '' // convert allowedfields option array data to comma separated values which will be use for display const allowFieldsStr = apiKey?.allowedFields.map((inp: string): string => { @@ -56,9 +52,8 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { {deleteDialog ? ( onDeleteDialog(false)} + apiKeyId={apiKey.id} /> ) : null} @@ -120,7 +115,7 @@ export default function AdminAPIKeysDrawer(props: Props): JSX.Element { - - diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx new file mode 100644 index 0000000000..6629169b01 --- /dev/null +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect } from 'react' +import { gql, useMutation, useQuery } from 'urql' +import { fieldErrors, nonFieldErrors } from '../../util/errutil' +import FormDialog from '../../dialogs/FormDialog' +import AdminAPIKeyForm from './AdminAPIKeyForm' +import { GQLAPIKey, UpdateGQLAPIKeyInput } from '../../../schema' +import Spinner from '../../loading/components/Spinner' +import { GenericError } from '../../error-pages' + +// query for updating api key which accepts UpdateGQLAPIKeyInput +const updateGQLAPIKeyQuery = gql` + mutation UpdateGQLAPIKey($input: UpdateGQLAPIKeyInput!) { + updateGQLAPIKey(input: $input) + } +` +// query for getting existing API Key information +const query = gql` + query gqlAPIKeysQuery { + gqlAPIKeys { + id + name + description + expiresAt + allowedFields + role + } + } +` +export default function AdminAPIKeyEditDialog(props: { + onClose: (param: boolean) => void + apiKeyId: string +}): JSX.Element { + const { apiKeyId, onClose } = props + const [{ fetching, data, error }] = useQuery({ + query, + }) + const [apiKeyActionStatus, apiKeyAction] = useMutation(updateGQLAPIKeyQuery) + const [apiKeyInput, setAPIKeyInput] = useState({ + name: '', + description: '', + id: '', + }) + let fieldErrs = fieldErrors(error) + + useEffect(() => { + // retrieve apiKey information by id + setAPIKeyInput( + data?.gqlAPIKeys?.find((d: GQLAPIKey) => { + return d.id === apiKeyId + }), + ) + }, [data]) + + if (fetching && !data) return + if (error) return + // handles form on submit event, based on the action type (edit, create) it will send the necessary type of parameter + // token is also being set here when create action is used + const handleOnSubmit = (): void => { + apiKeyAction( + { + input: { + name: apiKeyInput?.name, + description: apiKeyInput?.description, + id: apiKeyInput?.id, + }, + }, + { additionalTypenames: ['GQLAPIKey', 'UpdateGQLAPIKeyInput'] }, + ).then((result) => { + if (!result.error) { + onClose(false) + } + }) + } + + if (fetching && !data) { + return + } + + if (error) { + fieldErrs = fieldErrs.map((err) => { + return err + }) + } + + return ( + + } + /> + ) +} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyTokenDialog.tsx similarity index 81% rename from web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx rename to web/src/app/admin/admin-api-keys/AdminAPIKeyTokenDialog.tsx index 8d425b2efb..abb987277c 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysTokenDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyTokenDialog.tsx @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -import React, { useState, useEffect } from 'react' +import React from 'react' import { CreatedGQLAPIKey } from '../../../schema' import Dialog from '@mui/material/Dialog' import DialogActions from '@mui/material/DialogActions' @@ -12,14 +12,13 @@ import Grid from '@mui/material/Grid' import IconButton from '@mui/material/IconButton' import CloseIcon from '@mui/icons-material/Close' -export default function AdminAPIKeysTokenDialog(props: { - input: CreatedGQLAPIKey - onTokenDialogClose: (input: boolean) => void - tokenDialogClose: boolean +export default function AdminAPIKeyTokenDialog(props: { + value: CreatedGQLAPIKey + onClose: () => void }): JSX.Element { - const [close, onClose] = useState(props.tokenDialogClose) + const {onClose, value} = props const onClickCopy = (): void => { - navigator.clipboard.writeText(props.input.token) + navigator.clipboard.writeText(props.value.token) } // handles onclose dialog for the token dialog, rejects close for backdropclick or escapekeydown actions const onCloseDialog = ( @@ -30,20 +29,16 @@ export default function AdminAPIKeysTokenDialog(props: { return false } - onClose(!close) + onClose() } // handles close dialog button action const onCloseDialogByButton = (): void => { - onClose(!close) + onClose() } - // trigger token dialog close for parent container - useEffect(() => { - props.onTokenDialogClose(close) - }) return ( - {props.input.token} + {value.token} From 63f1f750c9b85ffaa042c0ae09bbb9f2393d8494 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 6 Oct 2023 15:53:48 -0500 Subject: [PATCH 41/56] cleanup edit dialog --- .../admin-api-keys/AdminAPIKeyEditDialog.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx index 6629169b01..499228e965 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx @@ -40,7 +40,6 @@ export default function AdminAPIKeyEditDialog(props: { description: '', id: '', }) - let fieldErrs = fieldErrors(error) useEffect(() => { // retrieve apiKey information by id @@ -64,11 +63,11 @@ export default function AdminAPIKeyEditDialog(props: { id: apiKeyInput?.id, }, }, - { additionalTypenames: ['GQLAPIKey', 'UpdateGQLAPIKeyInput'] }, + { additionalTypenames: ['GQLAPIKey'] }, ).then((result) => { - if (!result.error) { - onClose(false) - } + if (result.error) return + + onClose(false) }) } @@ -76,12 +75,6 @@ export default function AdminAPIKeyEditDialog(props: { return } - if (error) { - fieldErrs = fieldErrs.map((err) => { - return err - }) - } - return ( Date: Mon, 9 Oct 2023 21:04:46 +0800 Subject: [PATCH 42/56] Removed backdropclose flag and removed mui class declarations and moved style to sx field --- web/src/app/admin/AdminAPIKeys.tsx | 15 ++------------- .../admin-api-keys/AdminAPIKeyDeleteDialog.tsx | 14 +------------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index 2a426dc40e..5d96657c63 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -35,11 +35,6 @@ const query = gql` } ` const useStyles = makeStyles((theme: Theme) => ({ - root: { - '& .MuiListItem-root': { - 'border-bottom': '1px solid #333333', - }, - }, buttons: { 'margin-bottom': '15px', }, @@ -48,18 +43,12 @@ const useStyles = makeStyles((theme: Theme) => ({ maxWidth: '100%', transition: `max-width ${theme.transitions.duration.leavingScreen}ms ease`, }, - '& .MuiListItem-root': { - padding: '0px', - }, }, containerSelected: { [theme.breakpoints.up('md')]: { maxWidth: '70%', transition: `max-width ${theme.transitions.duration.enteringScreen}ms ease`, }, - '& .MuiListItem-root': { - padding: '0px', - }, }, })) @@ -134,7 +123,7 @@ export default function AdminAPIKeys(): JSX.Element { /> ) : null} { - if (reason === 'backdropClick' || reason === 'escapeKeyDown') { - return false - } - - props.onClose(false) - } - return ( ) From 6e85a3efed6a49b3f9bb27f268cc7f586e65d7b0 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 10 Oct 2023 15:02:50 -0500 Subject: [PATCH 43/56] simplify edit --- .../admin-api-keys/AdminAPIKeyEditDialog.tsx | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx index 499228e965..13e21066e7 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx @@ -34,21 +34,10 @@ export default function AdminAPIKeyEditDialog(props: { const [{ fetching, data, error }] = useQuery({ query, }) + const key: GQLAPIKey | null = + data?.gqlAPIKeys?.find((d: GQLAPIKey) => d.id === apiKeyId) || null const [apiKeyActionStatus, apiKeyAction] = useMutation(updateGQLAPIKeyQuery) - const [apiKeyInput, setAPIKeyInput] = useState({ - name: '', - description: '', - id: '', - }) - - useEffect(() => { - // retrieve apiKey information by id - setAPIKeyInput( - data?.gqlAPIKeys?.find((d: GQLAPIKey) => { - return d.id === apiKeyId - }), - ) - }, [data]) + const [apiKeyInput, setAPIKeyInput] = useState(null) if (fetching && !data) return if (error) return @@ -60,7 +49,7 @@ export default function AdminAPIKeyEditDialog(props: { input: { name: apiKeyInput?.name, description: apiKeyInput?.description, - id: apiKeyInput?.id, + id: apiKeyId, }, }, { additionalTypenames: ['GQLAPIKey'] }, @@ -71,7 +60,7 @@ export default function AdminAPIKeyEditDialog(props: { }) } - if (fetching && !data) { + if (fetching || key === null) { return } @@ -86,8 +75,7 @@ export default function AdminAPIKeyEditDialog(props: { } /> From 502bb337674474d0ceca70756a0355d161e7467f Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 11 Oct 2023 14:29:48 -0500 Subject: [PATCH 44/56] simplify create dialog based on calsub dialog --- .../AdminAPIKeyCreateDialog.tsx | 141 ++++++++++-------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx index 3007a3f3cb..b574d608d3 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx @@ -3,10 +3,13 @@ import { gql, useMutation } from 'urql' import { fieldErrors, nonFieldErrors } from '../../util/errutil' import FormDialog from '../../dialogs/FormDialog' import AdminAPIKeyForm from './AdminAPIKeyForm' -import { CreatedGQLAPIKey, CreateGQLAPIKeyInput } from '../../../schema' -import AdminAPIKeysTokenDialog from './AdminAPIKeyTokenDialog' -import Spinner from '../../loading/components/Spinner' +import { CreateGQLAPIKeyInput } from '../../../schema' +import { CheckCircleOutline as SuccessIcon } from '@mui/icons-material' +import makeStyles from '@mui/styles/makeStyles' + import { DateTime } from 'luxon' +import { Grid, Typography, FormHelperText } from '@mui/material' +import CopyText from '../../util/CopyText' // query for creating new api key which accepts CreateGQLAPIKeyInput param // return token created upon successfull transaction const newGQLAPIKeyQuery = gql` @@ -18,84 +21,94 @@ const newGQLAPIKeyQuery = gql` } ` +const useStyles = makeStyles(() => ({ + successIcon: { + marginRight: 8, // TODO: ts definitions are wrong, should be: theme.spacing(1), + }, + successTitle: { + display: 'flex', + alignItems: 'center', + }, +})) + +function AdminAPIKeyToken(props: { token: string }): React.ReactNode { + return ( + + + + + + Please copy and save the token as this is the only time you'll be able + to view it. + + + ) +} + export default function AdminAPIKeyCreateDialog(props: { - onClose: (param: boolean) => void -}): JSX.Element { - const { onClose } = props - const [apiKey, setAPIKey] = useState({ + onClose: () => void +}): React.ReactNode { + const classes = useStyles() + const [value, setValue] = useState({ name: '', description: '', expiresAt: DateTime.utc().plus({ days: 7 }).toISO(), allowedFields: [], role: 'user', }) - const [apiKeyActionStatus, apiKeyAction] = useMutation(newGQLAPIKeyQuery) - const { fetching, data, error } = apiKeyActionStatus - const [tokenDialogClose, onTokenDialogClose] = useState(true) - const [token, setToken] = useState({} as CreatedGQLAPIKey) - let fieldErrs = fieldErrors(error) + const [status, createKey] = useMutation(newGQLAPIKeyQuery) + const token = status.data?.createGQLAPIKey?.token || null + // handles form on submit event, based on the action type (edit, create) it will send the necessary type of parameter // token is also being set here when create action is used const handleOnSubmit = (): void => { - apiKeyAction( + createKey( { input: { - name: apiKey.name, - description: apiKey.description, - allowedFields: apiKey.allowedFields, - expiresAt: apiKey.expiresAt, - role: apiKey.role, + name: value.name, + description: value.description, + allowedFields: value.allowedFields, + expiresAt: value.expiresAt, + role: value.role, }, }, { additionalTypenames: ['GQLAPIKey'] }, - ).then((result) => { - if (!result.error) { - setToken(result.data.createGQLAPIKey) - onTokenDialogClose(false) - } - }) - } - - if (fetching && !data) { - return - } - - if (error) { - fieldErrs = fieldErrs.map((err) => { - return err - }) + ) } return ( - - {tokenDialogClose ? ( - { - props.onClose(false) - }} - onSubmit={handleOnSubmit} - disableBackdropClose - form={ - - } - /> - ) : ( - { - onTokenDialogClose(true) - onClose(false) - }} - /> - )} - + + + Success! +
+ ) : ( + 'Create New API Key' + ) + } + subTitle={token ? 'Your API key has been created!' : ''} + loading={status.fetching} + errors={nonFieldErrors(status.error)} + onClose={() => { + props.onClose() + }} + onSubmit={token ? props.onClose : handleOnSubmit} + alert={!!token} + disableBackdropClose={!!token} + form={ + token ? ( + + ) : ( + + ) + } + /> ) } From 52576b825060a8773ab2b2a197ace93a5c835a4f Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 11 Oct 2023 14:30:06 -0500 Subject: [PATCH 45/56] remove unused dialogs --- .../admin-api-keys/AdminAPIKeyTokenDialog.tsx | 85 ------------- .../AdminAPIKeysActionDialog.tsx | 112 ------------------ 2 files changed, 197 deletions(-) delete mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeyTokenDialog.tsx delete mode 100644 web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyTokenDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyTokenDialog.tsx deleted file mode 100644 index abb987277c..0000000000 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyTokenDialog.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable prettier/prettier */ -import React from 'react' -import { CreatedGQLAPIKey } from '../../../schema' -import Dialog from '@mui/material/Dialog' -import DialogActions from '@mui/material/DialogActions' -import DialogContent from '@mui/material/DialogContent' -import DialogContentText from '@mui/material/DialogContentText' -import DialogTitle from '@mui/material/DialogTitle' -import { Typography } from '@mui/material' -import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import Grid from '@mui/material/Grid' -import IconButton from '@mui/material/IconButton' -import CloseIcon from '@mui/icons-material/Close' - -export default function AdminAPIKeyTokenDialog(props: { - value: CreatedGQLAPIKey - onClose: () => void -}): JSX.Element { - const {onClose, value} = props - const onClickCopy = (): void => { - navigator.clipboard.writeText(props.value.token) - } - // handles onclose dialog for the token dialog, rejects close for backdropclick or escapekeydown actions - const onCloseDialog = ( - event: object, - reason: string, - ): boolean | undefined => { - if (reason === 'backdropClick' || reason === 'escapeKeyDown') { - return false - } - - onClose() - } - // handles close dialog button action - const onCloseDialogByButton = (): void => { - onClose() - } - - return ( - - - - - API Key Token - - - (Please copy and save the token as this is the only time you'll - be able to view it.) - - - - - - - - - - - - - - {value.token} - - - - - - - - - - ) -} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx deleted file mode 100644 index a577f0ca1d..0000000000 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeysActionDialog.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useState } from 'react' -import { gql, useMutation } from 'urql' -import { fieldErrors, nonFieldErrors } from '../../util/errutil' -import FormDialog from '../../dialogs/FormDialog' -import AdminAPIKeyForm from './AdminAPIKeyForm' -import { CreatedGQLAPIKey, GQLAPIKey } from '../../../schema' -import Spinner from '../../loading/components/Spinner' - -// query for creating new api key which accepts CreateGQLAPIKeyInput param -// return token created upon successfull transaction -const newGQLAPIKeyQuery = gql` - mutation CreateGQLAPIKey($input: CreateGQLAPIKeyInput!) { - createGQLAPIKey(input: $input) { - id - token - } - } -` -// query for updating api key which accepts UpdateGQLAPIKeyInput -const updateGQLAPIKeyQuery = gql` - mutation UpdateGQLAPIKey($input: UpdateGQLAPIKeyInput!) { - updateGQLAPIKey(input: $input) - } -` - -export default function AdminAPIKeysActionDialog(props: { - onClose: (param: boolean) => void - setToken: (token: CreatedGQLAPIKey) => void - onTokenDialogClose: (prama: boolean) => void - create: boolean - apiKey: GQLAPIKey - setAPIKey: (param: GQLAPIKey) => void - setSelectedAPIKey: (param: GQLAPIKey) => void -}): JSX.Element { - const { create, apiKey, setAPIKey, setSelectedAPIKey } = props - const query = props.create ? newGQLAPIKeyQuery : updateGQLAPIKeyQuery - const [apiKeyActionStatus, apiKeyAction] = useMutation(query) - const [reqAllowFieldsFlag, setReqAllowFieldsFlag] = useState(true) - const { fetching, data, error } = apiKeyActionStatus - let fieldErrs = fieldErrors(error) - // handles form on submit event, based on the action type (edit, create) it will send the necessary type of parameter - // token is also being set here when create action is used - const handleOnSubmit = (): void => { - const updateKey = { - name: apiKey.name, - description: apiKey.description, - id: apiKey.id, - } - - const createKey = { - name: apiKey.name, - description: apiKey.description, - allowedFields: apiKey.allowedFields, - expiresAt: apiKey.expiresAt, - role: apiKey.role, - } - - apiKeyAction( - { - input: create ? createKey : updateKey, - }, - { additionalTypenames: ['GQLAPIKey'] }, - ).then((result) => { - setReqAllowFieldsFlag(apiKey.allowedFields.length <= 0) - - if (!result.error) { - props.onClose(false) - - if (props.create) { - props.setToken(result.data.createGQLAPIKey) - props.onTokenDialogClose(true) - } else { - setSelectedAPIKey(apiKey) - } - } - }) - } - - if (fetching && !data) { - return - } - - if (error) { - fieldErrs = fieldErrs.map((err) => { - return err - }) - } - - return ( - { - props.onClose(false) - }} - onSubmit={handleOnSubmit} - disableBackdropClose - form={ - - } - /> - ) -} From f61cc06afec3003a5dd6933cd25c61bb7d3ad1a6 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 11 Oct 2023 14:32:29 -0500 Subject: [PATCH 46/56] fix clickaway when selecting different keys --- web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx index 6cac8043f7..85dad8f928 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx @@ -99,7 +99,7 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element { return ( - + Date: Wed, 11 Oct 2023 15:04:25 -0500 Subject: [PATCH 47/56] fix linting issues --- .../admin-api-keys/AdminAPIKeyEditDialog.tsx | 4 ++-- .../admin/admin-api-keys/AdminAPIKeyForm.tsx | 18 ++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx index 13e21066e7..c742e7d30a 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx @@ -1,9 +1,9 @@ -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { gql, useMutation, useQuery } from 'urql' import { fieldErrors, nonFieldErrors } from '../../util/errutil' import FormDialog from '../../dialogs/FormDialog' import AdminAPIKeyForm from './AdminAPIKeyForm' -import { GQLAPIKey, UpdateGQLAPIKeyInput } from '../../../schema' +import { GQLAPIKey } from '../../../schema' import Spinner from '../../loading/components/Spinner' import { GenericError } from '../../error-pages' diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx index 3171709189..e319d812fa 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyForm.tsx @@ -2,7 +2,7 @@ import React from 'react' import Grid from '@mui/material/Grid' import { FormContainer, FormField } from '../../forms' import { FieldError } from '../../util/errutil' -import { CreateGQLAPIKeyInput, UpdateGQLAPIKeyInput } from '../../../schema' +import { CreateGQLAPIKeyInput } from '../../../schema' import AdminAPIKeyExpirationField from './AdminAPIKeyExpirationField' import { gql, useQuery } from 'urql' import { GenericError } from '../../error-pages' @@ -16,21 +16,15 @@ const query = gql` } ` -type EditProps = { - value: UpdateGQLAPIKeyInput - onChange: (key: UpdateGQLAPIKeyInput) => void - create?: false -} +type AdminAPIKeyFormProps = { + errors: FieldError[] -type CreateProps = { + // even while editing, we need all the fields value: CreateGQLAPIKeyInput onChange: (key: CreateGQLAPIKeyInput) => void - create: true -} -type AdminAPIKeyFormProps = { - errors: FieldError[] -} & (EditProps | CreateProps) + create?: boolean +} export default function AdminAPIKeyForm( props: AdminAPIKeyFormProps, From 8c30138152ae7d7401ef7f1e2ed359f3d62bcd7e Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 11 Oct 2023 15:07:44 -0500 Subject: [PATCH 48/56] fix type issue --- web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx index c742e7d30a..b78f62fdc6 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx @@ -3,7 +3,7 @@ import { gql, useMutation, useQuery } from 'urql' import { fieldErrors, nonFieldErrors } from '../../util/errutil' import FormDialog from '../../dialogs/FormDialog' import AdminAPIKeyForm from './AdminAPIKeyForm' -import { GQLAPIKey } from '../../../schema' +import { CreateGQLAPIKeyInput, GQLAPIKey } from '../../../schema' import Spinner from '../../loading/components/Spinner' import { GenericError } from '../../error-pages' @@ -37,7 +37,9 @@ export default function AdminAPIKeyEditDialog(props: { const key: GQLAPIKey | null = data?.gqlAPIKeys?.find((d: GQLAPIKey) => d.id === apiKeyId) || null const [apiKeyActionStatus, apiKeyAction] = useMutation(updateGQLAPIKeyQuery) - const [apiKeyInput, setAPIKeyInput] = useState(null) + const [apiKeyInput, setAPIKeyInput] = useState( + null, + ) if (fetching && !data) return if (error) return From 46f75e4cbbee042f9f0c301b3ec8638f4dbc9e3d Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 16 Oct 2023 10:06:41 -0500 Subject: [PATCH 49/56] fix drawer flicker --- .../admin-api-keys/AdminAPIKeyDrawer.tsx | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx index 85dad8f928..51f779e034 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { ClickAwayListener, Divider, @@ -70,24 +70,14 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element { const isOpen = Boolean(apiKeyId) const [deleteDialog, onDeleteDialog] = useState(false) const [editDialog, onEditDialog] = useState(false) - const [apiKey, setAPIKey] = useState({} as GQLAPIKey) + // Get API Key triggers/actions const [{ data, fetching, error }] = useQuery({ query }) - let allowFieldsStr: string[] = [] - let comma = '' - - useEffect(() => { - const dataInfo = data?.gqlAPIKeys?.find((d: GQLAPIKey) => { + const apiKey = + data?.gqlAPIKeys?.find((d: GQLAPIKey) => { return d.id === apiKeyId - }) - // retrieve apiKey information by id - setAPIKey(dataInfo) - allowFieldsStr = dataInfo?.allowedFields.map((inp: string): string => { - const inpComma = comma + inp - comma = ', ' - return inpComma - }) - }, [data]) + }) || ({} as GQLAPIKey) + const allowFieldsStr = (apiKey?.allowedFields || []).join(', ') if (error) { return From 04746309d544b1106bdd2a6722a3a56abb022ed0 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 16 Oct 2023 10:08:27 -0500 Subject: [PATCH 50/56] fix read-only label --- web/src/app/admin/AdminAPIKeys.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index 5d96657c63..cfc93b9435 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -91,7 +91,11 @@ export default function AdminAPIKeys(): JSX.Element {
From 581f2ae0317482e29c068ab8f528d154599384aa Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 17 Oct 2023 13:25:36 -0700 Subject: [PATCH 51/56] update ui for consistency across components --- web/src/app/admin/AdminAPIKeys.tsx | 94 ++++++++----------- .../AdminAPIKeyCreateDialog.tsx | 25 ++--- web/src/app/dialogs/FormDialog.js | 34 ++++--- web/src/app/lists/FlatList.tsx | 1 + web/src/app/lists/FlatListItem.tsx | 13 ++- web/src/app/styles/materialStyles.ts | 5 - 6 files changed, 81 insertions(+), 91 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index cfc93b9435..ba0c72b2a3 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -1,13 +1,6 @@ import React, { useState } from 'react' import makeStyles from '@mui/styles/makeStyles' -import { - Button, - Grid, - Typography, - Card, - ButtonBase, - CardHeader, -} from '@mui/material' +import { Button, Grid, Typography, Card } from '@mui/material' import { Add } from '@mui/icons-material' import AdminAPIKeysDrawer from './admin-api-keys/AdminAPIKeyDrawer' import { GQLAPIKey } from '../../schema' @@ -18,7 +11,7 @@ import Spinner from '../loading/components/Spinner' import { GenericError } from '../error-pages' import { Theme } from '@mui/material/styles' import AdminAPIKeyCreateDialog from './admin-api-keys/AdminAPIKeyCreateDialog' -// query for getting existing API Keys + const query = gql` query gqlAPIKeysQuery { gqlAPIKeys { @@ -34,6 +27,7 @@ const query = gql` } } ` + const useStyles = makeStyles((theme: Theme) => ({ buttons: { 'margin-bottom': '15px', @@ -56,10 +50,12 @@ export default function AdminAPIKeys(): JSX.Element { const classes = useStyles() const [selectedAPIKey, setSelectedAPIKey] = useState(null) const [createAPIKeyDialogClose, onCreateAPIKeyDialogClose] = useState(false) + // handles the openning of the create dialog form which is used for creating new API Key const handleOpenCreateDialog = (): void => { onCreateAPIKeyDialogClose(!createAPIKeyDialogClose) } + // Get API Key triggers/actions const [{ data, fetching, error }] = useQuery({ query }) @@ -75,37 +71,32 @@ export default function AdminAPIKeys(): JSX.Element { (key: GQLAPIKey): FlatListListItem => ({ selected: (key as GQLAPIKey).id === selectedAPIKey?.id, highlight: (key as GQLAPIKey).id === selectedAPIKey?.id, + primaryText: {key.name}, + disableTypography: true, subText: ( - { - setSelectedAPIKey(key) - }} - style={{ width: '100%', textAlign: 'left', padding: '5px 15px' }} - > - - - - {key.name} - - - - - {key.allowedFields.length + - ' allowed fields' + - (key.allowedFields.some((f) => f.startsWith('Mutation.')) - ? '' - : ' (read-only)')} - - - - - - + + + + + {key.allowedFields.length + + ' allowed fields' + + (key.allowedFields.some((f) => f.startsWith('Mutation.')) + ? '' + : ' (read-only)')} + + + ), + secondaryAction: ( + + + + - + ), + onClick: () => setSelectedAPIKey(key), }), ) @@ -126,29 +117,24 @@ export default function AdminAPIKeys(): JSX.Element { }} /> ) : null} +
+ +
- } - > - Create API Key - - } - /> - +
) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx index b574d608d3..47f44c335b 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx @@ -1,15 +1,14 @@ import React, { useState } from 'react' import { gql, useMutation } from 'urql' +import CopyText from '../../util/CopyText' import { fieldErrors, nonFieldErrors } from '../../util/errutil' import FormDialog from '../../dialogs/FormDialog' import AdminAPIKeyForm from './AdminAPIKeyForm' import { CreateGQLAPIKeyInput } from '../../../schema' import { CheckCircleOutline as SuccessIcon } from '@mui/icons-material' -import makeStyles from '@mui/styles/makeStyles' - import { DateTime } from 'luxon' import { Grid, Typography, FormHelperText } from '@mui/material' -import CopyText from '../../util/CopyText' + // query for creating new api key which accepts CreateGQLAPIKeyInput param // return token created upon successfull transaction const newGQLAPIKeyQuery = gql` @@ -21,16 +20,6 @@ const newGQLAPIKeyQuery = gql` } ` -const useStyles = makeStyles(() => ({ - successIcon: { - marginRight: 8, // TODO: ts definitions are wrong, should be: theme.spacing(1), - }, - successTitle: { - display: 'flex', - alignItems: 'center', - }, -})) - function AdminAPIKeyToken(props: { token: string }): React.ReactNode { return ( @@ -48,7 +37,6 @@ function AdminAPIKeyToken(props: { token: string }): React.ReactNode { export default function AdminAPIKeyCreateDialog(props: { onClose: () => void }): React.ReactNode { - const classes = useStyles() const [value, setValue] = useState({ name: '', description: '', @@ -80,8 +68,13 @@ export default function AdminAPIKeyCreateDialog(props: { - +
+ theme.spacing(1) }} /> Success!
) : ( diff --git a/web/src/app/dialogs/FormDialog.js b/web/src/app/dialogs/FormDialog.js index 5ead1f9ca7..cf1611c980 100644 --- a/web/src/app/dialogs/FormDialog.js +++ b/web/src/app/dialogs/FormDialog.js @@ -92,7 +92,24 @@ function FormDialog(props) { return null } - return
{form}
+ return ( + +
{ + e.preventDefault() + if (valid) { + onNext ? onNext() : onSubmit() + } + }} + > + +
{form}
+
+
+
+ ) } function renderCaption() { @@ -186,20 +203,7 @@ function FormDialog(props) { title={title} subTitle={subTitle} /> - -
{ - e.preventDefault() - if (valid) { - onNext ? onNext() : onSubmit() - } - }} - > - {renderForm()} -
-
+ {renderForm()} {renderCaption()} {renderErrors()} {renderActions()} diff --git a/web/src/app/lists/FlatList.tsx b/web/src/app/lists/FlatList.tsx index eef32565bd..f000eb6117 100644 --- a/web/src/app/lists/FlatList.tsx +++ b/web/src/app/lists/FlatList.tsx @@ -101,6 +101,7 @@ export interface FlatListNotice extends Notice { } export interface FlatListItem extends ListItemProps { title?: string + primaryText?: React.ReactNode highlight?: boolean subText?: JSX.Element | string icon?: JSX.Element | null diff --git a/web/src/app/lists/FlatListItem.tsx b/web/src/app/lists/FlatListItem.tsx index e223da20ea..53672a7a32 100644 --- a/web/src/app/lists/FlatListItem.tsx +++ b/web/src/app/lists/FlatListItem.tsx @@ -45,6 +45,8 @@ export default function FlatListItem(props: FlatListItemProps): JSX.Element { draggable, disabled, disableTypography, + onClick, + primaryText, ...muiListItemProps } = props.item @@ -64,10 +66,19 @@ export default function FlatListItem(props: FlatListItemProps): JSX.Element { } } + const onClickProps = onClick && { + onClick, + + // NOTE: needed for error: button: false? not assignable to type 'true' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + button: true as any, + } + return ( {icon && {icon}} ({ paddingBottom: 0, margin: 0, }, - asLink: { - color: 'blue', - cursor: 'pointer', - textDecoration: 'underline', - }, block: { display: 'inline-block', }, From 32fcd06503089739beeac7914e6e45c23746eead Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 18 Oct 2023 10:54:46 -0700 Subject: [PATCH 52/56] call onsubmit for confirm dialog if no form prop --- web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx | 2 +- web/src/app/dialogs/FormDialog.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx index d127c9c13e..f5efd81f01 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx @@ -40,7 +40,7 @@ export default function AdminAPIKeyDeleteDialog(props: { return d.id === apiKeyId })?.name - const handleOnSubmit = (): void => { + function handleOnSubmit(): void { deleteAPIKey( { id: apiKeyId, diff --git a/web/src/app/dialogs/FormDialog.js b/web/src/app/dialogs/FormDialog.js index cf1611c980..58901b1732 100644 --- a/web/src/app/dialogs/FormDialog.js +++ b/web/src/app/dialogs/FormDialog.js @@ -161,6 +161,10 @@ function FormDialog(props) { if (!onNext) { setAttemptCount(attemptCount + 1) } + + if (!props.form) { + onSubmit() + } }} attemptCount={attemptCount} buttonText={primaryActionLabel || (confirm ? 'Confirm' : submitText)} From 2adc7c35158cb5ef95bc99a9ee037dd754f850ae Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 18 Oct 2023 11:16:03 -0700 Subject: [PATCH 53/56] updating padding on success --- web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx index 47f44c335b..85ac79f9ed 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx @@ -23,7 +23,7 @@ const newGQLAPIKeyQuery = gql` function AdminAPIKeyToken(props: { token: string }): React.ReactNode { return ( - + @@ -72,9 +72,10 @@ export default function AdminAPIKeyCreateDialog(props: { style={{ display: 'flex', alignItems: 'center', + marginTop: '8px', }} > - theme.spacing(1) }} /> + theme.spacing(1) }} /> Success! ) : ( From c66d9bc11a7bb506a85fae955bb46a9d6df02b47 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 18 Oct 2023 11:53:44 -0700 Subject: [PATCH 54/56] add edit and delete options to list --- web/src/app/admin/AdminAPIKeys.tsx | 50 ++++++++++++++++++- .../admin-api-keys/AdminAPIKeyDrawer.tsx | 16 +++--- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index ba0c72b2a3..f7b85d1938 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -11,6 +11,9 @@ import Spinner from '../loading/components/Spinner' import { GenericError } from '../error-pages' import { Theme } from '@mui/material/styles' import AdminAPIKeyCreateDialog from './admin-api-keys/AdminAPIKeyCreateDialog' +import AdminAPIKeyDeleteDialog from './admin-api-keys/AdminAPIKeyDeleteDialog' +import AdminAPIKeyEditDialog from './admin-api-keys/AdminAPIKeyEditDialog' +import OtherActions from '../util/OtherActions' const query = gql` query gqlAPIKeysQuery { @@ -50,6 +53,8 @@ export default function AdminAPIKeys(): JSX.Element { const classes = useStyles() const [selectedAPIKey, setSelectedAPIKey] = useState(null) const [createAPIKeyDialogClose, onCreateAPIKeyDialogClose] = useState(false) + const [editDialog, setEditDialog] = useState() + const [deleteDialog, setDeleteDialog] = useState() // handles the openning of the create dialog form which is used for creating new API Key const handleOpenCreateDialog = (): void => { @@ -89,11 +94,38 @@ export default function AdminAPIKeys(): JSX.Element { ), secondaryAction: ( - - + + + + setEditDialog(key.id), + }, + { + label: 'Delete', + onClick: () => setDeleteDialog(key.id), + }, + ]} + /> + ), onClick: () => setSelectedAPIKey(key), @@ -117,6 +149,20 @@ export default function AdminAPIKeys(): JSX.Element { }} /> ) : null} + {deleteDialog ? ( + { + setDeleteDialog('') + }} + apiKeyId={deleteDialog} + /> + ) : null} + {editDialog ? ( + setEditDialog('')} + apiKeyId={editDialog} + /> + ) : null}
- From 58bc3d3a7be8b9839775a98db40d488ffdb5289b Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 18 Oct 2023 11:59:18 -0700 Subject: [PATCH 55/56] fix type --- .../admin-api-keys/AdminAPIKeyDrawer.tsx | 177 +++++++++--------- 1 file changed, 88 insertions(+), 89 deletions(-) diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx index ea26283d23..7728f441ca 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx @@ -73,10 +73,11 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element { // Get API Key triggers/actions const [{ data, fetching, error }] = useQuery({ query }) - const apiKey = + const apiKey: GQLAPIKey = data?.gqlAPIKeys?.find((d: GQLAPIKey) => { return d.id === apiKeyId }) || ({} as GQLAPIKey) + const allowFieldsStr = (apiKey?.allowedFields || []).join(', ') if (error) { @@ -88,95 +89,93 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element { } return ( - - - - - {deleteDialog ? ( - { - setDialogDialog(false) + + + + {deleteDialog ? ( + { + setDialogDialog(false) - if (yes) { - onClose() - } - }} - apiKeyId={apiKey.id} - /> - ) : null} - {editDialog ? ( - setEditDialog(false)} - apiKeyId={apiKey.id} - /> - ) : null} - - - API Key Details - - - - - - - - - - - - - - } - /> - - - - - - } - /> - - - - - - - - - - - - - - + if (yes) { + onClose() + } + }} + apiKeyId={apiKey.id} + /> + ) : null} + {editDialog ? ( + setEditDialog(false)} + apiKeyId={apiKey.id} + /> + ) : null} + + + API Key Details + + + + + + + + + + + + + + } + /> + + + + + + } + /> + + + + + + + + + + + + + - - - + + + ) } From 22893f5e12447808c5728c36bc96c94b516d9b86 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 18 Oct 2023 13:49:17 -0700 Subject: [PATCH 56/56] update prop names, fix transition on create button --- web/src/app/admin/AdminAPIKeys.tsx | 49 +++++++++---------- .../AdminAPIKeyDeleteDialog.tsx | 8 +-- .../admin-api-keys/AdminAPIKeyDrawer.tsx | 12 ++--- .../admin-api-keys/AdminAPIKeyEditDialog.tsx | 8 +-- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index f7b85d1938..e99b32b8c7 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -134,14 +134,12 @@ export default function AdminAPIKeys(): JSX.Element { return ( - {selectedAPIKey ? ( - { - setSelectedAPIKey(null) - }} - apiKeyId={selectedAPIKey.id} - /> - ) : null} + { + setSelectedAPIKey(null) + }} + apiKeyID={selectedAPIKey?.id} + /> {createAPIKeyDialogClose ? ( { @@ -154,34 +152,35 @@ export default function AdminAPIKeys(): JSX.Element { onClose={(): void => { setDeleteDialog('') }} - apiKeyId={deleteDialog} + apiKeyID={deleteDialog} /> ) : null} {editDialog ? ( setEditDialog('')} - apiKeyId={editDialog} + apiKeyID={editDialog} /> ) : null} -
- -
- - - +
+ +
+ + + +
) } diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx index f5efd81f01..ae8f70219a 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyDeleteDialog.tsx @@ -24,26 +24,26 @@ const query = gql` ` export default function AdminAPIKeyDeleteDialog(props: { - apiKeyId: string + apiKeyID: string onClose: (yes: boolean) => void }): JSX.Element { const [{ fetching, data, error }] = useQuery({ query, }) - const { apiKeyId, onClose } = props + const { apiKeyID, onClose } = props const [deleteAPIKeyStatus, deleteAPIKey] = useMutation(deleteGQLAPIKeyQuery) if (fetching && !data) return if (error) return const apiKeyName = data?.gqlAPIKeys?.find((d: GQLAPIKey) => { - return d.id === apiKeyId + return d.id === apiKeyID })?.name function handleOnSubmit(): void { deleteAPIKey( { - id: apiKeyId, + id: apiKeyID, }, { additionalTypenames: ['GQLAPIKey'] }, ).then((result) => { diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx index 7728f441ca..02a6f01afc 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx @@ -53,7 +53,7 @@ const query = gql` // property for this object interface Props { onClose: () => void - apiKeyId: string + apiKeyID?: string } const useStyles = makeStyles(() => ({ @@ -65,9 +65,9 @@ const useStyles = makeStyles(() => ({ })) export default function AdminAPIKeyDrawer(props: Props): JSX.Element { - const { onClose, apiKeyId } = props + const { onClose, apiKeyID } = props const classes = useStyles() - const isOpen = Boolean(apiKeyId) + const isOpen = Boolean(apiKeyID) const [deleteDialog, setDialogDialog] = useState(false) const [editDialog, setEditDialog] = useState(false) @@ -75,7 +75,7 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element { const [{ data, fetching, error }] = useQuery({ query }) const apiKey: GQLAPIKey = data?.gqlAPIKeys?.find((d: GQLAPIKey) => { - return d.id === apiKeyId + return d.id === apiKeyID }) || ({} as GQLAPIKey) const allowFieldsStr = (apiKey?.allowedFields || []).join(', ') @@ -106,13 +106,13 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element { onClose() } }} - apiKeyId={apiKey.id} + apiKeyID={apiKey.id} /> ) : null} {editDialog ? ( setEditDialog(false)} - apiKeyId={apiKey.id} + apiKeyID={apiKey.id} /> ) : null} diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx index b78f62fdc6..74f118a141 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyEditDialog.tsx @@ -28,14 +28,14 @@ const query = gql` ` export default function AdminAPIKeyEditDialog(props: { onClose: (param: boolean) => void - apiKeyId: string + apiKeyID: string }): JSX.Element { - const { apiKeyId, onClose } = props + const { apiKeyID, onClose } = props const [{ fetching, data, error }] = useQuery({ query, }) const key: GQLAPIKey | null = - data?.gqlAPIKeys?.find((d: GQLAPIKey) => d.id === apiKeyId) || null + data?.gqlAPIKeys?.find((d: GQLAPIKey) => d.id === apiKeyID) || null const [apiKeyActionStatus, apiKeyAction] = useMutation(updateGQLAPIKeyQuery) const [apiKeyInput, setAPIKeyInput] = useState( null, @@ -51,7 +51,7 @@ export default function AdminAPIKeyEditDialog(props: { input: { name: apiKeyInput?.name, description: apiKeyInput?.description, - id: apiKeyId, + id: apiKeyID, }, }, { additionalTypenames: ['GQLAPIKey'] },