From 83e05d2342fdd66a03cd05115bdc2244e59027d3 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 17 Feb 2015 12:35:16 -0500 Subject: [PATCH] Add tracking of the kind of temporary access tokens, so we can display if a pull/push by token is for a build worker --- data/database.py | 8 +++- ...e2d38b52a75_add_access_token_kinds_type.py | 44 ++++++++++++++++++ data/model/legacy.py | 19 +++++--- endpoints/common.py | 3 +- endpoints/index.py | 2 +- endpoints/trackhelper.py | 4 ++ initdb.py | 5 +- static/directives/logs-view.html | 3 +- static/js/app.js | 14 +++++- test/data/test.db | Bin 712704 -> 724992 bytes 10 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 data/migrations/versions/3e2d38b52a75_add_access_token_kinds_type.py diff --git a/data/database.py b/data/database.py index 359072268..eebea7670 100644 --- a/data/database.py +++ b/data/database.py @@ -333,6 +333,10 @@ class PermissionPrototype(BaseModel): ) +class AccessTokenKind(BaseModel): + name = CharField(unique=True, index=True) + + class AccessToken(BaseModel): friendly_name = CharField(null=True) code = CharField(default=random_string_generator(length=64), unique=True, @@ -341,6 +345,7 @@ class AccessToken(BaseModel): created = DateTimeField(default=datetime.now) role = ForeignKeyField(Role) temporary = BooleanField(default=True) + kind = ForeignKeyField(AccessTokenKind, null=True) class BuildTriggerService(BaseModel): @@ -600,4 +605,5 @@ all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Notification, ImageStorageLocation, ImageStoragePlacement, ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification, RepositoryAuthorizedEmail, ImageStorageTransformation, DerivedImageStorage, - TeamMemberInvite, ImageStorageSignature, ImageStorageSignatureKind] + TeamMemberInvite, ImageStorageSignature, ImageStorageSignatureKind, + AccessTokenKind] diff --git a/data/migrations/versions/3e2d38b52a75_add_access_token_kinds_type.py b/data/migrations/versions/3e2d38b52a75_add_access_token_kinds_type.py new file mode 100644 index 000000000..53d0ae9df --- /dev/null +++ b/data/migrations/versions/3e2d38b52a75_add_access_token_kinds_type.py @@ -0,0 +1,44 @@ +"""Add access token kinds type + +Revision ID: 3e2d38b52a75 +Revises: 1d2d86d09fcd +Create Date: 2015-02-17 12:03:26.422485 + +""" + +# revision identifiers, used by Alembic. +revision = '3e2d38b52a75' +down_revision = '1d2d86d09fcd' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('accesstokenkind', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_accesstokenkind')) + ) + op.create_index('accesstokenkind_name', 'accesstokenkind', ['name'], unique=True) + op.add_column(u'accesstoken', sa.Column('kind_id', sa.Integer(), nullable=True)) + op.create_index('accesstoken_kind_id', 'accesstoken', ['kind_id'], unique=False) + op.create_foreign_key(op.f('fk_accesstoken_kind_id_accesstokenkind'), 'accesstoken', 'accesstokenkind', ['kind_id'], ['id']) + ### end Alembic commands ### + + op.bulk_insert(tables.accesstokenkind, + [ + {'id': 1, 'name':'build-worker'}, + {'id': 2, 'name':'pushpull-token'}, + ]) + + +def downgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('fk_accesstoken_kind_id_accesstokenkind'), 'accesstoken', type_='foreignkey') + op.drop_index('accesstoken_kind_id', table_name='accesstoken') + op.drop_column(u'accesstoken', 'kind_id') + op.drop_index('accesstokenkind_name', table_name='accesstokenkind') + op.drop_table('accesstokenkind') + ### end Alembic commands ### diff --git a/data/model/legacy.py b/data/model/legacy.py index deac1f02b..13b4b9471 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -15,7 +15,8 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite, DerivedImageStorage, ImageStorageTransformation, random_string_generator, db, BUILD_PHASE, QuayUserField, ImageStorageSignature, QueueItem, - ImageStorageSignatureKind, validate_database_url, db_for_update) + ImageStorageSignatureKind, validate_database_url, db_for_update, + AccessTokenKind) from peewee import JOIN_LEFT_OUTER, fn from util.validation import (validate_username, validate_email, validate_password, INVALID_PASSWORD_MESSAGE) @@ -1902,10 +1903,14 @@ def get_private_repo_count(username): .count()) -def create_access_token(repository, role): +def create_access_token(repository, role, kind=None, friendly_name=None): role = Role.get(Role.name == role) + kind_ref = None + if kind is not None: + kind_ref = AccessTokenKind.get(AccessTokenKind.name == kind) + new_token = AccessToken.create(repository=repository, temporary=True, - role=role) + role=role, kind=kind_ref, friendly_name=friendly_name) return new_token @@ -2024,10 +2029,10 @@ def create_repository_build(repo, access_token, job_config_obj, dockerfile_id, pull_robot = lookup_robot(pull_robot_name) return RepositoryBuild.create(repository=repo, access_token=access_token, - job_config=json.dumps(job_config_obj), - display_name=display_name, trigger=trigger, - resource_key=dockerfile_id, - pull_robot=pull_robot) + job_config=json.dumps(job_config_obj), + display_name=display_name, trigger=trigger, + resource_key=dockerfile_id, + pull_robot=pull_robot) def get_pull_robot_name(trigger): diff --git a/endpoints/common.py b/endpoints/common.py index 2f5ffc67c..9d4be7771 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -215,7 +215,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, host = urlparse.urlparse(request.url).netloc repo_path = '%s/%s/%s' % (host, repository.namespace_user.username, repository.name) - token = model.create_access_token(repository, 'write') + token = model.create_access_token(repository, 'write', kind='build-worker', + friendly_name='Repository Build Token') logger.debug('Creating build %s with repo %s tags %s and dockerfile_id %s', build_name, repo_path, tags, dockerfile_id) diff --git a/endpoints/index.py b/endpoints/index.py index 660ab94aa..d1a902915 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -50,7 +50,7 @@ def generate_headers(role='read'): if has_token_request: repo = model.get_repository(namespace, repository) if repo: - token = model.create_access_token(repo, role) + token = model.create_access_token(repo, role, 'pushpull-token') token_str = 'signature=%s' % token.code response.headers['WWW-Authenticate'] = token_str response.headers['X-Docker-Token'] = token_str diff --git a/endpoints/trackhelper.py b/endpoints/trackhelper.py index fb99a2c2d..070927eac 100644 --- a/endpoints/trackhelper.py +++ b/endpoints/trackhelper.py @@ -34,6 +34,10 @@ def track_and_log(event_name, repo, **kwargs): elif authenticated_token: metadata['token'] = authenticated_token.friendly_name metadata['token_code'] = authenticated_token.code + + if authenticated_token.kind: + metadata['token_type'] = authenticated_token.kind.name + analytics_id = 'token:' + authenticated_token.code else: metadata['public'] = True diff --git a/initdb.py b/initdb.py index 15c62bb0b..14fcc1441 100644 --- a/initdb.py +++ b/initdb.py @@ -192,6 +192,9 @@ def initialize_database(): BuildTriggerService.create(name='github') + AccessTokenKind.create(name='build-worker') + AccessTokenKind.create(name='pushpull-token') + LogEntryKind.create(name='account_change_plan') LogEntryKind.create(name='account_change_cc') LogEntryKind.create(name='account_change_password') @@ -393,7 +396,7 @@ def populate_database(): 'Empty repository which is building.', False, [], (0, [], None)) - token = model.create_access_token(building, 'write') + token = model.create_access_token(building, 'write', 'build-worker') trigger = model.create_build_trigger(building, 'github', '123authtoken', new_user_1, pull_robot=dtrobot[0]) diff --git a/static/directives/logs-view.html b/static/directives/logs-view.html index 5038895ea..cc3c51d2f 100644 --- a/static/directives/logs-view.html +++ b/static/directives/logs-view.html @@ -56,7 +56,8 @@
-
on behalf of
diff --git a/static/js/app.js b/static/js/app.js index 63f697755..6b936a302 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3314,7 +3314,11 @@ quayApp.directive('logsView', function () { } if (metadata.token) { - prefix += ' via token {token}'; + if (metadata.token_type == 'build-worker') { + prefix += ' by build worker'; + } else { + prefix += ' via token'; + } } else if (metadata.username) { prefix += ' by {username}'; } else { @@ -3325,7 +3329,13 @@ quayApp.directive('logsView', function () { }, 'pull_repo': function(metadata) { if (metadata.token) { - return 'Pull repository {repo} via token {token}'; + var prefix = 'Pull of repository' + if (metadata.token_type == 'build-worker') { + prefix += ' by build worker'; + } else { + prefix += ' via token'; + } + return prefix; } else if (metadata.username) { return 'Pull repository {repo} by {username}'; } else { diff --git a/test/data/test.db b/test/data/test.db index 63d2768c14cff78e694a40d7549b9ecf17abbb68..89d4b3d8cd3476fb923d54f677f65d9e0bcb1826 100644 GIT binary patch delta 13898 zcmeHu33wD$_HW;=s;;U|cRC@21V~7eg(M`MR8@CXbweOz-xCtZDhRzoARE~T0TLR6 zvI^LvM`sjpbWjI(k*MQe(HRg|bR5SSH*o$l<2a*(Iw~$G|I-Oe0P#8Fd*A!M_r3no zmC9Xy=iF0gyQgl}9ZXxdCw<4{@K%jR^8kL&es0{EZdF?~mOmvi*u0{FeruR&LGQGv$!Rxuztg5?-`GQHO*HLe4yU8(<69_l&qx_R2!PrltuVeT?tJX{= zK=@WZBCuP>29DCfrjWqnS_>HuFAtE}B=UmZ>$Duq7$hmwdS66fkWvEMuwm(D-T;-o zgnj5`DvEI9*r<|h;x7n%tepf02M8T7GE+zdIJ-$?;3K^OsTc!Ij6lKzTbS8o95^@g z5hL>hFH?MQMo8cb>iBMN-jPsZ>J+xv+zu!s8#F~E#I;vmLz^a2(=2ej3gVIav}}}|M*OkOZ60r*)9v|@RmtGtq=8g(Gt9r%*Ts78!wYm@e)1Wh*vEtxaoxHt%t(RFPnsLEu~5A z=;>P8(bLjm8Cj)?P?{uHPjidMva-E%xwlh4GV5)P=_AwIIO`Qt!Z<3GTsTQkLtYON^bhJI`GfqG+#_5#8_<2K+pU|$0R0i&Nxe!9P%+wGa+FkS z9)!hV`uKp^Jk=u9B5nNe9!d836wz*R*;SXtrr13er`ut*SXGDBBiL0@wkd)8JAj+{ z+wib?Pd>0FP_?3$i=El_QOXA|T@`q7#cqzP`t13T+Y;Xoba(FJtPj3&r;Gf;Q zxvakyJztyaSs%E*XBVfQNZ4Ij`FvksW^WUxb+)`o)rzMBxAazUO5A_nS$25#BY{tP zfzu~@mxijzI|Dmc_HyPuS1^`)%4Y;jeaPEy_kV0?5{3c?`gU>J%|~}q+v6dyU{w=0 zYt?H5bzvv(4?MA|B69rJ-~27+P{ckopAb#0#yZs>OHl!H|7M+V>8|&y?Z4-wO=;k<+ho4`FX4suf+5!UN!eVWLWo{iMc0* zswII%1Bnlv-p(A9#*Lgj_%3v;Sr>TS3kJf=1F3BOws7MT&t{5;aL%lnJLqY9f(fok*EY-p*jq>uh(mcgLqTWPm}WVu^TeQWA#l zsGV=mF-;s3<9<+Hm9CvNA+VK{kf;%`Gx-pIomTh5^0Ld$FV&84L?)ikP+*&%%p=k8 zt{>Bn$>-<=n?s>)JrQy6#`Ty;#0>vxJ?SE35@ZgMIq-`OBmt_1P~UbBkqn&q(h$ia zQ=nsr#KS*_Fq2U>km)1_oEu0!i5qP{vVr80STI~oDv5biP#ss3Hk@|sYLZB%0(}j+ z5^6W1%5`2tCLvNVUxJ@sLo#vvZ?C~I32+j};Ly5_B$XtN4!C|J4v2?C8%a7zg7g7; z5>yS+!3;4=yCwl@H{mj-jSjte6H(2HY*btv88VI}C@XrrJ>KSSZ|k|68C)+RwS1eY>Eba zj58*rvQd$d#IKH!>uhiF2EXa>-&;xTfyIOl8>Yj^sqDRG5_aG?S^34FVE1}`s@G$Y z6`N|YNiJ6~MX*~G!Q++$w@dM=imPW;TZ^-Ei8rmyiz&^L5jD&yt+3VR)|QnQH!3xG z^>x)&wN@^wx8>!QtA$ciVS%l*SZJJ|ugKAC6=%pwh9uf0(dv&23DwkEHGU(f;dqYAv5TrxRo${hkz7`b%_AxnOj)cJ zRS>*ZL2`LiNr37T+K9nQQOr@Q>P1N|uP&{!D!Ik;8}gNkGI6e2QEwG&`Hr&EipJWq znrdrdQ$=IF?5KB$_NqdmxVW~Wp)7BdtBaTtRGZzgQg77M#%VG+5jo;=6xHXmDNc{Y zWtH6)o70Y|mCqCEwfIDr-K9F*KE-2)V}I1jgJaewIvfsB^qYBH-&8-7%4-md%aP=f z^vrGX*xi_&iGsu8a=2BC%OyBGPPfNyvpU*)y1SY^UKD?#-O}|t1p8+Z)l^=vt-K< z6`NI-{4pG|H1Rx3F1y2}xP7WccKPh67btC)!+}|&sQBDAQTEwt$?6MCPTa`p-r?bt|-Wv4yXLxKXs}~06_RGEVT@A(g6)T&3^D}$Oy>ok4E?rsN z)KcGAl;`MQTD;0@U9vKNX>mc*T)EAPJlg&jJW4ivW;TC3gFHIUTaw@sTwc{CS?pd1 z%F!ju7N^&w;6_03cwAnG^{3HDPG*7*koU)w`=Jb$$4u^TbIpOE2|oowYFJ{ zGYU(rO&uBbWli!jSC8D9E7vH6P1Xj_@=mFGX;Ws&s!XBH3opE_jX$scQielPt%}1x zjY5u4{b!xwNO23QOGOj(IZ;G5MMjU~vid9%8oAqN72R&x)zRZ>X?CA^jt%pa-WwWRsHr3SC>*Z*S9oJYR>oatEyM7Xll+7ilFx$?bNaC zD3Tz{sy{}H>{!k}Hm9Q41)E~CNGft-b4ZfK;Xp6pusbBLLy%OT+wSr9jyU}^Pxpv} z7HY)$(s@czePdO1Ws}rcpvpOwW!2^N^XhBV`8o6R%j)YL#SM9lgJ^PgTXkJSQ)6yH znJCO}DynR#$zPr`cTkgqo6jjx9sT2dj;uWUOl)(~jR8Mup87Noh5H z4gS1D3dzk>33VIw0i8wvjQ&Vhp?irfWKVH>_^_6W!fK>hxIocpq5S7Ma4yu6(a+KY01bwFL+}a&6k#L zBU7fNu~D(H#GgH)%iUgQYjD8X-x+ZGHqu1W;q*3QCYfNqmdFz_;NE4(XE zDMocn7G41BWNJL3@|z*DhfL7R3w=doHYS<*(p0Bziy`#sGCWdAjW}OHWL;&sC1G&yvae`Ku8vtXHp8L z;J0Su8kWyQqO;(RnYiwpb4?*YWnsf?n3IJwbB9~AsK1i~W-6F#&Y@oY@wT{nZ5hRC z2=N=kL#Th>BnL0d@DNRV292S9tYsn?IRmDpE@PIdmwMdG*yZrIRaA(XeKho4(+X2+ zuv8d2Sj6DE3H4+B8GQ!l-xc5B#R)QX2I$91PFomU7=JmTot4?t^I_^TLOZaI`nocu zq0U-3-%*e+2)Xl=no4WFL#i#SQ0J=i3-ao7v8$@678CtCc=9Nf1jR?Gs+5MJT&b~G zs?9CRttcvQ%(dp$$o1tyZe3GBUSm^1MTxDpa&C#-G>lU|%B+?hcF84TW@5K{L`?WG z4Yj*v%=!hd%j>iWm=j<^+|fS6>1l0l>-Khak5q%l1W`$tA=+nHgS#AAvJ3m^w@isY z1a)s1J@X{RQ3&Bv>6U&Q*$VPL@o%O^h{D1^ap0hUO_uZ9;{kH&mcuW zy6Ae64~9;}=nA4L)GWPL@-2@4}Z`@ClFE$VJqoGQUUTxIybgN7nKxA z+DF1paMYra*wgF@-dDm;R?^a>(visIK0hS*dLh_f1;@!4&<&1@0y6Q6F5?`~h@ zadvy-XPD#XUoskX9XIu7N3R`Y38FOGm0`0>R#o-WgxAy(-p}N4E~_VKXEuvXo#+at zx=!3hNgm96C7;LP5mkq1#bDdc6b7@v^EiU6SPyhCFJ}rDGza0eu~ZKK-PA{B?O`4= zRs0aCXp^a0ehh<`g%l4N-=j#%SXsruNVmObaz__;LVC!=X#y-Io}fw!02qdWwmI(5INmuy2r#fUKvO z7wo?_C;f}pecPv+#JPM>#Ili|y!nRwC&R~}>}xcLxj zb=|KStrqS*%uI*%hnPw5$zdiPjvQj_MKA`=Y@D>!zz;W=c0 zxMAb-Oa^I&2e$LkaP)b`%6ZtR0;HP>J6~X?LfQ+Ao-7;fu6%)+iOg<(floeJu#=#{*x8xAheWhKfst6RYm7FLb0-zIu++lnO;iwmnr zHm^^{T7(nK3w0L6R9#NDBwCz48&>#aG!ustt0->O>GX*{S&=U8+f+y$@C3ePRn%}BH<4lE2JoE?n~{fXCVaXKZR#phM5STB;D zfM5W_hHe`YLl!EpAVE^tg~|yi-tC$hSjOR2$sCNSB@{TU8f29k0de^x|W12VJVui`7@fYL`52t4oxv zdl&0=NI^#tu5H!eTf}dvTWN_t$vn>9#W}b?^H=HP43i9389p(3jK2<@W*Tn_kUwfR zpqrS-u3gPWWhZE%BA-jsYSzQ5e6Eab?q{Pc*mz4E8?V*)N7e4NI5us{@>OhDgFX|jW?dLtN>-dR9uH}O@@f+icL4Rvr)B}@l=f&u`QYn4RWi81<|Yl?g@^) zHkzGHI^e}!aHhUs#K{i_^vN@!0Oy{)|Ssvb;$flD{;3ly#WYuue zB=#+W{A**_WT0c&Xvm3SQ<3}S!C$N4l~^_k?v6pc9(W^$oenPt$Hc_4nWPuWW7!0< zc67`&!TXi4FP2Rr1Hq;&vA&OuN=P7A_l!8fF^_o6?cfD&aAyS`rr{^BX6T&4J`9@& zF#GvsAr}TmujMn~)C4vRe)SUi-_*95nhZ%h^DJ>a+?1zDW?U{2eTCqwn$Hu?V*!+ud%P-7b3s$Z26G ze#iW(b~X~$wXouM^lfZo)8NAvHsw3|&dy_CPAi-99RqN33Ov^OokU0a45&uqMC;ki z=iG`eK&D|{8=Ejrlr9;D7}ej^{*N}cW}GNqG9F=bzpMSWcGmtK?ZY3ov-ea6>zSj8 z*;)1sQ?QD0X$E$-JLLPj_21vkU)()-A#v{F{tLU=3%W0ntnPyDOC-vCNB1CRy=Rw3 zU(kPvfT{2AzA$sWWOmS96Vr_v{2BXsXo}fyTIwTs?g&cgr32=GGJoIb}5s zg^gwTxk_G5eMxz3y;xmWAvRQ3R+KdsHL8ir-MnPa*ubkozOJT8gc5In6n9@gb@MmQSI>R@XgHwoWBpV)%2{rX_1!?eM&7Rzf6?WV2~ zG2k6L4r9?z@4yB=pX|PP`X?KB!5E2&Tefy6c2s{~8R92G@zwk!vTIbL2>dTH z9@?+rXOLUQ06cpQUqCT_-1ThdWpZ1mY$>64qB@N>I9ykAsIR zxoL!f@Qn!jMI{#te+ahX-FXXQU`H88LsJzuj_9DZikk_pO*m*%6=#CqS7A!&H~QHi zgabYfc33v@MtHP@iyWP}eIq}P<8U*G^pi*FKd})dv13$}_f>K_zy>{#O?(QTI4PgQ zrG>V4HFvjnt{OSFso6PZ6b`l2uh49H&{v%tR zvStg!e#-Z9@mG?sgjo3`{OnVHH)o#hd%=>JIt00&A!fp>Ggv8k_%`_4XNVakHPYr) z_x%=1KS#{z|MlL7Gk?D?8g_q;bugJ@!%3=0N#GIP3+Mdx}I2j&4iI^!>_pY_K zZ~GItzX)Qo4|qlWp=98{L`?3T*Fy};Ehbq0C1PrCz4tVAUy29^zeLOlAJ0xVPuV;R z7JY@7bcg9ZowDXGc>62FRBqnTQ{Mg9>oD{+V$O7XkIqYc>Q^xR6k_V_E!NP=Z;r#( zQ+!2aT-^I>(gG3p_+t{z9&X0U`ae(co4J{$&u_1}?~}j4z&A*M|I4-;jc7S<0x+z2tC8v=khKIclb$~z+5TR63?5s8m?3p9d~7=Qk6~~wMNHvKVMYFquwB45Bj#-D zk>>iyvcu5ajF|CvZ+=$TlJXEd(TtcGw<=%c6i<8#7A`}~jD1s16w-pnzo%|Ug#F9)EnL`7No-8Ty)ad?Kubotw@EdnsQgjttTIaQ?1DPjQ&1<^*eX&huhka^Ts){B;9jX8k6xvo^x^EhaAdN_OD-f|3E+0=Ha^kkR8xPfdMD zeg?9xLCARjyaah-ea>a!>zTAW z^@(8IqHkqa9^o~2Jg^tmZ$T>dCsVJj>ePJ&DO-_B^1@r4YRtXAg3VizN^Je-+f&Cs zc|9E4id2Y>f1k9yaW@QXL(cUUu_`pXfP+)pkn_j~-+Fk;;+JlK+pa}S>&@DT;yFk6 zL&eYrAv=k4{O!`ghh^Az4B44r{Z@>_ Hf8Y8auCqq} delta 12951 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 {| zY{;=T7ph7{MRSALoZshEs^q4;mX>@WKR2hbv{)%^D5`bJmrtOSiCzj7C15AWUt)l< z@zx>pv@0&FAlNKEpWBM6?-ebo*X^-*1zGeu9J1=R`_JESP1 zFb+-5qedgx^gL=T5#icAYAWJf-Z8NaB;->fL7K_}N+Nb3g;XN!E15G)_Lh19Rq-<-@flR4hEc ziB5sIV(Jrw{6{f%HYJqTs6fb7C6rw^b+83YE1^b1e+k9Hhb1Tj6_`>g!#M2{xWnbt z$U(S)o>Hoskje038D)n38Lq~(VoEimBlv5Tm6;78^ z86sQfM;tbIxL1BO%PljfWa8No8g+( zboQZZskeW*RV`mxO0gP30>(%K^`9G8-=(n{qG^ZG80weW7=n>uFfH{n29|cY@BA4H z8#vca8O-dW@Q+N7nbIDKG`|tvSIFSF3H3`o6FrRcpNg;Vr=c!24D?GSr!5E#jQ=^I zotN3z3k%NA2<^~5>br`Ga&4tjTvJ$@TT$0gqR8bf_4dj-xxs2HDU$MPOUf(NoZ6hq z0xKN;k{Sm^Us9D5Z54TyRk^a%B$XEy*@~O0i^U3COOcxERGN!gikqxrc|&DORlxu@ z`6{bg^@xJ%cKa;2q}nkC5?vOz%@>N?MT|ZLyUXJi7W)>qPj-11w6=BnIyyVs=lR-x zTn;6(AWXJOlWn+P5ha_|e#lI}Z%X}lVD}4jRcPzAVYP9MtYiI@S z{>3%)*#D4TF&13nq{wg;2eY_HlV^Y_3WI=@7AwPJnmU z(W6LlsAe3DT2E(;%pcsRuB{%7TsnQO1)*=rz}4&N6haDM)pc|#DTTYPqjM4qb+O}O zNc-TPI7BsTaAVwxfq`p58PM0$VocHCw%K!mB(!aYeb*s>p6ls&?0Mz&bQ-A`a_vuE zPv?*l_~Ck#OeJLBM8^%}-9T3o?C|CrX_-{Ps~gZyp1P4vCuI=50gI}Kiso#H7H<29i3jck6GfVaCfjj>8GmRW{p1&^R+r6h zlN^4F7sGqhX&-LfygrNk>U6z#@aWhE5nRi?gF3N9Mb6XdvBZBBH$-E z!<^jNKFjOs^d(O=C)Zs@Ee`I~&r`4b(I`Z-)jm{}X_Kw8LkiG@*VGd}z&JUKoE5Y= z+-3OUG1VYTq7OO4-JEO{eOATov&n81?%m83U}$#%M^db^a%c;4G)uU+co1HjNICf{ z_CD0=2=j=k{NIs^b^=w;U&P?g@^|Pcv&7UV`n`V1<5Ml7fLRh*^|&l9#p*%fdF(#H z>vDNS52el0avD<4<5H(rJhI#6a@p}Tz=5GL?&~dzBKj?o!!Fol+3FR&iWY;A!3IP_ z^lrhU;&#z)QSEq|AiLali(8N-Oa{0e{uwT-1J4pX6cx%GMv+%E_5c;nG%)9M z_p^2Ehul2g%0Hx!F{}^Mgk5Kx5dNX5J^U3@yQW%GzSS8O6Ub?0V_h~9SROJIgrV0? zZiELuW<>qf!-jpiQ%Og~Mg7Zw(I=P$_~Hb^Lh8qi8Pzs>6`ckfv6S!XYV{_YEB{xG z;l_`dWHQ7Cxq9IE#|*DUHP1fDq(RhY%qTeaF~foFGv;cpnT>76d}Aa0=~J|aZ$D#3 zfN~m*Vish6#w3Rd@<~gmWiD7gLyjLh$tc`aY^)O-O&DsFbehpY_$ej|-u{e9fx*8e zr)hf_=$j6H>njl*ss>?D&2cb;aFF!A{8 zX%vYUK0VD`jqK$7kr_vPLw3FLk4!qb7ViBc68Pb(vrOE;i9a%SLR=7k2Hp2;_$Z2v zgKN()5^=-LXBd&p87ewy9IHiP0=a=7oS#;CEj#nN%R$4+}@IQ{l_AD9=+-Yyun|!Db%%irIZrs?Dv4 zUJ3DuWKo5eR_Z3FI=mj)g(x!WN)T{EjR`Kr zFJT_k?!^?Gr1&f$?}V8Ymm39Rr&$d@mT2l{V=~m$*4gE1)A~ix<+m!>x8TRrog`yw z66Iv^dwdSbCm`R7A9Q`X>{Qj}_DMc;dLnud93Fxz9v`NMM2E-f61|eoV~4qY$h6z) z!u*QIZBb=Ramfw=ow-MJSUjSN$yk?P^ocHbxKEdn>b3i2)u*~GUPlPi}=J%gU2HS8T@iSgd> zFw+iW75RqzkLLGy!hokUIj!(*G%D1uqS+V_V%TWXevumPjb>ZO+z>?x9FJm$cSui$ zG!&~0kUfHBp*w~xLNz=b!zyIap!P$=NcILQV4jU91#AvH_$8AHv!mFlsP(A5Q{ny* zY-*^jg--jw!@vMCa4RA6;r>{58tE9awd3)u5lnGx23Y`8z(WaaEL@eqrjf<4F7&fB zRFsEY|0RK)1i1<5Irv26?r=16SDeTuAmjc-wD|`^Rkz_A>4N7H*>PmW5SVrhJC4VA zGd3lKtg8oM3_F^v4DK_2N%7=QCkT5-W0Pb9&fVO9}x)B%4fn zLn>f~%1G8s^aDg7R5l)jvfYLYE4Z5JP;8Q@z=jQMFvadyZ3;%&xPD@QVpDwtL!t-6V|UZL*#O>8=xy@{Q8g}U?Y7|7npIe95jN*a<#%jm zl`E7FoZZYmP=V>g;gIP(dtqZRe{p$ibiUf~&#U$Syqf=Mb>C3jcOISlY5k?u?8ViW zOICMD^<@%eu29{FCr9Tyr!T3$Ou*DXuf8OvynK2>5f{N-*x*JC--kbsj4`h^&Guom ziwIQ!UsL89Xs%loN z8f_)T1?8AfudBA@D%|B$r(_C}L$NB_gWM5Q_Al+=Py}~t*1u(tPtV|T_E&Mj4uPXH z!{`DHT|hs}SlN%bjr>&plzxLDH{28cj;YnuPG3W7sXM8u+Sjz{ny)lHTb(26fIgrD z{R%#v>JFIT`)*z!Fyv74d+7LS*Iy@a=s?L!O9_e=OXYF;1= zw)gQ9;O1UF2JY_TO>n%AH&ecV0W5v|I1+?E_43g$yAS)hDxingdvTH87AjP!rT`C} zy}X%h9U{87mk(oCEWwjnbYwS0u<4`^_C(@EFiQa^!oo;49+nhvQLsCb9S!>nxCvw# zyjFnMq5=?6&Lu)b8D|&}3b}GESq{C$Tp?LKG@#98oDtgdxasI&AI#&5$eN4F%S*U$ z*k8hlWM!yP3jCvlOC@VBqQC+pG%UvfhI)X`QZAC~#j|%+i)ZiF5-tILRmzQp;yiAA z=uHpbm2#QLdwLmH3V+SxA~79;@f>ndM>OzB8JAA3A3%=3(&`rn2pell*WwJgmSG3& zOZjMW!;gJ4aNAOx1TFk&8J_^3F69%+jTcp#m+>lIziL^=PXSXoHyAQThWcTkYZ)IA z8dVIugSh*%Y4f3doFV*zpGB>8*j3dI2@i7bUs^0UWC!pM{iN8AfuTC^*N3>6h2)-r zFf)JFq`SQd?d-OJAF}u`fjlq}b}jEQk^Qi+oiAqY3T5fxjZEHYq(j$qw5HH~LCyUG zVXOFACUPgt-^`biI|hEUnJ*GDLiRg?I-}u?Tt7daKd8?)Y&Dz-D+xPbG#j4@KWFM; z=joD|W%O^UGnleFLKyfrOp?jNZ}L47A``v_Ry$1;UJ4&S!<*lz5Jfjg8f=T`(Asv@USX_6_#_n%aGVnNT~nOYy^DV&5t~EgnuR}zOcBeuBkRp%*n6KsjVq3PbWGJu~~TS;JHVH zSSl74l$X}mOSu)Ls-yDIasEBixSuxCMo||2GSaVjoq6iR8dHDx-@~hoZyM#WWB=LJ zK@6+g;6Q}_|KA1tfA=mR^e5?C$^>IWbZ-8Kr$Vk{jOH4R={{3&_#MXYjlE$Xh20c3 z$*@-cfqoW$grCRroQHjqovz!fD`8GCIrOb`BGpT!Y5!Y0mMqhJueoNc6En7Eob(Zg z+zUpDH@pnrnf2Y#Hcop@$@JDo1F7op4M__P>&EJXoayX_i3@un|V5>btoN+mXZ5 zK6&>}*qN%|%VpkKb+@x@;c3VohnQm?{bbvubNl`9**L`1e>6#FnlkwhP&6JfwVjWB zN-cDBz@G7l8Jjb$!Myp)B`|dYVvc_y?ySAEAr2m&fS99CPRp7Qas7MXnuwSg4KvH9 zk66n9n}(R?;@~!m^x95nO+!rn)s7LG1Kq)ZPv7;&i7y<4wBRSPw#`I@z*VIV}`ywX2v(?-fP}! z-5D5_a{h)XMZuX2{f)ZGf61!30WWb|5I%Ec^up|Ql}F)w3&I;ZPA`by9uVP=PfUrt+Jn6RK8igqBT?#NmV-*H9*dv+iuKmMI3!e0Mgg{gp;h6kTa zi@1uN503+4rhn6Y)wC^(`@nSzVrD*a;2o#yTNki95i|C8k7~!Z2)m(mCt^y*q`8Gl zp1T>I-iesWC+5u)-rC`YYjz=Kn*FvH)B`7a;k{k@y(4WNxVhx%+q8j{yx~bm58Xo^ zdsiwPyj4G+o8Vsdr6Xm@DVVq$dkJ@~EsIW0+5`xFY*r@&Fv$jlCo#Jz`DY z_TTqG@1Bq}l!@DmN6)9=`#s3H*>zx!r9LsQ_n9w8g9%>0q3KLnaa%iMqF^_894bU zVorb1lG%9VuKiGaNMFGoA6r_Y+pdNFLkJoD)$f!Ey_hla(XzMy4}ZxB3jhEB