Skip to content

Commit

Permalink
apikey: add list and update of graphql api keys (#3292)
Browse files Browse the repository at this point in the history
* add list and update

* re-add missing field

* remove unused field

* remove unused func

* add comment about directives

* add comments describing goField options

* fix `updated_at`

* soft delete
  • Loading branch information
mastercactapus committed Sep 20, 2023
1 parent 4cad934 commit 6ca2658
Show file tree
Hide file tree
Showing 8 changed files with 1,849 additions and 149 deletions.
44 changes: 42 additions & 2 deletions apikey/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,36 @@
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_at = now(),
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 gql_api_keys
WHERE id = $1;
UPDATE
gql_api_keys
SET
deleted_at = now(),
deleted_by = $2
WHERE
id = $1;

-- name: APIKeyRecordUsage :exec
-- APIKeyRecordUsage records the usage of an API key.
Expand Down Expand Up @@ -35,3 +62,16 @@ WHERE
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
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
gql_api_keys.deleted_at IS NULL;

143 changes: 142 additions & 1 deletion apikey/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"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"
)
Expand All @@ -37,13 +38,153 @@ func NewStore(ctx context.Context, db *sql.DB, key keyring.Keyring) (*Store, err
return s, nil
}

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)
var byID uuid.NullUUID
if id, err := uuid.Parse(permission.UserID(ctx)); err == nil {
byID = uuid.NullUUID{UUID: id, Valid: true}
}

return gadb.New(s.db).APIKeyDelete(ctx, gadb.APIKeyDeleteParams{
DeletedBy: byID,
ID: id,
})
}

func (s *Store) AuthorizeGraphQL(ctx context.Context, tok, ua, ip string) (context.Context, error) {
Expand Down
140 changes: 136 additions & 4 deletions gadb/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 6ca2658

Please sign in to comment.