Giter Club home page Giter Club logo

getsentry-ldap-auth's Introduction

sentry-ldap-auth

A Django custom authentication backend for Sentry. This module extends the functionality of django-auth-ldap with Sentry specific features.

Features

  • Users created by this backend are managed users. Managed fields are not editable through the Sentry account page.
  • Users may be auto-added to an Organization upon creation.

Prerequisites

Versions 2.0 and newer require Sentry 8. For Sentry 7 support, use the 1.1 release

Installation

To install, simply add sentry-ldap-auth to your requirements.txt for your Sentry environment (or pip install sentry-ldap-auth).

Configuration

This module extends the django-auth-ldap and all the options it provides are supported (up to v1.2.x, at least).

To configure Sentry to use this module, add sentry_ldap_auth.backend.SentryLdapBackend to your AUTHENTICATION_BACKENDS in your sentry.conf.py, like this:

AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
    'sentry_ldap_auth.backend.SentryLdapBackend',
)

Then, add any applicable configuration options. Depending on your environment, and especially if you are running Sentry in containers, you might consider using python-decouple so you can set these options via environment variables.

sentry-ldap-auth Specific Options

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'My Organization Name'

Auto adds created user to the specified organization (matched by name) if it exists.

AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'

Role type auto-added users are assigned. Valid values in a default installation of Sentry are 'member', 'admin', 'manager' & 'owner'. However, custom roles can also be added to Sentry, in which case these are also valid.

AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True

Whether auto-created users should be granted global access within the default organization.

AUTH_LDAP_SENTRY_SUBSCRIBE_BY_DEFAULT = False

Whether new users should be subscribed to any new projects by default. Disabling this is useful for large organizations where a subscription to each project might be spammy.

AUTH_LDAP_SENTRY_USERNAME_FIELD = 'uid'

Specify which attribute to use as the Sentry username, if different from what the user enters on the login page. You can use this to prevent multiple accounts from being created when your AUTH_LDAP_USER_SEARCH allows users to log in with different usernames (e.g. (|(uid=%(user))(mail=%(user)))). If multiple values exist for the attribute, the first value will be used.

AUTH_LDAP_DEFAULT_EMAIL_DOMAIN = 'example.com'

Default domain to append to username as the Sentry user's e-mail address when the LDAP user has no mail attribute.

Sentry Options

SENTRY_MANAGED_USER_FIELDS = ('email', 'first_name', 'last_name', 'password', )

Fields which managed users may not modify through the Sentry accounts view. Applies to all managed accounts.

Example Configuration

import ldap
from django_auth_ldap.config import LDAPSearch, GroupOfUniqueNamesType

AUTH_LDAP_SERVER_URI = 'ldap://my.ldapserver.com'
AUTH_LDAP_BIND_DN = ''
AUTH_LDAP_BIND_PASSWORD = ''

AUTH_LDAP_USER_SEARCH = LDAPSearch(
    'dc=domain,dc=com',
    ldap.SCOPE_SUBTREE,
    '(mail=%(user)s)',
)

AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    '',
    ldap.SCOPE_SUBTREE,
    '(objectClass=groupOfUniqueNames)'
)

AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType()
AUTH_LDAP_REQUIRE_GROUP = None
AUTH_LDAP_DENY_GROUP = None

AUTH_LDAP_USER_ATTR_MAP = {
    'name': 'cn',
    'email': 'mail'
}

AUTH_LDAP_FIND_GROUP_PERMS = False
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'My Organization Name'
AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_GROUP_ROLE_MAPPING = {
    'owner': ['sysadmins'],
    'admin': ['devleads'],
    'member': ['developers', 'seniordevelopers']
}
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True
AUTH_LDAP_SENTRY_USERNAME_FIELD = 'uid'

AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
    'sentry_ldap_auth.backend.SentryLdapBackend',
)

import logging
logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.setLevel('DEBUG')

getsentry-ldap-auth's People

Contributors

barronhagerman avatar chadkillingsworth avatar creitve avatar jamesob avatar jellyfrog avatar kmlebedev avatar loisaidasam avatar matejc avatar metala avatar packetperception avatar ralish avatar ralphje avatar rodgomes avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

getsentry-ldap-auth's Issues

Update sentry user role if LDAP group membership is changed

This backend checks for user's group only if the user is not yet a member of an organization. So if the membership was changed in LDAP, Sentry won't know about it leaving the user with same role as it was on create step.
I'd like to manage users roles via LDAP and be sure the Sentry will reflect these changes as well.

'User' object has no attribute 'groups'

Following the example configuration and the configuration in the README, I'm getting the following error:

I'm running the latest version of Sentry

127.0.0.1 - - [17/Sep/2021:18:55:34 +0000] "POST /auth/login/sentry/ HTTP/1.0" 200 12233 "http://sentry.<redacted>.com/auth/login/sentry/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:92.0) Gecko/20100101 Firefox/92.0"
Traceback (most recent call last):
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/sentry/sentry/lib/python3.6/site-packages/sentry/../sentry_sdk/integrations/django/views.py", line 67, in sentry_wrapped_callback
    return callback(request, *args, **kwargs)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/views/generic/base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/sentry/sentry/lib/python3.6/site-packages/sentry/web/frontend/base.py", line 228, in dispatch
    return self.handle(request, *args, **kwargs)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/usr/lib64/python3.6/contextlib.py", line 52, in inner
    return func(*args, **kwds)
  File "/home/sentry/sentry/lib/python3.6/site-packages/sentry/web/frontend/auth_organization_login.py", line 68, in handle
    response = self.handle_basic_auth(request, organization=organization)
  File "/home/sentry/sentry/lib/python3.6/site-packages/sentry/web/frontend/auth_login.py", line 199, in handle_basic_auth
    elif login_form.is_valid():
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/forms/forms.py", line 185, in is_valid
    return self.is_bound and not self.errors
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/forms/forms.py", line 180, in errors
    self.full_clean()
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/forms/forms.py", line 382, in full_clean
    self._clean_form()
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/forms/forms.py", line 409, in _clean_form
    cleaned_data = self.clean()
  File "/home/sentry/sentry/lib/python3.6/site-packages/sentry/web/forms/accounts.py", line 129, in clean
    self.user_cache = authenticate(username=username, password=password)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django/contrib/auth/__init__.py", line 73, in authenticate
    user = backend.authenticate(request, **credentials)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django_auth_ldap/backend.py", line 150, in authenticate
    user = self.authenticate_ldap_user(ldap_user, password)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django_auth_ldap/backend.py", line 210, in authenticate_ldap_user
    return ldap_user.authenticate(password)
  File "/home/sentry/sentry/lib/python3.6/site-packages/django_auth_ldap/backend.py", line 350, in authenticate
    self._get_or_create_user()
  File "/home/sentry/sentry/lib/python3.6/site-packages/django_auth_ldap/backend.py", line 617, in _get_or_create_user
    self._mirror_groups()
  File "/home/sentry/sentry/lib/python3.6/site-packages/django_auth_ldap/backend.py", line 720, in _mirror_groups
    current_group_names = frozenset(self._user.groups.values_list('name', flat=True).iterator())
AttributeError: 'User' object has no attribute 'groups'
18:55:40 [ERROR] django.request: Internal Server Error: /auth/login/sentry/ (status_code=500 request=<WSGIRequest: POST '/auth/login/sentry/'>)

Configuration:

AUTH_LDAP_SERVER_URI = "ldaps://ip_address"
AUTH_LDAP_START_TLS = False
AUTH_LDAP_MIRROR_GROUPS = True
AUTH_LDAP_GLOBAL_OPTIONS = {
    ldap.OPT_X_TLS_REQUIRE_CERT: False
}
AUTH_LDAP_BIND_DN = "uid=removed_for_security, cn=users, cn=accounts, dc=idm, dc=<redacted>, dc=com"
AUTH_LDAP_BIND_PASSWORD = "password"
AUTH_LDAP_USER_SEARCH = LDAPSearch(
    "cn=users, cn=accounts, dc=idm, dc=<redacted>, dc=com",
    ldap.SCOPE_SUBTREE, "(uid=%(user)s)"
)


AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    "cn=groups, cn=accounts, dc=idm, dc=<redacted>, dc=com",
    ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
)


AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
AUTH_LDAP_REQUIRE_GROUP = None
AUTH_LDAP_DENY_GROUP = None

AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail"
}

AUTH_LDAP_FIND_GROUP_PERMS = False
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'LW NOC'
AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_GROUP_ROLE_MAPPING = {
    'owner': ['networking'],
}
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True
AUTH_LDAP_SENTRY_USERNAME_FIELD = 'uid'

AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
    'sentry_ldap_auth.backend.SentryLdapBackend',
)

Updated passwords in LDAP

Hello!
Got problem like this:
Sentry 21, integration with LDAP working like charm.
BUT, when user update thier password in LDAP (for security reason) and logout from sentry - he cant login with new password )and old password doesnt work too)
Any help?...

Logging in with LDAP user fails when user has multiple emails in right conditions

Relates to the currently open PR #29 and older PR #17.

Logging in with a user accounts fails under the right conditions if the user that logs in has multiple email addresses, because a wrong address can be fetched and an update can be attempted, but the update attempts to save the wrong UserEmail with conflicting email when the email value exists in another UserEmail object.

Steps to reproduce:

  1. there is an User with at least two UserEmail objects with e.g. the following addresses:
  1. the User is trying to log in with [email protected] address
  2. the LDAP backend attempts to fetch the UserEmail objects
  3. the LDAP fetches two addresses and uses a randomly selected one for storing the user email; in the buggy case it uses e.g. [email protected].
  4. the LDAP backend attempts to update the randomly selected UserEmail with existing [email protected] value with the new [email protected], but that UserEmail already exists with the same (pk, email='[email protected]') combination, which leads to the database throwing an IntegrityError and the user getting a failed login

An example traceback follows. Please note that the line number errors are not from the current master branch but from 2.5 release tag.

web_1        | 172.19.0.1 - - [19/Apr/2018:13:14:58 +0000] "GET / HTTP/1.0" 302 513 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
web_1        | 172.19.0.1 - - [19/Apr/2018:13:14:58 +0000] "GET /auth/login/ HTTP/1.0" 302 614 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
web_1        | 172.19.0.1 - - [19/Apr/2018:13:14:58 +0000] "GET /auth/login/example/ HTTP/1.0" 200 10742 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
web_1        | Traceback (most recent call last):
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 112, in get_response
web_1        |     response = wrapped_callback(request, *callback_args, **callback_kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/views/generic/base.py", line 69, in view
web_1        |     return self.dispatch(request, *args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 57, in wrapped_view
web_1        |     return view_func(*args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/web/frontend/base.py", line 212, in dispatch
web_1        |     return self.handle(request, *args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/views/decorators/cache.py", line 52, in _wrapped_view_func
web_1        |     response = view_func(request, *args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/transaction.py", line 371, in inner
web_1        |     return func(*args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/web/frontend/auth_organization_login.py", line 69, in handle
web_1        |     response = self.handle_basic_auth(request, organization)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/web/frontend/auth_login.py", line 118, in handle_basic_auth
web_1        |     elif login_form.is_valid():
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/forms/forms.py", line 129, in is_valid
web_1        |     return self.is_bound and not bool(self.errors)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/forms/forms.py", line 121, in errors
web_1        |     self.full_clean()
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/forms/forms.py", line 274, in full_clean
web_1        |     self._clean_form()
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/forms/forms.py", line 300, in _clean_form
web_1        |     self.cleaned_data = self.clean()
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/web/forms/accounts.py", line 155, in clean
web_1        |     self.user_cache = authenticate(username=username, password=password)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/contrib/auth/__init__.py", line 49, in authenticate
web_1        |     user = backend.authenticate(**credentials)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 173, in authenticate
web_1        |     user = ldap_user.authenticate(password)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 350, in authenticate
web_1        |     self._get_or_create_user()
web_1        |   File "/usr/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 577, in _get_or_create_user
web_1        |     self._user, created = self.backend.get_or_create_user(username, self)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry_ldap_auth/backend.py", line 42, in get_or_create_user
web_1        |     userEmail.save()
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
web_1        |     force_update=force_update, update_fields=update_fields)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
web_1        |     updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 635, in _save_table
web_1        |     forced_update)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 679, in _do_update
web_1        |     return filtered._update(values) > 0
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/models/query.py", line 510, in _update
web_1        |     return query.get_compiler(self.db).execute_sql(None)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 980, in execute_sql
web_1        |     cursor = super(SQLUpdateCompiler, self).execute_sql(result_type)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 786, in execute_sql
web_1        |     cursor.execute(sql, params)
web_1        |   File "/usr/local/lib/python2.7/site-packages/raven/contrib/django/client.py", line 112, in execute
web_1        |     return real_execute(self, sql, params)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
web_1        |     return self.cursor.execute(sql, params)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
web_1        |     six.reraise(dj_exc_type, dj_exc_value, traceback)
web_1        |   File "/usr/local/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
web_1        |     return self.cursor.execute(sql, params)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 80, in inner
web_1        |     raise_the_exception(self.db, e)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 78, in inner
web_1        |     return func(self, *args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 22, in inner
web_1        |     return func(self, *args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 101, in inner
web_1        |     six.reraise(exc_info[0], exc_info[0](msg), exc_info[2])
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 94, in inner
web_1        |     return func(self, sql, *args, **kwargs)
web_1        |   File "/usr/local/lib/python2.7/site-packages/sentry/db/postgres/base.py", line 39, in execute
web_1        |     return self.cursor.execute(sql, params)
web_1        | IntegrityError: IntegrityError('duplicate key value violates unique constraint "sentry_useremail_user_id_469ffbb142507df2_uniq"\nDETAIL:  Key (user_id, email)=(13, [email protected]) already exists.\n',)
web_1        | SQL: UPDATE "sentry_useremail" SET "user_id" = %s, "email" = %s, "validation_hash" = %s, "date_hash_added" = %s, "is_verified" = %s WHERE "sentry_useremail"."id" = %s
web_1        | 13:14:59 [ERROR] django.request: Internal Server Error: /auth/login/example/ (status_code=500 request=<WSGIRequest: POST u'/auth/login/example/'>)

The erronous line is here:

https://github.com/Banno/getsentry-ldap-auth/blob/master/sentry_ldap_auth/backend.py#L39

There is an suggested fix at:

https://github.com/ralphje/getsentry-ldap-auth/blob/9b5d356e46ada90843552dc99ee8363071944a8f/sentry_ldap_auth/backend.py

The only problem with the fix is that it changes the empty email identifier from ' ' to '' but does not remove old empty UserEmails that have been created with or set to ' ', creating potential problems in the populated databases. Old getsentry-ldap-auth email addresses won't work with this fix.

An alternative fix would be:

# use Q for fetching empty UserEmails with '' or ' ' as email value 
from django.db.models import Q

class SentryLdapBackend(LDAPBackend):
    def get_or_create_user(self, username, ldap_user):
        # ...
        try:    
            from sentry.models import (UserEmail)
        except ImportError:
            pass
        else:
            if 'mail' in ldap_user.attrs:
                email = ldap_user.attrs.get('mail')[0]        
            elif hasattr(settings, 'AUTH_LDAP_DEFAULT_EMAIL_DOMAIN'):
                email = username + '@' + settings.AUTH_LDAP_DEFAULT_EMAIL_DOMAIN
            else:
                email = ''

            if email:
                # note this change, which also removes unnecessary empty UserEmails,
                # but only in the case where we have a better alternative
                UserEmail.objects.filter(user=user, Q(email='') | Q(email=' ')).delete()
                UserEmail.objects.get_or_create(user=user, email=email)
        # ...

This alternative would allow a few things:

  • User not having empty emails in the Sentry database
  • User not getting his or her UserEmail objects overridden by getsentry-auth-ldap
  • Backwards compatibility for empty ' ' ("empty" email for getsentry-ldap-auth) and '' (empty email for Sentry) addresses for the user.

The current implementation just randomly overwrites the first UserEmail address is fetches if the user does not have a matching UserEmail but has something in the database.

The new implementation on the other hand just cleans up old empty email addresses from the database and inserts a new clean email if there is one available.

Users not getting added to organization after log in

From the logs, I can see that authentication is successful; but the user is not added to any organization.

This is the log:

sentry 18:12:24 [DEBUG] django_auth_ldap: search_s('ou=users,dc=company,dc=net', 2, '(uid=%(user)s)') returned 1 objects: uid=myuserid,ou=users,dc=company,dc=net (request_id=u'cfcd0ea7-ad83-4c38-94c1-49f75bce07cd')                   │
│ sentry 18:12:24 [DEBUG] django_auth_ldap: search_s('ou=users,dc=company,dc=net', 2, '(uid=%(user)s)') returned 1 objects: uid=myuserid,ou=users,dc=company,dc=net (request_id=u'cfcd0ea7-ad83-4c38-94c1-49f75bce07cd')                   │
│ sentry 18:12:24 [DEBUG] django_auth_ldap: Populating Django user myuserid (request_id=u'cfcd0ea7-ad83-4c38-94c1-49f75bce07cd')                                                                                                           │
│ sentry 18:12:24 [DEBUG] django_auth_ldap: Populating Django user myuserid (request_id=u'cfcd0ea7-ad83-4c38-94c1-49f75bce07cd')                                                                                                           │
│ sentry 18:12:24 [WARNING] django_auth_ldap: uid=myuserid,ou=users,dc=company,dc=net does not have a value for the attribute user (request_id=u'cfcd0ea7-ad83-4c38-94c1-49f75bce07cd')                                                    │
│ sentry 18:12:24 [WARNING] django_auth_ldap: uid=myuserid,ou=users,dc=company,dc=net does not have a value for the attribute user (request_id=u'cfcd0ea7-ad83-4c38-94c1-49f75bce07cd')                                                    │
│ sentry 18:12:24 [INFO] sentry.auth: user.auth.success (username=u'myuserid' organization_id=1L request_id=u'cfcd0ea7-ad83-4c38-94c1-49f75bce07cd' ip_address=u'ip.add.re.ss')                                                          │
│ sentry 10.4.82.9 - - [17/Dec/2019:18:12:24 +0000] "POST /auth/login/sentry/ HTTP/1.1" 302 1001 "https://sentry.company.net/auth/login/sentry/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebK │
│ it/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"                                                                                                                                                                        │
│ sentry 18:12:24 [INFO] sentry.web.frontend.base: User is not a member of any organizations (request_id=u'53dd66a0-0087-4966-9ea2-d87acc7c9247')

Env variables set

LDAP_DEFAULT_SENTRY_ORGANIZATION=u'Sentry'
AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_SUBSCRIBE_BY_DEFAULT = True
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True

Authentication backend is also correct:

    AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
        'sentry_ldap_auth.backend.SentryLdapBackend',
    )

I'm not sure if there's some issue with my configurations or it doesn't work in general.

Sentry version 10 Support

Hello, i know sentry 10 is just out for a month or so but I would like to know if the LDAP module should work with it?

I just tried to Install it on a fresh sentry 10 Installation on centos 7 and I'm unable to get of to Work.

Could be me and I'm doing something wrong but I wanted to ask to make sure.

Thanks

Feature request - Allow global "all orgs" added

At present AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION=='foo' can be used to have a user added to a single org when they login. Systems with multiple organisations would benefit from the following extension of this feature.

Ideally this feature can accommodate two additions usage modes:

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION==['foo', 'bar', 'alice']

Join all users to the three orgs in the list above

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION=='*'

Join all users to all orgs configured in system (wildcard)

Not able to login with Sentry 20

Dear getsentry-ldap-auth developers and another users,

I'am not able to login with Sentry 20 with python 3 (Sentry onpremise version https://github.com/getsentry/onpremise/releases/tag/20.11.1)

The installation it's ok, but when I try to login using the ldap credentials, I am not able to login. No log is generated in ldap2.log

sentry dockerfile (for the installation)

ARG SENTRY_IMAGE
ARG SENTRY_PYTHON3
FROM ${SENTRY_IMAGE}${SENTRY_PYTHON3:+-py3}

COPY . /usr/src/sentry

# Hook for installing additional plugins
RUN apt-get update && apt-get install -y build-essential libsasl2-dev python-dev libldap2-dev libssl-dev; gcc --version;
RUN pip install python-ldap sentry-ldap-auth
RUN if [ -s /usr/src/sentry/requirements.txt ]; then pip install -r /usr/src/sentry/requirements.txt; fi

requeriments.txt (for the installation)

# Add plugins here
sentry-ldap-auth

sentry.conf.py (remplazing sentry_ldap_server , sentry_ldap_bind_dn , sentry_ldap_bind_password with my values)

AUTH_LDAP_SERVER_URI = 'ldap://{{ sentry_ldap_server }}'
AUTH_LDAP_BIND_DN = '{{ sentry_ldap_bind_dn }}'
AUTH_LDAP_BIND_PASSWORD = '{{ sentry_ldap_bind_password }}'

AUTH_LDAP_USER_SEARCH = LDAPSearch(
    'DC=n,DC=local',
    ldap.SCOPE_SUBTREE,
    u"(sAMAccountName=%(user)s)",
)

AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    '',
    ldap.SCOPE_SUBTREE,
    u'(objectClass=group)'
)



AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType()
AUTH_LDAP_REQUIRE_GROUP = None
AUTH_LDAP_DENY_GROUP = None
AUTH_LDAP_MIRROR_GROUPS = False  # does not work with Sentry

AUTH_LDAP_USER_ATTR_MAP = {
    "username": "sAMAccountName",
    "first_name": u"givenName",
    "last_name": u"sn",
    "email": "mail",
}

AUTH_LDAP_CONNECTION_OPTIONS = {
    ldap.OPT_DEBUG_LEVEL: 0,
    ldap.OPT_REFERRALS: 0,
}


AUTH_LDAP_FIND_GROUP_PERMS = False
AUTH_LDAP_CACHE_GROUPS = False
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'Softlab'
AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_GROUP_ROLE_MAPPING = {
    'owner': ['S 2093'],
    'admin': ['S 2093'],
    'member': ['S  2093']
}
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = False
AUTH_LDAP_SENTRY_SUBSCRIBE_BY_DEFAULT = False
AUTH_LDAP_DEFAULT_EMAIL_DOMAIN = 'example.com'
# AUTH_LDAP_SENTRY_USERNAME_FIELD = 'sAMAccountName'

AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
    'sentry_ldap_auth.backend.SentryLdapBackend',
)

logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.addHandler(logging.FileHandler(r"/home/sentry/ldap2.log"))
logger.setLevel('DEBUG')

No log is generated in /home/sentry/ldap2.log, and no errors either.

looks like sentry is not using SentryLdapBackend at all

Are you tried this plugins with Sentry 20 with python 3 (Sentry onpremise version https://github.com/getsentry/onpremise/releases/tag/20.11.1)? Does you have any recommendation?

Thank you very much

getsentry-ldap-auth not works with django-auth-ldap 1.2.14+

How to reproduce:

  • Install latest Sentry and getsentry-ldap-auth (with django-auth-ldap 1.2.14 or later)
  • Configure LDAP authentification
  • Login to the Sentry via LDAP user
  • Logout from the Sentry
  • Login again with the same user

Expected result

  • You should successfully login second time

Actual result

  • "Wrong username or password" error

Logs

First, successful login:

05:01:30 [DEBUG] django_auth_ldap: search_s('ou=users,dc=orgz', 2, '(uid=%(user)s)') returned 1 objects: uid=a.markelov,ou=users,dc=orgz
05:01:30 [DEBUG] django_auth_ldap: search_s('ou=users,dc=orgz', 2, '(uid=%(user)s)') returned 1 objects: uid=a.markelov,ou=users,dc=orgz
05:01:30 [DEBUG] django_auth_ldap: Populating Django user a.markelov
05:01:30 [DEBUG] django_auth_ldap: Populating Django user a.markelov
05:01:30 [INFO] sentry.auth: user.auth.success (username=u'a.markelov' ip_address=u'178.218.107.255')

Second, unsucsessful login:

172.18.0.1 - - [06/Oct/2017:05:01:35 +0000] "GET /auth/login/ HTTP/1.0" 200 10783 "https://sentry.unitedtraders.work" "Go-http-client/1.1"
05:01:41 [DEBUG] django_auth_ldap: search_s('uid=a.markelov,ou=users,dc=orgz', 0, '(objectClass=*)') returned 0 objects: 
05:01:41 [DEBUG] django_auth_ldap: search_s('uid=a.markelov,ou=users,dc=orgz', 0, '(objectClass=*)') returned 0 objects: 
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 350, in authenticate
    self._get_or_create_user()
  File "/usr/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 577, in _get_or_create_user
    self._user, created = self.backend.get_or_create_user(username, self)
  File "/usr/local/lib/python2.7/site-packages/sentry_ldap_auth/backend.py", line 38, in get_or_create_user
    if 'mail' in ldap_user.attrs:
TypeError: argument of type 'NoneType' is not iterable
05:01:41 [ERROR] django_auth_ldap: Caught Exception while authenticating a.markelov
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 350, in authenticate
    self._get_or_create_user()
  File "/usr/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 577, in _get_or_create_user
    self._user, created = self.backend.get_or_create_user(username, self)
  File "/usr/local/lib/python2.7/site-packages/sentry_ldap_auth/backend.py", line 38, in get_or_create_user
    if 'mail' in ldap_user.attrs:
TypeError: argument of type 'NoneType' is not iterable
05:01:41 [ERROR] django_auth_ldap: Caught Exception while authenticating a.markelov

This behavior exists with django-auth-ldap 1.2.14, 1.2.15, 1.2.16. I think this is related to Under search/bind mode, the user's DN will now be cached for performance (https://bitbucket.org/psagers/django-auth-ldap/src/569b6ca46ce8a27af2b6d712caea09b2ed6c894e/CHANGES?at=default&fileviewer=file-view-default#CHANGES-41).

As a workaround, I suggest to pin django-auth-ldap requirement.

Group role mapping

Hi!
I have two groups:

  • sentry_admin
  • sentry_owner
    Where each group have members which are memberof each group.
# sentry_admin, groups, site.io
dn: cn=sentry_admin,ou=groups,dc=site,dc=io
objectClass: groupOfNames
cn: sentry_admin_user
member: uid=sentry_admin_user,ou=people,dc=site,dc=io
# sentry_owner, groups, site.io
dn: cn=sentry_owner,ou=groups,dc=site,dc=io
objectClass: groupOfNames
cn: sentry_owner_user
member: uid=sentry_owner_user,ou=people,dc=site,dc=io

My current ldap configuration is as follows

AUTH_LDAP_USER_SEARCH = LDAPSearch(
    "ou=people,dc=site,dc=io", 
    ldap.SCOPE_SUBTREE, 
    "(uid=%(user)s)"
)

AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    "ou=groups,dc=site,dc=io", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
)

AUTH_LDAP_SENTRY_USERNAME_FIELD = 'uid'
AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType()
AUTH_LDAP_REQUIRE_GROUP = None
AUTH_LDAP_DENY_GROUP = None

AUTH_LDAP_USER_ATTR_MAP = {
    'name': 'cn',
    'email': 'mail'
}

AUTH_LDAP_SENTRY_GROUP_ROLE_MAPPING = {
    'owner': ['cn=sentry_owner,ou=groups,dc=site,dc=io'],
    'admin': ['cn=sentry_admin,ou=groups,dc=site,dc=io']
}

AUTH_LDAP_FIND_GROUP_PERMS   = False
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'Sentry'
# AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True
AUTH_LDAP_SENTRY_SUBSCRIBE_BY_DEFAULT = True

User login works perfect, but always as "Member". Is there any way to approach a user management by its group? Not really sure if I'm using

AUTH_LDAP_SENTRY_GROUP_ROLE_MAPPING = {
    'owner': ['cn=sentry_owner,ou=groups,dc=site,dc=io'],
    'admin': ['cn=sentry_admin,ou=groups,dc=site,dc=io']
}

correctly.

Can't build on CentOS 7 : RUN pip install git+https://github.com/Banno/getsentry-ldap-auth.git

Env : CentOS Linux release 7.4.1708 (Core)

With these installed :

  • python-devel
  • openldap-devell
  • python-ldap.x86_64

How to reproduce:

git clone https://github.com/getsentry/onpremise.git

change requirements.txt like this :

# Add plugins here
sentry-ldap-auth

Hit error like this


......
    gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -DHAVE_SASL -DHAVE_TLS -DHAVE_LIBLDAP_R -DHAVE_LIBLDAP_R -DLDAPMODULE_VERSION=3.0.0 -DLDAPMODULE_AUTHOR=python-ldap project -DLDAPMODULE_LICENSE=Python style -IModules -I/usr/local/include/python2.7 -c Modules/LDAPObject.c -o build/temp.linux-x86_64-2.7/Modules/LDAPObject.o
    In file included from Modules/LDAPObject.c:8:0:
    Modules/constants.h:7:18: fatal error: lber.h: No such file or directory
     #include "lber.h"
                      ^
    compilation terminated.
    error: command 'gcc' failed with exit status 1

    ----------------------------------------
Command "/usr/local/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-n_e8sG/python-ldap/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-jDhsKa-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-n_e8sG/python-ldap/
The command '/bin/sh -c if [ -s requirements.txt ]; then pip install -r requirements.txt; fi' returned a non-zero code: 1
make: *** [build] Erreur 1
...........

Installation fails with docker and debian jessie

Hi. I tried to install the sentry ldap plugin on a sentry docker installation (by adding sentry-ldap-auth to requirements.txt). Docker version is 1.12.1.

When I try to build everything, it fails on the python-ldap pip installation, with this error code:

    Modules/errors.h:8:18: fatal error: lber.h: No such file or directory
     #include "lber.h"
                      ^
    compilation terminated.
    error: command 'gcc' failed with exit status 1

This is weird, because the package libldap2-dev is installed, the file lber.h is installed on my system. Also, if I try to install sentry-ldap-auth in a virtualenv in my host system, everything works well.

What do you think?

Get "No authentication providers are available" in sentry 8.14.1

Hi have copied the example configuration in sentry.conf.py and install the plugin via pip install sentry-ldap-auth. After I have restarted sentry I expect to see the new authentication provider at Manage -> Auth but instead I only get the message "No authentication providers are available." Is this a bug or did I make a mistake?

Logging does not work

Hello,

I'm using sentry (7.6.2) and sentry-ldap-auth (1.0), with:

[root@charlotte1 ~]# /sentry/env/bin/pip list | grep django
django-auth-ldap (1.2.7)
django-bitfield (1.7.1)
django-crispy-forms (1.4.0)
django-jsonfield (0.9.13)
django-paging (0.2.5)
django-picklefield (0.3.1)
django-recaptcha (1.0.4)
django-social-auth (0.7.28)
django-statsd-mozilla (0.3.14)
django-sudo (1.1.3)
django-templatetag-sugar (1.0)
djangorestframework (2.3.14)
pytest-django (2.8.0)

Also, I'm using in /etc/sentry/sentry.conf.py:

import logging
logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.setLevel('DEBUG')

But there is any logging generetady by the supervisord about the login

[root@charlotte1 ~]# egrep '\[|log' /etc/supervisord.d/sentry.ini
[program:sentry-web]
stdout_logfile=/logs/sentry/sentry-web.log
stderr_logfile=/logs/sentry/sentry-web-err.log
[program:sentry-worker]
stdout_logfile=/logs/sentry/sentry-worker.log
stderr_logfile=/logs/sentry/sentry-worker-err.log

There is something missing?

Thanks!

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION not working

Hi,

I'm trying to get this working in my docker sentry setup.
I managed to get the logging working basically. When I log in with ldap data the user gets created, but it's not added the organization.

My ldap configuration looks like this:

import ldap
from django_auth_ldap.config import LDAPSearch, GroupOfUniqueNamesType

AUTH_LDAP_GLOBAL_OPTIONS = env('AUTH_LDAP_GLOBAL_OPTIONS')

AUTH_LDAP_SERVER_URI = env('AUTH_LDAP_SERVER_URI')
AUTH_LDAP_START_TLS = Bool(env('AUTH_LDAP_START_TLS', False))
AUTH_LDAP_BIND_DN = env('AUTH_LDAP_BIND_DN')
AUTH_LDAP_BIND_PASSWORD = env('AUTH_LDAP_BIND_PASSWORD')

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'Sentry'
AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True

AUTH_LDAP_USER_SEARCH = LDAPSearch(
env('SEARCH_DN'),
ldap.SCOPE_SUBTREE,
'(mail=%(user)s)',
)

AUTH_LDAP_USER_ATTR_MAP = {
'name': 'cn',
'email': 'mail'
}

AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
'django_auth_ldap.backend.LDAPBackend',
)

As I said everything seems to be working, but the user is not getting added to the organization, which definitely exists in the database.

Anyone got an idea what's wrong?

I'm using Sentry 8.11 and latest sentry_ldap_auth.

Thank you

Sentry 20 !! Configuration error: ImportError: No module named ldap

I have installed Sentry using https://github.com/getsentry/sentry/releases/tag/20.9.0 I have started ./install.sh to prepare the environment and docker-compose up -d to start sentry.

My $SENTRY_INSTALL_DIR/sentry/requirements.txt:

# Add plugins here
sentry-ldap-auth

Also, I have appended $SENTRY_INSTALL_DIR/sentry/sentry.conf.py with required configuration, but after I am starting sentry I am getting:

ingest-consumer_1              | !! Configuration error: ImportError: No module named ldap
post-process-forwarder_1       | !! Configuration error: ImportError: No module named ldap
worker_1                       | !! Configuration error: ImportError: No module named ldap
cron_1                         | !! Configuration error: ImportError: No module named ldap
web_1                          | !! Configuration error: ImportError: No module named ldap

It seems that the plug-in is not being installed.

Incompatible w/ Sentry post role refactor

A huge refactor of Sentry's handling of roles landed in early November: The Great Role Refactor. This breaks compatibility with Sentry releases after this commit, including the recently released 8.0.0-rc1.

Fortunately, the changes required are minimal. I forked the repository and made the minimal changes to get everything working with the refactor for our own Sentry server, but without preserving any backwards compatibility (I just needed everything working again quickly). You can view them here, but you may prefer something which preserves backwards compatibility with earlier Sentry releases?

Happy to open a pull request though if you're interested or want to build off of these changes.

Unable to install, gcc missing.

I am unable to install in onpremise-21.1.0 due to missing gcc.

The pip install process fails after sentry-ldap-auth, when building python-ldap:

Successfully built sentry-ldap-auth
Failed to build python-ldap

The container being used does not contain gcc in container:

...
building '_ldap' extension
  creating build/temp.linux-x86_64-3.6
  creating build/temp.linux-x86_64-3.6/Modules
  gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -DHAVE_SASL -DHAVE_TLS -DHAVE_LIBLDAP_R -DHAVE_LIBLDAP_R -DLDAPMODULE_VERSION=3.3.1 -DLDAPMODULE_AUTHOR=python-ldap project -DLDAPMODULE_LICENSE=Python style -IModules -I/usr/local/include/python3.6m -c Modules/LDAPObject.c -o build/temp.linux-x86_64-3.6/Modules/LDAPObject.o
  unable to execute 'gcc': No such file or directory
  error: command 'gcc' failed with exit status 1
  ----------------------------------------
  ERROR: Failed building wheel for python-ldap
...

If this version of sentry is not supported, does anyone know if it is safe to rollback the version by running an older version of install.sh ?

Could not find a version that satisfies the requirement sentry>=8.0.0

I have sentry 8.0.0rc1. I ran pip install inside the sentry virtualenv.

$ pip install sentry-ldap-auth
[...]
Downloading/unpacking sentry>=8.0.0 (from sentry-ldap-auth)
  Could not find a version that satisfies the requirement sentry>=8.0.0 (from sentry-ldap-auth) (from versions: 7.7.1, 7.7.4, 8.0.0rc1, 2.0.0-Alpha1, 2.0.0-RC5, 2.0.0-RC6, 2.0.0-RC7, 2.0.0, 2.0.1, 2.0.2, 2.1.0, 2.1.1, 2.1.2, 2.1.3, 2.2.0, 2.2.1, 2.2.2, 2.2.3, 2.2.4, 2.2.5, 2.3.0, 2.3.1, 2.3.2, 2.4.0, 2.4.1, 2.4.2, 2.4.3, 2.4.4, 2.4.5, 2.4.6, 2.4.7, 2.5.0, 2.5.1, 2.5.2, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.8.1, 2.8.2, 2.9.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, 3.1.0, 3.1.1, 3.1.2, 3.1.3, 3.1.4, 3.2.0, 3.3.0, 3.3.1, 3.3.2, 3.4.0, 3.4.1, 3.4.2, 3.5.0, 3.5.1, 3.5.2, 3.5.3, 3.5.4, 3.5.5, 3.5.6, 3.5.7, 3.5.8, 3.5.9, 3.6.0, 3.6.1, 3.6.2, 3.6.3, 3.6.4, 3.7.0, 3.7.1, 3.7.2, 3.7.3, 3.7.4, 3.8.0, 3.8.1, 3.8.2, 4.0.0, 4.0.1, 4.0.10, 4.0.11, 4.0.12, 4.0.13, 4.0.14, 4.0.15, 4.0.16, 4.0.17, 4.0.2, 4.0.3, 4.0.4, 4.0.5, 4.0.6, 4.0.7, 4.0.8, 4.0.9, 4.1.0, 4.1.1, 4.1.2, 4.1.3, 4.1.4, 4.1.5, 4.1.6, 4.1.7, 4.10.0, 4.2.0, 4.2.1, 4.2.2, 4.2.4, 4.2.5, 4.3.0, 4.3.1, 4.3.2, 4.3.3, 4.4.0, 4.4.1, 4.4.2, 4.4.3, 4.4.4, 4.4.5, 4.4.6, 4.5.0, 4.5.1, 4.5.2, 4.5.3, 4.5.4.1, 4.5.4.2, 4.5.4, 4.5.5, 4.5.6, 4.5.7, 4.6.0, 4.7.0, 4.7.1, 4.7.2, 4.7.3, 4.7.4, 4.7.5, 4.7.6, 4.7.7, 4.7.8, 4.7.9, 4.8.0, 4.8.1, 4.8.2, 4.8.3, 4.8.4, 4.8.5, 4.8.6, 4.9.0, 4.9.1, 4.9.2, 4.9.3, 4.9.4, 4.9.5, 4.9.6, 4.9.7.1, 4.9.7, 4.9.8, 5.0.0, 5.0.1, 5.0.10, 5.0.11.1, 5.0.11, 5.0.12, 5.0.13, 5.0.14, 5.0.15, 5.0.16.1, 5.0.16, 5.0.17.1, 5.0.17.2, 5.0.17, 5.0.18.1, 5.0.18.2, 5.0.18, 5.0.19, 5.0.2, 5.0.20.1, 5.0.20, 5.0.21, 5.0.3, 5.0.4, 5.0.5, 5.0.6, 5.0.7, 5.0.8.1, 5.0.8, 5.0.9, 5.1.0, 5.1.1.1, 5.1.1.2, 5.1.1, 5.1.2, 5.1.3, 5.1.4, 5.1.5, 5.2.0, 5.2.1, 5.2.2, 5.3.0, 5.3.1, 5.3.2, 5.3.3, 5.3.4, 5.4.0, 5.4.1, 5.4.2, 5.4.3, 5.4.4, 5.4.5, 5.4.6, 5.4.7, 6.0.0, 6.0.1, 6.0.2, 6.0.3, 6.0.4, 6.0.5, 6.0.6, 6.1.0, 6.1.1, 6.1.2, 6.2.0, 6.2.1, 6.2.2, 6.2.3, 6.3.0, 6.3.1, 6.3.2, 6.3.3, 6.4.0, 6.4.1, 6.4.2.1, 6.4.2, 6.4.3, 6.4.4, 7.0.0, 7.0.1, 7.0.2, 7.1.0, 7.1.1, 7.1.2, 7.1.3, 7.1.4, 7.2.0, 7.3.0, 7.3.1, 7.3.2, 7.4.0, 7.4.1, 7.4.3, 7.5.0, 7.5.1, 7.5.2, 7.5.3, 7.5.4, 7.5.6, 7.6.0, 7.6.2, 7.7.0, 7.7.1, 7.7.4, 8.0.0rc1)
Cleaning up...
No distributions matching the version for sentry>=8.0.0 (from sentry-ldap-auth)
Storing debug log for failure in /home/bbigras/.pip/pip.log
$ pip freeze | grep sentry
sentry==8.0.0rc1

Add external dependencies to README

Please add external package dependencies to README

This package has the following external dependencies:

  • libsasl2-dev
  • python-dev
  • libldap2-dev
  • libssl-dev

sentry-ldap-auth for Active Directory

Hello!

I need to set up AD-authorization on Sentry. In the Internet on this theme I found this topic only: https://forum.sentry.io/t/how-to-set-up-to-auth-via-ms-active-directory-or-ldap/1880/5

But settings of this topic leads or not work AD-auth, or full non-work sentry (He not accept options "LOGGING").

I have next AD-server

IP 10.10.10.10
Domain: test.comp.com
Users: test.comp.com/Users
Groups: test.comp.com/Groups

Test User: test.comp.com/Users/user
Test Groups: test.comp.com/Groups/adminsupport (full right)
test.comp.com/Groups/support (read only)

Now my conf-file have follow parameters:

sentry.conf.py

from sentry.conf.server import *
 
import os.path
 
CONF_ROOT = os.path.dirname(__file__)
 
DATABASES = {
    'default': {
        'ENGINE': 'sentry.db.postgres',
        'NAME': 'sentry',
        'USER': 'sentry',
        'PASSWORD': '123',
        'HOST': '127.0.0.1',
        'PORT': '5432',
        'AUTOCOMMIT': True,
        'ATOMIC_REQUESTS': False,
    }
}
 
SENTRY_USE_BIG_INTS = True
SENTRY_SINGLE_ORGANIZATION = True
DEBUG = False
SENTRY_CACHE = 'sentry.cache.redis.RedisCache'
BROKER_URL = 'redis://localhost:6379'
SENTRY_RATELIMITER = 'sentry.ratelimits.redis.RedisRateLimiter'
SENTRY_BUFFER = 'sentry.buffer.redis.RedisBuffer'
SENTRY_QUOTAS = 'sentry.quotas.redis.RedisQuota'
SENTRY_TSDB = 'sentry.tsdb.redis.RedisTSDB'
SENTRY_DIGESTS = 'sentry.digests.backends.redis.RedisBackend'
# FORCE_SCRIPT_NAME = '/sentry'
SENTRY_WEB_HOST = '0.0.0.0'
SENTRY_WEB_PORT = 9000
SENTRY_WEB_OPTIONS = {
    # 'workers': 3,  # the number of web workers
    # 'protocol': 'uwsgi',  # Enable uwsgi protocol instead of http
}
 

import ldap
from django_auth_ldap.config import LDAPSearch, GroupOfUniqueNamesType

AUTH_LDAP_SERVER_URI = 'ldap://10.10.10.10.'
AUTH_LDAP_BIND_DN = 'CN=User,CN=Users,DC=test,DC=comp,DC=com'
AUTH_LDAP_BIND_PASSWORD = '123'

AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_DEBUG_LEVEL: 0,
ldap.OPT_REFERRALS: 0,
}

AUTH_LDAP_USER_SEARCH = LDAPSearch(
    'dc=test,dc=comp,dc=com',
    ldap.SCOPE_SUBTREE,
    'member={1}',
)

AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    'dc=test,dc=comp,dc=comp',
    ldap.SCOPE_SUBTREE,
    'objectClass=group'
)

AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType()
AUTH_LDAP_REQUIRE_GROUP = None
AUTH_LDAP_DENY_GROUP = None

AUTH_LDAP_USER_ATTR_MAP = {
    'name': 'cn',
    'email': 'mail'
}

AUTH_LDAP_FIND_GROUP_PERMS = False
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'TEST COMP COM'
AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True

AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
    'sentry_ldap_auth.backend.SentryLdapBackend',
)

import logging
logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.addHandler(logging.FileHandler(r"/tmp/ldap2.log"))
logger.setLevel('DEBUG')

#LOGGING[‘overridable’] = [‘sentry’, ‘django_auth_ldap’ ]
#LOGGING[‘loggers’][‘django_auth_ldap’] = {
#‘handlers’: [‘console’],
#‘level’: ‘DEBUG’
#}

Tell me, do you have AD-like config file for Sentry for example?

Auto populate 'Admin' and/or 'Superuser'

In webinterface (Admin --> Users --> ) there is an option to allow a user to be :

  • Admin ( Designates whether this user can perform administrative functions. )
  • Superuser ( Designates whether this user has all permissions without explicitly assigning them. )

By using mapping from Sentry ( 'owner', 'admin', 'manager', 'member') and LDAP groups, I have successfully integrated this functionality - so, a new user, as a member of a proper group in LDAP, is assigned a proper Sentry role.

But, users who are Owners and/or Admins don't have Admin and/or Superuser 'ticked' (turned on). So basically, a new member has a role of Owner or Admin, but he is not allowed in the Admin section.

Is it possible to assign this based on LDAP group membership (or any other parameter)?
Screenshot from 2020-08-20 10-51-12

getsentry-ldap-auth seems not work with sentry docker install

Env: CentOS 7.2

How to reproduce:

  1. git clone https://github.com/getsentry/onpremise
  2. change Dockerfile like this
FROM sentry:8.21-onbuild

RUN pip install git+https://github.com/Banno/getsentry-ldap-auth.git
  1. Hit error like this
Sending build context to Docker daemon 18.43 kB
Step 1 : FROM sentry:8.21-onbuild
# Executing 4 build triggers...
Step 1 : COPY . /usr/src/sentry
 ---> Using cache
Step 1 : RUN if [ -s requirements.txt ]; then pip install -r requirements.txt; fi
 ---> Running in 0476a02d5428
Collecting django-auth-ldap (from -r requirements.txt (line 2))
  Downloading django_auth_ldap-1.3.0-py2.py3-none-any.whl
Collecting django>=1.8 (from django-auth-ldap->-r requirements.txt (line 2))
  Downloading Django-2.0.tar.gz (8.0MB)
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-OyDT77/django/setup.py", line 32, in <module>
        version = __import__('django').get_version()
      File "django/__init__.py", line 1, in <module>
        from django.utils.version import get_version
      File "django/utils/version.py", line 61, in <module>
        @functools.lru_cache()
    AttributeError: 'module' object has no attribute 'lru_cache'

Login: UserEmail return more than 2 values

After upgrading sentry to the latest (8.11.0) version and the plugin to the 2.3, I started getting some weird issues, I ended up with duplicates of UserEmail entries for my users.

Possible reason is here

Or at least you misuse .get(...) method. This at least should be as follows:

try:
    userEmail = UserEmail.objects.get(user=user)
except UserEmail.DoesNotExist:
    userEmail = UserEmail.objects.create(user=user)

Sentry LDAP Plugin ignored with latest Version

Hi,

with the latest version I am facing the problem that sentry does not recognized this plugin as Auth Backend.
The Module does not show up in the auth backends, neither does the login over it work. With the same config it worked for older sentry versions.

Here is my config:


#########
#  LDAP #
#########
AUTH_LDAP_SERVER_URI = 'ldap://XXXXX
AUTH_LDAP_BIND_DN = ''
AUTH_LDAP_BIND_PASSWORD = ''

AUTH_LDAP_USER_SEARCH = LDAPSearch(
    'ou=users,dc=ldap,dc=XXXXXX,dc=io',
    ldap.SCOPE_SUBTREE,
    '(mail=%(user)s)',
)

AUTH_LDAP_USER_ATTR_MAP = {
    'name': 'cn',
    'email': 'displayName'
}
AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'XXXXX'

AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True
AUTH_LDAP_SENTRY_SUBSCRIBE_BY_DEFAULT = False
AUTH_LDAP_SENTRY_USERNAME_FIELD = 'cn'
SENTRY_MANAGED_USER_FIELDS = ('email', 'first_name', 'last_name', 'password', )

AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    '',
    ldap.SCOPE_SUBTREE,
    '(objectClass=groupOfUniqueNames)'
)

AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType()
AUTH_LDAP_REQUIRE_GROUP = None
AUTH_LDAP_DENY_GROUP = None

AUTH_LDAP_FIND_GROUP_PERMS = False
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600

AUTH_LDAP_DEFAULT_SENTRY_ORGANIZATION = u'My Organization Name'
AUTH_LDAP_SENTRY_ORGANIZATION_ROLE_TYPE = 'member'
AUTH_LDAP_SENTRY_ORGANIZATION_GLOBAL_ACCESS = True
AUTH_LDAP_SENTRY_USERNAME_FIELD = '(|(cn=%(user))(uid=%(user)))'

import logging
logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.setLevel('DEBUG')

AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (
    'sentry_ldap_auth.backend.SentryLdapBackend',
)

Plugin not populating "sentry_useremail" table

Hi!

I've been successfully using your plugin for the last couple of days to authenticate my company's users with Sentry. Everything works great, except for one detail: the information returned by the plugin populates all necessary fields in the "auth_user" table (in the Sentry db), but the email for the user is not added to the "sentry_useremail" table. Because of this, some Sentry emails fail to be sent ("no users to send to" error in Sentry logs). To test this behavior, you can do the following:

  1. Login as a new user with LDAP
  2. Go to "settings/mail" (you need to be admin for this). Try sending yourself a test email. This works.
  3. Go to "account". At the top, it should say "your email has not been verified. Resend verification email". Clicking that link will pop up a message saying "Verification email sent". However, email is not received. Checking logs, you will find the error "Did not build any messages, no users to send to"
  4. Checking out the 2 relevant tables in postgresql, you will see one has an email for your user, the other doesn't. Locally created users have emails in both tables.

I first thought this was a problem with Sentry itself and posted a ticket here. However, there they pointed out the issue was not on their end (I think the issue is partly on their end for having email fields in two different tables, but I'm sure they have a good reason for that).

Granted, I don't know the workings of your plugin too well, so I might just be doing something wrong. Any help will be appreciated!

IntegrityError duplicate key

Hey guys,

Not entirely sure if this is a Sentry issue, a Django issue, or an issue with the plugin. On a clean Sentry install (v 8.8.0) with only this plugin installed (tested versions 2.0 and 2.2) and Django version 1.6.11, I am able to create a new local user no problem. However, when trying to log in using an LDAP account, I get the "Oops! Something went wrong" error on the web interface, and the log throws the following error and traceback:

2016-09-28_19:42:51.98684 Traceback (most recent call last):
2016-09-28_19:42:51.98686   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/core/handlers/base.py", line 112, in get_response
2016-09-28_19:42:51.98686     response = wrapped_callback(request, *callback_args, **callback_kwargs)
2016-09-28_19:42:51.98686   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/views/generic/base.py", line 69, in view
2016-09-28_19:42:51.98687     return self.dispatch(request, *args, **kwargs)
2016-09-28_19:42:51.98687   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/utils/decorators.py", line 29, in _wrapper
2016-09-28_19:42:51.98688     return bound_func(*args, **kwargs)
2016-09-28_19:42:51.98688   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/utils/decorators.py", line 99, in _wrapped_view
2016-09-28_19:42:51.98688     response = view_func(request, *args, **kwargs)
2016-09-28_19:42:51.98689   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/utils/decorators.py", line 25, in bound_func
2016-09-28_19:42:51.98689     return func(self, *args2, **kwargs2)
2016-09-28_19:42:51.98689   File "/www/sentry/local/lib/python2.7/site-packages/sentry/web/frontend/base.py", line 188, in dispatch
2016-09-28_19:42:51.98690     return self.handle(request, *args, **kwargs)
2016-09-28_19:42:51.98691   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/views/decorators/cache.py", line 52, in _wrapped_view_func
2016-09-28_19:42:51.98691     response = view_func(request, *args, **kwargs)
2016-09-28_19:42:51.98691   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/transaction.py", line 371, in inner
2016-09-28_19:42:51.98692     return func(*args, **kwargs)
2016-09-28_19:42:51.98692   File "/www/sentry/local/lib/python2.7/site-packages/sentry/web/frontend/auth_organization_login.py", line 132, in handle
2016-09-28_19:42:51.98693     response = self.handle_basic_auth(request, organization)
2016-09-28_19:42:51.98693   File "/www/sentry/local/lib/python2.7/site-packages/sentry/web/frontend/auth_organization_login.py", line 67, in handle_basic_auth
2016-09-28_19:42:51.98693     elif login_form.is_valid():
2016-09-28_19:42:51.98694   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/forms/forms.py", line 129, in is_valid
2016-09-28_19:42:51.98694     return self.is_bound and not bool(self.errors)
2016-09-28_19:42:51.98694   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/forms/forms.py", line 121, in errors
2016-09-28_19:42:51.98695     self.full_clean()
2016-09-28_19:42:51.98695   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/forms/forms.py", line 274, in full_clean
2016-09-28_19:42:51.98696     self._clean_form()
2016-09-28_19:42:51.98696   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/forms/forms.py", line 300, in _clean_form
2016-09-28_19:42:51.98697     self.cleaned_data = self.clean()
2016-09-28_19:42:51.98697   File "/www/sentry/local/lib/python2.7/site-packages/sentry/web/forms/accounts.py", line 136, in clean
2016-09-28_19:42:51.98697     password=password)
2016-09-28_19:42:51.98698   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/contrib/auth/__init__.py", line 49, in authenticate
2016-09-28_19:42:51.98698     user = backend.authenticate(**credentials)
2016-09-28_19:42:51.98699   File "/www/sentry/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 167, in authenticate
2016-09-28_19:42:51.98700     user = ldap_user.authenticate(password)
2016-09-28_19:42:51.98701   File "/www/sentry/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 339, in authenticate
2016-09-28_19:42:51.98701     self._get_or_create_user()
2016-09-28_19:42:51.98701   File "/www/sentry/local/lib/python2.7/site-packages/django_auth_ldap/backend.py", line 548, in _get_or_create_user
2016-09-28_19:42:51.98702     self._user, created = self.backend.get_or_create_user(username, self)
2016-09-28_19:42:51.98702   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../sentry_ldap_auth/backend.py", line 29, in get_or_create_user
2016-09-28_19:42:51.98703     email=ldap_user.attrs.get('mail', ' ')[0] or '',
2016-09-28_19:42:51.98704   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/models/manager.py", line 214, in update
2016-09-28_19:42:51.98704     return self.get_queryset().update(*args, **kwargs)
2016-09-28_19:42:51.98704   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/models/query.py", line 493, in update
2016-09-28_19:42:51.98705     rows = query.get_compiler(self.db).execute_sql(None)
2016-09-28_19:42:51.98705   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/models/sql/compiler.py", line 980, in execute_sql
2016-09-28_19:42:51.98705     cursor = super(SQLUpdateCompiler, self).execute_sql(result_type)
2016-09-28_19:42:51.98706   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/models/sql/compiler.py", line 786, in execute_sql
2016-09-28_19:42:51.98706     cursor.execute(sql, params)
2016-09-28_19:42:51.98706   File "/www/sentry/local/lib/python2.7/site-packages/raven/contrib/django/client.py", line 112, in execute
2016-09-28_19:42:51.98707     return real_execute(self, sql, params)
2016-09-28_19:42:51.98707   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/backends/util.py", line 53, in execute
2016-09-28_19:42:51.98710     return self.cursor.execute(sql, params)
2016-09-28_19:42:51.98712   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/utils.py", line 99, in __exit__
2016-09-28_19:42:51.98713     six.reraise(dj_exc_type, dj_exc_value, traceback)
2016-09-28_19:42:51.98714   File "/www/sentry/local/lib/python2.7/site-packages/sentry/../django/db/backends/util.py", line 53, in execute
2016-09-28_19:42:51.98716     return self.cursor.execute(sql, params)
2016-09-28_19:42:51.98717   File "/www/sentry/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 75, in inner
2016-09-28_19:42:51.98719     raise_the_exception(self.db, e)
2016-09-28_19:42:51.98720   File "/www/sentry/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 73, in inner
2016-09-28_19:42:51.98722     return func(self, *args, **kwargs)
2016-09-28_19:42:51.98723   File "/www/sentry/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 21, in inner
2016-09-28_19:42:51.98725     return func(self, *args, **kwargs)
2016-09-28_19:42:51.98726   File "/www/sentry/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 95, in inner
2016-09-28_19:42:51.98728     six.reraise(exc_info[0], exc_info[0](msg), exc_info[2])
2016-09-28_19:42:51.98729   File "/www/sentry/local/lib/python2.7/site-packages/sentry/db/postgres/decorators.py", line 88, in inner
2016-09-28_19:42:51.98731     return func(self, sql, *args, **kwargs)
2016-09-28_19:42:51.98732   File "/www/sentry/local/lib/python2.7/site-packages/sentry/db/postgres/base.py", line 39, in execute
2016-09-28_19:42:51.98734     return self.cursor.execute(sql, params)
2016-09-28_19:42:51.98736 IntegrityError: IntegrityError('duplicate key value violates unique constraint "sentry_useremail_user_id_469ffbb142507df2_uniq"\nDETAIL:  Key (user_id, email)=(8, [email protected]) already exists.\n',)
2016-09-28_19:42:51.98737 SQL: UPDATE "sentry_useremail" SET "user_id" = %s, "email" = %s
2016-09-28_19:42:51.98911 19:42:51 [ERROR] django.request: Internal Server Error: /auth/login/sentry/ (status_code=500 request=<WSGIRequest
2016-09-28_19:42:51.98915 path:/auth/login/sentry/,
2016-09-28_19:42:51.98931 GET:<QueryDict: {}>,
2016-09-28_19:42:51.98932 POST:<QueryDict: {u'username': [u'myuser'], u'csrfmiddlewaretoken': [u'VQpi9JvWKHUjcwerWEwr6PslIpFws4yT'], u'password': [u'superSecurePass'], u'op': [u'login']}>,

The new user is not registered in the auth_user or sentry_useremail tables.
Not really sure where to go with this. Thank you in advance for any help you can provide!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.