LearningLocker / learninglocker

Learning Locker - The Open Source Learning Record Store. Started in 2014.
https://learningpool.com/solutions/learning-record-store-learning-locker/learning-locker-community-overview/
GNU General Public License v3.0
554 stars 276 forks source link

Create organisation via REST API? #1246

Open cutz opened 6 years ago

cutz commented 6 years ago

Is it possible to create a new organisation via the REST API? From the documentation it seems like I should be able to POST to the organisations endpoint but as clients are traditionally tied to an existing organisation it isn't clear to me what authentication credentials I would use to accomplish that. Is there some sort of higher level admin account I can use, or is this functionality not supported?

cutz commented 6 years ago

I've made some progress here, but first, here is some more background on what I'm actually trying to accomplish. I have an installation of learning locker v2 installed and functioning. I'd like to be able to programmatically provision, via the api, new organizations and within those organizations I'm trying to establish a statement store as well as a client for interacting with that store. I'm attempting to provision these pieces entirely through the API.

As the initial comment in the issue states, I was first blocked trying to create organizations through the API. That is until I found some comments in the gitter chat from August that helped out.

ht2 OK, vejeykarthick_twitter and AnhNguy95487607_twitter Looking at the code, this functionality is limited to only user API tokens only (not clients): https://github.com/LearningLocker/learninglocker/blob/master/lib/services/auth/modelFilters/organisation.js#L75 I believe this decision was to ensure a client in an organisation cannot create multiple other organisations. If you require a "super" client that can perform this action (given how much control it gives), I suggest the following change to your client in the database: db.client.update({_id: ObjectId("CLIENT_ID")}, {$push: {scopes: "SITE_ADMIN"}}); This will update the client you choose to have super admin privileges, which by pass all permissions at the highest level. https://github.com/LearningLocker/learninglocker/blob/master/lib/services/auth/modelFilters/organisation.js#L105 Note that this client will have a lot of power, so I would limit who can see/use it If I were you, I would make a new organisation in the UI, and create a client in there to give this scope too. Do not add ANY other users to this organization so that those credentials are not exposed.

I set up a new organization called O_Super and a corresponding client C_Super and off I went to the database to grant C_Super the site_admin scope. Note: the case of the scope differs but I believe it is correct. With the permissions of C_Super elevated, I was able to use that key and secret to successfully create a new organization O_New.

Then it was on to the next step of trying to create an lrs store and client for interacting with that organization. Thinking I need to set the client up first I attempted to use credentials for C_Super (the site admin) to POST to api/v2/client providing both a title and more importantly the organization id of O_New, the organization I wanted the client to belong to. Alas, my new client was created not in the organization I provided, but in the organization my auth credentials belonged to. Digging through the code it seems like the client creation always creates the client in the organization parsed from the auth token. Sure enough, shortly after the comments above I found this in gitter:

The super client can only manage models in its own super org For each org you create, you should create a client that can manage that org I'm not sure you can do this all via the API tbh You'll have to experiment

At this point I feel somewhat stuck. I can't see a way around this given the documented api. I've moved on to "hacking" at the undocumented api used by the front end (which I'm sure is a terrible idea). At this point I've pieced together the following process to accomplish this:

  1. Obtain a jwt by POSTing to /api/auth/jwt/password using basic auth and the username and password of the site admin account (these are the credential you log in to the UI with, not the aforementioned site_admin client).
  2. Use the resulting jwt to create an organization through /api/v2/organisation setting the owner to the userid of the site admin user. Note: This can be done with the site_admin client as well, but you need the bearer token from step 1 for the next step anyway.
  3. Use the bearer token from step 1 to obtain a bearer token scoped to the new organization. POST to /api/auth/jwt/organisation a json object containing the key organisation and the value of the organisation id from step 2.
  4. The resulting Bearer token can now be used to interact with the api in the context of my new organisation. I can create my stores/clients as necessary.

Am I missing something here? Is there a supported way to accomplish this using the documented api?

ryasmi commented 6 years ago

Hi @cutz, apologies for the delayed response and really impressed with your digging here, great work figuring out how to get the bearer token and creating the orgs/clients/stores via that token. That is honestly the best solution to this issue right now. With that now in place does it create any other issues or pains for you? If it does create other issues we can try to schedule some time to make some changes that improve the experience.

cutz commented 6 years ago

@ryansmith94 As far as I can tell this unblocks me for now. My only concern would be the stability of the /api/auth/* endpoints across learning locker updates going forward.

Given past chatter in gitter, it does seem like this warrants a potential api enhancement or additional documentation. If/when you all decide to tackle that, please feel free to reach out if you have any more questions on the ways in which we are utilizing things.

ryasmi commented 6 years ago

I think those endpoints will remain stable, we have no plans to change those right now. Yeah I agree I think this needs a API enhancement to allow instance level clients in addition to the existing organisation level clients, thanks @cutz. @Ian247 can you please make a ticket for adding instance level clients that references this issue?

Ian247 commented 6 years ago

Ticket number is LL-647.

SindreSvendby commented 5 years ago

We have the same issue. Based on the documentation. It really look like the http rest API can do this. I would update the docs to make it more clear that it's not possible to create orgs automatically.

The for the super clear work around @cutz

In general the documentation is s really good. Im impressed by the quality on LL. Great work

carnophager commented 5 years ago

Hello everyone, we also have this issue - trying to create an organisation through API and it fails with error message: "Prviliges not sufficient for this operation". Tried the mentioned approach with hacking the ui api calls and getting jwt token through /api/auth/jwt/password but we get "Incorrect login details". Do you perhaps have some progress on this issue? If not we can try to hack the creation of new organization through the call of "createSiteAdmin" with an existing admin user, but it doesn't look safe.

ryasmi commented 5 years ago

Hi @carnophager sounds like the user doesn't have access to create organisations. The user must be a site admin or have the create organisations scope.

carnophager commented 5 years ago

Ok, I see, thank you @ryansmith94 . How can I get the basic auth of such an user, I only see basic auth keys for clients?

ryasmi commented 5 years ago

We only support bearer auth for users at this time. We will likely introduce clients at an instance level in the future to allow basic auth to be used in this scenario.

On Mon, 12 Aug 2019 at 12:21, Trayko Traykov notifications@github.com wrote:

Ok, I see, thank you @ryansmith94 https://github.com/ryansmith94 . How can I get the basic auth of such an user, I only see basic auth keys for clients?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/LearningLocker/learninglocker/issues/1246?email_source=notifications&email_token=AAXHRCJX74Y4L5JVSVVMHW3QEFBUFA5CNFSM4FTFKOUKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD4CHMCI#issuecomment-520386057, or mute the thread https://github.com/notifications/unsubscribe-auth/AAXHRCKP7YNJPR3WU3SD32DQEFBUFANCNFSM4FTFKOUA .

-- https://www.ht2labs.com/ RYAN SMITH Email: ryan.smith@learningpool.com Phone: +44 (0)1865 873 862 Web: ht2labs.com https://www.ht2labs.com/

https://twitter.com/ht2labs https://www.linkedin.com/company/1219805/

carnophager commented 5 years ago

Ok, I got around my issue - so basically you follow @cutz directions, but he doesn't specify that the request to /api/auth/jwt/password should be done in a base64 encoded string which is in the format: username@domain.com:user_password so you encode this information to base64 pass it in the header as a POST request like Authorization "Basic dHJheWtvckBzdGFuZ2EubmV0OjJzdGFuZ2E3" and then you get your JWT and use that to create organizations.

Prothon commented 4 years ago

I've gotten it to work based on what https://github.com/LearningLocker/learninglocker/issues/1246#issuecomment-520442222 said, which was spot on. The returning body of text from that api call will just be the JWT token.

A place I got caught was getting a 401 after I've signed in. I fixed it by switching from basic authentication to Barer #{JWT} in the API Header.

The only issue I have now is during Organization create (POST#/api/v2/organisations/) when I send my form data it doesn't use it. I'm trying to give it a name and it fails. It just creates a organisation with a blank name. I try and update it, same thing.

Prothon commented 4 years ago

It was an issue with the library I was using (HTTParty). It works fine otherwise.

paul-em commented 4 years ago

Thank you @cutz for describing your solution. I thought I would contribute by sharing my code I implemented with your details in the hope it might help someone. I am sure you can optimize this to be more efficient, but I ended up not going for adding multiple organisations and instead using multiple stores, so I didn't put more work into that solution.


  const jwt = await request({
    method: 'POST',
    uri: `${config.learningLockerEndpoint}/api/auth/jwt/password`,
    headers: {
      Authorization: config.learningLockerAuth,
      'Content-Type': 'application/json',
      'User-Agent': 'Server',
    },
    json: true,
  });
  const bearerToken = `Bearer ${jwt}`;
  const users = await request({
    method: 'GET',
    uri: `${config.learningLockerEndpoint}/api/v2/user`,
    headers: {
      Authorization: bearerToken,
      'Content-Type': 'application/json',
      'User-Agent': 'Server',
    },
    json: true,
  });
  const user = users.find(u => u.scopes.includes('site_can_create_org'));
  const organisation = await request({
    method: 'POST',
    uri: `${config.learningLockerEndpoint}/api/v2/organisation`,
    headers: {
      Authorization: bearerToken,
      'Content-Type': 'application/json',
      'User-Agent': 'Server',
    },
    body: {
      owner: user._id,
      name: data.name,
    },
    json: true,
  });
  const scopedJwt = await request({
    method: 'POST',
    uri: `${config.learningLockerEndpoint}/api/auth/jwt/organisation`,
    headers: {
      Authorization: bearerToken,
      'Content-Type': 'application/json',
      'User-Agent': 'Server',
    },
    body: {
      organisation: organisation._id,
    },
    json: true,
  });
  const scopedBearerToken = `Bearer ${scopedJwt}`;
  await request({
    method: 'POST',
    uri: `${config.learningLockerEndpoint}/api/v2/lrs`,
    headers: {
      Authorization: scopedBearerToken,
      'Content-Type': 'application/json',
      'User-Agent': 'Server',
    },
    body: {
      title: 'E-Learning',
      owner: user._id,
    },
    json: true,
  });
  const clients = await request({
    method: 'GET',
    uri: `${config.learningLockerEndpoint}/api/v2/client`,
    headers: {
      Authorization: scopedBearerToken,
      'Content-Type': 'application/json',
      'User-Agent': 'Server',
    },
    json: true,
  });
  await request({
    method: 'PATCH',
    uri: `${config.learningLockerEndpoint}/api/v2/client/${clients[0]._id}`,
    headers: {
      Authorization: scopedBearerToken,
      'Content-Type': 'application/json',
      'User-Agent': 'Server',
    },
    body: {
      scopes: ['xapi/all', 'all'],
    },
    json: true,
  });`