Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

#!/usr/bin/env python 

 

""" 

@file ion/core/intercept/policy.py 

@author Michael Meisinger 

@brief Policy checking interceptor 

""" 

 

from twisted.internet import defer 

from zope.interface import implements, Interface 

 

import ion.util.ionlog 

log = ion.util.ionlog.getLogger(__name__) 

 

from ion.core import ioninit 

from ion.core.intercept.interceptor import EnvelopeInterceptor 

 

from ion.core.messaging.message_client import MessageInstance 

 

from ion.core.process.cprocess import Invocation 

 

import time 

 

from ion.util.config import Config 

 

from ion.services.coi.datastore_bootstrap.ion_preload_config \ 

    import OWNED_BY_ID, HAS_ROLE_ID, ROLE_NAMES_BY_ID, ROLE_IDS_BY_NAME 

from ion.services.dm.inventory.association_service import AssociationServiceClient, ASSOCIATION_QUERY_MSG_TYPE 

from ion.services.dm.inventory.association_service import IDREF_TYPE 

from ion.core.messaging.message_client import MessageClient 

 

from google.protobuf.internal.containers import RepeatedScalarFieldContainer 

 

CONF = ioninit.config(__name__) 

# Master set of roles and their user-friendly names 

all_roles = {'ANONYMOUS': 'Guest', 'AUTHENTICATED': 'User', 'DATA_PROVIDER': 'Data Provider', 

             'MARINE_OPERATOR': 'Marine Operator', 'EARLY_ADOPTER': 'Early Adopter', 

             'OWNER': 'Owner', 'ADMIN': 'Administrator'} 

 

def construct_policy_lists(policydb): 

    thedict = {} 

    try: 

        for policy_entry in policydb: 

            role, action, resources = policy_entry 

            service, opname = action.split('.', 1) 

            assert role in all_roles 

 

            if role == 'ADMIN': 

                role_set = set(['ADMIN']) 

            elif role == 'OWNER': 

                role_set = set(['OWNER', 'ADMIN']) 

            elif role == 'MARINE_OPERATOR': 

                role_set = set(['MARINE_OPERATOR', 'ADMIN']) 

            elif role == 'DATA_PROVIDER': 

                role_set = set(['DATA_PROVIDER', 'ADMIN']) 

            elif role == 'AUTHENTICATED': 

                role_set = set(['AUTHENTICATED', 'ADMIN']) 

            else: 

                role_set = set(['ANONYMOUS', 'AUTHENTICATED', 'ADMIN']) 

 

            service_dict = thedict.setdefault(service, {}) 

            op_dict = service_dict.setdefault(opname, {}) 

            set_of_roles = op_dict.setdefault('roles', set()) 

            set_of_roles.update(role_set) 

            op_dict['resources'] = resources 

 

    except Exception, ex: 

        log.exception('----- POLICY INIT ERROR -----') 

        raise ex 

    return thedict 

 

policydb_filename = ioninit.adjust_dir(CONF.getValue('policydecisionpointdb')) 

policy_dictionary = construct_policy_lists(Config(policydb_filename).getObject()) 

 

def construct_user_role_lists(userroledict): 

    roles = userroledict['roles'] 

 

    roledict = {} 

    for role_name in ('ADMIN', 'DATA_PROVIDER', 'MARINE_OPERATOR', 'EARLY_ADOPTER'): 

        roledict[role_name] = {'subject': set(roles[role_name]), 'ooi_id': set()} 

 

    return roledict 

 

userroledb_filename = ioninit.adjust_dir(CONF.getValue('userroledb')) 

role_user_dict = construct_user_role_lists(Config(userroledb_filename).getObject()) 

user_role_dict = {} # cache the current role for an ooi_id 

 

def subject_has_role(subject, role): 

    if role == 'ANONYMOUS': 

        return True 

    return subject in role_user_dict[role]['subject'] 

 

# Role methods 

def user_has_role(ooi_id, role): 

    if role == 'OWNER': 

        # Will be special handled within the policy flow 

        return False 

    if role == 'ANONYMOUS': 

        return True 

    elif role == 'AUTHENTICATED': 

        return ooi_id != 'ANONYMOUS' 

    else: 

        return ooi_id in role_user_dict[role]['ooi_id'] 

 

def get_current_roles(ooi_id): 

    roles = user_role_dict.get(ooi_id, None) 

    if roles is None:       return ['AUTHENTICATED'] 

 

    # If more than one role, just grab one for now. There should be only one. 

    roles -= set(['ANONYMOUS', 'AUTHENTICATED']) 

    if len(roles) == 0:     return ['AUTHENTICATED'] 

 

    return list(roles) 

 

def map_ooi_id_to_role(ooi_id, role): 

    if not role in role_user_dict: 

        role_user_dict[role] = {'subject': set(), 'ooi_id': set()} 

    role_user_dict[role]['ooi_id'].add(ooi_id) 

 

    if not ooi_id in user_role_dict: 

        user_role_dict[ooi_id] = set() 

    user_role_dict[ooi_id].add(role) 

 

def unmap_ooi_id_from_role(ooi_id, role): 

    if role in role_user_dict: 

        if ooi_id in role_user_dict[role]['ooi_id']: 

            role_user_dict[role]['ooi_id'].remove(ooi_id) 

    if ooi_id in user_role_dict: 

        if role in user_role_dict[ooi_id]: 

            user_role_dict[ooi_id].remove(role) 

 

def map_ooi_id_to_subject_role(subject, ooi_id, role): 

    if subject in role_user_dict[role]['subject']: 

        map_ooi_id_to_role(ooi_id, role) 

 

# Role convenience methods 

def map_ooi_id_to_subject_admin_role(subject, ooi_id): 

    map_ooi_id_to_subject_role(subject, ooi_id, 'ADMIN') 

 

def subject_has_admin_role(subject): 

    return subject_has_role(subject, 'ADMIN') 

 

def user_has_admin_role(ooi_id): 

    return user_has_role(ooi_id, 'ADMIN') 

 

def map_ooi_id_to_subject_data_provider_role(subject, ooi_id): 

    map_ooi_id_to_subject_role(subject, ooi_id, 'DATA_PROVIDER') 

 

def subject_has_data_provider_role(subject): 

    return subject_has_role(subject, 'DATA_PROVIDER') 

 

def user_has_data_provider_role(ooi_id): 

    return user_has_role(ooi_id, 'DATA_PROVIDER') 

 

def map_ooi_id_to_subject_marine_operator_role(subject, ooi_id): 

    map_ooi_id_to_subject_role(subject, ooi_id, 'MARINE_OPERATOR') 

 

def subject_has_marine_operator_role(subject): 

    return subject_has_role(subject, 'MARINE_OPERATOR') 

 

def user_has_marine_operator_role(ooi_id): 

    return user_has_role(ooi_id, 'MARINE_OPERATOR') 

 

def map_ooi_id_to_subject_early_adopter_role(subject, ooi_id): 

    map_ooi_id_to_subject_role(subject, ooi_id, 'EARLY_ADOPTER') 

 

def subject_has_early_adopter_role(subject): 

    return subject_has_role(subject, 'EARLY_ADOPTER') 

 

def user_has_early_adopter_role(ooi_id): 

    return user_has_role(ooi_id, 'EARLY_ADOPTER') 

 

@defer.inlineCallbacks 

def load_roles_from_associations(asc): 

    role_map = yield asc.get_associations_map({'predicate': HAS_ROLE_ID}) 

    for user_id, role_id in role_map.iteritems(): 

        map_ooi_id_to_role(user_id, ROLE_NAMES_BY_ID[role_id]) 

 

class PolicyInterceptor(EnvelopeInterceptor): 

    def before(self, invocation): 

        msg = invocation.content 

        return self.is_authorized(msg, invocation) 

 

    def after(self, invocation): 

        return invocation 

 

    @defer.inlineCallbacks 

    def is_authorized(self, msg, invocation): 

        """ 

        @brief Policy enforcement method which implements the functionality 

            conceptualized as the policy decision point (PDP). 

        This method 

        will take the specified user id, convert it into a role.  A search 

        will then be performed on the global policy_dictionary to determine if 

        the user has the appropriate authority to access the specified 

        resource via the specified action. A final check is made to determine 

        if the user's authentication has expired. 

        The following rules are applied to determine authority: 

        - If there are no policy tuple entries for service, or no policy 

        tuple entries for the specified role, the action is assumed to be allowed. 

        - Else, there is a policy tuple for this service:operation.  A check 

        is made to ensure the user role is equal to or greater than the 

        required role. 

        Role precedence from lower to higher is: 

            ANONYMOUS, AUTHORIZED, OWNER, ADMIN 

        @param msg: message content from invocation 

        @param invocation: invocation object passed on interceptor stack. 

        @return: invocation object indicating status of authority check 

        """ 

 

        # Ignore messages that are not of performative 'request' 

        if msg.get('performative', None) != 'request': 

            defer.returnValue(invocation) 

 

        # Reject improperly defined messages 

        if not 'user-id' in msg: 

            log.error("Policy Interceptor: Rejecting improperly defined message missing user-id [%s]." % str(msg)) 

            invocation.drop(note='Error: no user-id defined in message header!', code=Invocation.CODE_BAD_REQUEST) 

            defer.returnValue(invocation) 

        if not 'expiry' in msg: 

            log.error("Policy Interceptor: Rejecting improperly defined message missing expiry [%s]." % str(msg)) 

            invocation.drop(note='Error: no expiry defined in message header!', code=Invocation.CODE_BAD_REQUEST) 

            defer.returnValue(invocation) 

        if not 'receiver' in msg: 

            log.error("Policy Interceptor: Rejecting improperly defined message missing receiver [%s]." % str(msg)) 

            invocation.drop(note='Error: no receiver defined in message header!', code=Invocation.CODE_BAD_REQUEST) 

            defer.returnValue(invocation) 

        if not 'op'in msg: 

            log.error("Policy Interceptor: Rejecting improperly defined message missing op [%s]." % str(msg)) 

            invocation.drop(note='Error: no op defined in message header!', code=Invocation.CODE_BAD_REQUEST) 

            defer.returnValue(invocation) 

 

        user_id = msg['user-id'] 

        expirystr = msg['expiry'] 

 

        if not type(expirystr) is str: 

            log.error("Policy Interceptor: Rejecting improperly defined message with bad expiry [%s]." % str(expirystr)) 

            invocation.drop(note='Error: expiry improperly defined in message header!', code=Invocation.CODE_BAD_REQUEST) 

            defer.returnValue(invocation) 

 

        try: 

            expiry = int(expirystr) 

        except ValueError, ex: 

            log.error("Policy Interceptor: Rejecting improperly defined message with bad expiry [%s]." % str(expirystr)) 

            invocation.drop(note='Error: expiry improperly defined in message header!', code=Invocation.CODE_BAD_REQUEST) 

            defer.returnValue(invocation) 

 

        rcvr = msg['receiver'] 

        service = rcvr.rsplit('.',1)[-1] 

 

        operation = msg['op'] 

 

        log.info('Policy Interceptor: Authorization request for service [%s] operation [%s] user_id [%s] expiry [%s]' % (service, operation, user_id, expiry)) 

        if service in policy_dictionary: 

            service_list = policy_dictionary[service] 

            # TODO figure out how to handle non-wildcard resource ids 

            if operation in service_list: 

                role_entry = service_list[operation]['roles'] 

                log.info('Policy Interceptor: Policy tuple [%s]' % str(role_entry)) 

 

                role_match_found = False 

                for role in role_entry: 

                    if user_has_role(user_id, role): 

                        log.info('Policy Interceptor: Role <%s> authentication matches' % role) 

                        role_match_found = True 

                        break 

 

                if role_match_found == False: 

                    # Special handling for ownership role 

                    # ANONYMOUS can never own a resource, so return fail 

                    if user_id == 'ANONYMOUS': 

                        log.warn('Policy Interceptor: Authentication failed for service [%s] operation [%s] resource [%s] user_id [%s] expiry [%s] for roles [%s]. Returning Not Authorized.' % (service, operation, '*', user_id, expiry, str(role_entry))) 

                        invocation.drop(note='Not authorized', code=Invocation.CODE_UNAUTHORIZED) 

                        defer.returnValue(invocation) 

 

                    isOwnershipPolicy = False 

                    for role in role_entry: 

                        if role == 'OWNER': 

                            isOwnershipPolicy = True 

                            break 

 

                    if isOwnershipPolicy == True: 

                        return_uuid_list = self.find_uuids(invocation, msg, user_id, service_list[operation]['resources']) 

                        if invocation.status != Invocation.STATUS_PROCESS: 

                            log.warn('Policy Interceptor: Authentication failed for service [%s] operation [%s] resource [%s] user_id [%s] expiry [%s] for role [OWNER].' % (service, operation, '*', user_id, expiry)) 

                            defer.returnValue(invocation) 

 

                        yield self.check_owner(user_id, return_uuid_list, invocation) 

                        if invocation.status != Invocation.STATUS_PROCESS: 

                            log.warn('Policy Interceptor: Authentication failed for service [%s] operation [%s] resource [%s] user_id [%s] expiry [%s] for role [OWNER].' % (service, operation, '*', user_id, expiry)) 

                            defer.returnValue(invocation) 

                        else: 

                            log.info('Policy Interceptor: Role <OWNER> authentication matches') 

                    else: 

                        log.warn('Policy Interceptor: Authentication failed for service [%s] operation [%s] resource [%s] user_id [%s] expiry [%s] for roles [%s]. Returning Not Authorized.' % (service, operation, '*', user_id, expiry, str(role_entry))) 

                        invocation.drop(note='Not authorized', code=Invocation.CODE_UNAUTHORIZED) 

                        defer.returnValue(invocation) 

            else: 

                log.info('Policy Interceptor: operation not in policy dictionary.') 

        else: 

            log.info('Policy Interceptor: service not in policy dictionary.') 

 

        expiry_time = int(expiry) 

        if (expiry_time > 0): 

            current_time = time.time() 

 

            if current_time > expiry_time: 

                log.warn('Policy Interceptor: Current time [%s] exceeds expiry [%s] for service [%s] operation [%s] resource [%s] user_id [%s] . Returning Not Authorized.' % (str(current_time), expiry, service, operation, '*', user_id)) 

                invocation.drop(note='Authentication expired', code=Invocation.CODE_UNAUTHORIZED) 

                defer.returnValue(invocation) 

 

        log.info('Policy Interceptor: Returning Authorized.') 

        defer.returnValue(invocation) 

 

    @defer.inlineCallbacks 

    def check_owner(self, user_id, uuid_list, invocation): 

        self.mc = MessageClient(proc=invocation.process) 

        self.asc = AssociationServiceClient(proc=invocation.process) 

 

        for uuid in uuid_list: 

            request = yield self.mc.create_instance(ASSOCIATION_QUERY_MSG_TYPE) 

 

            request.object = request.CreateObject(IDREF_TYPE) 

            request.object.key = user_id 

 

            request.predicate = request.CreateObject(IDREF_TYPE) 

            request.predicate.key = OWNED_BY_ID 

 

            request.subject = request.CreateObject(IDREF_TYPE) 

            request.subject.key = uuid 

 

            # make the request 

            log.warn('Calling association service for user id <%s> and uuid <%s>' % (user_id, uuid)) 

            result = yield self.asc.association_exists(request) 

            log.warn('Return from association service call for user id <%s> and uuid <%s>' % (user_id, uuid)) 

            if result.result == False: 

                log.warn('Policy Interceptor: Authentication failed. User <%s> does not own resource <%s>.' % (user_id, uuid)) 

                invocation.drop(note='Not authorized', code=Invocation.CODE_UNAUTHORIZED) 

                return 

            else: 

                log.warn('Policy Interceptor: User <%s> owns resource <%s>.' % (user_id, uuid)) 

 

    def find_uuids(self, invocation, msg, user_id, resources): 

        """ 

        Traverses message structure looking 

        for occurrences of field "resource id". 

        For each found, association check is made 

        to see if user is an owner of the resource. 

        """ 

 

        log.info('Policy Interceptor: In check_resource_ownership. Resources: <%s>' % str(resources)) 

 

        content = msg.get('content','') 

        if isinstance(content, MessageInstance): 

            wrapper = content.Message 

            repo = content.Repository 

            uuid_list = self.find_uuids_traverse_gpbs(invocation, msg, wrapper, repo, user_id, resources) 

            if len(uuid_list) == 0: 

                log.error("Policy Interceptor: Rejecting improperly defined message.  No uuids found.") 

                invocation.drop(note='Error: Expected uuids missing from message payload!', code=Invocation.CODE_BAD_REQUEST) 

            else: 

                return uuid_list 

        else: 

            log.error("Policy Interceptor: Rejecting improperly defined message missing MessageInstance [%s]." % str(msg)) 

            invocation.drop(note='Error: MessageInstance missing from message payload!', code=Invocation.CODE_BAD_REQUEST) 

 

    def find_uuids_traverse_gpbs(self, invocation, msg, wrapper, repo, user_id, resources, uuid_list = None): 

        log.info('Policy Interceptor: In check_resource_ownership_traverse_gpbs') 

 

        if uuid_list is None: 

            uuid_list = [] 

 

        childLinksSet = wrapper.ChildLinks 

 

        if len(childLinksSet) == 0: 

            log.info('Policy Interceptor: Returning from check_resource_ownership_traverse_gpbs.  ChildLinksSet zero length.') 

            return 

 

        for link in wrapper.ChildLinks: 

            obj = repo.get_linked_object(link) 

            type = obj.ObjectType 

            typeId = type.object_id 

            log.info('Policy Interceptor: In check_resource_ownership_traverse_gpbs.  Child type: <%s>' % str(typeId)) 

            if typeId in resources: 

                log.info('Policy Interceptor: In check_resource_ownership_traverse_gpbs.  Child type match found in resources') 

                gpbMessage = obj.GPBMessage 

                uuid = getattr(gpbMessage,resources[typeId]) 

                log.info('Policy Interceptor: In check_resource_ownership_traverse_gpbs.  GPB type: %s UUID: %s' % (str(typeId),uuid)) 

                if not uuid: 

                    log.error("Policy Interceptor: Rejecting improperly defined message missing expected uuid [%s]." % str(msg)) 

                    invocation.drop(note='Error: Uuid missing from message payload!', code=Invocation.CODE_BAD_REQUEST) 

                    return 

                if uuid == '': 

                    log.error("Policy Interceptor: Rejecting improperly defined message missing expected uuid [%s]." % str(msg)) 

                    invocation.drop(note='Error: Uuid missing from message payload!', code=Invocation.CODE_BAD_REQUEST) 

                    return 

                if isinstance(uuid, RepeatedScalarFieldContainer): 

                    uuid_values = uuid._values 

                    for id in uuid_values: 

                        uuid_list.append(id.decode('utf-8')) 

                elif isinstance(uuid, unicode): 

                    uuid_list.append(uuid.decode('utf-8')) 

                else: 

                    log.error("Policy Interceptor: Rejecting improperly defined message with unexpected uuid variable type [%s]." % str(msg)) 

                    invocation.drop(note='Error: Uuid variable type not supported!', code=Invocation.CODE_BAD_REQUEST) 

                    return 

                log.info('Policy Interceptor: In check_resource_ownership_traverse_gpbs.  Added UUID: %s to return list' % uuid) 

 

            log.info('Policy Interceptor: Recursing.') 

            self.find_uuids_traverse_gpbs(invocation, msg, obj, repo, user_id, resources, uuid_list) 

        return uuid_list