From 85186a74c86fd35a1efbffea8af4733624da11bb Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 20 Apr 2025 19:09:13 +0200 Subject: [PATCH] Add apikeys routes --- auth/apikey.go | 98 +++++++++++++++++++- auth/dbc/apikeys.sql.go | 103 ++++++++++++++++++++++ auth/dbc/models.go | 11 +++ auth/main.go | 4 + auth/sql/migrations/000003_apikeys.up.sql | 1 + auth/sql/queries/apikeys.sql | 20 +++++ auth/sqlc.yaml | 5 ++ auth/users.go | 4 +- scanner/scanner/scanner.py | 1 - 9 files changed, 240 insertions(+), 7 deletions(-) create mode 100644 auth/dbc/apikeys.sql.go create mode 100644 auth/sql/queries/apikeys.sql diff --git a/auth/apikey.go b/auth/apikey.go index c73375e4..b4e0a44d 100644 --- a/auth/apikey.go +++ b/auth/apikey.go @@ -1,26 +1,50 @@ package main import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" "net/http" "time" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5" "github.com/labstack/echo/v4" + "github.com/zoriya/kyoo/keibi/dbc" ) type ApiKey struct { - Name string `json:"name" example:"my-app"` - Token string `json:"token" example:"lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="` + Name string `json:"name" example:"myapp"` CreatedAt time.Time `json:"createAt" example:"2025-03-29T18:20:05.267Z"` LastUsed time.Time `json:"lastUsed" example:"2025-03-29T18:20:05.267Z"` Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"` } +type ApiKeyWToken struct { + ApiKey + Token string `json:"token" example:"myapp-lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="` +} + type ApiKeyDto struct { Name string `json:"name" example:"my-app" validate:"alpha"` Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"` } +func MapDbKey(key *dbc.Apikey) ApiKeyWToken { + return ApiKeyWToken{ + ApiKey: ApiKey{ + Name: key.Name, + Claims: key.Claims, + CreatedAt: key.CreatedAt, + LastUsed: key.LastUsed, + }, + Token: key.Token, + } +} + // @Summary Create API key // @Description Create a new API key // @Tags apikeys @@ -28,10 +52,10 @@ type ApiKeyDto struct { // @Produce json // @Security Jwt[apikeys.write] // @Param key body ApiKeyDto false "Api key info" -// @Success 201 {object} ApiKey +// @Success 201 {object} ApiKeyWToken // @Failure 409 {object} KError "Duplicated api key" // @Failure 422 {object} KError "Invalid create body" -// @Router /users [get] +// @Router /keys [post] func (h *Handler) CreateApiKey(c echo.Context) error { var req ApiKeyDto err := c.Bind(&req) @@ -41,4 +65,70 @@ func (h *Handler) CreateApiKey(c echo.Context) error { if err = c.Validate(&req); err != nil { return err } + + id := make([]byte, 64) + _, err = rand.Read(id) + if err != nil { + return err + } + + dbkey, err := h.db.CreateApiKey(context.Background(), dbc.CreateApiKeyParams{ + Name: req.Name, + Token: fmt.Sprintf("%s-%s", req.Name, base64.RawURLEncoding.EncodeToString(id)), + Claims: req.Claims, + }) + if ErrIs(err, pgerrcode.UniqueViolation) { + return echo.NewHTTPError(409, "An apikey with the same name already exists.") + } else if err != nil { + return err + } + return c.JSON(201, MapDbKey(&dbkey)) +} + +// @Summary Delete API key +// @Description Delete an existing API key +// @Tags apikeys +// @Accept json +// @Produce json +// @Security Jwt[apikeys.write] +// @Success 200 {object} ApiKey +// @Failure 404 {object} KError "Invalid id" +// @Failure 422 {object} KError "Invalid id format" +// @Router /keys [delete] +func (h *Handler) DeleteApiKey(c echo.Context) error { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(422, "Invalid id given: not an uuid") + } + + dbkey, err := h.db.DeleteApiKey(context.Background(), id) + if err == pgx.ErrNoRows { + return echo.NewHTTPError(404, "No apikey found") + } else if err != nil { + return err + } + return c.JSON(200, MapDbKey(&dbkey).ApiKey) +} + +// @Summary List API keys +// @Description List all api keys +// @Tags apikeys +// @Accept json +// @Produce json +// @Security Jwt[apikeys.read] +// @Success 200 {object} Page[ApiKey] +// @Router /keys [get] +func (h *Handler) ListApiKey(c echo.Context) error { + dbkeys, err := h.db.ListApiKeys(context.Background()) + if err != nil { + return err + } + var ret []ApiKey + for _, key := range dbkeys { + ret = append(ret, MapDbKey(&key).ApiKey) + } + return c.JSON(200, Page[ApiKey]{ + Items: ret, + This: c.Request().URL.String(), + }) } diff --git a/auth/dbc/apikeys.sql.go b/auth/dbc/apikeys.sql.go new file mode 100644 index 00000000..26b63e04 --- /dev/null +++ b/auth/dbc/apikeys.sql.go @@ -0,0 +1,103 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: apikeys.sql + +package dbc + +import ( + "context" + + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +const createApiKey = `-- name: CreateApiKey :one +insert into apikeys(name, token, claims) + values ($1, $2, $3) +returning + pk, id, name, token, claims, created_by, created_at, last_used +` + +type CreateApiKeyParams struct { + Name string `json:"name"` + Token string `json:"token"` + Claims jwt.MapClaims `json:"claims"` +} + +func (q *Queries) CreateApiKey(ctx context.Context, arg CreateApiKeyParams) (Apikey, error) { + row := q.db.QueryRow(ctx, createApiKey, arg.Name, arg.Token, arg.Claims) + var i Apikey + err := row.Scan( + &i.Pk, + &i.Id, + &i.Name, + &i.Token, + &i.Claims, + &i.CreatedBy, + &i.CreatedAt, + &i.LastUsed, + ) + return i, err +} + +const deleteApiKey = `-- name: DeleteApiKey :one +delete from apikeys +where id = $1 +returning + pk, id, name, token, claims, created_by, created_at, last_used +` + +func (q *Queries) DeleteApiKey(ctx context.Context, id uuid.UUID) (Apikey, error) { + row := q.db.QueryRow(ctx, deleteApiKey, id) + var i Apikey + err := row.Scan( + &i.Pk, + &i.Id, + &i.Name, + &i.Token, + &i.Claims, + &i.CreatedBy, + &i.CreatedAt, + &i.LastUsed, + ) + return i, err +} + +const listApiKeys = `-- name: ListApiKeys :many +select + pk, id, name, token, claims, created_by, created_at, last_used +from + apikeys +order by + last_used +` + +func (q *Queries) ListApiKeys(ctx context.Context) ([]Apikey, error) { + rows, err := q.db.Query(ctx, listApiKeys) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Apikey + for rows.Next() { + var i Apikey + if err := rows.Scan( + &i.Pk, + &i.Id, + &i.Name, + &i.Token, + &i.Claims, + &i.CreatedBy, + &i.CreatedAt, + &i.LastUsed, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/auth/dbc/models.go b/auth/dbc/models.go index 2310f424..b64b96aa 100644 --- a/auth/dbc/models.go +++ b/auth/dbc/models.go @@ -11,6 +11,17 @@ import ( "github.com/google/uuid" ) +type Apikey struct { + Pk int32 `json:"pk"` + Id uuid.UUID `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + Claims jwt.MapClaims `json:"claims"` + CreatedBy int32 `json:"createdBy"` + CreatedAt time.Time `json:"createdAt"` + LastUsed time.Time `json:"lastUsed"` +} + type OidcHandle struct { UserPk int32 `json:"userPk"` Provider string `json:"provider"` diff --git a/auth/main.go b/auth/main.go index ab76c2a0..20dbea8c 100644 --- a/auth/main.go +++ b/auth/main.go @@ -224,6 +224,10 @@ func main() { r.DELETE("/sessions", h.Logout) r.DELETE("/sessions/:id", h.Logout) + r.GET("/keys", h.ListApiKey) + r.POST("/keys", h.CreateApiKey) + r.DELETE("/keys", h.DeleteApiKey) + g.GET("/jwt", h.CreateJwt) e.GET("/.well-known/jwks.json", h.GetJwks) e.GET("/.well-known/openid-configuration", h.GetOidcConfig) diff --git a/auth/sql/migrations/000003_apikeys.up.sql b/auth/sql/migrations/000003_apikeys.up.sql index cb333485..8d1f0a17 100644 --- a/auth/sql/migrations/000003_apikeys.up.sql +++ b/auth/sql/migrations/000003_apikeys.up.sql @@ -7,6 +7,7 @@ create table apikeys( token varchar(128) not null unique, claims jsonb not null, + created_by integer not null references users(pk) on delete cascade, created_at timestamptz not null default now()::timestamptz, last_used timestamptz not null default now()::temistamptz ); diff --git a/auth/sql/queries/apikeys.sql b/auth/sql/queries/apikeys.sql new file mode 100644 index 00000000..fb02171e --- /dev/null +++ b/auth/sql/queries/apikeys.sql @@ -0,0 +1,20 @@ +-- name: ListApiKeys :many +select + * +from + apikeys +order by + last_used; + +-- name: CreateApiKey :one +insert into apikeys(name, token, claims) + values ($1, $2, $3) +returning + *; + +-- name: DeleteApiKey :one +delete from apikeys +where id = $1 +returning + *; + diff --git a/auth/sqlc.yaml b/auth/sqlc.yaml index 2c48d06e..638f61b5 100644 --- a/auth/sqlc.yaml +++ b/auth/sqlc.yaml @@ -35,5 +35,10 @@ sql: import: "github.com/golang-jwt/jwt/v5" package: "jwt" type: "MapClaims" + - column: "apikeys.claims" + go_type: + import: "github.com/golang-jwt/jwt/v5" + package: "jwt" + type: "MapClaims" diff --git a/auth/users.go b/auth/users.go index 7aef41e4..24406478 100644 --- a/auth/users.go +++ b/auth/users.go @@ -250,8 +250,8 @@ func (h *Handler) Register(c echo.Context) error { // @Security Jwt[users.delete] // @Param id path string false "User id of the user to delete" Format(uuid) // @Success 200 {object} User -// @Failure 404 {object} KError "Invalid id format" // @Failure 404 {object} KError "Invalid user id" +// @Failure 422 {object} KError "Invalid id format" // @Router /users/{id} [delete] func (h *Handler) DeleteUser(c echo.Context) error { err := CheckPermissions(c, []string{"users.delete"}) @@ -261,7 +261,7 @@ func (h *Handler) DeleteUser(c echo.Context) error { uid, err := uuid.Parse(c.Param("id")) if err != nil { - return echo.NewHTTPError(400, "Invalid id given: not an uuid") + return echo.NewHTTPError(422, "Invalid id given: not an uuid") } ret, err := h.db.DeleteUser(context.Background(), uid) diff --git a/scanner/scanner/scanner.py b/scanner/scanner/scanner.py index a5209d93..9fceac58 100644 --- a/scanner/scanner/scanner.py +++ b/scanner/scanner/scanner.py @@ -11,7 +11,6 @@ logger = getLogger(__name__) def get_ignore_pattern(): - """Compile ignore pattern from environment variable.""" try: pattern = os.environ.get("LIBRARY_IGNORE_PATTERN") return re.compile(pattern) if pattern else None