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/apikey.go b/auth/apikey.go index aee4919d..c258b9f1 100644 --- a/auth/apikey.go +++ b/auth/apikey.go @@ -32,7 +32,7 @@ type ApiKeyWToken struct { } type ApiKeyDto struct { - Name string `json:"name" example:"my-app" validate:"alpha"` + Name string `json:"name" example:"myapp" validate:"alpha"` Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"` } @@ -61,8 +61,13 @@ func MapDbKey(key *dbc.Apikey) ApiKeyWToken { // @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) + err = c.Bind(&req) if err != nil { return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error()) } @@ -104,6 +109,11 @@ func (h *Handler) CreateApiKey(c echo.Context) error { // @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") @@ -127,6 +137,11 @@ func (h *Handler) DeleteApiKey(c echo.Context) error { // @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 diff --git a/auth/config.go b/auth/config.go index 402b7002..86ad7190 100644 --- a/auth/config.go +++ b/auth/config.go @@ -14,6 +14,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "github.com/zoriya/kyoo/keibi/dbc" ) @@ -103,11 +104,14 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) { } for _, env := range os.Environ() { - if !strings.HasPrefix(env, "KEIBI_APIKEY_") || strings.HasSuffix(env, "_CLAIMS") { + if !strings.HasPrefix(env, "KEIBI_APIKEY_"){ + continue + } + v := strings.Split(env, "=") + if strings.HasSuffix(v[0], "_CLAIMS") { continue } - v := strings.Split(env, "=") name := strings.TrimPrefix(v[0], "KEIBI_APIKEY_") cstr := os.Getenv(fmt.Sprintf("KEIBI_APIKEY_%s_CLAIMS", name)) @@ -117,10 +121,14 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) { 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, }, diff --git a/auth/main.go b/auth/main.go index a48aebd8..9c40cf09 100644 --- a/auth/main.go +++ b/auth/main.go @@ -162,7 +162,7 @@ func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc { } if jwt != nil { - c.Request().Header.Set("Authorization", *jwt) + c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", *jwt)) } return next(c) } @@ -236,7 +236,7 @@ func main() { r.GET("/keys", h.ListApiKey) r.POST("/keys", h.CreateApiKey) - r.DELETE("/keys", h.DeleteApiKey) + r.DELETE("/keys/:id", h.DeleteApiKey) g.GET("/jwt", h.CreateJwt) e.GET("/.well-known/jwks.json", h.GetJwks) diff --git a/auth/sql/migrations/000003_apikeys.up.sql b/auth/sql/migrations/000003_apikeys.up.sql index 8d1f0a17..40e5d3d8 100644 --- a/auth/sql/migrations/000003_apikeys.up.sql +++ b/auth/sql/migrations/000003_apikeys.up.sql @@ -9,7 +9,7 @@ create table apikeys( 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 + last_used timestamptz not null default now()::timestamptz ); commit; diff --git a/auth/tests/apikey.hurl b/auth/tests/apikey.hurl new file mode 100644 index 00000000..20d281d6 --- /dev/null +++ b/auth/tests/apikey.hurl @@ -0,0 +1,58 @@ +# perm check +POST {{host}}/keys +{ + "name": "dryflower", + "claims": { + "isAdmin": true, + "permssions": ["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, + "permssions": ["core.read"] + } +} +HTTP 201 +[Captures] +token: jsonpath "$.token" + +GET {{host}}/jwt +Authorization: Bearer {{token}} +HTTP 200 +[Captures] +id: jsonpath "$.id" +jwt: jsonpath "$.token" + +# Duplicates email +POST {{host}}/keys +X-API-KEY: hurl-1234apikey +{ + "name": "dryflower", + "claims": { + "isAdmin": true, + "permssions": ["core.read"] + } +} +HTTP 409 + +# List +GET {{host}}/keys +Authorization: Bearer {{token}} +HTTP 200 +[Asserts] +jsonpath "$.items[0].id" == {{id}} +jsonpath "$.items[0].name" == "dryflower" +jsonpath "$.items[0].claims.permissions" contains "core.read" + + + +DELETE {{host}}/keys/{{id}} +Authorization: Bearer {{jwt}} +HTTP 200 diff --git a/auth/utils.go b/auth/utils.go index e607cbee..942614b2 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) @@ -68,8 +73,6 @@ func CheckPermissions(c echo.Context, perms []string) error { if !ok { return echo.NewHTTPError(403, fmt.Sprintf("Missing permissions: %s.", ", ")) } - 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.")