diff --git a/.env.example b/.env.example index 8735891b..a8829d6c 100644 --- a/.env.example +++ b/.env.example @@ -99,6 +99,6 @@ RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha # v5 stuff, does absolutely nothing on master (aka: you can delete this) EXTRA_CLAIMS='{"permissions": ["core.read"], "verified": false}' -FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "users.delete", "core.read"], "verified": true}' +FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write"], "verified": true}' GUEST_CLAIMS='{"permissions": ["core.read"]}' PROTECTED_CLAIMS="permissions,verified" diff --git a/.github/workflows/auth-hurl.yml b/.github/workflows/auth-hurl.yml index 061e3743..26675789 100644 --- a/.github/workflows/auth-hurl.yml +++ b/.github/workflows/auth-hurl.yml @@ -53,8 +53,12 @@ jobs: env: POSTGRES_SERVER: localhost FIRST_USER_CLAIMS: '{"permissions": ["users.read"]}' + KEIBI_APIKEY_HURL: 1234apikey + KEIBI_APIKEY_HURL_CLAIMS: '{"permissions": ["apikeys.write", "apikeys.read"]}' + - name: Show logs + if: failure() working-directory: ./auth run: cat logs diff --git a/auth/.env.example b/auth/.env.example index c1ea11eb..9e3f410f 100644 --- a/auth/.env.example +++ b/auth/.env.example @@ -22,6 +22,12 @@ PROTECTED_CLAIMS="permissions" # The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance. PUBLIC_URL=http://localhost:8901 +# You can create apikeys at runtime via POST /apikey but you can also have some defined in the env. +# Replace $YOURNAME with the name of the key you want (only alpha are valid) +# The value will be the apikey (max 128 bytes) +# KEIBI_APIKEY_$YOURNAME=oaeushtaoesunthoaensuth +# KEIBI_APIKEY_$YOURNAME_CLAIMS='{"permissions": ["users.read"]}' + # Database things POSTGRES_USER=kyoo POSTGRES_PASSWORD=password diff --git a/auth/README.md b/auth/README.md index 12391e4d..0b446249 100644 --- a/auth/README.md +++ b/auth/README.md @@ -61,11 +61,11 @@ GET `/users/$id/sessions` can be used by admins to list others session ``` Get `/apikeys` -Post `/apikeys` {...nlaims} Create a new api keys with given claims +Post `/apikeys` {...claims} Create a new api keys with given claims ``` An api key can be used like an opaque token, calling /jwt with it will return a valid jwt with the claims you specified during the post request to create it. -Creating an apikeys requires the `apikey.create` permission, reading them requires the `apikey.read` permission. +Creating an apikeys requires the `apikey.write` permission, reading them requires the `apikey.read` permission. ### OIDC diff --git a/auth/apikey.go b/auth/apikey.go new file mode 100644 index 00000000..2f7736d4 --- /dev/null +++ b/auth/apikey.go @@ -0,0 +1,218 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "maps" + "net/http" + "strings" + "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 { + Id uuid.UUID `json:"id" example:"e05089d6-9179-4b5b-a63e-94dd5fc2a397"` + 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:"myapp" validate:"alpha"` + Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"` +} + +func MapDbKey(key *dbc.Apikey) ApiKeyWToken { + return ApiKeyWToken{ + ApiKey: ApiKey{ + Id: key.Id, + Name: key.Name, + Claims: key.Claims, + CreatedAt: key.CreatedAt, + LastUsed: key.LastUsed, + }, + Token: fmt.Sprintf("%s-%s", key.Name, key.Token), + } +} + +// @Summary Create API key +// @Description Create a new API key +// @Tags apikeys +// @Accept json +// @Produce json +// @Security Jwt[apikeys.write] +// @Param key body ApiKeyDto false "Api key info" +// @Success 201 {object} ApiKeyWToken +// @Failure 409 {object} KError "Duplicated api key" +// @Failure 422 {object} KError "Invalid create body" +// @Router /keys [post] +func (h *Handler) CreateApiKey(c echo.Context) error { + err := CheckPermissions(c, []string{"apikeys.write"}) + if err != nil { + return err + } + + var req ApiKeyDto + err = c.Bind(&req) + if err != nil { + return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error()) + } + if err = c.Validate(&req); err != nil { + return err + } + + if _, conflict := h.config.EnvApiKeys[req.Name]; conflict { + return echo.NewHTTPError(409, "An env apikey is already defined with the same name") + } + + id := make([]byte, 64) + _, err = rand.Read(id) + if err != nil { + return err + } + + var user *int32 + uid, err := GetCurrentUserId(c) + // if err, we probably are using an api key (so no user) + if err != nil { + u, _ := h.db.GetUser(context.Background(), dbc.GetUserParams{ + UseId: true, + Id: uid, + }) + user = &u[0].User.Pk + } + + dbkey, err := h.db.CreateApiKey(context.Background(), dbc.CreateApiKeyParams{ + Name: req.Name, + Token: base64.RawURLEncoding.EncodeToString(id), + Claims: req.Claims, + CreatedBy: user, + }) + 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 { + err := CheckPermissions(c, []string{"apikeys.write"}) + if err != nil { + return err + } + + 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 { + err := CheckPermissions(c, []string{"apikeys.read"}) + if err != nil { + return err + } + + 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) + } + + for _, key := range h.config.EnvApiKeys { + ret = append(ret, key.ApiKey) + } + + return c.JSON(200, Page[ApiKey]{ + Items: ret, + This: c.Request().URL.String(), + }) +} + +func (h *Handler) createApiJwt(apikey string) (string, error) { + info := strings.SplitN(apikey, "-", 2) + if len(info) != 2 { + return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key format") + } + + key, fromEnv := h.config.EnvApiKeys[info[0]] + if !fromEnv { + dbKey, err := h.db.GetApiKey(context.Background(), dbc.GetApiKeyParams{ + Name: info[0], + Token: info[1], + }) + if err == pgx.ErrNoRows { + return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key") + } else if err != nil { + return "", err + } + + go func() { + h.db.TouchApiKey(context.Background(), dbKey.Pk) + }() + + key = MapDbKey(&dbKey) + } + + claims := maps.Clone(key.Claims) + claims["username"] = key.Name + claims["sub"] = key.Id + claims["sid"] = key.Id + claims["iss"] = h.config.PublicUrl + claims["iat"] = &jwt.NumericDate{ + Time: time.Now().UTC(), + } + claims["exp"] = &jwt.NumericDate{ + Time: time.Now().UTC().Add(time.Hour), + } + jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + t, err := jwt.SignedString(h.config.JwtPrivateKey) + if err != nil { + return "", err + } + return t, nil +} diff --git a/auth/config.go b/auth/config.go index b503ca30..86ad7190 100644 --- a/auth/config.go +++ b/auth/config.go @@ -1,17 +1,20 @@ package main import ( + "context" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" + "fmt" "maps" "os" "strings" "time" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "github.com/zoriya/kyoo/keibi/dbc" ) @@ -25,6 +28,7 @@ type Configuration struct { GuestClaims jwt.MapClaims ProtectedClaims []string ExpirationDelay time.Duration + EnvApiKeys map[string]ApiKeyWToken } var DefaultConfig = Configuration{ @@ -32,6 +36,7 @@ var DefaultConfig = Configuration{ FirstUserClaims: make(jwt.MapClaims), ProtectedClaims: []string{"permissions"}, ExpirationDelay: 30 * 24 * time.Hour, + EnvApiKeys: make(map[string]ApiKeyWToken), } func LoadConfiguration(db *dbc.Queries) (*Configuration, error) { @@ -98,5 +103,51 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) { ret.JwtPublicKey = &ret.JwtPrivateKey.PublicKey } + for _, env := range os.Environ() { + if !strings.HasPrefix(env, "KEIBI_APIKEY_"){ + continue + } + v := strings.Split(env, "=") + if strings.HasSuffix(v[0], "_CLAIMS") { + continue + } + + name := strings.TrimPrefix(v[0], "KEIBI_APIKEY_") + cstr := os.Getenv(fmt.Sprintf("KEIBI_APIKEY_%s_CLAIMS", name)) + + var claims jwt.MapClaims + if cstr != "" { + err := json.Unmarshal([]byte(cstr), &claims) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("missing claims env var KEIBI_APIKEY_%s_CLAIMS", name) + } + + name = strings.ToLower(name) + ret.EnvApiKeys[name] = ApiKeyWToken{ + ApiKey: ApiKey{ + Id: uuid.New(), + Name: name, + Claims: claims, + }, + Token: v[1], + } + + } + apikeys, err := db.ListApiKeys(context.Background()) + if err != nil { + return nil, err + } + for _, key := range apikeys { + if _, defined := ret.EnvApiKeys[key.Name]; defined { + return nil, fmt.Errorf( + "an api key with the name %s is already defined in database. Can't specify a new one via env var", + key.Name, + ) + } + } + return &ret, nil } diff --git a/auth/dbc/apikeys.sql.go b/auth/dbc/apikeys.sql.go new file mode 100644 index 00000000..fa4d1643 --- /dev/null +++ b/auth/dbc/apikeys.sql.go @@ -0,0 +1,154 @@ +// 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, created_by) + values ($1, $2, $3, $4) +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"` + CreatedBy *int32 `json:"createdBy"` +} + +func (q *Queries) CreateApiKey(ctx context.Context, arg CreateApiKeyParams) (Apikey, error) { + row := q.db.QueryRow(ctx, createApiKey, + arg.Name, + arg.Token, + arg.Claims, + arg.CreatedBy, + ) + 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 getApiKey = `-- name: GetApiKey :one +select + pk, id, name, token, claims, created_by, created_at, last_used +from + apikeys +where + name = $1 + and token = $2 +` + +type GetApiKeyParams struct { + Name string `json:"name"` + Token string `json:"token"` +} + +func (q *Queries) GetApiKey(ctx context.Context, arg GetApiKeyParams) (Apikey, error) { + row := q.db.QueryRow(ctx, getApiKey, arg.Name, arg.Token) + 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 +} + +const touchApiKey = `-- name: TouchApiKey :exec +update + apikeys +set + last_used = now()::timestamptz +where + pk = $1 +` + +func (q *Queries) TouchApiKey(ctx context.Context, pk int32) error { + _, err := q.db.Exec(ctx, touchApiKey, pk) + return err +} diff --git a/auth/dbc/models.go b/auth/dbc/models.go index 2310f424..7bf7c38f 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/dbc/sessions.sql.go b/auth/dbc/sessions.sql.go index 212ab91f..bcb81869 100644 --- a/auth/dbc/sessions.sql.go +++ b/auth/dbc/sessions.sql.go @@ -88,6 +88,7 @@ func (q *Queries) DeleteSession(ctx context.Context, arg DeleteSessionParams) (S const getUserFromToken = `-- name: GetUserFromToken :one select + s.pk, s.id, s.last_used, u.pk, u.id, u.username, u.email, u.password, u.claims, u.created_date, u.last_seen @@ -100,6 +101,7 @@ limit 1 ` type GetUserFromTokenRow struct { + Pk int32 `json:"pk"` Id uuid.UUID `json:"id"` LastUsed time.Time `json:"lastUsed"` User User `json:"user"` @@ -109,6 +111,7 @@ func (q *Queries) GetUserFromToken(ctx context.Context, token string) (GetUserFr row := q.db.QueryRow(ctx, getUserFromToken, token) var i GetUserFromTokenRow err := row.Scan( + &i.Pk, &i.Id, &i.LastUsed, &i.User.Pk, @@ -169,10 +172,10 @@ update set last_used = now()::timestamptz where - id = $1 + pk = $1 ` -func (q *Queries) TouchSession(ctx context.Context, id uuid.UUID) error { - _, err := q.db.Exec(ctx, touchSession, id) +func (q *Queries) TouchSession(ctx context.Context, pk int32) error { + _, err := q.db.Exec(ctx, touchSession, pk) return err } diff --git a/auth/dbc/users.sql.go b/auth/dbc/users.sql.go index ecc89fbb..02964f00 100644 --- a/auth/dbc/users.sql.go +++ b/auth/dbc/users.sql.go @@ -261,11 +261,11 @@ update set last_used = now()::timestamptz where - id = $1 + pk = $1 ` -func (q *Queries) TouchUser(ctx context.Context, id uuid.UUID) error { - _, err := q.db.Exec(ctx, touchUser, id) +func (q *Queries) TouchUser(ctx context.Context, pk int32) error { + _, err := q.db.Exec(ctx, touchUser, pk) return err } diff --git a/auth/jwt.go b/auth/jwt.go index c854da19..ffc6dce6 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -19,7 +19,7 @@ type Jwt struct { } // @Summary Get JWT -// @Description Convert a session token to a short lived JWT. +// @Description Convert a session token or an API key to a short lived JWT. // @Tags jwt // @Produce json // @Security Token @@ -28,6 +28,17 @@ type Jwt struct { // @Header 200 {string} Authorization "Jwt (same value as the returned token)" // @Router /jwt [get] func (h *Handler) CreateJwt(c echo.Context) error { + apikey := c.Request().Header.Get("X-Api-Key") + if apikey != "" { + token, err := h.createApiJwt(apikey) + if err != nil { + return err + } + return c.JSON(http.StatusOK, Jwt{ + Token: &token, + }) + } + auth := c.Request().Header.Get("Authorization") var jwt *string @@ -85,8 +96,8 @@ func (h *Handler) createJwt(token string) (string, error) { } go func() { - h.db.TouchSession(context.Background(), session.Id) - h.db.TouchUser(context.Background(), session.User.Id) + h.db.TouchSession(context.Background(), session.Pk) + h.db.TouchUser(context.Background(), session.User.Pk) }() claims := maps.Clone(session.User.Claims) diff --git a/auth/main.go b/auth/main.go index ab76c2a0..9c40cf09 100644 --- a/auth/main.go +++ b/auth/main.go @@ -131,28 +131,38 @@ type Handler struct { func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - auth := c.Request().Header.Get("Authorization") var jwt *string - if auth == "" || !strings.HasPrefix(auth, "Bearer ") { - jwt = h.createGuestJwt() - } else { - token := auth[len("Bearer "):] - // this is only used to check if it is a session token or a jwt - _, err := base64.RawURLEncoding.DecodeString(token) - if err != nil { - return next(c) - } - - tkn, err := h.createJwt(token) + apikey := c.Request().Header.Get("X-Api-Key") + if apikey != "" { + token, err := h.createApiJwt(apikey) if err != nil { return err } - jwt = &tkn + jwt = &token + } else { + auth := c.Request().Header.Get("Authorization") + + if auth == "" || !strings.HasPrefix(auth, "Bearer ") { + jwt = h.createGuestJwt() + } else { + token := auth[len("Bearer "):] + // this is only used to check if it is a session token or a jwt + _, err := base64.RawURLEncoding.DecodeString(token) + if err != nil { + return next(c) + } + + tkn, err := h.createJwt(token) + if err != nil { + return err + } + jwt = &tkn + } } if jwt != nil { - c.Request().Header.Set("Authorization", *jwt) + c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", *jwt)) } return next(c) } @@ -224,6 +234,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/:id", 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.down.sql b/auth/sql/migrations/000003_apikeys.down.sql new file mode 100644 index 00000000..3bdbcde3 --- /dev/null +++ b/auth/sql/migrations/000003_apikeys.down.sql @@ -0,0 +1,5 @@ +begin; + +drop table apikeys; + +commit; diff --git a/auth/sql/migrations/000003_apikeys.up.sql b/auth/sql/migrations/000003_apikeys.up.sql new file mode 100644 index 00000000..22ddbd86 --- /dev/null +++ b/auth/sql/migrations/000003_apikeys.up.sql @@ -0,0 +1,15 @@ +begin; + +create table apikeys( + pk serial primary key, + id uuid not null default gen_random_uuid(), + name varchar(256) not null unique, + token varchar(128) not null unique, + claims jsonb not null, + + created_by integer references users(pk) on delete cascade, + created_at timestamptz not null default now()::timestamptz, + last_used timestamptz not null default now()::timestamptz +); + +commit; diff --git a/auth/sql/queries/apikeys.sql b/auth/sql/queries/apikeys.sql new file mode 100644 index 00000000..634038c1 --- /dev/null +++ b/auth/sql/queries/apikeys.sql @@ -0,0 +1,37 @@ +-- name: GetApiKey :one +select + * +from + apikeys +where + name = $1 + and token = $2; + +-- name: TouchApiKey :exec +update + apikeys +set + last_used = now()::timestamptz +where + pk = $1; + +-- name: ListApiKeys :many +select + * +from + apikeys +order by + last_used; + +-- name: CreateApiKey :one +insert into apikeys(name, token, claims, created_by) + values ($1, $2, $3, $4) +returning + *; + +-- name: DeleteApiKey :one +delete from apikeys +where id = $1 +returning + *; + diff --git a/auth/sql/queries/sessions.sql b/auth/sql/queries/sessions.sql index b665848f..a2a06727 100644 --- a/auth/sql/queries/sessions.sql +++ b/auth/sql/queries/sessions.sql @@ -1,5 +1,6 @@ -- name: GetUserFromToken :one select + s.pk, s.id, s.last_used, sqlc.embed(u) @@ -16,7 +17,7 @@ update set last_used = now()::timestamptz where - id = $1; + pk = $1; -- name: GetUserSessions :many select diff --git a/auth/sql/queries/users.sql b/auth/sql/queries/users.sql index 8bc00feb..b80181c2 100644 --- a/auth/sql/queries/users.sql +++ b/auth/sql/queries/users.sql @@ -49,7 +49,7 @@ update set last_used = now()::timestamptz where - id = $1; + pk = $1; -- name: CreateUser :one insert into users(username, email, password, claims) 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/tests/apikey.hurl b/auth/tests/apikey.hurl new file mode 100644 index 00000000..2daaf9dc --- /dev/null +++ b/auth/tests/apikey.hurl @@ -0,0 +1,61 @@ +# perm check +POST {{host}}/keys +{ + "name": "dryflower", + "claims": { + "isAdmin": true, + "permissions": ["core.read"] + } +} +HTTP 401 + +POST {{host}}/keys +# this is created from the gh workflow file's env var +X-API-KEY: hurl-1234apikey +{ + "name": "dryflower", + "claims": { + "isAdmin": true, + "permissions": ["apikeys.read"] + } +} +HTTP 201 +[Captures] +id: jsonpath "$.id" +token: jsonpath "$.token" + +GET {{host}}/jwt +X-API-KEY: {{token}} +HTTP 200 +[Captures] +jwt: jsonpath "$.token" + +# Duplicates email +POST {{host}}/keys +X-API-KEY: hurl-1234apikey +{ + "name": "dryflower", + "claims": { + "isAdmin": true, + "permissions": ["core.read"] + } +} +HTTP 409 + +# List +GET {{host}}/keys +Authorization: Bearer {{jwt}} +HTTP 200 +[Asserts] +jsonpath "$.items[0].id" == {{id}} +jsonpath "$.items[0].name" == "dryflower" +jsonpath "$.items[0].claims.permissions" contains "apikeys.read" + + +DELETE {{host}}/keys/{{id}} +Authorization: Bearer {{jwt}} +HTTP 403 + +DELETE {{host}}/keys/{{id}} +X-API-KEY: hurl-1234apikey +HTTP 200 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/auth/utils.go b/auth/utils.go index e607cbee..d336a7bd 100644 --- a/auth/utils.go +++ b/auth/utils.go @@ -56,7 +56,12 @@ func GetCurrentSessionId(c echo.Context) (uuid.UUID, error) { func CheckPermissions(c echo.Context, perms []string) error { token, ok := c.Get("user").(*jwt.Token) - if !ok { + if !ok{ + return echo.NewHTTPError(401, "Not logged in") + } + sub, err := token.Claims.GetSubject() + // ignore guests + if err != nil || sub == "00000000-0000-0000-0000-000000000000" { return echo.NewHTTPError(401, "Not logged in") } claims, ok := token.Claims.(jwt.MapClaims) @@ -66,10 +71,8 @@ func CheckPermissions(c echo.Context, perms []string) error { permissions_claims, ok := claims["permissions"] if !ok { - return echo.NewHTTPError(403, fmt.Sprintf("Missing permissions: %s.", ", ")) + return echo.NewHTTPError(403, fmt.Sprintf("No permissions on this account. Needs permissions: %s.", strings.Join(perms, ", "))) } - fmt.Printf("%v\n", permissions_claims) - fmt.Printf("%t\n", permissions_claims) permissions_int, ok := permissions_claims.([]any) if !ok { return echo.NewHTTPError(403, "Invalid permission claim.") 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