Commit db6cb6bf authored by Andrey Vertiprahov's avatar Andrey Vertiprahov

Merge branch 'noc-apikey-acl' into 'master'

APIKey ACL

See merge request !1516
parents e733fee0 f4db78a7
Pipeline #10421 passed with stages
in 1 minute and 7 seconds
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------
# ACL Address Checking implementation
# ----------------------------------------------------------------------
# Copyright (C) 2007-2019 The NOC Project
# See LICENSE for details
# ----------------------------------------------------------------------
# Python modules
import socket
import struct
def _iter_pairs(prefixes):
"""
Yield tuples of first, last addresses as integer
:param prefixes: Iterable of IPv4 prefixes
:return: yield (first, last)
"""
for prefix in prefixes:
p, mask = prefix.split("/")
p = struct.unpack("!L", socket.inet_aton(p))[0]
mask = int(mask)
p_mask = ((1 << mask) - 1) << (32 - mask)
b_mask = 0xFFFFFFFF ^ p_mask
yield p & p_mask, p | b_mask
def match(prefixes, ip):
"""
Check if ip is match against ACL
:param prefixes: Iterable of IPv4 prefixes
:param ip: IPv4 address
:return: True if ip is in prefixes
"""
a = struct.unpack("!L", socket.inet_aton(ip))[0]
return any(f <= a <= l for f, l in _iter_pairs(prefixes))
......@@ -2,7 +2,7 @@
# ----------------------------------------------------------------------
# APIKey model
# ----------------------------------------------------------------------
# Copyright (C) 2007-2018 The NOC Project
# Copyright (C) 2007-2019 The NOC Project
# See LICENSE for details
# ----------------------------------------------------------------------
......@@ -12,6 +12,8 @@ import datetime
from mongoengine.document import Document, EmbeddedDocument
from mongoengine.fields import (StringField, BooleanField, DateTimeField, ListField,
EmbeddedDocumentField)
# NOC modules
from noc.core.acl import match
class APIAccess(EmbeddedDocument):
......@@ -24,6 +26,17 @@ class APIAccess(EmbeddedDocument):
return "%s:%s" % (self.api, self.role)
class APIAccessACL(EmbeddedDocument):
prefix = StringField()
is_active = BooleanField(default=True)
description = StringField()
def __unicode__(self):
if self.is_active:
return self.prefix
return "%s (inactive)" % self.prefix
class APIKey(Document):
meta = {
"collection": "apikeys",
......@@ -39,15 +52,18 @@ class APIKey(Document):
key = StringField(unique=True)
# Access settings
access = ListField(EmbeddedDocumentField(APIAccess))
# Address restrictions
acl = ListField(EmbeddedDocumentField(APIAccessACL))
def __unicode__(self):
return self.name
@classmethod
def get_name_and_access(cls, key):
def get_name_and_access(cls, key, ip=None):
"""
Return access settings for key and key name
:param key: API key
:param ip: IP address to check against ACL
:return: (Name, [(api, role), ...]. Name is None for denied permissions
"""
# Find key
......@@ -64,6 +80,12 @@ class APIKey(Document):
if expires and expires < datetime.datetime.now():
# Expired
return None, []
# Check ACL
if ip:
acl = doc.get("acl")
if acl and not match((a["prefix"] for a in acl if a.get("is_active")), ip):
# Forbidden
return None, []
# Process key access
access = doc.get("access", [])
r = []
......@@ -72,33 +94,34 @@ class APIKey(Document):
return doc["name"], r
@classmethod
def get_access(cls, key):
def get_access(cls, key, ip=None):
"""
Return access settings for key
:param key: API key
:param ip: IP address to check against ACL
:return: List of (api, role). Empty list for denied permissions
"""
return cls.get_name_and_access(key)[1]
return cls.get_name_and_access(key, ip)[1]
@classmethod
def get_access_str(cls, key):
def get_access_str(cls, key, ip=None):
"""
Return access settings as string
:param key: API key
:param ip: IP address to check against ACL
:return: String of '<api>:<role>,<api>:<role>,...'
"""
r = ["%s:%s" % a for a in cls.get_access(key)]
return str(",".join(r))
return str(",".join("%s:%s" % a for a in cls.get_name_and_access(key, ip)[1]))
@classmethod
def get_name_and_access_str(cls, key):
def get_name_and_access_str(cls, key, ip=None):
"""
Return key name and access settings as string
:param key:
:param key: API key
:param ip: IP address to check against ACL
:return:
"""
name, permissions = cls.get_name_and_access(key)
name, permissions = cls.get_name_and_access(key, ip)
if not name:
return None, ""
r = ["%s:%s" % a for a in permissions]
return name, str(",".join(r))
return name, str(",".join("%s:%s" % a for a in permissions))
......@@ -2,7 +2,7 @@
# ---------------------------------------------------------------------
# Authentication handler
# ---------------------------------------------------------------------
# Copyright (C) 2007-2016 The NOC Project
# Copyright (C) 2007-2019 The NOC Project
# See LICENSE for details
# ---------------------------------------------------------------------
......@@ -41,7 +41,10 @@ class AuthRequestHandler(tornado.web.RequestHandler):
if user:
return success(user)
elif self.request.headers.get("Private-Token"):
name, access = self.service.get_api_access(self.request.headers.get("Private-Token"))
name, access = self.service.get_api_access(
self.request.headers.get("Private-Token"),
self.request.remote_ip
)
if name and access:
return api_success(access, name)
elif self.request.headers.get("Authorization"):
......
......@@ -3,7 +3,7 @@
# ---------------------------------------------------------------------
# Login service
# ---------------------------------------------------------------------
# Copyright (C) 2007-2018 The NOC Project
# Copyright (C) 2007-2019 The NOC Project
# See LICENSE for details
# ---------------------------------------------------------------------
......@@ -122,8 +122,8 @@ class LoginService(UIService):
return r
@cachetools.cachedmethod(operator.attrgetter("_apikey_cache"))
def get_api_access(self, key):
return APIKey.get_name_and_access_str(key)
def get_api_access(self, key, ip):
return APIKey.get_name_and_access_str(key, ip)
if __name__ == "__main__":
......
//---------------------------------------------------------------------
// main.apikey application
//---------------------------------------------------------------------
// Copyright (C) 2007-2018 The NOC Project
// Copyright (C) 2007-2019 The NOC Project
// See LICENSE for details
//---------------------------------------------------------------------
console.debug("Defining NOC.main.apikey.Application");
......@@ -107,6 +107,32 @@ Ext.define("NOC.main.apikey.Application", {
editor: "textfield"
}
]
},
{
name: "acl",
xtype: "gridfield",
fieldLabel: __("ACL"),
columns: [
{
text: __("Prefix"),
dataIndex: "prefix",
width: 150,
editor: "textfield"
},
{
text: __("Active"),
dataIndex: "is_active",
width: 50,
renderer: NOC.render.Bool,
editor: "checkbox"
},
{
text: __("Description"),
dataIndex: "description",
flex: 1,
editor: "textfield"
}
]
}
]
});
......
//---------------------------------------------------------------------
// main.apikey Model
//---------------------------------------------------------------------
// Copyright (C) 2007-2018 The NOC Project
// Copyright (C) 2007-2019 The NOC Project
// See LICENSE for details
//---------------------------------------------------------------------
console.debug("Defining NOC.main.apikey.Model");
......@@ -31,6 +31,10 @@ Ext.define("NOC.main.apikey.Model", {
name: "access",
type: "auto"
},
{
name: "acl",
type: "auto"
},
{
name: "key",
type: "string"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment