Added very basic permission system. #71
[zzz-spline-users.git] / splinext / users / model / __init__.py
1 # encoding: utf8
2 import colorsys
3 from math import sin, pi
4 import random
5
6 from sqlalchemy import Column, ForeignKey, or_
7 from sqlalchemy.ext.associationproxy import association_proxy
8 from sqlalchemy.orm import relation
9 from sqlalchemy.orm.session import Session
10 from sqlalchemy.types import Integer, Unicode
11
12 from spline.model.meta import TableBase
13
14 class AnonymousUser(object):
15 """Fake user object, for when the user isn't actually logged in.
16
17 Tests as false and tries to respond to method calls the expected way.
18 """
19
20 def __nonzero__(self):
21 return False
22 def __bool__(self):
23 return False
24
25 def can(self, action):
26 """Anonymous users aren't ever allowed to do anything."""
27 return False
28
29
30 class User(TableBase):
31 __tablename__ = 'users'
32 id = Column(Integer, primary_key=True)
33 name = Column(Unicode(length=20), nullable=False)
34 unique_identifier = Column(Unicode(length=32), nullable=False)
35
36 def __init__(self, *args, **kwargs):
37 # Generate a unique hash if one isn't provided (which it shouldn't be)
38 if 'unique_identifier' not in kwargs:
39 ident = u''.join(random.choice(u'0123456789abcdef')
40 for _ in range(32))
41 kwargs['unique_identifier'] = ident
42
43 super(User, self).__init__(*args, **kwargs)
44
45 _root_user_id = None
46 _default_permissions = None
47 def can(self, action):
48 """Returns True iff this user has permission to do `action`.
49
50 If `_root_user_id` is this user's id, all permissions are allowed. The
51 property is usually set by the spline-users after_setup hook.
52 """
53
54 if self.id == self._root_user_id:
55 return True
56
57 if action in self.permissions:
58 return True
59
60 # Permissions assigned to NULL apply to all roles
61 if self._default_permissions is None:
62 session = Session.object_session(self)
63 self._default_permissions = [
64 row.permission
65 for row in session.query(RolePermission)
66 .filter_by(role_id=None)
67 ]
68
69 return (action in self._default_permissions)
70
71 @property
72 def unique_colors(self):
73 """Returns a list of (width, '#rrggbb') tuples that semi-uniquely
74 identify this user.
75 """
76 width_blob, colors_blob = self.unique_identifier[0:8], \
77 self.unique_identifier[8:32]
78
79 widths = []
80 for i in range(4):
81 width_hex = width_blob[i*2:i*2+2]
82 widths.append(int(width_hex, 16))
83 total_width = sum(widths)
84
85 ret = []
86 last_hue = None
87 for i in range(4):
88 raw_hue = int(colors_blob[i*6:i*6+2], 16) / 256.0
89 if last_hue:
90 # Make adjacent hues relatively close together, to avoid green
91 # + purple sorts of clashes.
92 # Minimum distance is 0.1; maximum is 0.35. Leaves half the
93 # spectrum available for any given color.
94 # Change 0.0–0.1 to -0.35–-0.1, 0.1–0.35
95 hue_offset = raw_hue * 0.5 - 0.25
96 if raw_hue < 0:
97 raw_hue -= 0.1
98 else:
99 raw_hue += 0.1
100
101 h = last_hue + raw_hue
102 else:
103 h = raw_hue
104 last_hue = h
105
106 l = int(colors_blob[i*6+2:i*6+4], 16) / 256.0
107 s = int(colors_blob[i*6+4:i*6+6], 16) / 256.0
108
109 # Secondary colors are extremely biased against when picking
110 # randomly from the hue spectrum.
111 # To alleviate this, try to bias hue towards secondary colors.
112 # This adjustment is based purely on experimentation; sin() works
113 # well because hue is periodic, * 6 means each period is 1/3 the
114 # hue spectrum, and the final / 24 is eyeballed
115 h += sin(h * pi * 6) / 24
116
117 # Cap lightness to 0.4 to 0.95, so it's not too close to white or
118 # black
119 l = l * 0.6 + 0.3
120
121 # Cap saturation to 0.5 to 1.0, so the color isn't too gray
122 s = s * 0.6 + 0.3
123
124 r, g, b = colorsys.hls_to_rgb(h, l, s)
125 color = "#{0:02x}{1:02x}{2:02x}".format(
126 int(r * 256),
127 int(g * 256),
128 int(b * 256),
129 )
130
131 ret.append((1.0 * widths[i] / total_width, color))
132
133 return ret
134
135 class OpenID(TableBase):
136 __tablename__ = 'openid'
137 openid = Column(Unicode(length=255), primary_key=True)
138 user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
139
140
141 # Permissions stuff
142 class Role(TableBase):
143 __tablename__ = 'roles'
144 id = Column(Integer, primary_key=True, nullable=False)
145 name = Column(Unicode(64), nullable=False)
146 icon = Column(Unicode(64), nullable=False)
147
148 class UserRole(TableBase):
149 __tablename__ = 'user_roles'
150 user_id = Column(Integer, ForeignKey('users.id'), primary_key=True, nullable=False, autoincrement=False)
151 role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True, nullable=False, autoincrement=False)
152
153 class RolePermission(TableBase):
154 __tablename__ = 'role_permissions'
155 id = Column(Integer, nullable=False, primary_key=True)
156 role_id = Column(Integer, ForeignKey('roles.id'), nullable=True)
157 permission = Column(Unicode(64), nullable=False)
158
159
160 ### Relations
161 OpenID.user = relation(User, lazy=False, backref='openids')
162
163 Role.role_permissions = relation(RolePermission, backref='role')
164
165 User.roles = relation(Role, secondary=UserRole.__table__, backref='users')
166 User.role_permissions = relation(RolePermission,
167 primaryjoin=User.id==UserRole.user_id,
168 secondary=UserRole.__table__,
169 secondaryjoin=UserRole.role_id==RolePermission.role_id,
170 foreign_keys=[UserRole.user_id, RolePermission.role_id],
171 )
172 User.permissions = association_proxy('role_permissions', 'permission')
173
174 UserRole.user = relation(User)
175 UserRole.role = relation(Role)