diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f5caf3a --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +DB_DIALECT=mysql +DB_USER=superset +DB_PASSWORD=password +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=superset +SUPERSET_ENV=production +SUPERSET_SECRET_KEY=key +OIDC_ENABLE="True" \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..eaea9fc --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,86 @@ +name: Docker SuperSet LTS Keycloak +on: + push: + branches: + - main + tags: + - v* +# +jobs: + superset-docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + + # Repo metadata + - name: Repo metadata + id: repo + uses: actions/github-script@v4 + with: + script: | + const repo = await github.repos.get(context.repo) + return repo.data + # Prepare variables + - name: Prepare + id: prep + run: | + REG=ghcr.io + IMAGE=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') + DOCKER_IMAGE=${REG}/${IMAGE} + VERSION=nool + if [ "${{ github.event_name }}" = "schedule" ]; then + VERSION=nightly + elif [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/} + elif [[ $GITHUB_REF == refs/heads/* ]]; then + VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') + if [ "${{ github.event.repository.default_branch }}" = "$VERSION" ]; then + VERSION=latest + fi + elif [[ $GITHUB_REF == refs/pull/* ]]; then + VERSION=pr-${{ github.event.number }} + fi + TAGS="${DOCKER_IMAGE}:${VERSION}" + if [[ $VERSION =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + MINOR=${VERSION%.*} + MAJOR=${MINOR%.*} + TAGS="$TAGS,${DOCKER_IMAGE}:${MINOR},${DOCKER_IMAGE}:${MAJOR},${DOCKER_IMAGE}:latest" + fi + echo ::set-output name=version::${VERSION} + echo ::set-output name=tags::${TAGS} + echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') + # Set up Buildx env + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # Login + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Build image and push to registry + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.prep.outputs.tags }} + labels: | + org.opencontainers.image.title=${{ fromJson(steps.repo.outputs.result).name }} + org.opencontainers.image.description=${{ fromJson(steps.repo.outputs.result).description }} + org.opencontainers.image.url=${{ fromJson(steps.repo.outputs.result).html_url }} + org.opencontainers.image.source=${{ fromJson(steps.repo.outputs.result).html_url }} + org.opencontainers.image.version=${{ steps.prep.outputs.version }} + org.opencontainers.image.created=${{ steps.prep.outputs.created }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.licenses=${{ fromJson(steps.repo.outputs.result).license.spdx_id }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..30656e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM apache/superset:latest +USER root +RUN pip install mysqlclient itsdangerous==2.0.1 flask-oidc==1.4.0 Flask-OpenID==1.3.0 +# +# Add custom superset_config.py file and shell files +COPY superset_config.py /app/ +ENV SUPERSET_CONFIG_PATH /app/superset_config.py +# +ADD keycloak_security_manager.py /app/pythonpath +# +#RUN pip install --upgrade pip +#RUN pip uninstall fbprophet pystan +#RUN pip install --upgrade pip setuptools +RUN pip install lunarcalendar==0.0.9 tqdm==4.64.0 +RUN pip install cython==0.29.21 +RUN pip install "pystan<3.0" +RUN pip install "prophet>=1.0.1,<1.1" +# +CMD ["/bin/sh","-c","/usr/bin/run-server.sh"] +USER superset +# \ No newline at end of file diff --git a/keycloak_security_manager.py b/keycloak_security_manager.py new file mode 100644 index 0000000..a83c3ca --- /dev/null +++ b/keycloak_security_manager.py @@ -0,0 +1,55 @@ +from flask_appbuilder.security.manager import AUTH_OID +from superset.security import SupersetSecurityManager +from flask_oidc import OpenIDConnect +from flask_appbuilder.security.views import AuthOIDView +from flask_login import login_user +from urllib.parse import quote +from flask_appbuilder.views import expose +from flask import request, redirect +# +AUTH_ROLES_SYNC_AT_LOGIN = True +# +class OIDCSecurityManager(SupersetSecurityManager): + # + def __init__(self, appbuilder): + super(OIDCSecurityManager, self).__init__(appbuilder) + if self.auth_type == AUTH_OID: + self.oid = OpenIDConnect(self.appbuilder.get_app) + self.authoidview = AuthOIDCView +# +class AuthOIDCView(AuthOIDView): + # + @expose('/login/', methods=['GET', 'POST']) + def login(self, flag=True): + sm = self.appbuilder.sm + oidc = sm.oid + default_role = "Gamma" + # + @self.appbuilder.sm.oid.require_login + def handle_login(): + user = sm.auth_user_oid(oidc.user_getfield('email')) + if user is None: + info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email', 'roles']) + roles = info.get('roles', []) + roles += [default_role, ] + user = sm.add_user(info.get('preferred_username'), info.get('given_name', ''), info.get('family_name', ''), + info.get('email'), [sm.find_role(role) for role in roles]) + # need to check if is it correct + #setattr(user, "is_active", True) + # + login_user(user, remember=False) + return redirect(self.appbuilder.get_url_for_index) + # + return handle_login() + # + @expose('/logout/', methods=['GET', 'POST']) + def logout(self): + oidc = self.appbuilder.sm.oid + oidc.logout() + super(AuthOIDCView, self).logout() + redirect_url = request.url_root.strip('/') + # redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login + return redirect( + oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url)) + # +# \ No newline at end of file diff --git a/superset_config.py b/superset_config.py new file mode 100644 index 0000000..eddffd6 --- /dev/null +++ b/superset_config.py @@ -0,0 +1,78 @@ +import os +from typing import Optional +# +def get_env_variable(var_name: str, default: Optional[str] = None) -> str: + """Get the environment variable or raise exception.""" + try: + return os.environ[var_name] + except KeyError: + if default is not None: + return default + else: + error_msg = "The environment variable {} was missing, abort...".format( + var_name + ) + raise EnvironmentError(error_msg) + # + # +# +ENABLE_PROXY_FIX = True +DASHBOARD_RBAC = True +FEATURE_FLAGS = {"DASHBOARD_RBAC": True} +################################# +# METADATA DATABASE # +################################# +DATABASE_DIALECT = get_env_variable("DB_DIALECT") +DATABASE_USER = get_env_variable("DB_USER") +DATABASE_PASSWORD = get_env_variable("DB_PASSWORD") +DATABASE_HOST = get_env_variable("DB_HOST") +DATABASE_PORT = get_env_variable("DB_PORT") +DATABASE_DB = get_env_variable("DB_NAME") +# +SQLALCHEMY_DATABASE_URI = "%s://%s:%s@%s:%s/%s?charset=utf8" % ( + DATABASE_DIALECT, + DATABASE_USER, + DATABASE_PASSWORD, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_DB, +) +# +SECRET_KEY = get_env_variable("SECRET_KEY", 'secret_key') +# +#---------------------------KEYCLOACK ---------------------------- +# See: https://github.com/apache/superset/discussions/13915 +# See: https://stackoverflow.com/questions/54010314/using-keycloakopenid-connect-with-apache-superset/54024394#54024394 +from keycloak_security_manager import OIDCSecurityManager +from flask_appbuilder.security.manager import AUTH_OID, AUTH_OAUTH +# +OIDC_ENABLE = get_env_variable("OIDC_ENABLE", 'False') +# +if OIDC_ENABLE == 'True': + AUTH_TYPE = AUTH_OID + OIDC_CLIENT_SECRETS = get_env_variable("OIDC_CLIENT_SECRETS", '/app/pythonpath/client_secret.json') + OIDC_ID_TOKEN_COOKIE_SECURE = False + OIDC_REQUIRE_VERIFIED_EMAIL = False + OIDC_OPENID_REALM = get_env_variable("OIDC_OPENID_REALM",'realm') + OIDC_INTROSPECTION_AUTH_METHOD = 'client_secret_post' + CUSTOM_SECURITY_MANAGER = OIDCSecurityManager + AUTH_ROLES_SYNC_AT_LOGIN = True + AUTH_USER_REGISTRATION = True + AUTH_USER_REGISTRATION_ROLE = get_env_variable("AUTH_USER_REGISTRATION_ROLE", 'Gamma') +#-------------------------------------------------------------- +# +# +SMTP_ENABLE = get_env_variable("SMTP_ENABLE", 'False') +# +############################################# +# EMAIL REPORTS CONFIGURATION # +############################################# +if SMTP_ENABLE == 'True': + SMTP_HOST = get_env_variable("SMTP_HOST") + SMTP_STARTTLS = True + SMTP_SSL = False + SMTP_USER = get_env_variable("SMTP_USER") + SMTP_PORT = get_env_variable("SMTP_PORT") + SMTP_PASSWORD = get_env_variable("SMTP_PASSWORD") + SMTP_MAIL_FROM = get_env_variable("SMTP_MAIL_FROM") +# \ No newline at end of file