Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cd31da9

Browse files
committedDec 3, 2019
feat: initial commit
0 parents  commit cd31da9

9 files changed

+1841
-0
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
*.log
3+
build

‎LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Shelley Vohr
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# node-mac-permissions
2+
3+
```js
4+
$ npm i node-mac-permissions
5+
```
6+
7+
This native Node.js module allows you to manage an app's access to:
8+
9+
* Contacts
10+
* Full Disk Access
11+
* Calendar
12+
* Photos
13+
14+
## API
15+
16+
## `permissions.getAuthStatus(type)`
17+
18+
* `type` - The type of system component to which you are requesting access. Can be one of `contacts`, `full-disk-access`, `photos`, or `calendar`.
19+
20+
Returns `String` - Can be one of 'Not Determined', 'Denied', 'Authorized', or 'Restricted'.
21+
22+
Checks the authorization status of the application to access `type` on macOS.
23+
24+
Return Value Descriptions:
25+
* 'Not Determined' - The user has not yet made a choice regarding whether the application may access `type` data.
26+
* 'Not Authorized' - The application is not authorized to access `type` data. The user cannot change this application’s status, possibly due to active restrictions such as parental controls being in place.
27+
* 'Denied' - The user explicitly denied access to `type` data for the application.
28+
* 'Authorized' - The application is authorized to access `type` data.
29+
30+
**Note:** Access to `contacts` will always return a status of 'Authorized' prior to macOS 10.13 High Sierra, as access to contacts was unilaterally allowed until that version.

‎binding.gyp

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"targets": [{
3+
"target_name": "permissions",
4+
"sources": [ ],
5+
"conditions": [
6+
['OS=="mac"', {
7+
"sources": [
8+
"permissions.mm"
9+
],
10+
}]
11+
],
12+
'include_dirs': [
13+
"<!@(node -p \"require('node-addon-api').include\")"
14+
],
15+
'libraries': [],
16+
'dependencies': [
17+
"<!(node -p \"require('node-addon-api').gyp\")"
18+
],
19+
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
20+
"xcode_settings": {
21+
"OTHER_CPLUSPLUSFLAGS": ["-std=c++14", "-stdlib=libc++", "-mmacosx-version-min=10.10"],
22+
"OTHER_LDFLAGS": ["-framework CoreFoundation -framework AppKit -framework Contacts -framework EventKit -framework Photos"]
23+
}
24+
}]
25+
}

‎index.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const permissions = require('bindings')('permissions.node')
2+
3+
function getAuthStatus(type) {
4+
const validTypes = ['contacts', 'calendar', 'photos', 'full-disk-access']
5+
if (!validTypes.includes(type)) {
6+
throw new TypeError(`${type} is not a valid type`)
7+
}
8+
9+
return permissions.getAuthStatus.call(this, type)
10+
}
11+
12+
module.exports = {
13+
getAuthStatus
14+
}

‎package-lock.json

+1,554
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "node-mac-permissions",
3+
"version": "1.0.0",
4+
"description": "A native node module to manage system permissions on macOS",
5+
"main": "index.js",
6+
"scripts": {
7+
"build": "node-gyp rebuild",
8+
"clean": "node-gyp clean",
9+
"test": "./node_modules/.bin/mocha --reporter spec"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/codebytere/node-mac-permissions.git"
14+
},
15+
"keywords": [
16+
"permissions",
17+
"macos",
18+
"node",
19+
"native"
20+
],
21+
"author": "Shelley Vohr <shelley.vohr@gmail.com>",
22+
"license": "MIT",
23+
"bugs": {
24+
"url": "https://github.com/codebytere/node-mac-permissions/issues"
25+
},
26+
"homepage": "https://github.com/codebytere/node-mac-permissions#readme",
27+
"dependencies": {
28+
"bindings": "^1.5.0",
29+
"node-addon-api": "^2.0.0"
30+
},
31+
"devDependencies": {
32+
"chai": "^4.2.0",
33+
"mocha": "^6.2.2",
34+
"node-gyp": "^6.0.1"
35+
}
36+
}

‎permissions.mm

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#include <napi.h>
2+
3+
// Apple APIs
4+
#import <AppKit/AppKit.h>
5+
#import <Contacts/Contacts.h>
6+
#import <EventKit/EventKit.h>
7+
#import <Foundation/Foundation.h>
8+
#import <Photos/Photos.h>
9+
#import <pwd.h>
10+
11+
/***** HELPER FUNCTIONS *****/
12+
13+
NSString* GetUserHomeFolderPath() {
14+
NSString* path;
15+
BOOL isSandboxed = (nil != NSProcessInfo.processInfo.environment[@"APP_SANDBOX_CONTAINER_ID"]);
16+
17+
if (isSandboxed) {
18+
struct passwd *pw = getpwuid(getuid());
19+
assert(pw);
20+
path = [NSString stringWithUTF8String:pw->pw_dir];
21+
} else {
22+
path = NSHomeDirectory();
23+
}
24+
25+
return path;
26+
}
27+
28+
// Returns a status indicating whether or not the user has authorized Contacts access
29+
std::string ContactAuthStatus() {
30+
std::string auth_status = "Not Determined";
31+
32+
CNEntityType entityType = CNEntityTypeContacts;
33+
CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:entityType];
34+
35+
if (status == CNAuthorizationStatusAuthorized)
36+
auth_status = "Authorized";
37+
else if (status == CNAuthorizationStatusDenied)
38+
auth_status = "Denied";
39+
else if (status == CNAuthorizationStatusRestricted)
40+
auth_status = "Restricted";
41+
42+
return auth_status;
43+
}
44+
45+
// Returns a status indicating whether or not the user has authorized Calendar access
46+
std::string CalendarAuthStatus() {
47+
std::string auth_status = "Not Determined";
48+
49+
EKEntityType entityType = EKEntityTypeEvent;
50+
EKAuthorizationStatus status = [EKEventStore authorizationStatusForEntityType:entityType];
51+
52+
if (status == EKAuthorizationStatusAuthorized)
53+
auth_status = "Authorized";
54+
else if (status == EKAuthorizationStatusDenied)
55+
auth_status = "Denied";
56+
else if (status == EKAuthorizationStatusRestricted)
57+
auth_status = "Restricted";
58+
59+
return auth_status;
60+
}
61+
62+
// Returns a status indicating whether or not the user has authorized Photos access
63+
std::string PhotosAuthStatus() {
64+
std::string auth_status = "Not Determined";
65+
66+
if (@available(macOS 10.13, *)) {
67+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
68+
69+
if (status == PHAuthorizationStatusAuthorized)
70+
auth_status = "Authorized";
71+
else if (status == PHAuthorizationStatusDenied)
72+
auth_status = "Denied";
73+
else if (status == PHAuthorizationStatusRestricted)
74+
auth_status = "Restricted";
75+
} else {
76+
auth_status = "Authorized";
77+
}
78+
79+
return auth_status;
80+
}
81+
82+
std::string FDAAuthStatus() {
83+
std::string auth_status = "Not Determined";
84+
NSString *path;
85+
NSString* home_folder = GetUserHomeFolderPath();
86+
87+
if (@available(macOS 10.15, *)) {
88+
path = [home_folder stringByAppendingPathComponent:@"Library/Safari/CloudTabs.db"];
89+
} else {
90+
path = [home_folder stringByAppendingPathComponent:@"Library/Safari/Bookmarks.plist"];
91+
}
92+
93+
NSFileManager* manager = [NSFileManager defaultManager];
94+
BOOL file_exists = [manager fileExistsAtPath:path];
95+
NSData *data = [NSData dataWithContentsOfFile:path];
96+
if (data == nil && file_exists) {
97+
auth_status = "Denied";
98+
} else if (file_exists) {
99+
auth_status = "Authorized";
100+
}
101+
102+
return auth_status;
103+
}
104+
105+
/***** EXPORTED FUNCTIONS *****/
106+
107+
// Returns the user's access consent status as a string
108+
Napi::Value GetAuthStatus(const Napi::CallbackInfo &info) {
109+
Napi::Env env = info.Env();
110+
std::string auth_status;
111+
112+
const std::string type = info[0].As<Napi::String>().Utf8Value();
113+
if (type == "contacts") {
114+
auth_status = ContactAuthStatus();
115+
} else if (type == "calendar") {
116+
auth_status = CalendarAuthStatus();
117+
} else if (type == "photos") {
118+
auth_status = PhotosAuthStatus();
119+
} else if (type == "full-disk-access") {
120+
auth_status = FDAAuthStatus();
121+
}
122+
123+
return Napi::Value::From(env, auth_status);
124+
}
125+
126+
// Initializes all functions exposed to JS
127+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
128+
exports.Set(
129+
Napi::String::New(env, "getAuthStatus"), Napi::Function::New(env, GetAuthStatus)
130+
);
131+
132+
return exports;
133+
}
134+
135+
NODE_API_MODULE(permissions, Init)

‎test/module.spec.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const { expect } = require('chai')
2+
const {
3+
getAuthStatus
4+
} = require('../index')
5+
6+
describe('node-mac-permissions', () => {
7+
describe('getAuthStatus()', () => {
8+
it('should throw on invalid types', () => {
9+
expect(() => {
10+
getAuthStatus('bad-type')
11+
}).to.throw(/bad-type is not a valid type/)
12+
})
13+
14+
it('should return a string', () => {
15+
const types = ['contacts', 'calendar', 'photos', 'full-disk-access']
16+
const statuses = ['Not Determined', 'Denied', 'Authorized', 'Restricted']
17+
for (const type of types) {
18+
const status = getAuthStatus(type)
19+
expect(statuses).to.contain(status)
20+
}
21+
})
22+
})
23+
})

0 commit comments

Comments
 (0)
Please sign in to comment.