165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
import logging
|
|
from typing import Any
|
|
|
|
from flask import request, Response
|
|
from flask_appbuilder import expose
|
|
from flask_appbuilder.api import safe
|
|
from flask_appbuilder.security.decorators import permission_name, protect
|
|
from flask_wtf.csrf import generate_csrf
|
|
from marshmallow import EXCLUDE, fields, post_load, Schema, ValidationError
|
|
|
|
from superset.embedded_dashboard.commands.exceptions import (
|
|
EmbeddedDashboardNotFoundError,
|
|
)
|
|
from superset.extensions import event_logger
|
|
from superset.security.guest_token import GuestTokenResourceType
|
|
from superset.views.base_api import BaseSupersetApi, statsd_metrics
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PermissiveSchema(Schema):
|
|
"""
|
|
A marshmallow schema that ignores unexpected fields, instead of throwing an error.
|
|
"""
|
|
|
|
class Meta: # pylint: disable=too-few-public-methods
|
|
unknown = EXCLUDE
|
|
|
|
|
|
class UserSchema(PermissiveSchema):
|
|
username = fields.String()
|
|
first_name = fields.String()
|
|
last_name = fields.String()
|
|
|
|
|
|
class ResourceSchema(PermissiveSchema):
|
|
type = fields.Enum(GuestTokenResourceType, by_value=True, required=True)
|
|
id = fields.String(required=True)
|
|
|
|
@post_load
|
|
def convert_enum_to_value(
|
|
self, data: dict[str, Any], **kwargs: Any # pylint: disable=unused-argument
|
|
) -> dict[str, Any]:
|
|
# we don't care about the enum, we want the value inside
|
|
data["type"] = data["type"].value
|
|
return data
|
|
|
|
|
|
class RlsRuleSchema(PermissiveSchema):
|
|
dataset = fields.Integer()
|
|
clause = fields.String(required=True) # todo other options?
|
|
|
|
|
|
class GuestTokenCreateSchema(PermissiveSchema):
|
|
user = fields.Nested(UserSchema)
|
|
resources = fields.List(fields.Nested(ResourceSchema), required=True)
|
|
rls = fields.List(fields.Nested(RlsRuleSchema), required=True)
|
|
|
|
|
|
guest_token_create_schema = GuestTokenCreateSchema()
|
|
|
|
|
|
class SecurityRestApi(BaseSupersetApi):
|
|
resource_name = "security"
|
|
allow_browser_login = True
|
|
openapi_spec_tag = "Security"
|
|
|
|
@expose("/csrf_token/", methods=("GET",))
|
|
@event_logger.log_this
|
|
@protect()
|
|
@safe
|
|
@statsd_metrics
|
|
@permission_name("read")
|
|
def csrf_token(self) -> Response:
|
|
"""
|
|
Return the csrf token
|
|
---
|
|
get:
|
|
description: >-
|
|
Fetch the CSRF token
|
|
responses:
|
|
200:
|
|
description: Result contains the CSRF token
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
result:
|
|
type: string
|
|
401:
|
|
$ref: '#/components/responses/401'
|
|
500:
|
|
$ref: '#/components/responses/500'
|
|
"""
|
|
return self.response(200, result=generate_csrf())
|
|
|
|
@expose("/guest_token/", methods=("POST",))
|
|
@event_logger.log_this
|
|
@protect()
|
|
@safe
|
|
@statsd_metrics
|
|
@permission_name("grant_guest_token")
|
|
def guest_token(self) -> Response:
|
|
"""Response
|
|
Returns a guest token that can be used for auth in embedded Superset
|
|
---
|
|
post:
|
|
description: >-
|
|
Fetches a guest token
|
|
requestBody:
|
|
description: Parameters for the guest token
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: GuestTokenCreateSchema
|
|
responses:
|
|
200:
|
|
description: Result contains the guest token
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
token:
|
|
type: string
|
|
401:
|
|
$ref: '#/components/responses/401'
|
|
400:
|
|
$ref: '#/components/responses/400'
|
|
500:
|
|
$ref: '#/components/responses/500'
|
|
"""
|
|
try:
|
|
body = guest_token_create_schema.load(request.json)
|
|
self.appbuilder.sm.validate_guest_token_resources(body["resources"])
|
|
|
|
# todo validate stuff:
|
|
# make sure username doesn't reference an existing user
|
|
# check rls rules for validity?
|
|
token = self.appbuilder.sm.create_guest_access_token(
|
|
body["user"], body["resources"], body["rls"]
|
|
)
|
|
return self.response(200, token=token)
|
|
except EmbeddedDashboardNotFoundError as error:
|
|
return self.response_400(message=error.message)
|
|
except ValidationError as error:
|
|
return self.response_400(message=error.messages)
|