Exemplo n.º 1
0
def account_bot_permissions(account_id, user_id):
    """
        Allows reading and assigning permissions to account bots (bots which are tied to a specific account).
    """
    # make sure the bot really belongs to the account:
    rec = Bot.get(user_id, account_id)
    if not rec:
        return "No such bot", 404

    if flask.request.method in ['GET', 'HEAD']:
        rec = Permission.get_list(user_id)
        return json.dumps({'list': rec}), 200

    elif flask.request.method == 'POST':
        granting_user_id = flask.g.grafolean_data['user_id']
        permission = Permission.forge_from_input(flask.request.get_json(),
                                                 user_id)
        try:
            permission_id = permission.insert(granting_user_id)
            mqtt_publish_changed([
                'persons/{}'.format(permission.user_id),
                'bots/{}'.format(permission.user_id),
            ])
            return json.dumps({
                'user_id': permission.user_id,
                'resource_prefix': permission.resource_prefix,
                'methods': permission.methods,
                'id': permission_id,
            }), 201
        except AccessDeniedError as ex:
            return str(ex), 401
        except psycopg2.IntegrityError:
            return "Invalid parameters", 400
Exemplo n.º 2
0
def users_bot_token_get(user_id):
    # make sure the user who is requesting to see the bot token has every permission that this token has, and
    # also that this user can add the bot:
    request_user_permissions = Permission.get_list(
        int(flask.g.grafolean_data['user_id']))
    if not Permission.has_all_permissions(request_user_permissions, user_id):
        return "Not enough permissions to see this bot's token", 401
    if not Permission.can_grant_permission(request_user_permissions, 'bots',
                                           'POST'):
        return "Not enough permissions to see this bot's token - POST to /bots not allowed", 401
    token = Bot.get_token(user_id, None)
    if not token:
        return "No such bot", 404
    return {'token': token}, 200
Exemplo n.º 3
0
def admin_first_post():
    """
        ---
        post:
          summary: Create first admin user
          tags:
            - Admin
          description:
            This endpoint helps with setting up a new installation. It allows us to set up just one initial
            admin access (with name, email and password). Later requests to the same endpoint will fail.
            At the same time a systemwide (ICMP ping) bot is configured and its token shared via file with
            a grafolean-ping-bot Docker container (in default setup).
          parameters:
            - name: "body"
              in: body
              description: "First admin data and credentials"
              required: true
              schema:
                "$ref": '#/definitions/PersonPOST'
          responses:
            201:
              content:
                application/json:
                  schema:
                    type: object
                    properties:
                      id:
                        type: integer
                        description: "User id of created admin"
            401:
              description: System already initialized
    """
    if Auth.first_user_exists():
        return 'System already initialized', 401
    admin = Person.forge_from_input(flask.request.get_json())
    admin_id = admin.insert()
    # make it a superuser:
    permission = Permission(admin_id, None, None)
    permission.insert(None, skip_checks=True)

    # Help users by including a systemwide ping bot in the package by default:
    Bot.ensure_default_systemwide_bots_exist()

    mqtt_publish_changed(['status/info'])
    return json.dumps({
        'id': admin_id,
    }), 201
Exemplo n.º 4
0
def auth_login_post():
    credentials = PersonCredentials.forge_from_input(flask.request.get_json())
    user_id = credentials.check_user_login()
    if not user_id:
        return "Invalid credentials", 401

    session_data = {
        'user_id': user_id,
        'session_id': secrets.token_hex(32),
        'permissions': Permission.get_list(user_id),
    }
    response = flask.make_response(json.dumps(session_data), 200)
    response.headers['X-JWT-Token'], _ = JWT(
        session_data).encode_as_authorization_header()
    return response
Exemplo n.º 5
0
def account_bot_permission_delete(account_id, user_id, permission_id):
    """ Revoke permission from account bot """
    # make sure the bot really belongs to the account:
    rec = Bot.get(user_id, account_id)
    if not rec:
        return "No such bot", 404

    granting_user_id = flask.g.grafolean_data['user_id']
    try:
        rowcount = Permission.delete(permission_id, user_id, granting_user_id)
    except AccessDeniedError as ex:
        return str(ex), 401
    if not rowcount:
        return "No such permission", 404
    mqtt_publish_changed([
        'persons/{user_id}'.format(user_id=user_id),
        'bots/{user_id}'.format(user_id=user_id),
    ])
    return "", 204
Exemplo n.º 6
0
def users_permission_delete(permission_id, user_id):
    """
        ---
        delete:
          summary: Revoke permission
          tags:
            - Users
          description:
            Revokes a specific permission, as specified by permission id.
          parameters:
            - name: permission_id
              in: path
              description: "Permission id"
              required: true
              schema:
                type: integer
          responses:
            204:
              description: Permission removed successfully
            401:
              description: Not allowed to revoke this permission
            404:
              description: No such permission
    """
    granting_user_id = flask.g.grafolean_data['user_id']
    try:
        rowcount = Permission.delete(permission_id, user_id, granting_user_id)
    except AccessDeniedError as ex:
        return str(ex), 401
    if not rowcount:
        return "No such permission", 404
    mqtt_publish_changed([
        'persons/{user_id}'.format(user_id=user_id),
        'users/{user_id}'.format(user_id=user_id),
        'bots/{user_id}'.format(user_id=user_id),
    ])
    return "", 204
Exemplo n.º 7
0
def test_Permission_can_grant_permission(granting_user_permissions, requested_resource_prefix, requested_methods, expected):
    assert expected == Permission.can_grant_permission(granting_user_permissions, requested_resource_prefix, requested_methods)
Exemplo n.º 8
0
def users_user_get(user_id):
    rec = User.get(user_id)
    if not rec:
        return "No such user", 404
    rec['permissions'] = Permission.get_list(user_id)
    return json.dumps(rec), 200
Exemplo n.º 9
0
def users_permissions_get_post(user_id):
    """
        ---
        get:
          summary: Get a list of all permissions granted to a specified user
          tags:
            - Users
          description:
            Returns a list of all permissions granted to the user. The list is returned in a single array (no pagination).


            Note that when comparing, resource prefix is checked either for equality (resource must match prefix), otherwise
            resource location must start with the prefix, followed by forward slash ('/'). In other words, allowing users
            access to 'accounts/123' does **not** grant them access to 'accounts/1234'.
          parameters:
            - name: user_id
              in: path
              description: "User id"
              required: false
              schema:
                type: integer
          responses:
            200:
              content:
                application/json:
                  schema:
                    type: object
                    properties:
                      list:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: integer
                              description: "Permission id"
                            resource_prefix:
                              type: string
                              nullable: true
                              description: "Resource prefix (e.g., 'admin/permissions' or 'accounts/123'); if null, this permission applies to any resource"
                            methods:
                              type: array
                              items:
                                type: string
                                enum:
                                  - "GET"
                                  - "POST"
                                  - "PUT"
                                  - "DELETE"
                              nullable: true
                              description: "List of HTTP methods allowed; if null, this permission applies to any method"
        post:
          summary: Grant permission to the user
          tags:
            - Users
          description:
            Grants a specified permission to the user. Permissions are defined with a combination of resource prefix and a list of methods.
            Since both persons and bots are users, this endpoint can be used for granting permissions to either of them.


            Note that when comparing, resource prefix is checked either for equality (resource must match prefix), otherwise
            resource location must start with the prefix, followed by forward slash ('/'). In other words, allowing users
            access to 'accounts/123' does **not** grant them access to 'accounts/1234'.


          parameters:
            - name: user_id
              in: path
              description: "User id"
              required: false
              schema:
                type: integer
            - name: "body"
              in: body
              description: "Permission to be granted"
              required: true
              schema:
                "$ref": '#/definitions/Permission'
          responses:
            201:
              content:
                application/json:
                  schema:
                    type: object
                    properties:
                      id:
                        type: integer
                        description: "Permission id"
            400:
              description: Invalid parameters
            401:
              description: Not allowed to grant permissions
    """
    if flask.request.method in ['GET', 'HEAD']:
        rec = Permission.get_list(user_id=user_id)
        return json.dumps({'list': rec}), 200

    elif flask.request.method == 'POST':
        granting_user_id = flask.g.grafolean_data['user_id']
        permission = Permission.forge_from_input(flask.request.get_json(),
                                                 user_id)
        try:
            permission_id = permission.insert(granting_user_id)
            mqtt_publish_changed([
                'persons/{user_id}'.format(user_id=user_id),
                'bots/{user_id}'.format(user_id=user_id),
            ])
            return json.dumps({
                'id': permission_id,
            }), 201
        except AccessDeniedError as ex:
            return str(ex), 401
        except psycopg2.IntegrityError:
            return "Invalid parameters", 400
Exemplo n.º 10
0
def users_person_crud(user_id):
    """
        ---
        get:
          summary: Get person data
          tags:
            - Users
          description:
            Returns person data.
          parameters:
            - name: user_id
              in: path
              description: "User id"
              required: true
              schema:
                type: integer
          responses:
            200:
              content:
                application/json:
                  schema:
                    "$ref": '#/definitions/PersonGETWithPermissions'
            404:
              description: No such person
        put:
          summary: Update the bot
          tags:
            - Users
          description:
            Updates person data.
          parameters:
            - name: user_id
              in: path
              description: "User id"
              required: true
              schema:
                type: integer
            - name: "body"
              in: body
              description: "Person data"
              required: true
              schema:
                "$ref": '#/definitions/PersonPOST'
          responses:
            204:
              description: Update successful
            404:
              description: No such person
        delete:
          summary: Remove the person data
          tags:
            - Users
          description:
            Removes the person data. Also removes user's permissions, if any.
          parameters:
            - name: user_id
              in: path
              description: "User id"
              required: true
              schema:
                type: integer
          responses:
            204:
              description: Person data removed successfully
            403:
              description: Can't remove yourself
            404:
              description: No such person
    """
    if flask.request.method in ['GET', 'HEAD']:
        rec = Person.get(user_id)
        if not rec:
            return "No such person", 404
        rec['permissions'] = Permission.get_list(user_id)
        return json.dumps(rec), 200

    elif flask.request.method == 'PUT':
        person = Person.forge_from_input(flask.request.get_json(),
                                         force_id=user_id)
        rowcount = person.update()
        if not rowcount:
            return "No such person", 404
        mqtt_publish_changed([
            'persons/{user_id}'.format(user_id=user_id),
            'persons',
        ])
        return "", 204

    elif flask.request.method == 'DELETE':
        # user should not be able to delete himself, otherwise they could lock themselves out:
        if int(flask.g.grafolean_data['user_id']) == int(user_id):
            return "Can't delete yourself", 403
        rowcount = Person.delete(user_id)
        if not rowcount:
            return "No such person", 404
        mqtt_publish_changed([
            'persons/{user_id}'.format(user_id=user_id),
            'persons',
        ])
        return "", 204
Exemplo n.º 11
0
def before_request():

    # http://flask.pocoo.org/docs/1.0/api/#application-globals
    flask.g.grafolean_data = {}

    if not flask.request.endpoint in app.view_functions:
        # Calling /api/admin/migratedb with GET (instead of POST) is a common mistake, so it deserves a warning in the log:
        if flask.request.path == '/api/admin/migratedb' and flask.request.method == 'GET':
            log.warning("Did you want to use POST instead of GET?")
        return "Resource not found", 404

    # Browser might (if frontend and backend are not on the same origin) send a pre-flight OPTIONS request to get the
    # CORS settings. In this case 'Authorization' header will not be set, which could lead to 401 response, which browser
    # doesn't like. So let's just return 200 on all OPTIONS:
    if flask.request.method == 'OPTIONS':
        # we need to set 'Allow' header to notify caller which methods are available:
        methods = set()
        for rule in app.url_map.iter_rules():
            if flask.request.url_rule == rule:
                methods |= rule.methods
        response = flask.make_response('', 200)
        response.headers['Allow'] = ",".join(sorted(methods))
        return response

    if flask.request.method in ['GET', 'HEAD', 'POST']:
        # While it is true that CORS is client-side protection, the rules about preflights allow these 3 types of requests
        # to be sent to the server without OPTIONS preflight - which means that browser will learn about violation too late.
        # To combat this, we still check Origin header and explicitly deny non-whitelisted requests:
        origin_header = flask.request.headers.get('Origin', None)
        if origin_header:  # is it a cross-origin request?
            # still, we sometimes get origin header even if it is not a cross-origin request, so let's double check that we
            # indeed are doing CORS:
            if flask.request.url_root.rstrip('/') != origin_header:
                if origin_header not in CORS_DOMAINS and flask.request.path != '/api/status/info':  # this path is an exception
                    return 'CORS not allowed for this origin', 403

    if dbutils.db is None:
        dbutils.db_connect()
        if dbutils.db is None:
            # oops, DB error... we should return 5xx:
            return 'Service unavailable', 503

    view_func = app.view_functions[flask.request.endpoint]
    # unless we have explicitly used @noauth decorator, do authorization check here:
    if not hasattr(view_func, '_noauth'):
        try:
            user_id = None
            user_is_bot = False
            authorization_header = flask.request.headers.get('Authorization')
            query_params_bot_token = flask.request.args.get('b')
            if authorization_header is not None:
                received_jwt = JWT.forge_from_authorization_header(authorization_header, allow_leeway=0)
                flask.g.grafolean_data['jwt'] = received_jwt
                user_id = received_jwt.data['user_id']
            elif query_params_bot_token is not None:
                user_id = Bot.authenticate_token(query_params_bot_token)
                user_is_bot = True

            if user_id is None:
                log.info("Authentication failed (no such user)")
                return "Access denied", 401

            # check permissions:
            resource = flask.request.path[len('/api/'):]
            resource = resource.rstrip('/')
            is_allowed = Permission.is_access_allowed(
                user_id=user_id,
                resource=resource,
                method=flask.request.method,
            )
            if not is_allowed:
                log.info("Access denied (permissions check failed) {} {} {}".format(user_id, resource, flask.request.method))
                return "Access to resource denied, insufficient permissions", 403

            flask.g.grafolean_data['user_id'] = user_id
            flask.g.grafolean_data['user_is_bot'] = user_is_bot
        except AuthFailedException as ex:
            log.info(f"Authentication failed: {str(ex)}")
            return "Access denied", 401
        except:
            log.exception("Exception while checking access rights")
            return "Could not validate access", 500
Exemplo n.º 12
0
def profile_permissions():
    user_id = flask.g.grafolean_data['user_id']
    rec = Permission.get_list(user_id)
    return json.dumps({'list': rec}), 200
Exemplo n.º 13
0
def admin_mqttauth_plug(check_type):
    """
        ---
        post:
          summary: Authorization for Mosquitto with mosquitto-auth-plug plugin
          tags:
            - Admin
          description:
            >
              If using MQTT (with iegomez/mosquitto-go-auth plugin), it should be configured
              to ask this endpoint about access rights via JWT tokens (Authorization header). The JWT token is supplied
              to MQTT by frontend via websockets through username (password is not used).
              See [mosquitto-go-auth](https://github.com/iegomez/mosquitto-go-auth) for more info.
          parameters:
            - name: check_type
              in: path
              description: "One of the 3 modes of calling this endpoint"
              required: true
              schema:
                type: "string"
                enum:
                  - "getuser"
                  - "superuser"
                  - "aclcheck"
            - name: Authorization
              in: header
              description: "JWT token (in other words: MQTT username)"
              schema:
                type: string
              required: true
          responses:
            200:
              description: Access allowed
            401:
              description: Access denied
    """
    # mqtt-auth-plug urlencodes JWT tokens, so we must decode them here:
    authorization_header = flask.request.headers.get('Authorization')
    authorization_header = urllib.parse.unquote(authorization_header, encoding='utf-8')
    # debugging:
    # if authorization_header == 'Bearer secret':
    #     log.info('--- secret account authenticated ---')
    #     return "", 200
    # log_received_jwt = JWT.forge_from_authorization_header(authorization_header, allow_leeway=3600*24*365*10)
    # log.info('mqtt-auth {}: {}, {}'.format(check_type.upper(), log_received_jwt.data, flask.request.form.to_dict()))
    try:
        if check_type == 'getuser':
            # we don't complicate about newly expired tokens here - if they are at all valid, browser will refresh them anyway.
            received_jwt = JWT.forge_from_authorization_header(authorization_header, allow_leeway=JWT.TOKEN_CAN_BE_REFRESHED_FOR)
            # jwt token was successfully decoded, so we can allow for the fact that this is a valid user - we'll still see about
            # access rights though (might be superuser, in which case everything goes, or it might be checked via aclcheck)
            return "", 200

        elif check_type == 'superuser':
            # we don't complicate about newly expired tokens here - if they are at all valid, browser will refresh them anyway.
            received_jwt = JWT.forge_from_authorization_header(authorization_header, allow_leeway=JWT.TOKEN_CAN_BE_REFRESHED_FOR)
            # is this our own attempt to publish something to MQTT, and the mosquitto auth plugin is asking us to authenticate ourselves?
            is_superuser = bool(received_jwt.data.get('superuser', False))
            if is_superuser:
                return "", 200

            log.info("Access denied (not a superuser)")
            return "Access denied", 401

        elif check_type == 'aclcheck':
            params = flask.request.form.to_dict()
            # When client connects, username is jwt token. However subscribing to topics doesn't necessarily reconnect so
            # fresh JWT token is not sent and we are getting the old one. This is OK though - if user kept the connection
            # we can assume that they would just keep refreshing the token. So we allow for some large leeway (10 years)
            received_jwt = JWT.forge_from_authorization_header(authorization_header, allow_leeway=3600*24*365*10)
            # superusers can do whatever they want to:
            is_superuser = bool(received_jwt.data.get('superuser', False))
            if is_superuser:
                return "", 200


            # From https://github.com/iegomez/mosquitto-go-auth#acl-access-values:
            #     ACL access values: Mosquitto 1.5 introduced a new ACL access value, MOSQ_ACL_SUBSCRIBE, which is similar
            #     to the classic MOSQ_ACL_READ value but not quite the same:
            #
            #      *  MOSQ_ACL_SUBSCRIBE when a client is asking to subscribe to a topic string.
            #      *                     This differs from MOSQ_ACL_READ in that it allows you to
            #      *                     deny access to topic strings rather than by pattern. For
            #      *                     example, you may use MOSQ_ACL_SUBSCRIBE to deny
            #      *                     subscriptions to '#', but allow all topics in
            #      *                     MOSQ_ACL_READ. This allows clients to subscribe to any
            #      *                     topic they want, but not discover what topics are in use
            #      *                     on the server.
            #      *  MOSQ_ACL_READ      when a message is about to be sent to a client (i.e. whether
            #      *                     it can read that topic or not).
            #
            #     The main difference is that subscribe is checked at first, when a client connects and tells the broker it
            #     wants to subscribe to some topic, while read is checked when an actual message is being published to that
            #     topic, which makes it particular. So in practice you could deny general subscriptions such as # by returning
            #     false from the acl check when you receive MOSQ_ACL_SUBSCRIBE, but allow any particular one by returning true
            #     on MOSQ_ACL_READ. Please take this into consideration when designing your ACL records on every backend.

            # - from now on we only allow READ access, all other access levels are denied for non-superusers
            # - subscribing is only allowed for resources to which user has read access too
            requested_access = int(params['acc'])
            if requested_access not in [1, 4] :  # NONE = 0, READ = 1, WRITE = 2, SUBSCRIBE = 4
                instead_got = {0: "0/NONE", 2: "2/WRITE"}.get(requested_access, requested_access)
                log.info(f"Access denied (only 1/READ or 4/SUBSCRIBE allowed, requested access: {instead_got})")
                return "Access denied", 401

            # only 'changed/#' can actually be read by normal users:
            if params['topic'][:8] != 'changed/':
                log.info("Access denied (wrong topic)")
                return "Access denied", 401
            resource = params['topic'][8:]  # remove 'changed/' from the start of the topic to get the resource
            resource = resource.rstrip('/')

            # finally, make sure user has access rights:
            user_id = received_jwt.data['user_id']
            is_allowed = Permission.is_access_allowed(
                user_id=user_id,
                resource=resource,
                method='GET',  # users can only request read access (apart from backend, which is superuser anyway)
            )
            if is_allowed:
                return "", 200

            log.info("Access denied (permissions check failed for user '{}', url '{}', method 'GET')".format(user_id, resource))
            return "Access denied", 401

        return "Invalid endpoint", 404

    except AuthFailedException as ex:
        log.info(f"Authentication failed: {str(ex)}")
        return "Access denied", 401
    except:
        log.exception("Exception while checking access rights")
        return "Access denied", 401