Skip to content

Commit

Permalink
fix compatibility with node, add scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
Tiago Posse committed Aug 29, 2023
1 parent 09c0d68 commit c2832bb
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 77 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,7 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*

# build dirs
build
build.zip
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.ts
Binary file modified bun.lockb
Binary file not shown.
5 changes: 2 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GitlabScim } from './src/gitlab/scim';
import { GitlabApi } from './src/gitlab/api';
import { Google } from './src/google';
import { logger } from "./src/utils/logging";
import { PrivilegeMap, getGroupPrivilege, getUserCustomMembership, loadMappings } from "./src/utils/mappings";
import { getGroupPrivilege, getUserCustomMembership, loadMappings } from "./src/utils/mappings";
import { Slack } from './src/utils/slack';


Expand Down Expand Up @@ -96,6 +96,7 @@ async function execute() {
notes: email,
})
}

for (const key of Object.keys(membership)) {
membershipUpdates.push({
user: email,
Expand Down Expand Up @@ -224,5 +225,3 @@ export const handler: Handler = async () => {

return {}
};

export default handler;
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"name": "gitlab-google-scim",
"module": "index.ts",
"version": "0.1.2",
"main": "index.ts",
"type": "module",
"scripts": {
"compile": "rm -rf build/ && tsc -p ."
"compile": "rm -rf build/ && ./node_modules/typescript/bin/tsc -p . --noEmit false --allowImportingTsExtensions false",
"package": "rm build.zip && zip -r build.zip package.json build node_modules",
"lambda": "npm run compile && npm run package"
},
"devDependencies": {
"bun-types": "latest"
},
"peerDependencies": {
"bun-types": "latest",
"typescript": "^5.0.0"
},

"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.395.0",
"@slack/webhook": "^6.1.0",
Expand All @@ -19,6 +21,7 @@
"google-auth-library": "^9.0.0",
"js-yaml": "^4.1.0",
"minimatch": "^9.0.3",
"node-fetch": "^3.3.2",
"winston": "3"
}
}
34 changes: 12 additions & 22 deletions src/gitlab/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GitlabGroup, GitlabApiUser, GitlabAccessUpdate, GitlabMembership, GitlabAccessUpdateOperation } from "./types"
import { Gitlab } from './index';
import { getSecretFromAws } from "../utils/aws";
import fs from 'fs';

const GITLAB_API_TOKEN = await resolveGitlabApiToken()

Expand All @@ -10,12 +11,10 @@ async function resolveGitlabApiToken(): Promise<string> {
}

if (process.env.GITLAB_API_TOKEN_FILE !== undefined) {
const f = Bun.file(process.env.GITLAB_API_TOKEN_FILE)

if (!f.exists()) {
if (fs.existsSync(process.env.GITLAB_API_TOKEN_FILE)) {
throw Error(`Gitlab API token file does not exist: ${process.env.GITLAB_API_TOKEN_FILE}`)
}
return await f.text()
return (fs.readFileSync(process.env.GITLAB_API_TOKEN_FILE)).toString()
}

if (process.env.GITLAB_API_TOKEN !== undefined) {
Expand All @@ -38,9 +37,7 @@ export class GitlabApi extends Gitlab {
}

async listUserMembership(userId: string): Promise<GitlabMembership[]> {
let membership = await this.request({
url: `/${this.group}/billable_members/${userId}/memberships`
}, [200, 404]) as GitlabMembership[]
let membership = await this.request(`/${this.group}/billable_members/${userId}/memberships`, {}, [200, 404]) as GitlabMembership[]

if (Array.isArray(membership)) {
membership.forEach(value => { value.source_full_name = value.source_full_name.replaceAll(" ", ""); return value })
Expand All @@ -56,9 +53,7 @@ export class GitlabApi extends Gitlab {
var groups: GitlabGroup[] = []

while (page !== -1) {
const resp = await this.rawRequest({
url: `?all_available=true&page=${page}`
})
const resp = await this.rawRequest(`?all_available=true&page=${page}`, {})
if (resp.headers.get("x-next-page") !== null) {
page = +resp.headers.get("x-next-page")!
} else {
Expand All @@ -82,9 +77,7 @@ export class GitlabApi extends Gitlab {
}

while (page !== -1) {
const resp = await this.rawRequest({
url: `/${groupName}/members?page=${page}`
})
const resp = await this.rawRequest(`/${groupName}/members?page=${page}`, {})
if (resp.headers.get("x-next-page") !== null) {
page = +resp.headers.get("x-next-page")!
} else {
Expand All @@ -96,7 +89,7 @@ export class GitlabApi extends Gitlab {
continue
}

if (user.group_saml_identity !== null) {
if (user.group_saml_identity !== null && user.group_saml_identity !== undefined) {
users[user.group_saml_identity.extern_uid] = user
}
}
Expand All @@ -108,27 +101,24 @@ export class GitlabApi extends Gitlab {
async changeUserAccessLevel(change: GitlabAccessUpdate) {
switch (change.op) {
case GitlabAccessUpdateOperation.ADD:
await this.rawRequest({
url: `/${change.group.replace("/", "%2F")}/invitations`,
await this.rawRequest(`/${change.group.replace("/", "%2F")}/invitations`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: {
body: JSON.stringify({
email: change.user,
access_level: change.role
}
})
})
break;
case GitlabAccessUpdateOperation.REMOVE:
await this.rawRequest({
url: `/${change.group.replace("/", "%2F")}/members/${change.user}`,
await this.rawRequest(`/${change.group.replace("/", "%2F")}/members/${change.user}`, {
method: "DELETE"
})
break;
case GitlabAccessUpdateOperation.UPDATE:
await this.rawRequest({
url: `/${change.group.replace("/", "%2F")}/members/${change.user}?access_level=${change.role}`,
await this.rawRequest(`/${change.group.replace("/", "%2F")}/members/${change.user}?access_level=${change.role}`, {
method: "PUT"
})
break;
Expand Down
41 changes: 23 additions & 18 deletions src/gitlab/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { logger } from "../utils/logging"
import fetch, {
Headers,
Request,
RequestInit,
Response,
} from 'node-fetch'

export abstract class Gitlab {
url: string
Expand All @@ -14,14 +21,14 @@ export abstract class Gitlab {
this.headers = {}
}

async request(config: { url: string, body?: any, headers?: { [key: string]: string }, method?: string }, accepted?: number[]): Promise<any> {
async request(url: string, config: RequestInit, accepted?: number[]): Promise<any> {
if (accepted === undefined) {
accepted = [201, 204, 200]
}
return await (await this.rawRequest(config, accepted)).json()
return await (await this.rawRequest(url, config, accepted)).json()
}

async rawRequest(config: { url: string, body?: any, headers?: { [key: string]: string }, method?: string }, accepted?: number[]): Promise<Response> {
async rawRequest(url: string, config: RequestInit, accepted?: number[]): Promise<Response> {
if (accepted === undefined) {
accepted = [201, 204, 200]
}
Expand All @@ -34,25 +41,23 @@ export abstract class Gitlab {
}

config.headers = { ...this.headers, ...config.headers }
if (config.body !== undefined) {
if (config.headers["Content-Type"] === "application/x-www-form-urlencoded") {
let formBody = []
for (const k of Object.keys(config.body)) {
formBody.push(`${k}=${config.body[k]}`)
}
config.body = formBody.join("&")
} else {
config.body = JSON.stringify(config.body)
}
}

config.url = `${this.url}${config.url}`
// if (config.body !== undefined) {
// if ((config.headers as Headers).get("Content-Type") === "application/x-www-form-urlencoded") {
// let formBody = []
// for (const k of Object.keys(config.body)) {
// formBody.push(`${k}=${config.body[k]}`)
// }
// config.body = formBody.join("&")
// } else {
// config.body = JSON.stringify(config.body)
// }
// }

const req = new Request(config)
const req = new Request(`${this.url}${url}`, config)
const resp = await fetch(req)

if (!accepted.includes(resp.status)) {
throw Error(`Could not execute gitlab request ${config.method} to ${config.url} (${resp.status}): ${resp.statusText}`)
throw Error(`Could not execute gitlab request ${config.method} to ${this.url}${url} (${resp.status}): ${resp.statusText}`)
}

return resp
Expand Down
24 changes: 11 additions & 13 deletions src/gitlab/scim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GitlabScimUser, GitlabScimUsersListResponse, GitlabUserUpdate, GitlabUs
import { Gitlab } from './index';
import { getSecretFromAws } from "../utils/aws";
import { logger } from "../utils/logging";
import fs from 'fs';

const GITLAB_SCIM_TOKEN = await resolveGitlabScimToken()

Expand All @@ -13,12 +14,10 @@ async function resolveGitlabScimToken(): Promise<string> {
}

if (process.env.GITLAB_SCIM_TOKEN_FILE !== undefined) {
const f = Bun.file(process.env.GITLAB_SCIM_TOKEN_FILE)

if (!f.exists()) {
if (fs.existsSync(process.env.GITLAB_SCIM_TOKEN_FILE)) {
throw Error(`Gitlab SCIM token file does not exist: ${process.env.GITLAB_SCIM_TOKEN_FILE}`)
}
return await f.text()
return (fs.readFileSync(process.env.GITLAB_SCIM_TOKEN_FILE)).toString()
}

if (process.env.GITLAB_SCIM_TOKEN !== undefined) {
Expand All @@ -42,10 +41,9 @@ export class GitlabScim extends Gitlab {
}

async createUser(user: GoogleUser) {
console.log(await this.request({
url: "/",
console.log(await this.request("/", {
method: "POST",
body: {
body: JSON.stringify({
externalId: user.primaryEmail,
userName: `${user.name.givenName[0]}${user.name.familyName}`,
active: null,
Expand All @@ -63,32 +61,32 @@ export class GitlabScim extends Gitlab {
primary: true
}
]
}
})
}))
}

async activateUser(user: GitlabScimUser) {
this.request(
`/${user.id}`,
{
url: `/${user.id}`,
method: 'PATCH',
body: {
body: JSON.stringify({
Operations: [
{
op: "Update",
path: "active",
value: true
}
]
}
})
}
)
}

async removeScimUser(user: GitlabScimUser) {
this.request(
`/${user.id}`,
{
url: `/${user.id}`,
method: 'DELETE'
}
)
Expand All @@ -99,7 +97,7 @@ export class GitlabScim extends Gitlab {

var startIndex = 1
while (startIndex > -1) {
const resp = await this.request({ url: "" }) as GitlabScimUsersListResponse
const resp = await this.request("", {}) as GitlabScimUsersListResponse

for (var u of resp.Resources) {
for (const email of u.emails) {
Expand Down
8 changes: 4 additions & 4 deletions src/google/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GoogleAuth } from 'google-auth-library';
import { GoogleUser, GoogleGroupMember, GoogleGroupsListResponse, GoogleUsersListResponse, GoogleGroup, GoogleGroupMembersListResponse } from './types';
import { minimatch } from 'minimatch';
import { getSecretFromAws } from "../utils/aws";
import fs from 'fs';

const SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user.readonly',
Expand All @@ -17,18 +18,17 @@ const GOOGLE_TARGET_SA_FILE = GOOGLE_SA_KEY_FILE || "/tmp/service_account.json"
async function resolveGoogleServiceAcccount() {
if (process.env.GOOGLE_SA_KEY_SECRET !== undefined) {
const secret = await getSecretFromAws(process.env.GOOGLE_SA_KEY_SECRET)
await Bun.write(GOOGLE_TARGET_SA_FILE, secret)
fs.writeFileSync(GOOGLE_TARGET_SA_FILE, secret)
return
}

if (process.env.GOOGLE_SA_KEY !== undefined) {
await Bun.write(GOOGLE_TARGET_SA_FILE, process.env.GOOGLE_SA_KEY)
fs.writeFileSync(GOOGLE_TARGET_SA_FILE, process.env.GOOGLE_SA_KEY)
return
}

if (process.env.GOOGLE_SA_KEY_FILE !== undefined) {
const f = Bun.file(process.env.GOOGLE_SA_KEY_FILE)
if (!f.exists()) {
if (fs.existsSync(process.env.GOOGLE_SA_KEY_FILE)) {
throw Error(`Google service account file does not exist: ${process.env.GOOGLE_SA_KEY_FILE}`)
}
return
Expand Down
22 changes: 16 additions & 6 deletions src/utils/logging.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import winston from 'winston';
import { createLogger, format, transports, LogEntry } from 'winston';
import tty from 'tty';

const LOG_LEVEL = process.env.LOG_LEVEL || 'info';

export const logger = winston.createLogger({
export const logger = createLogger({
level: LOG_LEVEL,
format: winston.format.json(),
defaultMeta: { service: 'gitlab-sso-sync' },
format: tty.isatty(process.stdout.fd) ? format.combine(
format.colorize(),
format.simple()
) : format.combine(
format.timestamp(),
format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.simple()
}),
new transports.Console({
format: format.combine(format.printf((log: LogEntry) => {
return JSON.stringify(log, null);
}))
})
],
});
6 changes: 3 additions & 3 deletions src/utils/mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { GitlabRole, GitlabRoleMapping } from '../gitlab/types';
import { load } from "js-yaml";
import { getSecretFromAws } from './aws';
import { logger } from './logging';
import fs from 'fs';

async function resolveMappings(): Promise<string> {
if (process.env.ROLE_MAPPINGS_SECRET !== undefined) {
return await getSecretFromAws(process.env.ROLE_MAPPINGS_SECRET)
}

if (process.env.ROLE_MAPPINGS_FILE !== undefined) {
const f = Bun.file(process.env.ROLE_MAPPINGS_FILE)
if (!f.exists()) {
if (fs.existsSync(process.env.ROLE_MAPPINGS_FILE)) {
throw Error(`Role mappings file does not exist: ${process.env.ROLE_MAPPINGS_FILE}`)
}
return await f.text()
return (fs.readFileSync(process.env.ROLE_MAPPINGS_FILE)).toString()
}

if (process.env.ROLE_MAPPINGS !== undefined) {
Expand Down
Loading

0 comments on commit c2832bb

Please sign in to comment.