Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP authentication, API key and some CLI improvements #250

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions config.sample
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ log_level = WARNING
; for testing purposes. Default: on
;mount_api = on

; Require API key for authentication. Default: yes
require_api_key = yes

; Enable the administrative web interface. Default: on
;mount_webui = on

Expand Down Expand Up @@ -87,3 +90,21 @@ default_transcode_target = mp3
;mp3 = audio/mpeg
;ogg = audio/vorbis

[ldap]
; Server URL. Default: none
;url = ldapi://%2Frun%2Fslapd%2Fldapi
;url = ldap://127.0.0.1:389

; Bind credentials. Default: none
;bind_dn = cn=username,dc=example,dc=org
;bind_pw = password

; Base DN. Default: none
;base_dn = ou=Users,dc=example,dc=org

; User filter. The variable '{username}' is used for filtering. Default: none
;user_filter = (&(objectClass=inetOrgperson)(uid={username}))

; Mail attribute. Default: mail
;mail_attr = mail

15 changes: 13 additions & 2 deletions supysonic/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import uuid
from flask import request
from flask import Blueprint
from flask import current_app
from peewee import IntegrityError

from ..db import ClientPrefs, Folder
Expand Down Expand Up @@ -56,10 +57,16 @@ def decode_password(password):

@api.before_request
def authorize():
require_api_key = current_app.config["WEBAPP"]["require_api_key"]

if request.authorization:
user = UserManager.try_auth(
user = UserManager.try_auth_api(
request.authorization.username, request.authorization.password
)
if user is None and not require_api_key:
user = UserManager.try_auth(
request.authorization.username, request.authorization.password
)
if user is not None:
request.user = user
return
Expand All @@ -69,7 +76,11 @@ def authorize():
password = request.values["p"]
password = decode_password(password)

user = UserManager.try_auth(username, password)
user = UserManager.try_auth_api(username, password)
if user is None and not require_api_key:
user = UserManager.try_auth(
request.authorization.username, request.authorization.password
)
if user is None:
raise Unauthorized()

Expand Down
125 changes: 109 additions & 16 deletions supysonic/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import click
import time
import uuid

from click.exceptions import ClickException

Expand Down Expand Up @@ -236,20 +237,20 @@ def user():
def user_list():
"""Lists users."""

click.echo("Name\t\tAdmin\tJukebox\tEmail")
click.echo("----\t\t-----\t-------\t-----")
click.echo("Name\t\tLDAP\tAdmin\tJukebox\tEmail")
click.echo("----\t\t-----\t-----\t-------\t-----")
for u in User.select():
click.echo(
"{: <16}{}\t{}\t{}".format(
u.name, "*" if u.admin else "", "*" if u.jukebox else "", u.mail
"{: <16}{}\t{}\t{}\t{}".format(
u.name, "*" if u.ldap else "", "*" if u.admin else "", "*" if u.jukebox else "", u.mail
)
)


@user.command("add")
@click.argument("name")
@click.password_option("-p", "--password", help="Specifies the user's password")
@click.option("-e", "--email", default="", help="Sets the user's email address")
@click.option("-e", "--email", default=None, help="Sets the user's email address")
def user_add(name, password, email):
"""Adds a new user.

Expand All @@ -262,10 +263,42 @@ def user_add(name, password, email):
raise ClickException(str(e)) from e


@user.group("edit")
def user_edit():
"""User edit commands"""
pass


@user_edit.command("email")
@click.argument("name")
@click.option("-e", "--email", prompt=True, default="", help="Sets the user's email address")
def user_edit_email(name, email):
"""Changes an user's email.


NAME is the name (or login) of the user to edit.
"""

user = User.get(name=name)
if user is None:
raise ClickException("No such user")

if user.ldap:
raise ClickException("Unavailable for LDAP users")

if email == "":
email = None

if user.mail != email:
user.mail = email
user.save()
click.echo(f"Updated user '{name}'")


@user.command("delete")
@click.argument("name")
def user_delete(name):
"""Deletes a user.
"""Deletes an user.

NAME is the name of the user to delete.
"""
Expand Down Expand Up @@ -295,7 +328,7 @@ def _echo_role_change(username, name, value):
help="Grant or revoke jukebox rights",
)
def user_roles(name, admin, jukebox):
"""Enable/disable rights for a user.
"""Enable/disable rights for an user.

NAME is the login of the user to which grant or revoke rights.
"""
Expand All @@ -314,27 +347,31 @@ def user_roles(name, admin, jukebox):
user.save()


@user.command("changepass")
@user_edit.command("password")
@click.argument("name")
@click.password_option("-p", "--password", help="New password")
def user_changepass(name, password):
"""Changes a user's password.
"""Changes an user's password.

NAME is the login of the user to which change the password.
"""

try:
UserManager.change_password2(name, password)
click.echo(f"Successfully changed '{name}' password")
except User.DoesNotExist as e:
raise ClickException(f"User '{name}' does not exist.") from e
user = User.get(name=name)
if user is None:
raise ClickException(f"User '{name}' does not exist.")

if user.ldap:
raise ClickException("Unavailable for LDAP users")

UserManager.change_password2(name, password)
click.echo(f"Successfully changed '{name}' password")

@user.command("rename")

@user_edit.command("username")
@click.argument("name")
@click.argument("newname")
def user_rename(name, newname):
"""Renames a user.
"""Renames an user.

User NAME will then be known as NEWNAME.
"""
Expand All @@ -361,6 +398,62 @@ def user_rename(name, newname):
click.echo(f"User '{name}' renamed to '{newname}'")


@user.group("apikey")
def user_apikey():
"""User API key commands"""
pass


@user_apikey.command("show")
@click.argument("name")
def user_apikey_show(name):
"""Shows the API key of an user.

NAME is the name (or login) of the user to show the API key.
"""

user = User.get(name=name)
if user is None:
raise ClickException(f"User '{name}' does not exist.")

click.echo(f"'{name}' API key: {user.api_key}")


@user_apikey.command("new")
@click.argument("name")
def user_apikey_new(name):
"""Generates a new API key for an user.

NAME is the name (or login) of the user to generate the API key for.
"""

user = User.get(name=name)
if user is None:
raise ClickException(f"User '{name}' does not exist.")

user.api_key = str(uuid.uuid4()).replace("-", "")
user.save()
click.echo(f"Updated '{name}' API key")


@user_apikey.command("delete")
@click.argument("name")
def user_apikey_delete(name):
"""Deletes the API key of an user.

NAME is the name (or login) of the user to delete the API key.
"""

user = User.get(name=name)
if user is None:
raise ClickException(f"User '{name}' does not exist.")

if user.api_key is not None:
user.api_key = None
user.save()
click.echo(f"Deleted '{name}' API key")


def main():
config = IniConfig.from_common_locations()
init_database(config.BASE["database_uri"])
Expand Down
9 changes: 9 additions & 0 deletions supysonic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class DefaultConfig:
"log_level": "WARNING",
"mount_webui": True,
"mount_api": True,
"require_api_key": True,
"index_ignored_prefixes": "El La Le Las Les Los The",
}
DAEMON = {
Expand All @@ -51,6 +52,14 @@ class DefaultConfig:
LASTFM = {"api_key": None, "secret": None}
TRANSCODING = {}
MIMETYPES = {}
LDAP = {
"url": None,
"bind_dn": None,
"bind_pw": None,
"base_dn": None,
"user_filter": None,
"mail_attr": "mail"
}

def __init__(self):
current_config = self
Expand Down
7 changes: 5 additions & 2 deletions supysonic/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,12 +428,15 @@ class User(_Model):
id = PrimaryKeyField()
name = CharField(64, unique=True)
mail = CharField(null=True)
password = FixedCharField(40)
salt = FixedCharField(6)
password = FixedCharField(40, null=True)
salt = FixedCharField(6, null=True)

ldap = BooleanField(default=False)
admin = BooleanField(default=False)
jukebox = BooleanField(default=False)

api_key = CharField(32, null=True)

lastfm_session = FixedCharField(32, null=True)
lastfm_status = BooleanField(
default=True
Expand Down
Loading